feat(#134): show firmware version + model in Device Info

Parse the device-info reply's readable strings (build date / model / firmware
version, NUL-terminated after the 8-byte header) and show firmwareVersion +
deviceModel in Settings → Device Info. Raw strings are logged on connect.
End-anchored field mapping (deviceInfoFields) keeps the version correct on
partial frames.

Gemini (fix-then-ship): removed the unused deviceBuildDate field/getter (dead
state) — the build date stays in the connect log via the raw strings, and
deviceInfoFields still parses + tests it.

Closes #134

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/143/head
Strycher 4 weeks ago
parent cd7794b69f
commit 13e38230c6

@ -196,6 +196,8 @@ class MeshCoreConnector extends ChangeNotifier {
bool? _clientRepeat;
MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState;
int? _firmwareVerCode;
String? _firmwareVersion;
String? _deviceModel;
int? _offbandCaps;
int _pathHashByteWidth = 1;
CompanionRadioStats? _latestRadioStats;
@ -448,6 +450,12 @@ class MeshCoreConnector extends ChangeNotifier {
int? get firmwareVerCode => _firmwareVerCode;
/// Readable firmware version / model from the device-info reply (#134); null
/// until a device-info frame arrives, or on older firmware that omits the
/// strings. (Build date is parsed too but only surfaced in the connect log.)
String? get firmwareVersion => _firmwareVersion;
String? get deviceModel => _deviceModel;
/// `offband_caps` capability bitfield from the device-info reply (v14+);
/// null on older firmware that sends a shorter frame.
int? get offbandCaps => _offbandCaps;
@ -2539,6 +2547,8 @@ class MeshCoreConnector extends ChangeNotifier {
_clientRepeat = null;
_rememberedNonRepeatRadioState = null;
_firmwareVerCode = null;
_firmwareVersion = null;
_deviceModel = null;
_batteryMillivolts = null;
_repeaterBatterySnapshots.clear();
_batteryRequested = false;
@ -4115,6 +4125,17 @@ class MeshCoreConnector extends ChangeNotifier {
}
_firmwareVerCode = frame[1];
// Readable build date / model / firmware version NUL-terminated strings
// after the 8-byte header, before the binary config block. (#134)
final infoStrings = parseDeviceInfoStrings(frame);
final info = deviceInfoFields(infoStrings);
_firmwareVersion = info.version;
_deviceModel = info.model;
_appDebugLogService?.info(
'Device info: ${infoStrings.isEmpty ? '(no strings)' : infoStrings.join(' · ')}',
tag: 'Device',
);
// Parse client_repeat from firmware v9+ (byte 80)
if (frame.length >= 81) {
_clientRepeat = frame[80] != 0;

@ -465,6 +465,42 @@ int? realHopCount(int? rawByteLen, int hashWidth) {
return rawByteLen ~/ w;
}
/// Readable strings from a RESP_CODE_DEVICE_INFO frame: build date, model, and
/// firmware version, NUL-terminated after the 8-byte header and before the
/// binary config block (client_repeat/path-hash/caps live at bytes 80-82).
/// Collects printable-ASCII runs (>=2 chars) from byte 8 up to byte 80. (#134)
List<String> parseDeviceInfoStrings(Uint8List frame) {
final out = <String>[];
final buf = StringBuffer();
final end = frame.length < 80 ? frame.length : 80;
for (var i = 8; i < end; i++) {
final b = frame[i];
if (b >= 0x20 && b < 0x7f) {
buf.writeCharCode(b);
} else {
if (buf.length >= 2) out.add(buf.toString());
buf.clear();
}
}
if (buf.length >= 2) out.add(buf.toString());
return out;
}
/// Maps a [parseDeviceInfoStrings] list to (version, model, build date),
/// anchored from the END: the firmware version is the last string and the most
/// likely to be present, model second-to-last, build date third-to-last. A
/// partial set keeps the version correct instead of shifting every field. (#134)
({String? version, String? model, String? buildDate}) deviceInfoFields(
List<String> strings,
) {
final n = strings.length;
return (
version: n >= 1 ? strings[n - 1] : null,
model: n >= 2 ? strings[n - 2] : null,
buildDate: n >= 3 ? strings[n - 3] : null,
);
}
// Helper to convert uint32 to hex string
String ackHashToHex(int ackHash) {
return ackHash.toRadixString(16).padLeft(8, '0');

@ -224,6 +224,8 @@
"settings_infoName": "Name",
"settings_infoId": "ID",
"settings_infoStatus": "Status",
"settings_infoFirmware": "Firmware",
"settings_infoModel": "Model",
"settings_infoBattery": "Battery",
"settings_infoPublicKey": "Public Key",
"settings_infoContactsCount": "Contacts Count",

@ -1102,6 +1102,18 @@ abstract class AppLocalizations {
/// **'Status'**
String get settings_infoStatus;
/// No description provided for @settings_infoFirmware.
///
/// In en, this message translates to:
/// **'Firmware'**
String get settings_infoFirmware;
/// No description provided for @settings_infoModel.
///
/// In en, this message translates to:
/// **'Model'**
String get settings_infoModel;
/// No description provided for @settings_infoBattery.
///
/// In en, this message translates to:

@ -552,6 +552,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_infoStatus => 'Статус';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Батерия';

@ -547,6 +547,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_infoStatus => 'Status';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Akku';

@ -537,6 +537,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_infoStatus => 'Status';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Battery';

@ -548,6 +548,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_infoStatus => 'Estado';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batería';

@ -552,6 +552,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_infoStatus => 'État';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batterie';

@ -550,6 +550,12 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get settings_infoStatus => 'Állapot';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Akku';

@ -550,6 +550,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_infoStatus => 'Stato';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batteria';

@ -523,6 +523,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get settings_infoStatus => 'ステータス';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'バッテリー';

@ -523,6 +523,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get settings_infoStatus => '상태';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => '배터리';

@ -544,6 +544,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_infoStatus => 'Status';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batterij';

@ -552,6 +552,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_infoStatus => 'Stan';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Bateria';

@ -550,6 +550,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_infoStatus => 'Estado';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Bateria';

@ -549,6 +549,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_infoStatus => 'Статус';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Батарея';

@ -544,6 +544,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_infoStatus => 'Stav';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batéria';

@ -541,6 +541,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_infoStatus => 'Stanje';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Baterija';

@ -539,6 +539,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_infoStatus => 'Status';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Batteri';

@ -547,6 +547,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_infoStatus => 'Статус';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => 'Батарея';

@ -515,6 +515,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_infoStatus => '状态';
@override
String get settings_infoFirmware => 'Firmware';
@override
String get settings_infoModel => 'Model';
@override
String get settings_infoBattery => '电池';

@ -369,6 +369,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
MeshCoreConnector connector,
) {
final l10n = context.l10n;
final firmwareVersion = connector.firmwareVersion;
final deviceModel = connector.deviceModel;
return Card(
child: Padding(
@ -384,6 +386,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
? l10n.common_connected
: l10n.common_disconnected,
),
if (firmwareVersion != null)
_buildInfoRow(l10n.settings_infoFirmware, firmwareVersion),
if (deviceModel != null)
_buildInfoRow(l10n.settings_infoModel, deviceModel),
_buildBatteryInfoRow(context, connector),
if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),

@ -0,0 +1,78 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
void main() {
group('parseDeviceInfoStrings', () {
Uint8List frameWith(List<String> strings, {bool pad = true}) {
final frame = <int>[0x0d, 0x0e, 0x02, 0x03, 0, 0, 0, 0]; // 8-byte header
for (final s in strings) {
frame.addAll(s.codeUnits);
frame.add(0); // NUL terminator
}
if (pad) {
while (frame.length < 80) {
frame.add(0);
}
frame.addAll([1, 1, 0]); // client_repeat, path-hash mode, caps
}
return Uint8List.fromList(frame);
}
test('extracts build date, model, and firmware version in order', () {
final out = parseDeviceInfoStrings(
frameWith(['24-Jun-2026', 'RAK 3401', '1.7.1']),
);
expect(out, ['24-Jun-2026', 'RAK 3401', '1.7.1']);
});
test('returns empty for a header-only frame', () {
expect(
parseDeviceInfoStrings(Uint8List.fromList(List<int>.filled(8, 0))),
isEmpty,
);
});
test('ignores printable bytes in the binary config block (byte 80+)', () {
final frame = <int>[0x0d, 0x0e, 0, 0, 0, 0, 0, 0];
frame.addAll('1.7.1'.codeUnits);
frame.add(0);
while (frame.length < 80) {
frame.add(0);
}
frame.addAll('ABCD'.codeUnits); // printable but past the scan window
expect(parseDeviceInfoStrings(Uint8List.fromList(frame)), ['1.7.1']);
});
});
group('deviceInfoFields', () {
test('three strings map to build date, model, and version', () {
final f = deviceInfoFields(['24-Jun-2026', 'RAK 3401', '1.7.1']);
expect(f.version, '1.7.1');
expect(f.model, 'RAK 3401');
expect(f.buildDate, '24-Jun-2026');
});
test('two strings keep version + model, drop build date', () {
final f = deviceInfoFields(['RAK 3401', '1.7.1']);
expect(f.version, '1.7.1');
expect(f.model, 'RAK 3401');
expect(f.buildDate, isNull);
});
test('one string is the version', () {
final f = deviceInfoFields(['1.7.1']);
expect(f.version, '1.7.1');
expect(f.model, isNull);
expect(f.buildDate, isNull);
});
test('empty maps to all null', () {
final f = deviceInfoFields([]);
expect(f.version, isNull);
expect(f.model, isNull);
expect(f.buildDate, isNull);
});
});
}

@ -2,6 +2,8 @@
"bg": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -14,6 +16,8 @@
"de": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -26,6 +30,8 @@
"es": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -38,6 +44,8 @@
"fr": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -50,6 +58,8 @@
"hu": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -62,6 +72,8 @@
"it": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -74,6 +86,8 @@
"ja": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -86,6 +100,8 @@
"ko": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -98,6 +114,8 @@
"nl": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -110,6 +128,8 @@
"pl": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -122,6 +142,8 @@
"pt": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -134,6 +156,8 @@
"ru": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -146,6 +170,8 @@
"sk": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -158,6 +184,8 @@
"sl": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -170,6 +198,8 @@
"sv": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -182,6 +212,8 @@
"uk": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",
@ -194,6 +226,8 @@
"zh": [
"settings_messageSettings",
"settings_messageSettingsSubtitle",
"settings_infoFirmware",
"settings_infoModel",
"appSettings_clock",
"appSettings_clockSystem",
"appSettings_clock12h",

Loading…
Cancel
Save

Powered by TurnKey Linux.