feat(#480): read offband_caps byte 2 at device-info offset 84

Adds parseOffbandCaps2 alongside the existing tail-byte helpers, a
_offbandCaps2 field plus getter, and byte 2 in the device-info
capability log line.

Byte 2 sits at offset 84, deliberately NOT adjacent to byte 1 at 82:
offset 83 is already the FEM LNA state byte and every tail field is read
at a fixed absolute offset, so an adjacent insert would shift the FEM
state and make shipped clients misread a bitmask as the LNA toggle.
Firmware appends it at the end of the frame for that reason.

Absence is "no byte-2 capabilities", never an error, so any radio
predating the firmware change reads null and behaves unchanged.

No bit constants yet: byte-2 bits 0 and 1 are earmarked for firmware
epics but neither is claimed, so nothing gates on them here.

Wire contract from OffbandMesh/meshcore-firmware PR #515 (branch
feat/508-caps-byte2, commit 7664c29f). That PR is open pending this
client-side validation, tracked at #481.

Epic: #474
Agent: CalmBay (session d14220d9)
feat/483-button-buzzer-ui
Strycher 2 months ago
parent a48edb209f
commit 44ff206b18

@ -294,6 +294,7 @@ class MeshCoreConnector extends ChangeNotifier {
String? _firmwareVersion;
String? _deviceModel;
int? _offbandCaps;
int? _offbandCaps2;
bool? _femLnaEnabled;
int _pathHashByteWidth = 1;
CompanionRadioStats? _latestRadioStats;
@ -610,6 +611,11 @@ class MeshCoreConnector extends ChangeNotifier {
/// null on older firmware that sends a shorter frame.
int? get offbandCaps => _offbandCaps;
/// Second `offband_caps` bitfield (frame offset 84, firmware #508). Null when
/// the frame is too short, which means "no byte-2 capabilities" and is never
/// an error: every radio predating #508 reports null here. (#480)
int? get offbandCaps2 => _offbandCaps2;
/// Whether the connected firmware speaks the `0xC1` GPS extension, i.e. it
/// advertised the Offband-fork `offband_caps` byte. Stock MeshCore omits it,
/// so 0xC1 is suppressed there rather than pinged blindly. (#144)
@ -5022,6 +5028,21 @@ class MeshCoreConnector extends ChangeNotifier {
static bool? parseFemLnaState(Uint8List frame) =>
frame.length >= 84 ? frame[83] != femLnaBypass : null;
/// Second `offband_caps` byte, at frame offset **84** (firmware #508).
///
/// Deliberately NOT adjacent to byte 1 at offset 82: offset 83 is already the
/// FEM LNA state byte, and every field here is read at a FIXED ABSOLUTE
/// offset, so inserting byte 2 next to byte 1 would shift the FEM state and
/// make shipped clients misread a bitmask as the LNA toggle. Firmware appends
/// byte 2 at the end of the frame for exactly that reason
/// (`OffbandConfigProtocol.h`, firmware PR #515).
///
/// Null on any firmware predating #508, which sends a shorter frame. Null
/// means "no byte-2 capabilities", never an error. Bounds-checked like its
/// siblings, so a truncated or hostile short frame never indexes OOB.
static int? parseOffbandCaps2(Uint8List frame) =>
frame.length >= 85 ? frame[84] : null;
void _handleDeviceInfo(Uint8List frame) {
if (frame.length < 4) return;
if (_shouldGateInitialChannelSync) {
@ -5072,11 +5093,14 @@ class MeshCoreConnector extends ChangeNotifier {
// FEM LNA state rides one byte past the caps byte on v16+ (#304). Primary
// read on connect, a 0xC3 GET is only the fallback.
_femLnaEnabled = parseFemLnaState(frame);
// Second caps byte rides at offset 84, past the FEM state byte (#480).
_offbandCaps2 = parseOffbandCaps2(frame);
// Capability-gated features are invisible when a bit is clear, which looks
// identical to a bug. Log the raw inputs so "the toggle didn't appear" can
// be told apart from "this radio says it can't". (#304)
_appDebugLogService?.info(
'Offband caps=0x${(_offbandCaps ?? 0).toRadixString(16).padLeft(2, '0')} '
'caps2=${_offbandCaps2 == null ? 'absent' : '0x${_offbandCaps2!.toRadixString(16).padLeft(2, '0')}'} '
'verCode=${_firmwareVerCode ?? 0} frameLen=${frame.length} '
'femLnaByte=${_femLnaEnabled == null ? 'absent' : (_femLnaEnabled! ? '1' : '0')} '
'femCapable=$supportsOffbandFemLna blockCapable=$supportsOffbandBlock',

@ -0,0 +1,104 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
void main() {
/// Device-info frame with the three additive tail bytes at their fixed
/// absolute offsets: caps byte 1 at 82, FEM LNA state at 83, caps byte 2 at
/// 84 (firmware #508 / PR #515).
Uint8List deviceInfo({
required int length,
int caps1 = 0,
int femByte = 0,
int caps2 = 0,
}) {
final frame = Uint8List(length);
if (length >= 1) frame[0] = respCodeDeviceInfo;
if (length >= 83) frame[82] = caps1;
if (length >= 84) frame[83] = femByte;
if (length >= 85) frame[84] = caps2;
return frame;
}
group('offband_caps byte 2, device-info offset 84 (#480)', () {
test('reads byte 84 when the frame is long enough', () {
expect(
MeshCoreConnector.parseOffbandCaps2(
deviceInfo(length: 85, caps2: 0x01),
),
equals(0x01),
);
expect(
MeshCoreConnector.parseOffbandCaps2(
deviceInfo(length: 85, caps2: 0xFF),
),
equals(0xFF),
);
});
test('a present byte 2 of zero is still present, not absent', () {
// "No byte-2 capabilities set" and "firmware predates byte 2" are
// different states; only the second is null.
expect(
MeshCoreConnector.parseOffbandCaps2(deviceInfo(length: 85, caps2: 0)),
equals(0),
);
});
test('null on firmware predating #508 (frame stops at 84)', () {
expect(
MeshCoreConnector.parseOffbandCaps2(deviceInfo(length: 84)),
isNull,
);
});
test('null on a short or truncated frame, never an OOB index', () {
for (final length in [0, 1, 4, 81, 82, 83, 84]) {
expect(
MeshCoreConnector.parseOffbandCaps2(deviceInfo(length: length)),
isNull,
reason: 'frame length $length must yield null, not throw',
);
}
});
});
group('byte 2 does not disturb the fields before it', () {
// This is the regression the offset-84 choice exists to prevent: byte 2 is
// appended at the END of the frame, not adjacent to byte 1, because the FEM
// state byte already occupies 83 and every field is read at a fixed
// absolute offset.
final frame = deviceInfo(
length: 85,
caps1: offbandCapFemLna | offbandCapBlock,
femByte: 1,
caps2: 0x03,
);
test('caps byte 1 still reads at offset 82', () {
expect(
MeshCoreConnector.parseOffbandCaps(frame),
equals(offbandCapFemLna | offbandCapBlock),
);
});
test('FEM LNA state still reads at offset 83', () {
expect(MeshCoreConnector.parseFemLnaState(frame), isTrue);
});
test('byte-1 gate predicates are unaffected by byte 2', () {
final caps1 = MeshCoreConnector.parseOffbandCaps(frame);
expect(firmwareSupportsOffbandFemLna(caps1), isTrue);
expect(firmwareSupportsOffbandBlock(caps1, 15), isTrue);
});
test('all three tail fields are independent', () {
final onlyCaps2 = deviceInfo(length: 85, caps2: 0x02);
expect(MeshCoreConnector.parseOffbandCaps(onlyCaps2), equals(0));
expect(MeshCoreConnector.parseFemLnaState(onlyCaps2), isFalse);
expect(MeshCoreConnector.parseOffbandCaps2(onlyCaps2), equals(0x02));
});
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.