DeviceRecorder.Core
0.5.0
dotnet add package DeviceRecorder.Core --version 0.5.0
NuGet\Install-Package DeviceRecorder.Core -Version 0.5.0
<PackageReference Include="DeviceRecorder.Core" Version="0.5.0" />
<PackageVersion Include="DeviceRecorder.Core" Version="0.5.0" />
<PackageReference Include="DeviceRecorder.Core" />
paket add DeviceRecorder.Core --version 0.5.0
#r "nuget: DeviceRecorder.Core, 0.5.0"
#:package DeviceRecorder.Core@0.5.0
#addin nuget:?package=DeviceRecorder.Core&version=0.5.0
#tool nuget:?package=DeviceRecorder.Core&version=0.5.0
DeviceRecorder.Core
Reusable serial device session and UI state helpers for device recorder applications.
This library is intended to be reused for other serial devices, not just the KERN sample app in this repository.
What is included
SerialDeviceClientfor serial port lifecycle and recovery handlingRecordingSessionController<TClient, TReadResult>for reusable session orchestrationIRecordableDeviceClient<TReadResult>for device-specific recording clientsIRecordingSessionStatefor UI-facing session stateFormViewStatefor simple connect/record UI state mappingAsyncFormsTimerfor non-overlapping async WinForms timer callbacksTimedDeviceRecorder<TState>andPumaRecorder<TState>for reusable recording pipelinesRecorderPathResolverfor resolving recorder directories from relative or absolute pathsReplayFileLoader,ReplayFileRecorder<TReadResult>,ReplayDeviceClient<TState, TReadResult>, andDeviceReplayEntry<TReadResult>for replay-file loading, writing, and log-based device emulationIPortProviderimplementations for real or emulated port enumeration
Target framework
.NET 10(net10.0-windows)
Basic usage
The reusable design is split into a few layers:
- a device-specific read result type such as
ScaleReadResultorPumpReadResult - a real device client that talks to the serial port
- an optional state type plus recorder for normal timed recording output
- an optional replay recorder for replay-log output
- an optional replay/emulation client for offline playback
- a session controller that exposes connect, read, record, and replay logging behavior to the UI
If you are adapting the library to another device, start with the real device client and session controller first. Add timed recording and replay/emulation once basic reads are working.
1. Create a device client
Your device client inherits SerialDeviceClient and implements IRecordableDeviceClient<TReadResult>.
using DeviceRecorder.Core.Controllers;
using DeviceRecorder.Core.Serial;
public sealed class SampleReadResult
{
public string RawResponse { get; init; } = string.Empty;
}
public sealed class SampleSerialClient : SerialDeviceClient, IRecordableDeviceClient<SampleReadResult>
{
private readonly SampleReplayRecorder _replayRecorder = new();
public SampleSerialClient(string portName, int baudRate = 9600)
: base(portName, baudRate)
{
}
public bool IsRecording { get; private set; }
public bool IsRecordingPaused { get; private set; }
public string? RecordingFilePath { get; private set; }
public bool IsReplayLogging => _replayRecorder.IsRecording;
public string? ReplayFilePath => _replayRecorder.RecordingFilePath;
public Task<SampleReadResult> ReadAsync()
{
return Task.Run(Read);
}
public SampleReadResult Read()
{
var raw = ReadLine();
SetConnectedState();
var result = new SampleReadResult { RawResponse = raw };
_replayRecorder.Record(result);
return result;
}
public void StartRecording(string? directoryPath = null)
{
IsRecording = true;
IsRecordingPaused = false;
RecordingFilePath ??= Path.Combine(directoryPath ?? AppContext.BaseDirectory, "sample.txt");
}
public void PauseRecording()
{
if (IsRecording)
{
IsRecordingPaused = true;
}
}
public void StopRecording()
{
IsRecording = false;
IsRecordingPaused = false;
RecordingFilePath = null;
}
public void StartReplayLogging(string? directoryPath = null)
{
_replayRecorder.StartRecording(directoryPath);
}
public void StopReplayLogging()
{
_replayRecorder.StopRecording();
}
public void SetRecordingTimeStep(TimeSpan recordingTimeStep)
{
}
}
public sealed class SampleReplayRecorder : ReplayFileRecorder<SampleReadResult>
{
protected override void WriteReplayColumns(StreamWriter writer, SampleReadResult result)
{
writer.Write(result.RawResponse);
}
}
For a real application, the device client is usually responsible for:
- sending any device-specific commands before reading, if needed
- parsing the raw serial response into
TReadResult - updating connection state by calling
SetConnectedState()after a successful read - forwarding normal recording to a
TimedDeviceRecorder<TState>orPumaRecorder<TState>if the device needs standard recorder output - forwarding replay logging to a
ReplayFileRecorder<TReadResult>if the device needs replay-log output
The simplest usable shape for a new device is:
TReadResult= parsed read result returned to the UITState= current device state used by timed recorder outputReplayFileRecorder<TReadResult>= replay log writerTimedDeviceRecorder<TState>orPumaRecorder<TState>= normal recorder file writer
2. Create a session controller
using DeviceRecorder.Core.Controllers;
public sealed class SampleSessionController : RecordingSessionController<SampleSerialClient, SampleReadResult>
{
protected override SampleSerialClient CreateClient(string selectedPort)
{
return new SampleSerialClient(selectedPort, 9600);
}
}
The controller is the main abstraction your UI should depend on. It already provides:
SetSelectedPort(...)ConnectAsync(...)DisconnectAsync()ReadAsync()ToggleRecording(...)StopRecording()StartReplayLogging(...)StopReplayLogging()SetReplayLogging(...)for boolean-driven UISetRecordingTimeStep(...)
3. Use it from an application
using DeviceRecorder.Core.UI;
var controller = new SampleSessionController();
controller.SetSelectedPort("COM3");
var firstResult = await controller.ConnectAsync(TimeSpan.FromSeconds(1));
Console.WriteLine(firstResult.RawResponse);
controller.ToggleRecording(TimeSpan.FromSeconds(1), "data");
controller.StartReplayLogging("replay");
var viewState = FormViewState.Create(controller);
Console.WriteLine(viewState.ConnectionText);
Console.WriteLine(viewState.RecordButtonText);
Console.WriteLine(viewState.IsStartReplayLoggingEnabled);
Console.WriteLine(viewState.IsStopReplayLoggingEnabled);
For a WinForms-style polling UI, the common pattern is:
- create the controller
- let the user choose a COM port
- call
ConnectAsync(...)once - use
AsyncFormsTimerto callReadAsync()periodically - map controller state to buttons and labels via
FormViewState.Create(controller)
AsyncFormsTimer and FormViewState are optional helpers. The controller and recording components can also be used from WPF, console apps, services, or test harnesses.
Adapting the library to another device
Use this checklist when creating a new device recorder app on top of DeviceRecorder.Core:
- Define a device-specific
TReadResult- include the raw device response
- include parsed fields the UI and recorders need
- Define a device-specific state type if you want timed recorder files
- keep only the values needed to write one recorder row
- Implement the real serial client
- inherit
SerialDeviceClient - implement
IRecordableDeviceClient<TReadResult> - parse responses and update recorders from each successful read
- inherit
- Implement recorder output
- inherit
TimedDeviceRecorder<TState>for custom tabular output, or - inherit
PumaRecorder<TState>if the default Puma-style layout fits your device
- inherit
- Implement replay logging
- inherit
ReplayFileRecorder<TReadResult> - write only the device-specific columns after the shared timestamp column
- inherit
- Optionally implement emulation
- load replay rows with
ReplayFileLoader.Load(...) - inherit
ReplayDeviceClient<TState, TReadResult> - apply replay results into your device state in
ApplyResult(...)
- load replay rows with
- Create an app-specific controller
- inherit
RecordingSessionController<TClient, TReadResult> - create either the real client or an emulated client based on app settings
- inherit
- Build the UI around controller state
- use explicit start/stop actions for replay logging if desired
- use
FormViewStateif your UI follows the same connect/record model
If you are unsure how these pieces fit together, use KernRecorderApp in this repository as the end-to-end example.
Recording directories
- Recording and replay directory paths can be relative or absolute.
- Relative paths are resolved from
AppContext.BaseDirectory. RecorderPathResolver.ResolveDirectoryPath(...)is used by the built-in recorders.
Replay logging
ReplayFileRecorder<TReadResult>writes timestamped tab-separated replay files.- The first column is always the timestamp in
yyyy-MM-dd HH:mm:ss.fffformat. - Device-specific implementations write the remaining columns.
RecordingSessionController<TClient, TReadResult>.StartReplayLogging(...)andStopReplayLogging()provide explicit replay logging control for start/stop UI buttons.RecordingSessionController<TClient, TReadResult>.SetReplayLogging(...)remains available as a convenience wrapper for boolean-driven UI state.
Notes
FormViewStatedepends only onIRecordingSessionState, so it can be reused with any compatible controller, including explicit replay logging start/stop buttons.RecordingSessionController<TClient, TReadResult>contains the reusable connect/disconnect/read/record orchestration.SerialDeviceClientincludes serial port recovery behavior used by the sample app.TimedDeviceRecorder<TState>andReplayFileRecorder<TReadResult>resolve relative paths fromAppContext.BaseDirectory, so application code can keep configuration simple.TimedDeviceRecorder<TState>andReplayFileRecorder<TReadResult>use built-in en-US numeric formatting helpers for recorder output.
Emulation
You can emulate a device by deriving from ReplayDeviceClient<TState, TReadResult> and providing replay entries. The common replay-file support assumes only:
- the first column is a timestamp in
yyyy-MM-dd HH:mm:ss.fff - columns are tab-separated
Everything after that is device-specific and parsed by the device implementation.
using DeviceRecorder.Core.Emulation;
using DeviceRecorder.Core.Recording;
public sealed class SampleState
{
public float Value { get; set; }
}
public readonly record struct SampleResult(string RawResponse, float Value);
public sealed class SampleReplayClient : ReplayDeviceClient<SampleState, SampleResult>
{
public SampleReplayClient(IReadOnlyList<DeviceReplayEntry<SampleResult>> entries)
: base(entries)
{
}
protected override SampleState CreateState() => new();
protected override TimedDeviceRecorder<SampleState> CreateRecorder(SampleState state)
{
throw new NotImplementedException();
}
protected override ReplayFileRecorder<SampleResult> CreateReplayRecorder()
{
throw new NotImplementedException();
}
protected override void ApplyResult(SampleResult result, SampleState state)
{
state.Value = result.Value;
}
}
You can also load replay entries from a generic tab-separated replay file:
var entries = ReplayFileLoader.Load("sample-replay.txt", row =>
{
var rawResponse = row.Columns[1];
var value = float.Parse(row.Columns[2], CultureInfo.InvariantCulture);
return new SampleResult(rawResponse, value);
});
Set ReplayDeviceClientOptions.RestartFromBeginningOnOpen to restart playback from the first replay entry each time the emulated device is opened.
Choosing which pieces to reuse
You do not need every abstraction for every device.
- If you only need live serial reads, implement
SerialDeviceClient+IRecordableDeviceClient<TReadResult>+RecordingSessionController<TClient, TReadResult>. - If you also need standard recorder files, add
TimedDeviceRecorder<TState>orPumaRecorder<TState>. - If you also need replay-log files, add
ReplayFileRecorder<TReadResult>. - If you also need offline playback or demo mode, add
ReplayFileLoader+ReplayDeviceClient<TState, TReadResult>.
That makes the library suitable for simple devices as well as more feature-rich recorder applications.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-windows7.0 is compatible. |
-
net10.0-windows7.0
- System.IO.Ports (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.