Skip to content

feat(app): keep the SNR the repeater heard us at - #489

Merged
efiten merged 2 commits into
efiten:masterfrom
khagele:feat/482-heard-us
Aug 30, 2026
Merged

efiten merged 2 commits into
efiten:masterfrom
khagele:feat/482-heard-us

Conversation

@khagele

@khagele khagele commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

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::checkRecv calls logRxRaw for 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

  • Its own field, heard_us_snr. Not folded into snr, which means our own radio's reading on every other row.
  • Only on a reply we provoked, attached where feat(app): name a trace reply after the node we pinged #483 recognises our tag. On an overheard trace those same bytes describe other nodes hearing each other, which would be a number with no owner.
  • Not published. buildPayload is an explicit shape and the ingestor has no column for it; app-local until something reads it.
  • Nothing renders it, so there is no changelog entry — a user cannot see this yet (AGENTS §6).

Verification

  • app/: 42 files, 741 tests green. Build and npx eslint src clean.
  • Mutation-checked: reading the second hop instead of the first (3 red), and feeding the field from our own snr instead of the packet's (2 red).
  • The firmware encoding is pinned through the real decoder rather than a hand-built fixture: a path byte of 0xf0 is −16 as int8, ÷4 = −4 dB, asserted end to end from hex.
  • A composition test runs the sequence processFrame runs 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.

@efiten efiten left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. Publish it. A field on buildPayload plus 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.
  3. 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.

khagele and others added 2 commits August 29, 2026 16:15
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>
@khagele
khagele force-pushed the feat/482-heard-us branch from 59c0374 to 5ccfb66 Compare August 29, 2026 14:18
@khagele

khagele commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Built as option 1, 5ccfb66.

The reading lands on the trace reply's row in the ticker. The meta cell shows heard us at -11.5 dB where the type label stood; a decrypted text and a channel name keep precedence in that cell. One decimal, the same format as #hud-snr, and a real 0 dB reading stays visible.

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 receptionlog.test.js; the three mutations of the branch (removed, truthy guard, reordered above text) go red. app 859 + build, web 504, eslint clean. CI is green.

@efiten efiten left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • != null rather than truthiness. Changed it to if (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 _text and 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.

@efiten
efiten merged commit 0cd0a7c into efiten:master Aug 30, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 30, 2026
efiten pushed a commit that referenced this pull request Aug 30, 2026
🤖 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>
@khagele
khagele deleted the feat/482-heard-us branch September 12, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

app: a trace reply carries the SNR that node heard us at, and we drop it

2 participants