Files
udpscope/docs/superpowers/specs/2026-11-02-e1608-network-scan-design.md
T

3.6 KiB

E-1608 network scan -- design

Date: 2026-11-02. Status: approved in conversation, awaiting written-spec review.

Goal

The E-1608 connection form gets a "Scan network" row that finds boards and fills the address or the MAC from a click. Today a user must already know the address, or give a MAC and rely on a lookup.

Finding (tested on a real board, 2026-11-02)

The board was at 169.254.88.75 on net0; the default route is wlan0. A D probe sent to 255.255.255.255 with no binding found nothing (it left through wlan0). The same probe sent with SO_BINDTODEVICE net0, or bound to the interface address, or sent to a directed broadcast, or by unicast, was answered (64 bytes, first byte D). So a single limited broadcast is not enough on a host with several interfaces; the existing "find by MAC" lookup has the same flaw.

Discovery layer (e1608_link.{hpp,cpp})

  • class Scan (non-blocking, one instance per scan): Scan(port) enumerates up, non-loopback IPv4 interfaces with IFF_BROADCAST (getifaddrs), opens one UDP socket per interface bound to it (SO_BINDTODEVICE, falling back to binding the interface address if the call is refused), enables SO_BROADCAST, and sends D to 255.255.255.255 on each. poll() drains every socket without blocking and returns the devices found so far (E-1608 product id only, one entry per MAC, the same reply parsing as discover). The caller owns the clock.
  • discover() keeps its signature. A new overload or helper discover_broadcast(port, wait_ms) is Scan plus a blocking wait. E1608Source's lookup uses it when the subnet is empty; a given subnet still uses sweep_targets + discover as today.
  • An interface that cannot be opened or sent on is skipped, never an error; no interfaces is an empty result.

Form (e1608_kind.cpp)

  • At the top of the Connection expander, an AdwActionRow "Scan network" with a text suffix button "Scan" (widget name e1608-scan). The row subtitle reads "Scanning...", "N boards found" or "No board found". No theme icons.
  • A scan starts a Scan on the port in the Port field and a 50 ms g_timeout_add poll for about 1.5 s. There is no thread: the project has exactly two. The timer is removed when the form is destroyed (Form::alive guard plus the destroy hook) and when a new scan starts.
  • Each device found becomes an AdwActionRow in the Connection expander right after the scan row, replacing the previous results. Title: IP. Subtitle: MAC. Activating the row fills Address and clears MAC and Subnet. A suffix text button "Use MAC" (e1608-use-mac) fills MAC and clears Address and Subnet. Fill is done under the form's updating guard, then changed fires once.
  • Result rows are not read or written by the session: only the filled fields persist.
  • The description line mentions the scan.

Verification

  • A scan probe (env var in the existing pattern): starts against tools/mock_e1608.py on a loopback-reachable port, presses the scan button, and prints [e1608] scan: found=N ip=... mac=... fill_addr=1 fill_mac=1 ok=1, covering both fills and that the results are replaced on a rescan. If the sandbox refuses the broadcast, the line says so and the limitation is documented, not claimed.
  • A manual run of the scan against the real board, recorded in the README.
  • Baselines unchanged: 3 warnings clean, gtk warnings: 0, no Gtk-CRITICAL.

Docs

README.md (E-1608 paragraph), docs/claude/{build-and-run,verification,probe-readings,architecture}.md.

Out of scope

Remembering scan results across runs; scanning other subnets than the host's interfaces (the subnet sweep field remains for that); a worker thread.