Repository navigation
feat: SerialDeviceFinder probes ports to identify DAQiFi devices - #98
Conversation
Add ConnectSerialAsync and ConnectSerial methods for serial port connections: - ConnectSerialAsync(portName, options?, cancellationToken) - default 115200 baud - ConnectSerialAsync(portName, baudRate, options?, cancellationToken) - custom baud - ConnectSerial(portName, options?) - sync version with default baud - ConnectSerial(portName, baudRate, options?) - sync version with custom baud Update ConnectFromDeviceInfoAsync to handle ConnectionType.Serial by calling the new serial connection methods. Refactor ConnectWithTransportAsync to accept IStreamTransport instead of TcpStreamTransport for transport-agnostic connection logic. Closes #95 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
DAQiFi firmware initializes USB CDC with 9600 baud rate: firmware/src/services/UsbCdc/UsbCdc.c:776 Updated defaults in: - SerialDefaults.BaudRate - SerialStreamTransport constructor - SerialDeviceFinder.DefaultBaudRate Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously SerialDeviceFinder just listed available serial ports without verifying if they were actually DAQiFi devices. Now it probes each port: - Opens serial port with DTR enabled - Sends initialization commands (DisableEcho, StopStreaming, TurnDeviceOn, SetProtobufStreamFormat) - Sends GetDeviceInfo command with retry logic (up to 3 retries) - Waits for protobuf status message response - Extracts device info (serial number, firmware version, part number) - Returns null for non-DAQiFi ports Also adds port filtering to improve discovery: - Excludes debug, Bluetooth, and wlan ports - On macOS, prefers /dev/cu.* over /dev/tty.* ports Closes #97 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates the --discover-serial option to use Core's enhanced SerialDeviceFinder which now probes ports to identify actual DAQiFi devices. Changes: - Renamed DiscoverSerialPorts() to DiscoverSerialDevicesAsync() - Uses SerialDeviceFinder.DiscoverAsync() instead of just listing ports - Shows device info (name, serial, firmware) for discovered devices - Falls back to listing raw ports if no DAQiFi devices found - Shows real-time discovery progress via DeviceDiscovered event Depends on: daqifi/daqifi-core#98 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||||||||||||
Resolved conflict in SerialDeviceFinder.cs by keeping the new probing constants and removing the unused QuickProbeTimeout.
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||||||||
- Fix XML docs to show correct default baud rate (9600, not 115200) - Use ArgumentException for empty/whitespace port names (not ArgumentNullException) - Use RuntimeInformation.IsOSPlatform for reliable OS detection - Update tests to match new exception types and baud rate Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… surface unreadable tool trees Qodo round 1 on #423. 1. Unused logger in WincFlashReader - removed the field and the ctor parameter. Qodo's stated mechanism was wrong: it claimed CS0414 under TreatWarningsAsErrors makes this a merge-blocking compile failure. It does not. A clean rebuild (obj/bin removed) reports 0 warnings, 0 errors, and a verbose build contains zero CS0414 occurrences - CS0414 does not fire for a field assigned a non-constant expression. The field was still genuinely dead, so it is gone on its own merit, not because the build was broken. 2. Cancellation and the uncancellable SerialPort.Open hang - the real one. Open() takes no token and can block indefinitely on a wedged or half-enumerated USB CDC device, so it now runs under a hard deadline (default 5s) and is abandoned on timeout or cancel. An abandoned open is still running on a pool thread and still owns the handle, so disposal is handed to a continuation instead of being done underneath it - disposing under a blocked open turns a hang into a crash. An ownership flag keeps the caller from double-disposing. Cancellation is also now observed between bridge exchanges and during the baud settle wait (was Thread.Sleep). 3. Flash address wrap - ReadFlash added a uint offset to an int and cast back to uint with no validation, so a large span wrapped past 32 bits and silently read the wrong part of flash, returning plausible data with no error. Now bounds-checked in 64-bit against the 24-bit address the fast-read command can actually carry. 4. Tool lookup masking IO errors - TryResolveToolPath no longer swallows IOException/UnauthorizedAccessException. A tree that exists but cannot be read was being reported as "could not locate the tool - WiFi flashing is Windows-only", which is actively misleading when the tool is sitting there behind a permissions problem. IsAvailable stays total (a probe answers yes/no) and catches for itself. No logging added in either catch, per #98. Five tests added, covering the open deadline (including that the abandoned open is not disposed underneath), cancellation during a hanging open, address-wrap rejection, the exact-last-address boundary, and the unreadable-tree split between TryResolveToolPath and IsAvailable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e genuinely total Qodo round 2 on #423. Both findings are the tail of round 1's own fixes. 1. Unobserved open task fault. The abandonment path added last round left the Task.Run(Open) with nobody watching it, so a later Open() failure was a silently swallowed background error - exactly what #377/#394 set out to eliminate. The continuation now reads completed.Exception (which marks it observed) and surfaces it at Debug. Debug rather than Warning because the caller already received a TimeoutException or a cancellation; this is diagnostic context, not a second failure to act on. Note this is not the logging-in-catch pattern rejected in #98 - it is a continuation observing a faulted task, and the log sits outside the dispose try/catch. 2. IsAvailable was documented "total by design" but only caught the IO family, so an ArgumentException from a malformed path could escape a probe whose whole purpose is to be safe to call with anything. Made the implementation match the contract rather than weakening the doc: the catch is now broad and the comment says why, and the returns tag spells out every case that yields false. Five tests added. The unobserved-fault test is mutation-verified: removing the completed.Exception read makes it fail, so it is not a no-op. It hooks TaskScheduler.UnobservedTaskException, forces a GC after the abandoned open has faulted, and also asserts the continuation still disposed the port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… surface unreadable tool trees Qodo round 1 on #423. 1. Unused logger in WincFlashReader - removed the field and the ctor parameter. Qodo's stated mechanism was wrong: it claimed CS0414 under TreatWarningsAsErrors makes this a merge-blocking compile failure. It does not. A clean rebuild (obj/bin removed) reports 0 warnings, 0 errors, and a verbose build contains zero CS0414 occurrences - CS0414 does not fire for a field assigned a non-constant expression. The field was still genuinely dead, so it is gone on its own merit, not because the build was broken. 2. Cancellation and the uncancellable SerialPort.Open hang - the real one. Open() takes no token and can block indefinitely on a wedged or half-enumerated USB CDC device, so it now runs under a hard deadline (default 5s) and is abandoned on timeout or cancel. An abandoned open is still running on a pool thread and still owns the handle, so disposal is handed to a continuation instead of being done underneath it - disposing under a blocked open turns a hang into a crash. An ownership flag keeps the caller from double-disposing. Cancellation is also now observed between bridge exchanges and during the baud settle wait (was Thread.Sleep). 3. Flash address wrap - ReadFlash added a uint offset to an int and cast back to uint with no validation, so a large span wrapped past 32 bits and silently read the wrong part of flash, returning plausible data with no error. Now bounds-checked in 64-bit against the 24-bit address the fast-read command can actually carry. 4. Tool lookup masking IO errors - TryResolveToolPath no longer swallows IOException/UnauthorizedAccessException. A tree that exists but cannot be read was being reported as "could not locate the tool - WiFi flashing is Windows-only", which is actively misleading when the tool is sitting there behind a permissions problem. IsAvailable stays total (a probe answers yes/no) and catches for itself. No logging added in either catch, per #98. Five tests added, covering the open deadline (including that the abandoned open is not disposed underneath), cancellation during a hanging open, address-wrap rejection, the exact-last-address boundary, and the unreadable-tree split between TryResolveToolPath and IsAvailable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e genuinely total Qodo round 2 on #423. Both findings are the tail of round 1's own fixes. 1. Unobserved open task fault. The abandonment path added last round left the Task.Run(Open) with nobody watching it, so a later Open() failure was a silently swallowed background error - exactly what #377/#394 set out to eliminate. The continuation now reads completed.Exception (which marks it observed) and surfaces it at Debug. Debug rather than Warning because the caller already received a TimeoutException or a cancellation; this is diagnostic context, not a second failure to act on. Note this is not the logging-in-catch pattern rejected in #98 - it is a continuation observing a faulted task, and the log sits outside the dispose try/catch. 2. IsAvailable was documented "total by design" but only caught the IO family, so an ArgumentException from a malformed path could escape a probe whose whole purpose is to be safe to call with anything. Made the implementation match the contract rather than weakening the doc: the catch is now broad and the comment says why, and the returns tag spells out every case that yields false. Five tests added. The unobserved-fault test is mutation-verified: removing the completed.Exception read makes it fail, so it is not a no-op. It hooks TaskScheduler.UnobservedTaskException, forces a GC after the abandoned open has faulted, and also asserts the continuation still disposed the port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review follow-up on #763. No behaviour change. The twelve deleted helpers carried the reasoning for the containment, not just the containment itself, and some of it had no home in the shared type: - Both transports now say why the trace line is composed inside a lambda. Without that, `SafeTrace(() => $"...")` reads as idle indirection and the obvious "simplification" back to `SafeTrace($"...")` silently reopens #494 -- a subscriber exception whose ToString() throws escaping the catch that was containing it. - DiagnosticGuard's remarks state the contract the empty catch depends on: purely diagnostic work only, nothing the device's correctness rests on, and no logging from inside the catch (#98). SafeTrace_WritesWithoutThrowing only asserted that nothing was thrown, which is equally true of a guard that quietly writes nothing. Replaced with two tests that capture the trace and assert the line actually lands, through the parallel-safe synchronized-listener pattern DropPathSubscriberIsolationTests established. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
User description
Summary
Previously
SerialDeviceFinderjust listed available serial ports without verifying if they were actually DAQiFi devices. Now it probes each port to identify genuine DAQiFi devices.Changes
Port Probing Logic
GetDeviceInfocommand with retry logic (up to 3 retries, 1 second apart)nullfor ports that don't respond as DAQiFi devicesPort Filtering
Adds
FilterProbableDaqifiPorts()to improve discovery efficiency:/dev/cu.*over/dev/tty.*ports (cu is for outgoing connections)Device Info Extraction
From the status message, extracts:
DeviceTypeDetector.DetectFromPartNumber()Test Plan
SerialDeviceFindertests pass/dev/cu.usbmodem101:Closes #97
🤖 Generated with Claude Code
PR Type
Enhancement, Bug fix
Description
Add serial port connection support to DaqifiDeviceFactory with ConnectSerialAsync and ConnectSerial methods
Implement port probing in SerialDeviceFinder to identify genuine DAQiFi devices via SCPI commands
Change default serial baud rate from 115200 to 9600 to match DAQiFi firmware initialization
Add port filtering to exclude debug, Bluetooth, and wlan ports; prefer /dev/cu.* on macOS
Diagram Walkthrough
File Walkthrough
DaqifiDeviceFactoryTests.cs
Add serial connection factory testssrc/Daqifi.Core.Tests/Device/DaqifiDeviceFactoryTests.cs
null/empty/whitespace port names and invalid baud rates
ArgumentException instead of NotSupportedException
support
SerialStreamTransport.cs
Update default serial baud rate to 9600src/Daqifi.Core/Communication/Transport/SerialStreamTransport.cs
DaqifiDeviceFactory.cs
Add serial connection factory methodssrc/Daqifi.Core/Device/DaqifiDeviceFactory.cs
with full validation
device connections from device info
instead of throwing NotSupportedException
of TcpStreamTransport for transport-agnostic logic
SerialDeviceFinder.cs
Implement serial port probing and device discoverysrc/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs
initialization and protobuf response handling
1-second intervals
ports and prefer /dev/cu.* on macOS
number) from status message responses
finally block
Discovery namespace enums