Repository navigation
feat(app): keep the SNR the repeater heard us at - #489
Conversation
07c4089 to
a22f765
Compare
1879d95 to
22dfa8f
Compare
efiten
left a comment
There was a problem hiding this comment.
The decode work is right and the reasoning about where the number belongs is right. app/ 855 tests and a clean build with master merged in. One thing to settle before it goes in, and it is about the field's life rather than its correctness.
As it stands the value is written and then deleted unread
heard_us_snr has exactly one reference in the whole tree outside tests: the write in capture.js:51. Nothing renders it, buildPayload does not carry it, and the server has no column.
The PR says "app-local until something reads it", which reads as "kept safe until then". It is not kept: queue.prune deletes rows older than RETENTION_MS, which is 7 days (queue.js:26), and the guard it respects is the publish watermark, not whether anything consumed the field. So a published row ages out on schedule and takes the number with it.
That inverts the argument for landing it now. "Capture it before it is lost" holds only if capturing preserves it, and here it postpones the loss by a week. Unless a consumer ships inside that window, every value this collects is deleted having been read by nothing.
Which is not an argument against the change, only against this shape of it
The decode itself is the part that cannot be redone later: it is genuinely the reciprocal measurement, our radio's reading of a node against that node's reading of us from the same position at the same moment, and #484's sweep produces one per repeater per cycle. That is worth having.
Three ways to give it somewhere to go, in increasing size:
- Render it. The HUD or the ticker showing it on a trace reply makes it useful the moment it is captured, and the 7-day window stops mattering.
- Publish it. A field on
buildPayloadplus a column, so it accumulates where the asymmetry could actually be analysed across drives. That is where the interesting version of this lives, since one reading tells you little and a hundred over a route tells you about an antenna. - Hold this until one of those exists, and land them together.
I would take 1 for now, since it is small and makes the number visible to the person standing under the antenna. But it is your call which, and I would rather ask than approve a field that today has no reader.
Everything else checks out: its own field rather than folded into snr, only on a reply we provoked, and the 0x89 handler correctly ruled out because logRxRaw already puts the completing packet in the 0x88 log.
A trace reply carries, in its path bytes, the SNR at which each hop heard the transmission it forwarded -- the firmware writes (int8_t)(SNR*4) before retransmitting. For a trace we sent, the first of those is the node we addressed reading OUR ping: the reciprocal of every measurement this app makes, from the same position at the same moment. An asymmetry between the two is what an obstruction or a bad antenna looks like. We were decoding the packet and dropping the number. It is kept only on a reply we provoked, attached where efiten#481 recognises our own tag: on an overheard trace those same bytes describe other nodes hearing each other. It gets its own field rather than being folded into snr, which means our radio's reading on every other row. Nothing renders it yet and nothing publishes it -- the payload and the ingestor schema have no field for it, and no reader needs one until something reads it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The field was written and then pruned unread: nothing rendered it, so a 7-day retention deleted every value it collected. The ticker's meta cell is where a reception explains itself, and on the one row that has it the reciprocal reading says more than the bare type label - so it takes that slot, and only that slot (text and channel name keep precedence). One decimal, the same format as the HUD's SNR readout. A 0 dB reading stays visible. Changelog entry in both copies. Verified: app 859 tests + build, web 504 tests, eslint clean. All three lineMeta mutations (branch removed, truthy guard, reordered above text) go red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59c0374 to
5ccfb66
Compare
|
Built as option 1, The reading lands on the trace reply's row in the ticker. The meta cell shows The ticker rather than the HUD, measured: the HUD readout row's worst case is ~332px against a 328px content box at 360px (app.css:154), so a fifth item does not fit, and a second row would move the HUD height the FAB clearance depends on (#264). The log row also keeps the value on screen for the 200-row window instead of until the next packet. Publishing (your option 2) is not in this PR. Also in this push: the branch is rebased onto ae9ba97. That drops the merge commit the branch top carried (§6) and the #483 commit master already has, and adds the changelog entry the change owed, in both copies. Tests: quarter-dB rounding, 0 dB, null, and the precedence order are pinned in |
efiten
left a comment
There was a problem hiding this comment.
Option 1, and it answers the objection properly: the number now has a reader on the row it belongs to, so the 7-day prune no longer deletes it unseen.
Verified with master merged in, app/ 859 tests and a clean build. Both decisions in lineMeta are pinned, which I checked by breaking them:
!= nullrather than truthiness. Changed it toif (r.heard_us_snr)and a test goes red. That matters more here than in most places: a repeater hearing us at exactly 0 dB is an ordinary reading, not an absent one, and truthiness would hide precisely the boundary case.- Precedence. Moved the reading above
_textand a test goes red. A decrypted message keeping the cell is right: the message is the thing, the reciprocal reading is context.
The cell choice is good. It is the type label's slot, and on a trace reply we provoked the type is the least interesting thing on the row, since we already know what it is: we asked for it.
One thing left open on purpose, and I am content with it: the reading is still app-local and unpublished, so the asymmetry cannot be looked at across drives, which is where the interesting version lives. As a visible number under the antenna it earns its place now, and publishing it is a separate change with a server column behind it.
🤖 I have created a release *beep* *boop* --- <details><summary>app: 1.24.0</summary> ## [1.24.0](app-v1.23.0...app-v1.24.0) (2026-08-30) ### Features * **app:** give the theme three states and a memory, and end the Settings tab ([#566](#566)) ([e67c319](e67c319)) * **app:** keep the SNR the repeater heard us at ([#489](#489)) ([0cd0a7c](0cd0a7c)) * **app:** make every recorded reception audible, one instrument per type (+ sound tweaks) ([#470](#470)) ([6244c0f](6244c0f)) * **web:** gate the point layer, and say what an account opens ([#513](#513)) ([71856f1](71856f1)) ### Bug Fixes * **app:** take the gate's coach marks out of #splash's stacking context ([#565](#565)) ([6fba59e](6fba59e)) </details> <details><summary>web: 1.20.0</summary> ## [1.20.0](web-v1.19.0...web-v1.20.0) (2026-08-30) ### Features * **app:** give the theme three states and a memory, and end the Settings tab ([#566](#566)) ([e67c319](e67c319)) * **app:** keep the SNR the repeater heard us at ([#489](#489)) ([0cd0a7c](0cd0a7c)) * **app:** make every recorded reception audible, one instrument per type (+ sound tweaks) ([#470](#470)) ([6244c0f](6244c0f)) * **web:** gate the point layer, and say what an account opens ([#513](#513)) ([71856f1](71856f1)) ### Bug Fixes * **app:** take the gate's coach marks out of #splash's stacking context ([#565](#565)) ([6fba59e](6fba59e)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes #482
Stacked on #483 (
feat/481-trace-tag): the reading is only meaningful on a reply we provoked, and knowing that a reply is ours is what #483 added. Until it merges its commit shows in this diff; the change here is the second commit.What we were throwing away
A trace reply carries, in its path bytes, the SNR at which each hop heard the transmission it forwarded — the firmware writes
(int8_t)(pkt->getSNR() * 4)before retransmitting (src/Mesh.cpp). For a trace we sent, the first of those is the node we addressed reading our ping.That is the reciprocal of every measurement this app makes. The RX log says how well we hear a node; this says how well that node hears us, from the same position at the same moment. An asymmetry between the two is what an obstruction or a bad antenna looks like — and with #484 sweeping, every repeater in earshot produces one of these per cycle.
We were decoding the packet and dropping the number.
The issue asked for a 0x89 handler; it is not needed
#482 originally proposed parsing
PUSH_CODE_TRACE_DATA(0x89), the frame the companion emits when a trace completes. That turned out to be redundant:Dispatcher::checkRecvcallslogRxRawfor every raw reception before parsing (src/Dispatcher.cpp:199), so the completing packet is already in the 0x88 log the app processes, and the decoder exposes its path SNRs. No new frame handler, no new companion surface — only reading what we already have. The issue is rewritten to match.Where it is kept, and where it is not
heard_us_snr. Not folded intosnr, which means our own radio's reading on every other row.buildPayloadis an explicit shape and the ingestor has no column for it; app-local until something reads it.Verification
app/: 42 files, 741 tests green. Build andnpx eslint srcclean.snrinstead of the packet's (2 red).0xf0is −16 as int8, ÷4 = −4 dB, asserted end to end from hex.processFrameruns and asserts both readings land side by side and mean different things:heard_us_snr−4 from the path byte,snr−4.25 from the 0x88 header.