DeviceRecorder.Core 0.5.0

dotnet add package DeviceRecorder.Core --version 0.5.0
                    
NuGet\Install-Package DeviceRecorder.Core -Version 0.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DeviceRecorder.Core" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DeviceRecorder.Core" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="DeviceRecorder.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DeviceRecorder.Core --version 0.5.0
                    
#r "nuget: DeviceRecorder.Core, 0.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DeviceRecorder.Core@0.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DeviceRecorder.Core&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=DeviceRecorder.Core&version=0.5.0
                    
Install as a Cake Tool

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

  • SerialDeviceClient for serial port lifecycle and recovery handling
  • RecordingSessionController<TClient, TReadResult> for reusable session orchestration
  • IRecordableDeviceClient<TReadResult> for device-specific recording clients
  • IRecordingSessionState for UI-facing session state
  • FormViewState for simple connect/record UI state mapping
  • AsyncFormsTimer for non-overlapping async WinForms timer callbacks
  • TimedDeviceRecorder<TState> and PumaRecorder<TState> for reusable recording pipelines
  • RecorderPathResolver for resolving recorder directories from relative or absolute paths
  • ReplayFileLoader, ReplayFileRecorder<TReadResult>, ReplayDeviceClient<TState, TReadResult>, and DeviceReplayEntry<TReadResult> for replay-file loading, writing, and log-based device emulation
  • IPortProvider implementations 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 ScaleReadResult or PumpReadResult
  • 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> or PumaRecorder<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 UI
  • TState = current device state used by timed recorder output
  • ReplayFileRecorder<TReadResult> = replay log writer
  • TimedDeviceRecorder<TState> or PumaRecorder<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 UI
  • SetRecordingTimeStep(...)

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:

  1. create the controller
  2. let the user choose a COM port
  3. call ConnectAsync(...) once
  4. use AsyncFormsTimer to call ReadAsync() periodically
  5. 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:

  1. Define a device-specific TReadResult
    • include the raw device response
    • include parsed fields the UI and recorders need
  2. Define a device-specific state type if you want timed recorder files
    • keep only the values needed to write one recorder row
  3. Implement the real serial client
    • inherit SerialDeviceClient
    • implement IRecordableDeviceClient<TReadResult>
    • parse responses and update recorders from each successful read
  4. Implement recorder output
    • inherit TimedDeviceRecorder<TState> for custom tabular output, or
    • inherit PumaRecorder<TState> if the default Puma-style layout fits your device
  5. Implement replay logging
    • inherit ReplayFileRecorder<TReadResult>
    • write only the device-specific columns after the shared timestamp column
  6. Optionally implement emulation
    • load replay rows with ReplayFileLoader.Load(...)
    • inherit ReplayDeviceClient<TState, TReadResult>
    • apply replay results into your device state in ApplyResult(...)
  7. Create an app-specific controller
    • inherit RecordingSessionController<TClient, TReadResult>
    • create either the real client or an emulated client based on app settings
  8. Build the UI around controller state
    • use explicit start/stop actions for replay logging if desired
    • use FormViewState if 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.fff format.
  • Device-specific implementations write the remaining columns.
  • RecordingSessionController<TClient, TReadResult>.StartReplayLogging(...) and StopReplayLogging() 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

  • FormViewState depends only on IRecordingSessionState, 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.
  • SerialDeviceClient includes serial port recovery behavior used by the sample app.
  • TimedDeviceRecorder<TState> and ReplayFileRecorder<TReadResult> resolve relative paths from AppContext.BaseDirectory, so application code can keep configuration simple.
  • TimedDeviceRecorder<TState> and ReplayFileRecorder<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> or PumaRecorder<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 Compatible and additional computed target framework versions.
.NET net10.0-windows7.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 26 5/6/2026
0.1.0 27 5/6/2026