From 8bf37152b0566fa0e544cbb55d2d125091303cd5 Mon Sep 17 00:00:00 2001 From: Tyler Kron Date: Thu, 14 May 2026 16:41:57 -0600 Subject: [PATCH] feat: add --via-discovery for canonical "find then connect" flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new --via-discovery flag that routes the streaming session through WiFiDeviceFinder.DiscoverAsync + DaqifiDeviceFactory.ConnectFromDeviceInfoAsync instead of the direct ConnectTcpAsync call. This exercises the public API surface that real applications should prefer for WiFi devices, and is the only path that honors IDeviceInfo.LocalInterfaceAddress — required on multi-homed hosts so the outbound socket binds to the NIC that received the discovery reply (see daqifi-core PR #187). UX: - --via-discovery alone connects to the first discovered device. - --via-discovery + --ip filters discovery to that address. - --via-discovery + --serial is rejected (WiFi-only). Existing --ip and --serial paths are unchanged, so this is purely additive. Co-Authored-By: Claude Opus 4.7 (1M context) --- Program.cs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 11 +++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/Program.cs b/Program.cs index 94dbed8..d459103 100644 --- a/Program.cs +++ b/Program.cs @@ -69,18 +69,18 @@ private static async Task Main(string[] args) return await RunFirmwareDownloadByTagAsync(options); } - // Check if we have a connection target (IP or serial) + // Check if we have a connection target (IP, serial, or via-discovery) var hasIpTarget = !string.IsNullOrWhiteSpace(options.IpAddress); var hasSerialTarget = !string.IsNullOrWhiteSpace(options.SerialPort); - if (!hasIpTarget && !hasSerialTarget) + if (!hasIpTarget && !hasSerialTarget && !options.ViaDiscovery) { if (options.Discover || options.DiscoverSerial) { return 0; } - Console.Error.WriteLine("Missing required option: --ip or --serial"); + Console.Error.WriteLine("Missing required option: --ip, --serial, or --via-discovery"); Console.Error.WriteLine("Use --help to see available options."); return 1; } @@ -91,6 +91,12 @@ private static async Task Main(string[] args) return 1; } + if (options.ViaDiscovery && hasSerialTarget) + { + Console.Error.WriteLine("--via-discovery is only valid for WiFi devices and cannot be combined with --serial."); + return 1; + } + if (hasIpTarget) { var ipAddress = options.IpAddress!.Trim(); @@ -157,6 +163,15 @@ private static async Task RunStreamingSessionAsync(CliOptions options) connectionOptions); connectionDescription = $"{options.SerialPort} @ {options.BaudRate} baud"; } + else if (options.ViaDiscovery) + { + var (discoveredDevice, deviceInfo) = await ConnectViaDiscoveryAsync(options, connectionOptions); + device = discoveredDevice; + var ifaceStr = deviceInfo.LocalInterfaceAddress?.ToString() ?? "(none — OS-chosen)"; + connectionDescription = + $"{deviceInfo.Name} at {deviceInfo.IPAddress}:{deviceInfo.Port} via discovery, " + + $"bound to local interface {ifaceStr}"; + } else { device = await DaqifiDeviceFactory.ConnectTcpAsync( @@ -324,6 +339,64 @@ private static async Task RunStreamingSessionAsync(CliOptions options) } } + private static async Task<(DaqifiDevice Device, IDeviceInfo DeviceInfo)> ConnectViaDiscoveryAsync( + CliOptions options, + DeviceConnectionOptions connectionOptions) + { + IPAddress? targetIp = null; + if (!string.IsNullOrWhiteSpace(options.IpAddress)) + { + if (!IPAddress.TryParse(options.IpAddress, out targetIp)) + { + throw new InvalidOperationException($"Invalid --ip value: {options.IpAddress}"); + } + } + + var filterDescription = targetIp != null ? $" matching {targetIp}" : string.Empty; + Console.WriteLine($"Discovering WiFi devices{filterDescription} (timeout {options.DiscoveryTimeoutSeconds}s)..."); + + using var finder = new WiFiDeviceFinder(); + var timeout = TimeSpan.FromSeconds(options.DiscoveryTimeoutSeconds <= 0 ? 5 : options.DiscoveryTimeoutSeconds); + var discovered = (await finder.DiscoverAsync(timeout)).ToList(); + + IDeviceInfo? match; + if (targetIp != null) + { + match = discovered.FirstOrDefault(d => Equals(d.IPAddress, targetIp)); + if (match == null) + { + var seen = string.Join(", ", discovered.Select(d => d.IPAddress?.ToString() ?? "?")); + throw new InvalidOperationException( + $"Target device {targetIp} was not discovered. Discovered: [{seen}]"); + } + } + else + { + if (discovered.Count == 0) + { + throw new InvalidOperationException( + "No WiFi devices discovered. Pass --ip to filter to a specific device or increase --discover-timeout."); + } + + if (discovered.Count > 1) + { + var summary = string.Join(", ", discovered.Select(d => $"{d.Name}@{d.IPAddress}")); + Console.WriteLine( + $"Discovered {discovered.Count} devices ({summary}); connecting to the first. " + + "Pass --ip to choose a specific device."); + } + + match = discovered[0]; + } + + Console.WriteLine( + $"Discovered {match.Name} at {match.IPAddress}:{match.Port} " + + $"(LocalInterfaceAddress={match.LocalInterfaceAddress?.ToString() ?? "null"})"); + + var device = await DaqifiDeviceFactory.ConnectFromDeviceInfoAsync(match, connectionOptions); + return (device, match); + } + private static async Task RunFirmwareUpdateAsync( CliOptions options, string? firmwareHexPathOverride = null) @@ -1390,12 +1463,19 @@ private static void PrintHelp() Console.WriteLine("Usage:"); Console.WriteLine(" dotnet run -- --ip
[options]"); Console.WriteLine(" dotnet run -- --serial [options]"); + Console.WriteLine(" dotnet run -- --via-discovery [--ip
] [options]"); Console.WriteLine(); Console.WriteLine("Connection Options:"); Console.WriteLine(" --ip
Device IP address (for TCP/WiFi connection)."); Console.WriteLine($" --port TCP port (default: {DefaultPort})."); Console.WriteLine(" --serial Serial port name (e.g., COM3, /dev/ttyUSB0, /dev/cu.usbmodem101)."); Console.WriteLine($" --baud Baud rate for serial connection (default: {DefaultBaudRate})."); + Console.WriteLine(" --via-discovery Discover WiFi devices over UDP, then connect via"); + Console.WriteLine(" DaqifiDeviceFactory.ConnectFromDeviceInfoAsync. This"); + Console.WriteLine(" binds the outbound socket to the NIC that received the"); + Console.WriteLine(" discovery reply (required on multi-homed hosts). If --ip"); + Console.WriteLine(" is set, filter discovery to that address; otherwise"); + Console.WriteLine(" connect to the first discovered device."); Console.WriteLine(); Console.WriteLine("Discovery Options:"); Console.WriteLine(" -d, --discover Discover WiFi devices over UDP."); @@ -1502,6 +1582,7 @@ private sealed class CliOptions public string? FirmwareHexPath { get; private set; } public string? FirmwareUpdateLatestDirectory { get; private set; } public bool LanChipInfo { get; private set; } + public bool ViaDiscovery { get; private set; } public List Errors { get; } = new(); public static CliOptions Parse(string[] args) @@ -1620,6 +1701,9 @@ public static CliOptions Parse(string[] args) case "--lan-chip-info": options.LanChipInfo = true; break; + case "--via-discovery": + options.ViaDiscovery = true; + break; case "-h": case "--help": options.ShowHelp = true; diff --git a/README.md b/README.md index 6d0537a..c8210e2 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,22 @@ dotnet run -- \ --duration 10 ``` +Discover and stream (canonical "find then connect" pattern; required on multi-homed hosts): + +```bash +dotnet run -- \ + --via-discovery \ + --rate 10 \ + --channels 0000000011 \ + --duration 10 +``` + ## Options - `--discover` discover devices over UDP - `--ip
` device IP address - `--port ` TCP port (default 9760) +- `--via-discovery` discover first, then connect via `DaqifiDeviceFactory.ConnectFromDeviceInfoAsync`. Required on multi-homed hosts so the outbound socket binds to the NIC that received the discovery reply. With `--ip`, filters to that address; without `--ip`, connects to the first discovered device. - `--rate ` sampling rate in Hz (default 100) - `--duration ` streaming duration (default 10) - `--channels ` ADC channel enable mask (0/1 string)