Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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 |
Expand Down
68 changes: 67 additions & 1 deletion docs/DEVICE_INTERFACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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();
```
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

`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);

Expand Down
10 changes: 5 additions & 5 deletions src/Daqifi.Core/Communication/Producers/ScpiMessageProducer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ namespace Daqifi.Core.Communication.Producers;
/// Example usage:
/// <code>
/// // Send a command
/// messageProducer.Send(ScpiMessageProducer.Reboot);
///
/// messageProducer.Send(ScpiMessageProducer.RebootDevice);
///
/// // Send a query
/// messageProducer.Send(ScpiMessageProducer.SystemInfo);
/// messageProducer.Send(ScpiMessageProducer.GetDeviceInfo);
/// </code>
/// </remarks>
public class ScpiMessageProducer
Expand All @@ -27,7 +27,7 @@ public class ScpiMessageProducer
/// <remarks>
/// This command will cause the device to perform a complete restart.
/// Command: SYSTem:REboot
/// Example: messageProducer.Send(ScpiMessageProducer.Reboot);
/// Example: messageProducer.Send(ScpiMessageProducer.RebootDevice);
/// </remarks>
public static IOutboundMessage<string> RebootDevice => new ScpiMessage("SYSTem:REboot");

Expand All @@ -37,7 +37,7 @@ public class ScpiMessageProducer
/// <remarks>
/// Returns device information including firmware version, serial number, and capabilities.
/// Command: SYSTem:SYSInfoPB?
/// Example: messageProducer.Send(ScpiMessageProducer.SystemInfo);
/// Example: messageProducer.Send(ScpiMessageProducer.GetDeviceInfo);
/// </remarks>
public static IOutboundMessage<string> GetDeviceInfo => new ScpiMessage("SYSTem:SYSInfoPB?");

Expand Down
2 changes: 1 addition & 1 deletion src/Daqifi.Mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down