From b3386882ba74cbc34ce99a5e12a0b2e896fde11c Mon Sep 17 00:00:00 2001 From: Tyler Kron Date: Fri, 10 Jul 2026 17:06:06 -0600 Subject: [PATCH 1/2] docs: fix stale IStreamingDevice claim and document SampleReceived pipeline (#284) - Correct DEVICE_INTERFACES.md's false claim that IStreamingDevice is desktop-only; document DaqifiStreamingDevice and the interfaces it actually implements (IStreamingDevice, INetworkConfigurable, ISdCardOperations, ILanChipInfoProvider, IDeviceDiagnostics). - Document the decoded per-channel SampleReceived pipeline (#279) in README's quickstart/capability row and DEVICE_INTERFACES.md's Streaming Data section, alongside the existing raw MessageReceived path. - Add missing ConnectSerialAsync/ConnectSerial/ConnectFromDeviceInfo overloads to the factory method table. - Mention DIO/PWM tools in the MCP server pitch. - Remove dangling "streaming-evolution plan" reference with no doc. - Fix stale Reboot/SystemInfo names in ScpiMessageProducer XML doc examples to the actual RebootDevice/GetDeviceInfo members. Co-Authored-By: Claude Sonnet 5 --- README.md | 23 ++++---- docs/DEVICE_INTERFACES.md | 59 ++++++++++++++++++- .../Producers/ScpiMessageProducer.cs | 10 ++-- src/Daqifi.Mcp/README.md | 2 +- 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d0b4df0c..00f543d8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ DAQiFi builds wireless data acquisition hardware designed to get out of the way Prefer a ready-made GUI? Check out [DAQiFi Desktop](https://github.com/daqifi/daqifi-desktop), which is built on top of this library. -Want to drive a device from an AI assistant? The repo also ships an **[MCP server](src/Daqifi.Mcp)** — point Claude, Cursor, Codex, or any MCP-aware client at it to discover, configure channels, set the sample rate, and run SD-card logging through plain conversation. +Want to drive a device from an AI assistant? The repo also ships an **[MCP server](src/Daqifi.Mcp)** — point Claude, Cursor, Codex, or any MCP-aware client at it to discover, configure channels, drive digital I/O and PWM outputs, set the sample rate, and run SD-card logging through plain conversation. ## See it in 30 seconds @@ -34,23 +34,22 @@ dotnet add package Daqifi.Core ```csharp using Daqifi.Core.Device; using Daqifi.Core.Communication.Producers; +using Daqifi.Core.Channel; // Connect — transport and device initialization handled for you using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); -// Subscribe to incoming samples -device.MessageReceived += (_, e) => -{ - if (e.Message.Data is DaqifiOutMessage msg) - Console.WriteLine($"{msg.MsgTimeStamp}: {string.Join(", ", msg.AnalogInData)}"); -}; +// Subscribe to decoded, per-channel samples +var ai0 = device.Channels.First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); +ai0.SampleReceived += (_, e) => Console.WriteLine($"{e.Sample.Timestamp}: {e.Sample.Value} V"); -// Enable analog channels via bitmask (0b11 = first 2 channels), then stream at 100 Hz -device.Send(ScpiMessageProducer.EnableAdcChannels("3")); +// Enable channel 0, then stream at 100 Hz +device.Send(ScpiMessageProducer.EnableAdcChannels("1")); device.Send(ScpiMessageProducer.StartStreaming(100)); ``` -A real, working program — no GUI required. +A real, working program — no GUI required. Prefer the raw protobuf frame instead? Subscribe to +`device.MessageReceived` — see [Streaming Data](docs/DEVICE_INTERFACES.md#streaming-data). ## Common applications @@ -71,7 +70,7 @@ More examples at [daqifi.com](https://daqifi.com). | Hardware | Nyquist 1 / Nyquist 3 — wireless DAQ devices (and their on-device firmware) | | **SDK** | **DAQiFi Core — this library** | | App | [DAQiFi Desktop](https://github.com/daqifi/daqifi-desktop) — GUI built on this SDK | -| Agent | [MCP server](src/Daqifi.Mcp) — drive a device from Claude / Cursor / any MCP client | +| Agent | [MCP server](src/Daqifi.Mcp) — drive a device from Claude / Cursor / any MCP client: discover, configure channels, DIO/PWM, and SD logging | | Your code | Custom apps, dashboards, pipelines, test rigs | ## What you can do @@ -80,7 +79,7 @@ More examples at [daqifi.com](https://daqifi.com). |---|---| | **Auto-discovery** | Find any DAQiFi on WiFi or USB in seconds — no IP hunting, no config files | | **One-line connect** | `DaqifiDeviceFactory.ConnectTcpAsync(...)` wraps transport setup and device init; retries are opt-in via `DeviceConnectionOptions` | -| **Real-time streaming** | Event-driven async API; no polling loops to write | +| **Real-time streaming** | Per-channel `IChannel.SampleReceived` events with decoded, scaled values — or subscribe to the raw protobuf frame directly; no polling loops to write | | **Digital I/O** | Set any DIO pin as input or output and drive outputs high/low; inputs stream alongside analog data | | **PWM outputs** | Drive PWM on capable DIO pins with per-channel duty cycle and a shared, device-wide frequency | | **SD card operations** | List, download, delete, format, and start/stop SD logging over USB / serial | diff --git a/docs/DEVICE_INTERFACES.md b/docs/DEVICE_INTERFACES.md index f6b62e89..e0819553 100644 --- a/docs/DEVICE_INTERFACES.md +++ b/docs/DEVICE_INTERFACES.md @@ -53,7 +53,10 @@ The base interface for all DAQiFi devices, providing fundamental connection and ### IStreamingDevice -Extends `IDevice` with data streaming functionality for devices that support continuous data acquisition. Note: This interface is primarily implemented in the desktop application; the core library provides the base `DaqifiDevice` class. +Extends `IDevice` with data streaming functionality for devices that support continuous data acquisition: +starting/stopping a stream, per-channel enable/disable, digital I/O, PWM, and analog output. +`DaqifiStreamingDevice` implements it in this library — see [DaqifiStreamingDevice](#daqifistreamingdevice) +below. ## Implementation Classes @@ -67,6 +70,20 @@ The primary device class that provides: - Protocol buffer message handling - Channel population from device status +### DaqifiStreamingDevice + +Extends `DaqifiDevice` with the full streaming/configuration surface — this is the class +`DaqifiDeviceFactory` actually constructs for a connection. It implements: + +- `IStreamingDevice` — streaming start/stop, channel enable/disable, digital I/O, PWM, analog output + (see [Channel Management](#channel-management) below) +- `INetworkConfigurable` — WiFi/LAN configuration (see the + [Network configuration](../README.md#network-configuration) recipe in the root README) +- `ISdCardOperations` — list/download/delete/format SD card contents and start/stop on-device logging +- `ILanChipInfoProvider` — WiFi-module firmware/version info used during firmware updates +- `IDeviceDiagnostics` — system log, runtime log levels, command history, and performance counters + (see [Device Diagnostics](#device-diagnostics) below) + ### DaqifiDeviceFactory Static factory class for simplified device connections: @@ -76,7 +93,11 @@ Static factory class for simplified device connections: | `ConnectTcpAsync(host, port, options?, token?)` | Connect by hostname | | `ConnectTcpAsync(ipAddress, port, options?, token?)` | Connect by IP address | | `ConnectTcp(...)` | Synchronous versions | +| `ConnectSerialAsync(portName, options?, token?)` | Connect over serial/USB at the default baud rate (9600) | +| `ConnectSerialAsync(portName, baudRate, options?, token?)` | Connect over serial/USB at an explicit baud rate | +| `ConnectSerial(...)` | Synchronous versions | | `ConnectFromDeviceInfoAsync(deviceInfo, options?, token?)` | Connect from discovery result | +| `ConnectFromDeviceInfo(...)` | Synchronous version | ### DeviceConnectionOptions @@ -250,6 +271,42 @@ Console.WriteLine($"Digital I/O: {caps.DigitalPortCount}"); ### Streaming Data +Two ways to consume streamed data: decoded per-channel samples via `IChannel.SampleReceived` +(recommended for most consumers), or the raw protobuf frame via `MessageReceived` (for hand-decoding +or bridging into another pipeline). + +#### Per-channel samples (recommended) + +While a stream is active, `DaqifiStreamingDevice` decodes every frame and raises `SampleReceived` on +each enabled channel — no protobuf field names or ADC bitmasks to interpret client-side. + +```csharp +using Daqifi.Core.Channel; + +using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); + +var ai0 = device.Channels.First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); +ai0.SampleReceived += (sender, e) => +{ + Console.WriteLine($"{e.Channel.Name}: {e.Sample.Value} (raw: {e.Sample.RawValue}, {e.Sample.Timestamp})"); +}; + +device.Send(ScpiMessageProducer.EnableAdcChannels("1")); // Enable channel 0 +device.Send(ScpiMessageProducer.StartStreaming(100)); // 100 Hz + +await Task.Delay(TimeSpan.FromSeconds(10)); + +device.Send(ScpiMessageProducer.StopStreaming); +``` + +`IDataSample.Value` is already scaled (volts for analog, 0/1 for digital). `RawValue` carries the raw +ADC count or bit when one exists (`null` for the USB pre-scaled float path), and `DeviceTimestamp` +carries the raw device tick count alongside the rollover-adjusted host `Timestamp`. Decoding only runs +while the device considers itself streaming; a stray frame that arrives outside a session is still +re-raised via `MessageReceived` but is not decoded into samples. + +#### Raw protobuf frames + ```csharp using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); diff --git a/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs b/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs index 86fbbb44..e972073f 100644 --- a/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs +++ b/src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs @@ -13,10 +13,10 @@ namespace Daqifi.Core.Communication.Producers; /// Example usage: /// /// // Send a command -/// messageProducer.Send(ScpiMessageProducer.Reboot); -/// +/// messageProducer.Send(ScpiMessageProducer.RebootDevice); +/// /// // Send a query -/// messageProducer.Send(ScpiMessageProducer.SystemInfo); +/// messageProducer.Send(ScpiMessageProducer.GetDeviceInfo); /// /// public class ScpiMessageProducer @@ -27,7 +27,7 @@ public class ScpiMessageProducer /// /// This command will cause the device to perform a complete restart. /// Command: SYSTem:REboot - /// Example: messageProducer.Send(ScpiMessageProducer.Reboot); + /// Example: messageProducer.Send(ScpiMessageProducer.RebootDevice); /// public static IOutboundMessage RebootDevice => new ScpiMessage("SYSTem:REboot"); @@ -37,7 +37,7 @@ public class ScpiMessageProducer /// /// Returns device information including firmware version, serial number, and capabilities. /// Command: SYSTem:SYSInfoPB? - /// Example: messageProducer.Send(ScpiMessageProducer.SystemInfo); + /// Example: messageProducer.Send(ScpiMessageProducer.GetDeviceInfo); /// public static IOutboundMessage GetDeviceInfo => new ScpiMessage("SYSTem:SYSInfoPB?"); diff --git a/src/Daqifi.Mcp/README.md b/src/Daqifi.Mcp/README.md index b3a47f0e..c97f39cf 100644 --- a/src/Daqifi.Mcp/README.md +++ b/src/Daqifi.Mcp/README.md @@ -28,7 +28,7 @@ The server speaks MCP over **stdio**, so the client launches it as a subprocess. | `stop_sd_logging` | Stop SD logging. | > SD logging is on-device: the device writes to its own SD card. Data does not stream back to the -> agent in this version (see the streaming-evolution plan). +> agent in this version. ## Run it From ea4c6bf5e92bc0e5759be4878b088c2eb17b560b Mon Sep 17 00:00:00 2001 From: Tyler Kron Date: Fri, 10 Jul 2026 17:18:32 -0600 Subject: [PATCH 2/2] docs: fix SampleReceived examples to actually enable/start via stateful API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qodo review on #291: the new SampleReceived examples subscribed to the event but drove streaming/channel-enable through raw device.Send(...) SCPI calls, which never set the local IsStreaming/IsEnabled state the decode pipeline gates on — so SampleReceived would never have fired as written. Cast to DaqifiStreamingDevice and use EnableChannel/ StreamingFrequency/StartStreaming instead, and switch to GetChannelsSnapshot() to avoid enumerating the live, concurrently mutable Channels list. Verified both snippets compile against the built library. Co-Authored-By: Claude Sonnet 5 --- README.md | 13 +++++++------ docs/DEVICE_INTERFACES.md | 27 ++++++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 00f543d8..8da403f6 100644 --- a/README.md +++ b/README.md @@ -33,19 +33,20 @@ dotnet add package Daqifi.Core ```csharp using Daqifi.Core.Device; -using Daqifi.Core.Communication.Producers; using Daqifi.Core.Channel; -// Connect — transport and device initialization handled for you -using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); +// Connect — transport and device initialization handled for you. The factory returns the base +// DaqifiDevice type, but the constructed instance is always a DaqifiStreamingDevice. +using var device = (DaqifiStreamingDevice)await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); // Subscribe to decoded, per-channel samples -var ai0 = device.Channels.First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); +var ai0 = device.GetChannelsSnapshot().First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); ai0.SampleReceived += (_, e) => Console.WriteLine($"{e.Sample.Timestamp}: {e.Sample.Value} V"); // Enable channel 0, then stream at 100 Hz -device.Send(ScpiMessageProducer.EnableAdcChannels("1")); -device.Send(ScpiMessageProducer.StartStreaming(100)); +device.EnableChannel(ai0); +device.StreamingFrequency = 100; +device.StartStreaming(); ``` A real, working program — no GUI required. Prefer the raw protobuf frame instead? Subscribe to diff --git a/docs/DEVICE_INTERFACES.md b/docs/DEVICE_INTERFACES.md index e0819553..d30e2a12 100644 --- a/docs/DEVICE_INTERFACES.md +++ b/docs/DEVICE_INTERFACES.md @@ -278,32 +278,41 @@ or bridging into another pipeline). #### Per-channel samples (recommended) While a stream is active, `DaqifiStreamingDevice` decodes every frame and raises `SampleReceived` on -each enabled channel — no protobuf field names or ADC bitmasks to interpret client-side. +each enabled channel — no protobuf field names or ADC bitmasks to interpret client-side. Decoding is +gated on the device's own `IsStreaming` flag and each channel's `IsEnabled` flag, so this only fires +when streaming is started via `StartStreaming()`/channels are enabled via `EnableChannel(s)` — sending +the equivalent raw SCPI commands directly (as in the raw-frame example below) drives the hardware but +never sets that local state, so `SampleReceived` would not fire. ```csharp using Daqifi.Core.Channel; +using Daqifi.Core.Device; -using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); +// DaqifiDeviceFactory methods return the base DaqifiDevice type, but the constructed instance is +// always a DaqifiStreamingDevice — cast (or pattern-match with `is`) to reach its streaming API. +using var device = (DaqifiStreamingDevice)await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760); -var ai0 = device.Channels.First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); +var ai0 = device.GetChannelsSnapshot().First(c => c.Type == ChannelType.Analog && c.ChannelNumber == 0); ai0.SampleReceived += (sender, e) => { Console.WriteLine($"{e.Channel.Name}: {e.Sample.Value} (raw: {e.Sample.RawValue}, {e.Sample.Timestamp})"); }; -device.Send(ScpiMessageProducer.EnableAdcChannels("1")); // Enable channel 0 -device.Send(ScpiMessageProducer.StartStreaming(100)); // 100 Hz +device.EnableChannel(ai0); +device.StreamingFrequency = 100; // Hz +device.StartStreaming(); await Task.Delay(TimeSpan.FromSeconds(10)); -device.Send(ScpiMessageProducer.StopStreaming); +device.StopStreaming(); ``` `IDataSample.Value` is already scaled (volts for analog, 0/1 for digital). `RawValue` carries the raw ADC count or bit when one exists (`null` for the USB pre-scaled float path), and `DeviceTimestamp` -carries the raw device tick count alongside the rollover-adjusted host `Timestamp`. Decoding only runs -while the device considers itself streaming; a stray frame that arrives outside a session is still -re-raised via `MessageReceived` but is not decoded into samples. +carries the raw device tick count alongside the rollover-adjusted host `Timestamp`. A stray frame that +arrives outside a streaming session is still re-raised via `MessageReceived` but is not decoded into +samples. `GetChannelsSnapshot()` is used above (rather than the live `Channels` property) because the +channel list can be repopulated concurrently when a new device status message arrives. #### Raw protobuf frames