perf(discovery): VID/PID-filter serial ports + minimal probe (closes #157)#201
perf(discovery): VID/PID-filter serial ports + minimal probe (closes #157)#201cptkoolbeenz wants to merge 15 commits into
Conversation
…157) Serial discovery used to take ~1 minute on systems with many COM ports because every port was opened and probed sequentially with a ~5.4s worst-case timeout, AND the probe path sent SCPI commands to ports belonging to other vendors (Bluetooth radios, GPS receivers, etc.). The desktop USB tab showed an indefinite spinner so users assumed it was broken and fell back to manual COM entry. Changes (in priority order from the ticket): 1. **Filter ports by USB VID/PID before opening anything** — adds an IUsbPortDescriptorProvider abstraction with platform-specific impls: - WindowsUsbPortDescriptorProvider via WMI (Win32_PnPEntity) - LinuxUsbPortDescriptorProvider via /sys/class/tty - NullUsbPortDescriptorProvider fallback for macOS / unknown platforms (preserves legacy probe-everything behavior) Ports whose descriptor doesn't match a known DAQiFi VID:PID (0x04D8:0xF794 for CDC mode) are skipped without ever being opened. System.Management is added as a Windows-only conditional package. 2. **Reduced probe to GetDeviceInfo only** — the previous DisableEcho / StopStreaming / TurnDeviceOn / SetProtobufStreamFormat sequence was for connection setup; identity probing only needs SYSTem:SYSInfoPB?. A healthy DAQiFi answers it regardless of stream format / power state. 3. **Tightened timeouts** — wake delay 1000→200ms, response timeout 4000→1000ms, retry interval 1000→300ms, max attempts 3→2. USB CDC is fast; the previous values were defensive against ports we no longer probe at all. Expected impact: discovery drops from ~1min → <1s on a typical Windows system (only DAQiFi ports get probed; each at ~200-500ms vs ~5.4s). The internal constructor takes a custom IUsbPortDescriptorProvider for tests; production callers use the existing public constructors. Test plan: 7 new tests cover (a) DaqifiUsbIds constants + classifier positive/negative cases, (b) NullUsbPortDescriptorProvider contract, (c) end-to-end DiscoverAsync filter — non-DAQiFi VID/PID returns 0 devices in <1s without probing, (d) null-descriptor fall-through preserves legacy behavior. 897/899 pass (2 hardware skips); build clean on net9.0 + net10.0. Closes #157
Review Summary by QodoOptimize serial discovery with USB VID/PID filtering and minimal probe
WalkthroughsDescription• Filter serial ports by USB VID/PID before probing, reducing discovery from ~1min to <1s • Implement platform-specific descriptor providers (Windows WMI, Linux sysfs, macOS fallback) • Simplify probe sequence to GetDeviceInfo only, removing unnecessary setup commands • Tighten timeouts and retry logic for faster USB CDC enumeration • Add comprehensive tests for VID/PID filtering and descriptor classification Diagramflowchart LR
A["Serial Ports"] --> B["Name Filter"]
B --> C["USB Descriptor Provider"]
C --> D{DAQiFi VID/PID?}
D -->|Yes| E["Probe GetDeviceInfo"]
D -->|No| F["Skip Port"]
D -->|Unknown| E
E --> G["Device Found"]
F --> H["Discovery Complete"]
G --> H
File Changes1. src/Daqifi.Core/Device/Discovery/DaqifiUsbIds.cs
|
Code Review by Qodo
Context used 1.
|
|
/improve |
|
/agentic_review |
Code Review by Qodo
1.
|
PR Code Suggestions ✨Latest suggestions up to 8b69f6b Warning
Previous suggestions✅ Suggestions up to commit 5f213f6
✅ Suggestions up to commit f0f5926
✅ Suggestions up to commit 4cc0f08
✅ Suggestions up to commit 1164d84
✅ Suggestions up to commit 43d82c2
✅ Suggestions up to commit 1f24638
✅ Suggestions up to commit d3d869a
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/sys/class/tty/<base>/device is a symlink into the actual USB device tree. Path.GetFullPath returned the logical path, so walking parents ended at /sys/class/tty/<base> instead of reaching the USB node where idVendor/idProduct live — every Linux lookup silently returned null. Use DirectoryInfo.ResolveLinkTarget(returnFinalTarget: true) to follow the symlink to the physical device path before traversal.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 6c7cd42 |
|
Persistent suggestions updated to latest commit 6c7cd42 |
The WQL query string is built by interpolation. SerialPort.GetPortNames() returns COM<n> on Windows, but a stray single quote in unexpected input would corrupt the LIKE clause. Reject anything that doesn't match ^COM\d+$ before sending the query.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit ba62343 |
|
Persistent suggestions updated to latest commit ba62343 |
Hex parsing happens to be culture-invariant in practice, but the defensive style fix passes CultureInfo.InvariantCulture explicitly so analyzers don't flag the call sites and behavior is independent of ambient culture.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 643a988 |
|
Persistent suggestions updated to latest commit 643a988 |
ManagementObjectSearcher.Get() returns a ManagementObjectCollection that owns native handles and must be disposed; without this the enumerator leaks every call. Also narrows the LIKE-based search to PNPClass='Ports' so WMI doesn't enumerate non-port PnP devices before filtering.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 97d0959 |
|
Persistent suggestions updated to latest commit 97d0959 |
The shipped IUsbPortDescriptorProvider implementations already swallow their own errors and return null, but a custom provider could throw — that must not abort the whole discovery pass. Catch unconditionally and fall through to legacy probing for the port. Adds a test that exercises a throwing provider end-to-end.
|
/agentic_review |
|
Persistent review updated to latest commit 43d82c2 |
|
Persistent suggestions updated to latest commit 43d82c2 |
…tion ProbeSafelyAsync was returning null on caller cancellation, swallowing the signal and forcing every other probe in the parallel set to run to completion. Rethrow OperationCanceledException when cancellationToken.IsCancellationRequested so Task.WhenAll short- circuits — matches the docstring contract and the upstream DiscoverAsync expectation.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 1164d84 |
|
Persistent suggestions updated to latest commit 1164d84 |
…n event Two coordinated changes: 1. SemaphoreSlim cap (MaxParallelProbes = 4) — caps concurrent SerialPort opens so a system with many unclassified ports doesn't exhaust file handles or hit IO failures. Common case (descriptor classifier identifies the DAQiFi port) leaves 0-1 candidates so the cap rarely engages. 2. Catch OCE around Task.WhenAll — ProbeSafelyAsync now rethrows OCE on caller cancellation (correct: lets WhenAll short-circuit the remaining probes). DiscoverAsync swallows that OCE, drains tasks that finished cleanly before cancellation, and still fires DiscoveryCompleted. Restores the historical contract that the completion event always signals "this discovery pass terminated" regardless of how it ended.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 4cc0f08 |
|
Persistent suggestions updated to latest commit 4cc0f08 |
Add Func<string[]> port-name test seam to SerialDeviceFinder so the DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery test deterministically exercises the FilterByUsbDescriptor catch path on hosts (CI containers) with zero real serial ports — previously the test could pass vacuously when SerialPort.GetPortNames() returned []. Also instrument RecordingUsbPortDescriptorProvider with a CallCount so the test can assert the throwing provider was actually invoked.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit f0f5926 |
|
Persistent /agentic_review findings — intentional design choices:
If we hardened to strict VID/PID-only, every macOS user and any Windows user with a non-standard PnP class would lose discovery entirely. That's a regression worse than the perf win. The "Null descriptor falls through" behavior is documented in code ( Test infrastructure gap (#5 "Throwing-provider test may no-op") is fixed in the latest commit by adding a |
|
Persistent suggestions updated to latest commit f0f5926 |
Replace "COM999" with "MOCK_PORT_DOES_NOT_EXIST" in the throwing-provider test so SerialPort.Open() fails immediately on every platform — COM999 can exist on Windows hosts with virtual serial drivers, which would risk unintended hardware interaction during test runs.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 5f213f6 |
|
Persistent suggestions updated to latest commit 5f213f6 |
Inject a fixed 3-port list into DiscoverAsync_WithNonDaqifiVidPid_DoesNotProbePort so the classifier filter path is exercised even on CI hosts with no real serial ports. Replaces the host-dependent classifierCallCount counter with the existing fakeProvider.CallCount and asserts == 3.
|
/improve |
|
/agentic_review |
|
Persistent review updated to latest commit 8b69f6b |
|
Persistent suggestions updated to latest commit 8b69f6b |
Summary
Drops serial discovery from ~1 minute to <1 second on systems with many COM ports, AND fixes the correctness issue of sending SCPI commands to other vendors' devices (Bluetooth radios, GPS receivers, etc.).
Changes (issue priority order)
VID/PID port filtering before opening — new
IUsbPortDescriptorProviderabstraction with platform-specific impls:WindowsUsbPortDescriptorProvidervia WMIWin32_PnPEntityLinuxUsbPortDescriptorProvidervia/sys/class/ttyNullUsbPortDescriptorProviderfallback (preserves legacy probe-everything behavior)Ports whose descriptor doesn't match a known DAQiFi VID:PID (
0x04D8:0xF794for CDC mode) are skipped without ever being opened.System.Managementadded as a Windows-only conditional package.Reduced probe to
GetDeviceInfoonly — the previousDisableEcho/StopStreaming/TurnDeviceOn/SetProtobufStreamFormatsequence was for connection setup; identity probing only needsSYSTem:SYSInfoPB?. A healthy DAQiFi answers it regardless of stream format / power state.Tightened timeouts — wake delay 1000→200ms, response timeout 4000→1000ms, retry interval 1000→300ms, max attempts 3→2. USB CDC is fast; the previous values were defensive against ports we no longer probe at all.
Expected impact
Discovery drops from ~1min → <1s on a typical Windows system. Only DAQiFi-VID/PID ports get probed, each at ~200-500ms vs ~5.4s.
Test plan
DaqifiUsbIdsconstants + classifier positive/negative cases including bootloader-PID + CH340-vendor false-positives, (b)NullUsbPortDescriptorProvidercontract, (c) end-to-endDiscoverAsyncfilter — non-DAQiFi VID/PID returns 0 devices in <1s without probing, (d) null-descriptor fall-through preserves legacy behaviorSerialDeviceFinderTestsstill passAPI
The
internalconstructorSerialDeviceFinder(int baudRate, IUsbPortDescriptorProvider?)is added for tests; production callers continue to use the existing public constructors which now use a platform-default provider automatically.Closes #157