Revolutionizing the data collection experience with convenient, portable device connectivity.
The official cross-platform .NET SDK for DAQiFi wireless data acquisition devices.
daqifi.com · DAQiFi Desktop · Report an issue
DAQiFi builds wireless data acquisition hardware designed to get out of the way so you can focus on the data, not the collection process.
DAQiFi Core is how you integrate that hardware into your own .NET applications — custom dashboards, automated test rigs, research pipelines, production-monitoring tools. Discover devices, connect over WiFi or USB, stream samples in real time, configure networks, push firmware updates — all from one async, strongly-typed .NET API.
Prefer a ready-made GUI? Check out 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 — point Claude, Cursor, Codex, or any MCP-aware client at it to discover, configure channels, drive digital I/O, PWM and analog outputs, set the sample rate, and run SD-card logging — then list, download, and CSV the recorded data back — through plain conversation.
dotnet add package Daqifi.Coreusing Daqifi.Core.Device;
using Daqifi.Core.Channel;
// Connect — transport and device initialization handled for you.
await using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760);
// 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 channel 0, then stream at 100 Hz
device.EnableChannel(ai0);
device.StreamingFrequency = 100;
device.StartStreaming();A real, working program — no GUI required. Prefer the raw protobuf frame instead? Subscribe to
device.MessageReceived — see Streaming Data.
DAQiFi hardware is in the field for work like:
- Research labs — moon regolith testing and similar materials studies
- Medical R&D — prosthetic socket pressure testing
- Industrial monitoring — wireless multi-channel sensing
- Engineering education — SCPI command structure and LabVIEW compatibility
- Test automation — scripted benchtop measurements
More examples at daqifi.com.
| Layer | What it is |
|---|---|
| Hardware | Nyquist 1 / Nyquist 3 — wireless DAQ devices (and their on-device firmware) |
| SDK | DAQiFi Core — this library |
| App | DAQiFi Desktop — GUI built on this SDK |
| Agent | MCP server — drive a device from Claude / Cursor / any MCP client: discover, configure channels, DIO/PWM/analog output, SD logging, and SD data retrieval |
| Your code | Custom apps, dashboards, pipelines, test rigs |
| Capability | What it gives you |
|---|---|
| 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 | Per-channel IChannel.SampleReceived events with decoded, scaled values — or subscribe to the raw protobuf frame directly; no polling loops to write |
| Acquisition health | Attach AcquisitionStatistics to a stream and read back the rate you are really getting, per-channel jitter, value range, and how far behind the device's clock the host is |
| Record to CSV | device.RecordLiveSamplesToCsvAsync(writer) writes a live stream to CSV as it arrives — no buffering the session in memory — and reports what reached the file and what was dropped |
| 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 |
| Network configuration | Push WiFi credentials and static LAN IPs from your app |
| Firmware updates | PIC32 and WiFi-module flashing with progress, cancellation, and automatic recovery to a clean re-flashable bootloader state on mid-flash failure |
| Cross-platform | .NET 9.0 and 10.0 on Windows, macOS, Linux |
Pick whichever transport fits your setup — each snippet is a standalone, copy-paste-ready starting point.
TCP with a resilient retry preset (5 retries, longer timeouts):
await using var device = await DaqifiDeviceFactory.ConnectTcpAsync(
"192.168.1.100", 9760, DeviceConnectionOptions.Resilient);Serial / USB:
// Replace with your OS-specific port:
// Windows: "COM3" • macOS: "/dev/cu.usbmodem1" • Linux: "/dev/ttyACM0"
await using var device = await DaqifiDeviceFactory.ConnectSerialAsync("COM3");From a discovered device:
using var finder = new WiFiDeviceFinder();
var devices = await finder.DiscoverAsync(TimeSpan.FromSeconds(5));
await using var device = await DaqifiDeviceFactory.ConnectFromDeviceInfoAsync(devices.First());using Daqifi.Core.Communication.Transport;
var options = new DeviceConnectionOptions
{
DeviceName = "My DAQiFi",
ConnectionRetry = new ConnectionRetryOptions
{
MaxAttempts = 3,
ConnectionTimeout = TimeSpan.FromSeconds(10)
},
InitializeDevice = true
};
await using var device = await DaqifiDeviceFactory.ConnectTcpAsync("192.168.1.100", 9760, options);Connecting takes control of the device. A DAQiFi unit has a single global acquisition, and the default connect sequence stops it — so connecting to a device another session is already streaming silently ends that session's data. Use
DeviceConnectionOptions.Observingfor a secondary session that only needs to look, andDaqifiDeviceRegistryto avoid opening the same unit twice in one process. See Connecting stops any stream already running.
using Daqifi.Core.Device.Discovery;
// WiFi — UDP broadcast on port 30303 by default
using var wifiFinder = new WiFiDeviceFinder();
wifiFinder.DeviceDiscovered += (_, e) =>
Console.WriteLine($"Found: {e.DeviceInfo.Name} at {e.DeviceInfo.IPAddress}");
var wifiDevices = await wifiFinder.DiscoverAsync(TimeSpan.FromSeconds(5));
// USB / Serial
using var serialFinder = new SerialDeviceFinder();
var serialDevices = await serialFinder.DiscoverAsync();Need fine-grained control? Pass a CancellationToken or override the discovery port:
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(10));
var devices = await wifiFinder.DiscoverAsync(cts.Token);
using var customFinder = new WiFiDeviceFinder(discoveryPort: 12345);"Am I actually getting 1 kHz?" — attach an AcquisitionStatistics for the duration of a stream and
read a snapshot whenever you want the answer. It observes the same per-channel sample events
streaming already raises, so nothing changes for consumers that do not attach one.
using Daqifi.Core.Device;
using var stats = new AcquisitionStatistics(device);
device.StreamingFrequency = 1000;
device.StartStreaming();
await Task.Delay(TimeSpan.FromSeconds(5));
device.StopStreaming();
var snapshot = stats.Snapshot();
foreach (var channel in snapshot.Channels)
{
Console.WriteLine(
$"{channel.Name}: {channel.SampleCount} samples, " +
$"{channel.MeasuredSampleRateHz:F1} Hz measured vs {channel.DeviceClockSampleRateHz:F1} Hz by the device clock, " +
$"{channel.MinValue:F3}..{channel.MaxValue:F3} V, worst gap {channel.MaxSampleInterval.TotalMilliseconds:F2} ms");
}The two rates are reported side by side on purpose. Both dropping below the commanded rate means
samples went missing; the two disagreeing means the device's own clock is not keeping real time, and
it is MeasuredSampleRateHz that describes what your application actually received. Reset() starts
a fresh window mid-session, and stats.Record(sample) feeds one by hand from StreamSamplesAsync
instead of attaching.
Streaming and exporting used to be two halves with nothing between them. RecordLiveSamplesToCsvAsync
joins them: it writes rows through CsvExporter as frames decode, so the recording's memory does not
grow with its length, and it hands back what reached the file and what did not.
using Daqifi.Core.Logging.Export;
device.StreamingFrequency = 100;
device.StartStreaming();
await using var writer = new StreamWriter("run.csv");
var result = await device.RecordLiveSamplesToCsvAsync(writer, duration: TimeSpan.FromSeconds(30));
device.StopStreaming();
Console.WriteLine($"{result.RowCount} rows from {result.SampleCount} samples");
if (result.DroppedSampleCount > 0)
{
Console.WriteLine($"{result.DroppedSampleCount} samples dropped — raise bufferCapacity or lower the rate");
}The columns are the channels that were enabled when the call started, in device order. duration
elapsing is a clean finish — the last frame is written and the result comes back; cancelling the
CancellationToken is an abort and throws, so a recording cut short is never mistaken for a complete
one. Need the rows somewhere other than a TextWriter? Build a LiveSampleSource over
StreamSamplesAsync and hand it to CsvExporter (or any other ISampleSource consumer) yourself.
Digital channels default to inputs. Flip one to output and drive it — the level is applied immediately, and flipping back to input releases the pin to high-impedance.
using Daqifi.Core.Channel;
var channels = device.GetChannelsSnapshot();
var dio3 = channels.First(c => c.Type == ChannelType.Digital && c.ChannelNumber == 3);
device.SetDioDirection(dio3, ChannelDirection.Output);
device.SetDioValue(dio3, true); // drive high
device.SetDioValue(dio3, false); // drive low
device.SetDioDirection(dio3, ChannelDirection.Input); // back to a streamed inputEvery IStreamingDevice method above (and the rest of the channel/PWM/analog-output/reboot surface)
has a cancellable ...Async twin declared on the interface — see
IStreamingDevice for the full list.
PWM runs on capable DIO pins (IDigitalChannel.IsPwmCapable — channels 0, 3, 4, 5, 6 and 7 on
Nyquist hardware). Duty cycle is per channel; the frequency is shared by all PWM channels, since
one hardware timer drives them all.
using Daqifi.Core.Channel;
var pwm = device.GetChannelsSnapshot()
.OfType<IDigitalChannel>()
.First(c => c.IsPwmCapable);
device.SetPwmDutyCycle(pwm, 25); // 1-100 percent
device.SetPwmFrequency(1000); // 6-50000 Hz, applies to every PWM channel
device.SetPwmEnabled(pwm, true); // start
device.SetPwmDutyCycle(pwm, 75); // duty changes take effect live
device.SetPwmEnabled(pwm, false); // stop — the pin is left high-impedanceDaqifiStreamingDevice implements INetworkConfigurable for programmatic WiFi and LAN configuration. Mode, Ssid, and Password are always applied on every call; only StaticIP, SubnetMask, and Gateway honor null as "leave unchanged" — so DHCP-only callers can omit the static-IP fields without affecting their DHCP setup.
using System.Net;
using Daqifi.Core.Device.Network;
var config = new NetworkConfiguration
{
Ssid = "MyNetwork",
Password = "secret",
Mode = WifiMode.ExistingNetwork,
StaticIP = IPAddress.Parse("192.168.1.42"),
SubnetMask = IPAddress.Parse("255.255.255.0"),
Gateway = IPAddress.Parse("192.168.1.1"),
};
await device.UpdateNetworkConfigurationAsync(config);IFirmwareUpdateService orchestrates both PIC32 and WiFi-module flashing with explicit state transitions and IProgress<FirmwareUpdateProgress> for UI / CLI reporting.
UpdateFirmwareAsync(...)— PIC32 firmware flashing from a local Intel HEX fileUpdateWifiModuleAsync(...)— WiFi module flashing via an external tool runner. Automatically checks the device's current WiFi-chip firmware against the latest GitHub release and skips the flash if already up to date.
Safe failure cleanup (PIC32). If a PIC32 update fails — or is canceled — after flash has been written (ErasingFlash, Programming, or Verifying) and the HID bootloader is still connected, the service automatically re-erases the application flash so the device is never abandoned half-flashed: a half-flashed image would otherwise boot into garbage on the next power cycle, recoverable only by the physical button-hold procedure. The flow surfaces two extra states:
CleaningUp— the re-erase is running; progress percent stays frozen at the failure point (never 100) so a percent-only UI can't mistake cleanup for successRecovered— terminal: the update failed (the call still throws), but the device is in a clean bootloader state and safe to simply re-flash
FirmwareUpdateException.RecoveryGuidance tells the operator whether to just re-run the update (Recovered) or power-cycle into bootloader mode first (cleanup couldn't run — the device may be half-flashed). FirmwareUpdateException.FailedState always reports where the original failure occurred, independent of cleanup outcome.
Note: The default WiFi flash tool config uses
winc_flash_tool.cmdconventions. On macOS / Linux, supply a compatible executable and argument template viaFirmwareUpdateServiceOptions.
| Device | Channels | Resolution | Range |
|---|---|---|---|
| Nyquist 1 | 16 analog in | 12-bit | 0–5 V |
| Nyquist 3 | 8 analog in | 18-bit | ±10 V |
These are auto-detected by part number during discovery. The SDK also recognizes and
supports Nyquist 2 (Nq2 → DeviceType.Nyquist2); it's left out of the spec table
above rather than listed with fabricated headline numbers. For any connected device the
authoritative channel counts, resolution, and ranges are reported by the hardware and
surfaced on device.Metadata.Capabilities after initialization.
Don't have one yet? See the DAQiFi lineup →
- WiFi — discovered via UDP broadcast (port 30303)
- Serial — USB-connected, enumerated as serial ports
- HID — used during firmware updates (HidSharp backend)
- .NET 9.0 or .NET 10.0 on Windows, macOS, or Linux
- WiFi discovery: UDP port 30303 reachable (firewall may need configuring; admin may be required on Windows)
- Serial discovery: appropriate USB drivers for your platform
- Open an issue for bugs or feature requests
- Reach the team via daqifi.com for commercial integrations and custom hardware needs
This library follows semantic versioning. Releases are automated via GitHub Actions:
- Create a new GitHub Release
- Tag it
vX.Y.Z(pre-releases use-alpha.1,-beta.1,-rc.1suffixes) - Publishing to NuGet happens automatically on release
The same release also packs and publishes the Daqifi.Mcp MCP server as a .NET tool (dotnet tool install -g Daqifi.Mcp).
Semver here tracks source compatibility, not binary compatibility: appending a parameter to a public positional record (with a default) is not treated as a breaking change requiring a major bump, and is called out in release notes instead. Consumers who need binary compatibility across versions should recompile against each release rather than swap the DLL in place. See ADR 0002 for the reasoning.