feat(emitters): four-state triage for the transmitters a fleet can hear - #1833
Open
clonea1 wants to merge 8 commits into
Open
feat(emitters): four-state triage for the transmitters a fleet can hear#1833clonea1 wants to merge 8 commits into
clonea1 wants to merge 8 commits into
Conversation
New tab (index.html nav + section, wired in app.js following the same
container-lookup + component pattern as the other tabs) backed by
GET/POST /api/v1/config/room. A 2D top-down canvas: set room width/
depth, add/remove sensor nodes, drag them into place or type exact
X/Y/Z, save. Save applies immediately (live, no restart) and persists
to room_config.json, which future launches load automatically -
replacing the --node-positions CLI-only workflow.
Metric/imperial unit toggle (display-only - this.config and everything
sent to the API always stay in meters, remembered per-browser via
localStorage). Compass badge + explanatory text establishing (0,0,0)
as the room's Northwest corner, +X = East, +Y = South, so placements
can be oriented to the real room.
Rows are matched to their node by array index rather than by id -
id is itself one of the editable fields, and using it as the lookup
key meant renaming a node into an id already in use broke the row<->
node link (two rows silently reading/writing the same node object,
which looked like one node jumping onto or stacking with another).
Duplicate ids are now also caught client-side (warned while editing,
hard-blocked on save) ahead of the server's own validation, and a
server-side validation rejection (200 OK with an {"error": ...} body,
not a failure status) is now checked for explicitly rather than
silently treated as a successful save.
Co-Authored-By: claude-flow <ruv@ruv.net>
(cherry picked from commit 4cc202a)
Extracted POST /api/v1/config/room's validation into a pure validate_room_config(&RoomConfig) -> Result<(), String> function so it's unit-testable without a full SharedState/axum harness; behavior unchanged, the handler just delegates to it now. 9 new tests: load-missing/malformed-file defaults, save+load round trip, and validation for non-positive/non-finite dimensions, non-finite node coordinates, duplicate node ids, and an empty node list. Also adds a "Reload from Saved" button to the Room Builder UI (discards unsaved local edits, re-fetches the live server config). Co-Authored-By: claude-flow <ruv@ruv.net> (cherry picked from commit a3b9907)
Extends the Room Builder from a single flat rectangle to a building: multiple storeys, walls drawn per storey, and a placed access point. Storeys share one origin. The origin (0, 0, 0) is the north-west corner of the first floor at floor level, and every storey is measured from it rather than being re-zeroed, so a node's X/Y means the same thing on every floor and distances between nodes on different storeys are plain Euclidean. Nodes and walls on other storeys are drawn faintly rather than hidden, which is what lets an upper-floor node be lined up against the wall below it. Storey elevation is derived from ceiling height plus subfloor thickness rather than typed in directly, because those are the two numbers someone can actually measure. Heights are entered in inches. A node carries `floor` alongside `z`. That is redundant for geometry and deliberately so: it is what the builder groups by and what walls are associated with, and deriving it from `z` would guess wrong for a node mounted high in a stairwell. `floor` is optional, so a config written before storeys existed keeps working unchanged. Every persisted field is served back by GET /api/v1/config/room. That response body is hand-maintained and will not complain when it falls behind the struct, so it carries a comment saying so at the point where the next field will be added -- a field that is saved but not served looks exactly like "the setting will not stick". Wall entry takes a start and an end and refuses a zero-length wall, which is otherwise easy to stage by double-tapping a corner. Co-Authored-By: claude-flow <ruv@ruv.net>
…ngle The Room Builder modelled a storey as width x depth. Most buildings are not rectangles, and the ones that are not tend to be exactly where the interesting propagation happens: a wing off the main block, an L around a stairwell, a garage bay that swallows a link. Adds a footprint editor. A storey is a polygon of points placed on the canvas rather than two numbers, so a node in a wing has coordinates that mean something relative to the building it is actually in, instead of being placed in empty space outside an assumed rectangle. Width and depth are kept as the polygon's bounding box, so existing configs and anything reading those two fields keep working unchanged. Co-Authored-By: claude-flow <ruv@ruv.net>
The frame parser accepts one magic (0xC5110001) and a fixed 20-byte header, so
a frame carrying transmitter or transmission identity is rejected outright.
That makes it impossible for a node to tell the server anything about WHICH
link a measurement belongs to, or WHICH transmission produced it.
Adds named constants for the three wire versions and dispatches on the magic:
v1 0xC5110001 20-byte header, unchanged
v2 0xC5110008 + transmitter MAC (addr2) at bytes 20..25
v3 0xC511000A + 802.11 rx_seq (u16 LE) at bytes 26..27
Esp32Frame gains source_mac and rx_seq, both Option, both None for a v1 frame.
A mixed fleet therefore keeps working: older firmware parses exactly as before
and newer firmware simply carries more.
This is the receiver half of the wire change and should land BEFORE any
firmware starts emitting v2/v3, so there is never a moment where a node sends
something the server rejects.
It is also a prerequisite for two follow-on changes that cannot work without
per-frame identity: per-link CSI state keyed on (receiver, transmitter), and
cross-node pairing keyed on (transmitter, rx_seq).
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds a LinkTable that accumulates CSI per (rx_node, tx_mac) pair, and runs it BEFORE the existing per-node subcarrier-grid gate. The gate locks each node to the densest grid it has seen. A node associates with one AP and receives HE-SU data from it at 256 bins, while everything else it merely overhears sends beacons and management frames at 64. The lock therefore always settles on the associated AP's format, and every non-associated transmitter is sparser BY CONSTRUCTION -- dropped by a bare continue before it can become a link. Grid consistency is a property of the TRANSMITTER, so it belongs on the link rather than the node. LinkTable applies the same densest-wins policy per (rx_node, tx_mac). The existing gate is NOT modified. The per-node feature path behaves exactly as before; the link path simply runs ahead of the continue, so a frame the node path cannot use stays available to consumers that can. MEASURED 2026-09-01, before -> after: links 31 -> 137 transmitters 10 -> 32 renderable 29 -> 88 grids in use 256 only -> 64:97, 256:33, 128:7 illuminators with two or more receivers: 1 -> 24 97 of the 137 recovered links are 64-bin: precisely the population being discarded. No firmware, hardware or network change -- the frames were always arriving. Adds GET /api/v1/links and GET /api/v1/links/inventory. The inventory endpoint reports the table's full contents including links the metrics view omits, with the reason for each, because that filter_map dropped links silently. Co-Authored-By: claude-flow <ruv@ruv.net>
A passive WiFi sensing fleet hears whatever is transmitting nearby. Some of it
is a reliable illuminator, some is a neighbour's router in an unknown room, and
some is a phone in someone's pocket. Treating them alike is the problem: an
emitter that moves teaches an association that is only true while it stands
still, which is worse than having no illuminator at all.
Adds an explicit state per emitter, in triage order:
excluded it moves. Refused at INGESTION, so it leaves the link table and
every consumer at once and stops occupying a slot.
pending heard, shortlisted, not yet judged. Feeds position-free
pattern-matching work but is never a geometric focus.
approved trusted as fixed, position ESTIMATED. Requires a position and a
non-zero uncertainty -- a neighbour's router does not move, but you
know which house, not which room.
surveyed trusted as fixed, position MEASURED. Requires a position and
forbids uncertainty.
Status and uncertainty answer different questions -- "do I trust this to stay
put" versus "how well do I know where it is" -- and are orthogonal except at
the ends. That is why `surveyed` is a state rather than an inference from a
zero uncertainty: it is the difference between "I measured this" and "I left
the box empty". Validation refuses the contradictory pairs in both directions.
A new transmitter defaults to `pending`. Being audible is not evidence of
standing still, so promotion is a decision rather than a consequence of being
loud.
Exclusion is enforced at ingestion rather than filtered downstream, and the
exclusion set is seeded from the persisted config at STARTUP as well as on
save. Without the startup seed a restart would silently begin admitting every
emitter the operator had already judged as moving.
`uncertainty_m` is carried rather than rounded away because positional error
matters in proportion to distance: fifteen metres of doubt about a router sixty
metres away is roughly fourteen degrees of bearing. A consumer can weight by
uncertainty over distance; it cannot recover that from a bare coordinate.
Adds ui/illuminators.html for triage, reading the link inventory and the room
config. Test fixtures use locally-administered example MACs.
Co-Authored-By: claude-flow <ruv@ruv.net>
…al Vec ruvnet#1791 landed node_positions_config as a positional Vec<[f32;3]> keyed by active-node rank, not the HashMap<u8,[f32;3]> the Room Builder handlers carried in this stack, so they stopped compiling once rebased onto current main. Read path enumerates and derives the id from the index, matching the convention the fusion path uses. Write path builds the vector indexed BY node id before assigning, so a sparse id set still lands each node in the right slot rather than shifting everything after a gap. Co-Authored-By: claude-flow <ruv@ruv.net>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#1791 landed node_positions_config as a positional Vec<[f32;3]> keyed by
active-node rank, not the HashMap<u8,[f32;3]> the Room Builder handlers carried
in this stack, so they stopped compiling once rebased onto current main.
Read path enumerates and derives the id from the index, matching the convention
the fusion path uses. Write path builds the vector indexed BY node id before
assigning, so a sparse id set still lands each node in the right slot rather
than shifting everything after a gap.
Stacked, eight commits — it carries the Room Builder work and the per-link metrics as well as its own. Merge the earlier PRs in the stack first (#1822/#1824 room builder, #1828 links) and this shrinks accordingly.
Rebased onto current
main. Conflicts were additive and resolved by keeping both sides; one real fix was needed because #1791 changednode_positions_configfrom aHashMap<u8,[f32;3]>to a positionalVec, which the Room Builder handlers in this stack were written against. 329 server tests pass.