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