chore(#298): log path bytes + applied hash width

Captures previously logged only a path byte length, making it impossible
to tell one 2-byte hop from two 1-byte hops - the exact question #240 and
#279 turn on. Bandit's 2026-07-18 log hit this wall.

Adds a shared _pathDiag() rendering (len, applied width, derived hops,
hop-grouped bytes) to the existing contact path log sites, and the same
detail to the repeater/room "Login routing" lines.

Also logs the path-hash width transition on every device-info frame, with
the frame length, so a short-frame downgrade to width 1 (#240) is visible
in any capture without needing the device. Warn on change, info otherwise.

Existing log sites only - no new per-frame logging (SAFELANE 11.10).
Display labels (#279) and the calculateTimeout unit bug (#299) are out of
scope here.
chore/298-path-logging
Strycher 3 days ago
parent 2ff190cee1
commit e632395b02

@ -22,6 +22,7 @@ import '../helpers/time_anomaly.dart';
import '../helpers/cyr2lat.dart';
import '../helpers/smaz.dart';
import '../helpers/cayenne_lpp.dart';
import '../helpers/path_helper.dart';
import '../services/app_debug_log_service.dart';
import '../services/ble_debug_log_service.dart';
import '../services/linux_ble_error_classifier.dart';
@ -491,6 +492,23 @@ class MeshCoreConnector extends ChangeNotifier {
int get pathHashByteWidth => _pathHashByteWidth;
/// Compact path rendering for logs: the raw byte length, the width it is
/// being sliced at, the resulting hop count, and the hop-grouped bytes.
///
/// Captures previously logged only the byte length, which made it impossible
/// to tell a single 2-byte hop from two 1-byte hops the exact question
/// #240/#279 turn on. One line, existing log sites only, no new per-frame
/// logging. (#298)
String _pathDiag(List<int> pathBytes, int byteLen) {
if (byteLen < 0) return 'flood';
final w = _pathHashByteWidth;
final hops = realHopCount(byteLen, w);
final bytes = pathBytes.isEmpty
? 'none'
: PathHelper.formatPathHex(pathBytes, w);
return 'len=$byteLen w=$w hops=$hops [$bytes]';
}
CompanionRadioStats? get latestRadioStats => _latestRadioStats;
bool get supportsCompanionRadioStats => (_firmwareVerCode ?? 0) >= 8;
@ -4599,7 +4617,8 @@ class MeshCoreConnector extends ChangeNotifier {
_firmwareVersion = info.version;
_deviceModel = info.model;
_appDebugLogService?.info(
'Device info: ${infoStrings.isEmpty ? '(no strings)' : infoStrings.join(' · ')}',
'Device info: ${infoStrings.isEmpty ? '(no strings)' : infoStrings.join(' · ')}'
' (frameLen=${frame.length})',
tag: 'Device',
);
@ -4608,12 +4627,26 @@ class MeshCoreConnector extends ChangeNotifier {
_clientRepeat = frame[80] != 0;
}
// Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
final priorPathHashByteWidth = _pathHashByteWidth;
if (frame.length >= 82) {
final mode = (frame[81] & 0xFF).clamp(0, 2);
_pathHashByteWidth = mode + 1;
} else {
_pathHashByteWidth = 1;
}
// A short frame silently downgrades a known-good width to 1 (#240). Log the
// frame length and the before/after width so any capture shows whether that
// happened, without needing the device. (#298)
final widthChanged = _pathHashByteWidth != priorPathHashByteWidth;
final widthNote =
'Path hash width: $priorPathHashByteWidth -> $_pathHashByteWidth '
'(device-info frame len=${frame.length}'
'${frame.length < 82 ? ', SHORT: no byte 81, forced to 1' : ''})';
if (widthChanged) {
_appDebugLogService?.warn(widthNote, tag: 'Device');
} else {
_appDebugLogService?.info(widthNote, tag: 'Device');
}
// Offband config capability v14+ (byte 82). Extracted + bounds-checked in a
// testable helper; offset verified against firmware (see parseOffbandCaps).
_offbandCaps = parseOffbandCaps(frame);
@ -4918,7 +4951,7 @@ class MeshCoreConnector extends ChangeNotifier {
: contact.lastMessageAt;
appLogger.info(
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
'Refreshing contact ${contact.name}: devicePath=${_pathDiag(contact.path, contact.pathLength)}, existingOverride=${existing.pathOverride}',
tag: 'Connector',
);
@ -4933,7 +4966,7 @@ class MeshCoreConnector extends ChangeNotifier {
);
appLogger.info(
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength)}',
tag: 'Connector',
);
} else {
@ -4944,7 +4977,7 @@ class MeshCoreConnector extends ChangeNotifier {
isContact) {
_contacts.add(contact);
appLogger.info(
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
'Added new contact ${contact.name}: pathLen=${_pathDiag(contact.path, contact.pathLength)}',
tag: 'Connector',
);
} else {
@ -5032,7 +5065,7 @@ class MeshCoreConnector extends ChangeNotifier {
);
appLogger.info(
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength)}',
tag: 'Connector',
);
} else {
@ -7154,7 +7187,7 @@ class MeshCoreConnector extends ChangeNotifier {
: existing.lastMessageAt;
appLogger.info(
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
'Refreshing contact ${existing.name}: devicePath=${_pathDiag(existing.path, existing.pathLength)}, existingOverride=${existing.pathOverride}',
tag: 'Connector',
);
@ -7180,7 +7213,7 @@ class MeshCoreConnector extends ChangeNotifier {
_updateDirectRepeater(_contacts[existingIndex], snr, path);
appLogger.info(
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_pathDiag(_contacts[existingIndex].path, _contacts[existingIndex].pathLength)}',
tag: 'Connector',
);
}

@ -11,6 +11,7 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
import '../helpers/path_helper.dart';
import 'path_management_dialog.dart';
class RepeaterLoginDialog extends StatefulWidget {
@ -115,9 +116,15 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs + 2000);
// selection.hopCount is a BYTE count, not hops (#279). Log bytes, the
// applied width, the derived hop count and the hop-grouped bytes so a
// capture is unambiguous. (#298)
final routingWidth = _connector.pathHashByteWidth;
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
: 'bytes=${selection.hopCount} w=$routingWidth '
'hops=${realHopCount(selection.hopCount, routingWidth)} '
'[${PathHelper.formatPathHex(selection.pathBytes, routingWidth)}]';
appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin');
bool? loginResult;
bool isAdmin = false;

@ -11,6 +11,7 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
import '../helpers/path_helper.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_management_dialog.dart';
@ -111,9 +112,15 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs + 2000);
// selection.hopCount is a BYTE count, not hops (#279). Log bytes, the
// applied width, the derived hop count and the hop-grouped bytes so a
// capture is unambiguous. (#298)
final routingWidth = _connector.pathHashByteWidth;
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
: 'bytes=${selection.hopCount} w=$routingWidth '
'hops=${realHopCount(selection.hopCount, routingWidth)} '
'[${PathHelper.formatPathHex(selection.pathBytes, routingWidth)}]';
appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin');
bool? loginResult;
bool isAdmin = false;

Loading…
Cancel
Save

Powered by TurnKey Linux.