diff --git a/README.md b/README.md
index 51ee730d..6a9dce31 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
@@ -33,24 +33,24 @@ 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 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.GetChannelsSnapshot().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"));
-device.Send(ScpiMessageProducer.StartStreaming(100));
+// Enable channel 0, then stream at 100 Hz
+device.EnableChannel(ai0);
+device.StreamingFrequency = 100;
+device.StartStreaming();
```
-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 +71,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 +80,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 46a17827..28c0460b 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
@@ -284,6 +305,51 @@ 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. 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;
+
+// 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.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.EnableChannel(ai0);
+device.StreamingFrequency = 100; // Hz
+device.StartStreaming();
+
+await Task.Delay(TimeSpan.FromSeconds(10));
+
+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`. 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
+
```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