Files
HaloKeymind/UPSTREAM_BUGS.md
T
mikecarper 7605b12cc7 Merge remote-tracking branch 'review/observer-firmware' into keymindCascade
# Conflicts:
#	.github/workflows/build-observer-firmwares.yml
#	.gitignore
#	MQTT_IMPLEMENTATION.md
#	MQTT_INTERNALS.md
#	README.md
#	STABILITY_TESTABILITY_HANDOFF.md
#	docs/cli_commands.md
#	examples/companion_radio/MyMesh.cpp
#	examples/simple_repeater/MyMesh.cpp
#	examples/simple_repeater/UITask.cpp
#	examples/simple_room_server/MyMesh.cpp
#	examples/simple_room_server/UITask.cpp
#	examples/simple_sensor/SensorMesh.cpp
#	platformio.ini
#	scripts/generate_webconfig_html.py
#	scripts/webconfig_mock_server.py
#	src/helpers/CommonCLI.cpp
#	src/helpers/CommonCLI.h
#	src/helpers/JWTHelper.cpp
#	src/helpers/MQTTDefaults.h
#	src/helpers/MQTTMessageBuilder.cpp
#	src/helpers/MQTTPayloadBuilder.h
#	src/helpers/MQTTPresets.h
#	src/helpers/bridges/MQTTBridge.h
#	src/helpers/esp32/WebConfigServer.cpp
#	src/helpers/esp32/WebConfigServer.h
#	src/helpers/ui/SH1106Display.cpp
#	variants/lilygo_tbeam_SX1262/platformio.ini
#	variants/lilygo_tbeam_SX1276/platformio.ini
#	variants/lilygo_tlora_v2_1/platformio.ini
#	variants/thinknode_m7/platformio.ini
#	webui/index.html
2026-08-14 08:53:02 -07:00

123 lines
5.4 KiB
Markdown

# Upstream Bug Candidates
Bugs in code this fork inherits from upstream (`meshcore-dev/dev`), each with the
fix we would propose upstream. Any fork-local workaround is recorded so it can be
dropped once upstream lands a real fix.
Status: **open** (not reported) * **reported** (issue/PR filed) * **landed**
(upstream fixed, fork workaround removable).
---
## 1. Neighbour "heard" ages are derived from a steppable wall clock
**Status:** open
**Files:** `examples/simple_repeater/MyMesh.cpp`, `examples/simple_room_server/MyMesh.cpp`
**Impact:** wrong ages reported to companion apps, CLI, and (in this fork) MQTT. No crash.
### Symptom
A repeater published a neighbour age of 69681995 seconds -- 806 days -- for two of
three neighbours that had just answered a live query (capture abridged; see
`MQTT_IMPLEMENTATION.md` for the full payload shape):
```json
{"timestamp":"2026-07-30T22:59:09.000000+00:00","total_neighbors":3,
"neighbors":[
{"pubkey":"0CE5EA7C...","snr":12.5,"heard_secs_ago":57,"status":"responded"},
{"pubkey":"B0D17C59...","snr":12.5,"heard_secs_ago":69681995,"status":"responded"},
{"pubkey":"D19BEE16...","snr":12.25,"heard_secs_ago":69681995,"status":"responded"}]}
```
### Root cause
`heard_secs_ago` is a *relative* quantity computed by subtracting two samples of
an *absolute* wall clock that can step by years mid-session.
`putNeighbour()` stamps `heard_timestamp = getRTCClock()->getCurrentTime()` when a
zero-hop advert or node-discover reply arrives (`MyMesh.cpp:93`). On a cold boot
the radio is receiving within a second or two, while the clock is still at the
firmware's unset default -- `1715770351`, 15 May 2024 (`src/helpers/ArduinoHelpers.h`,
`src/helpers/ESP32Board.h`). NTP (or an admin over the CLI) then steps the clock
forward by years, and every later reader subtracts the two:
```
publish epoch 1785452349 (2026-07-30T22:59:09Z)
- reported age 69681995
= stored heard_timestamp 1715770354 = unset-clock default + 3 s of uptime
```
So both stale entries were stamped 3 seconds after boot. The entries do not
self-heal: `putNeighbour()` is the only writer of `heard_timestamp`, so an entry
stays wrong until that specific neighbour is physically heard again. In the capture
above the two stale neighbours missed the node-discover refresh (responder-side
rate limit `discover_limiter`, a reply collision, or firmware without
`CTL_TYPE_NODE_DISCOVER_RESP`) yet still answered a direct query -- hence
`"status":"responded"` next to a 2-year age.
Deferring the *read* until after time sync does not fix this. The bad value is
written at packet-reception time, which on any cold boot precedes WiFi and NTP.
### Affected readers
| Path | Location |
| --- | --- |
| Companion `REQ_TYPE_GET_NEIGHBOURS` binary reply | `MyMesh.cpp:404` |
| CLI `neighbors` text output | `MyMesh.cpp:1350` |
| MQTT neighbors JSON (fork-only) | `MyMesh.cpp:2036` |
### Proposed upstream fix
Stamp neighbours from a monotonic source and derive every age from that:
- add `uint32_t heard_millis` to `NeighbourInfo`, set from `millis()` in
`putNeighbour()` alongside the existing wall-clock stamp;
- compute every "seconds ago" readout as `(now_millis - heard_millis) / 1000`
using the unsigned-subtraction idiom already used by `millisHasNowPassed()` /
`futureMillis()`, which is immune to clock steps and stays correct across one
32-bit rollover;
- clamp the result to a documented maximum so an entry untouched for longer than
the 49.7-day `millis()` period cannot alias back to a small age;
- keep `heard_timestamp` for anything that needs an absolute instant -- sort order
is equivalent under either field.
Cost: 4 bytes x `MAX_NEIGHBOURS` of RAM.
### Fork-local containment (in place)
Not a substitute for the above; it stops the fork from publishing a fabricated
number while upstream still stamps from the wall clock.
- `finishNeighborDiscover()` reports the age as unknown -- `"heard_secs_ago":null`
-- when the stored stamp predates `MQTTConnectionPolicy::kSyncedClockEpoch`
(2025-01-01, above the unset-clock default) while the current clock is past it,
or when the clock has stepped backwards. Previously a backwards clock reported
`0`, i.e. "heard just now".
- `handleNeighborDiscoverResponse()` re-stamps `heard_timestamp` on a zero-hop
scope reply, in both the snapshot and the live table. The reply is proof of
reception, so entries now heal once per discovery cycle instead of waiting for
the neighbour's next advert. This would have made 2 of the 3 entries above
correct.
- Publish ordering places known ages before unknown ones, so an unusable stamp
cannot masquerade as the freshest entry when the JSON buffer truncates.
---
## 2. Neighbour age subtraction is unclamped in the companion and CLI readouts
**Status:** open
**Files:** `examples/simple_repeater/MyMesh.cpp:404`, `:1350` (and the room-server twins)
**Impact:** age underflows to ~4.29e9 instead of a sane value.
Both readers subtract without checking ordering:
```cpp
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
```
Any state where `heard_timestamp > now` -- clock stepped backwards by NTP or an
admin, or a stamp taken before a backwards step -- wraps the unsigned result to
near `UINT32_MAX`. Distinct from #1 in that it is a missing guard rather than the
choice of clock: worth fixing on its own even before the monotonic change, by
clamping to `0` (or reporting "unknown", which is what the age actually is).