From 8920cf928f67401b9cd35782788cff6776bc26da Mon Sep 17 00:00:00 2001 From: Strycher Date: Thu, 30 Jul 2026 00:28:34 -0400 Subject: [PATCH] feat(#439): read per-message RSSI from reserved2 and show it on the Path screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware #465 now writes RX RSSI into reserved2 (byte[3]) of the v3 contact-msg-recv frame as a clamped int8 dBm, 0 when unset (MyMesh.cpp:550-551). Read it in the parse instead of skipping: gate on != 0 (RSSI is always negative for a real RX, so 0 = no data), null for device-composed outgoing messages. reserved1 (byte[2]) is untouched — that's #429's outgoing flag; RSSI lives in byte[3] after the res1/res2 collision fix (#464/#465). The Message.rssi field, persistence, param-passing, and the (hidden- while-null) RSSI row on the Packet Path screen were all staged in #438, so this is just the wire read + 3 gate tests. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 13 +++++++++---- test/models/message_snr_test.dart | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 387dcfd..ab5abd4 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5884,17 +5884,21 @@ class MeshCoreConnector extends ChangeNotifier { // Companion radio layout: // [code][snr][res1][res2][prefix x6][path_len][txt_type][timestamp x4][extra?][text...] - // Firmware writes snr as (int8)(snr_dB * 4), so dB = byte / 4.0 - // (MyMesh.cpp:512). res1 bit0 = device-composed outgoing flag (#429); - // res2 will carry RSSI once firmware ships it (#439/#456) — not read yet. + // snr: (int8)(snr_dB * 4), so dB = byte / 4.0 (MyMesh.cpp:512). + // res1 bit0 = device-composed outgoing flag (#429). + // res2 = RX RSSI as clamped int8 dBm, or 0 when unset (older firmware or + // no-RX text) — gate on != 0 (MyMesh.cpp:550-551, #465). RSSI is always + // negative for a real RX, so 0 unambiguously means "no data". double? snr; bool isOutgoing = false; + int? rssi; if (code == respCodeContactMsgRecvV3) { snr = reader.readInt8() / 4.0; // reserved1 bit0 = outgoing flag: set for a message composed on the // device itself, so it renders as sent-by-me (#429 part B). isOutgoing = (reader.readByte() & 0x01) != 0; - reader.skipBytes(1); // reserved2 (RSSI pending — #439/#456) + final rssiByte = reader.readInt8(); // reserved2 = RX RSSI (#439/#465) + rssi = rssiByte != 0 ? rssiByte : null; } final senderPrefix = reader.readBytes(6); @@ -5969,6 +5973,7 @@ class MeshCoreConnector extends ChangeNotifier { // path-type / rxTime are meaningless — null them like rxTime (#429/#438). rxTime: isOutgoing ? null : DateTime.now(), snr: isOutgoing ? null : snr, + rssi: isOutgoing ? null : rssi, isFloodRoute: isOutgoing ? null : isFloodRoute, ); } catch (e) { diff --git a/test/models/message_snr_test.dart b/test/models/message_snr_test.dart index e909ce1..f3404c0 100644 --- a/test/models/message_snr_test.dart +++ b/test/models/message_snr_test.dart @@ -72,4 +72,25 @@ void main() { test('0 hops is flood, not direct', () => expect(isFlood(0), isTrue)); test('3 hops is flood', () => expect(isFlood(3), isTrue)); }); + + group('RSSI reserved2 gate (#439)', () { + // Firmware writes reserved2 (byte[3]) = clamped int8 RX RSSI, or 0 when + // unset (older firmware / no-RX text). RSSI is always negative for a real + // RX, so 0 unambiguously means "no data" -> null. (MyMesh.cpp:550-551) + int? rssiFromByte(int b) { + final v = b.toSigned(8); + return v != 0 ? v : null; + } + + test('0 means no data -> null (older firmware / no RX)', () { + expect(rssiFromByte(0), isNull); + }); + test('typical negative dBm is decoded signed', () { + expect(rssiFromByte(0xAB), -85); // 0xAB as int8 = -85 dBm + expect(rssiFromByte(-40), -40); + }); + test('int8 floor (-128) shows, per firmware clamp', () { + expect(rssiFromByte(-128), -128); + }); + }); }