feat(discovery): resolve TCP data port for manually-entered IPs (closes #244) - #330
Conversation
#244) A host that never answered the UDP-30303 broadcast (i.e. a manually-entered IP) had no port to connect to, forcing consumers to hardcode 9760. Give Core both a documented default and a way to resolve the real port. - DaqifiDeviceFactory.DefaultTcpDataPort (9760): the firmware-default data port, so consumers reference one documented constant instead of a magic literal for manual connections. - WiFiDeviceFinder.ProbeTcpDataPortAsync(host, [discoveryPort,] timeout, ct): unicasts the discovery query directly to a single host and returns the DevicePort from its reply, or null if it does not answer within the timeout (callers fall back to DefaultTcpDataPort). Reuses the finder's existing query constant and ParseDeviceInfo/IsValidDiscoveryMessage rather than duplicating discovery parsing; ignores replies from other devices on the segment, maps its own timeout to null, and rethrows only genuine caller cancellation. Tests: argument validation; a loopback responder that answers with a delimited status protobuf, asserting the advertised port is returned; a silent-host timeout returning null; and caller-cancellation propagation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by QodoResolve TCP data port for manually-entered device IPs via unicast probe
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo
Context used 1.
|
…ports (Qodo #330) Two fixes from review: - ProbeTcpDataPortAsync created an unbound UdpClient, sending from an ephemeral source port. DAQiFi firmware replies to the well-known discovery port (the same reason the broadcast sweep binds to it), so the probe would miss the reply and wrongly return null against real hardware. Bind the local socket to the discovery port (ReuseAddress, coexisting with a concurrent broadcast sweep), falling back to an ephemeral port if the bind fails. - The result contract is a usable TCP port, but the check only required > 0, so a DevicePort above 65535 could be returned as non-null and defeat the caller's default-port fallback. Require 1..65535. Tests updated: the loopback responder now binds IPAddress.Any so the probe deterministically falls back to an ephemeral port (modelling firmware replying to the sender). Added a case asserting an out-of-range advertised port yields null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
A host that never answered the UDP-30303 broadcast (a manually-entered IP) had no data port to connect with, forcing consumers like daqifi-desktop to hardcode
9760. This gives Core both a documented default and a way to resolve the real port.Closes #244.
Changes
DaqifiDeviceFactory.DefaultTcpDataPort(9760) — the firmware-default TCP data port, so consumers reference one documented constant instead of a magic literal for manual connections.WiFiDeviceFinder.ProbeTcpDataPortAsync(host, [discoveryPort,] timeout, ct)— unicasts the discovery query directly to a single host and returns theDevicePortfrom its reply, ornullif it does not answer within the timeout (callers then fall back toDefaultTcpDataPort). It reuses the finder's existing query constant andParseDeviceInfo/IsValidDiscoveryMessagerather than duplicating discovery parsing, ignores replies from other devices on the segment, maps its own timeout tonull, and rethrows only genuine caller cancellation.Together these implement both options the ticket proposed (a documented default and an active probe).
Tests
null.OperationCanceledException.Bench validation
The bench rig is USB-serial, so the probe's UDP round-trip was exercised via the loopback integration test above rather than a WiFi device. The pieces that touch real hardware were confirmed against a live Nyquist (firmware 3.7.2): the device reports
DevicePort = 9760in its status frame — the same fieldProbeTcpDataPortAsyncreads — validating bothDefaultTcpDataPortand the value the probe resolves.🤖 Generated with Claude Code