This commit is contained in:
HDDen
2026-05-09 02:18:00 +03:00
55 changed files with 13669 additions and 1800 deletions
+78
View File
@@ -166,6 +166,8 @@ class MeshCoreConnector extends ChangeNotifier {
Timer? _selfInfoRetryTimer;
Timer? _reconnectTimer;
Timer? _batteryPollTimer;
Timer? _gpsLocationPollTimer;
static const _gpsLocationPollInterval = Duration(minutes: 1);
Timer? _radioStatsPollTimer;
int _radioStatsPollRefCount = 0;
final ValueNotifier<CompanionRadioStats?> radioStatsNotifier =
@@ -253,6 +255,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> _pathOpLock = Future.value();
Map<String, String>? _currentCustomVars;
/// Maps repeater pubkey-prefix hex (12 hex chars = first 6 bytes) → the
/// repeater's RTC clock at the moment of the most recent successful login.
/// Reported by firmware in the login-success push frame at byte offset 8.
final Map<String, DateTime> _repeaterLoginClocks = {};
// Channel syncing state (sequential pattern)
bool _isSyncingChannels = false;
bool _channelSyncInFlight = false;
@@ -404,6 +411,17 @@ class MeshCoreConnector extends ChangeNotifier {
int get advertLocationPolicy => _advertLocPolicy;
int get multiAcks => _multiAcks;
bool? get clientRepeat => _clientRepeat;
/// Returns the repeater's RTC clock at the time of the most recent
/// successful login, looked up by the contact's full public key.
/// Returns null if no login response has been observed for this repeater
/// since connection.
DateTime? repeaterClockAtLogin(Uint8List publicKey) {
if (publicKey.length < 6) return null;
final prefix = pubKeyToHex(publicKey.sublist(0, 6));
return _repeaterLoginClocks[prefix];
}
void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) {
_rememberedNonRepeatRadioState = snapshot;
}
@@ -2467,6 +2485,25 @@ class MeshCoreConnector extends ChangeNotifier {
_batteryPollTimer = null;
}
/// Start polling the radio's GPS-backed self-info every minute.
/// No-op if already running. Triggered when the radio reports `gps=1`.
void _startGpsLocationPolling() {
if (_gpsLocationPollTimer != null) return;
_gpsLocationPollTimer = Timer.periodic(_gpsLocationPollInterval, (timer) {
if (!isConnected) {
timer.cancel();
_gpsLocationPollTimer = null;
return;
}
unawaited(sendFrame(buildAppStartFrame()));
});
}
void _stopGpsLocationPolling() {
_gpsLocationPollTimer?.cancel();
_gpsLocationPollTimer = null;
}
void setPollingInterval(int i) {
_pollingInterval = i.clamp(1, 60);
if (isConnected) {
@@ -3304,6 +3341,18 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> setCustomVar(String value) async {
if (!isConnected) return;
await sendFrame(buildSetCustomVarFrame(value));
final sep = value.indexOf(':');
if (sep > 0) {
final key = value.substring(0, sep);
final val = value.substring(sep + 1);
(_currentCustomVars ??= <String, String>{})[key] = val;
notifyListeners();
}
if (value == 'gps:1') {
_startGpsLocationPolling();
} else if (value == 'gps:0') {
_stopGpsLocationPolling();
}
}
Future<void> sendSelfAdvert({bool flood = true}) async {
@@ -3611,6 +3660,8 @@ class MeshCoreConnector extends ChangeNotifier {
_handlePathUpdated(frame);
break;
case pushCodeLoginSuccess:
_handleLoginSuccess(frame);
break;
case pushCodeLoginFail:
case pushCodeStatusResponse:
break;
@@ -5672,6 +5723,7 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleDisconnection() {
_stopBatteryPolling();
_stopGpsLocationPolling();
_stopRadioStatsPolling();
_latestRadioStats = null;
radioStatsNotifier.value = null;
@@ -5757,10 +5809,35 @@ class MeshCoreConnector extends ChangeNotifier {
return result;
}
/// Parse PUSH_CODE_LOGIN_SUCCESS (0x85) frame and stash the repeater's
/// reported clock. Frame layout (firmware companion_radio/MyMesh.cpp:678+):
/// [0]=0x85, [1]=permissions, [2..7]=pubkey prefix (6 bytes),
/// [8..11]=repeater RTC unix seconds (LE), [12]=ACL perms, [13]=fw level
/// The timestamp is only present in the v7+ "new login response" — older
/// firmware emits a shorter frame that we silently skip.
void _handleLoginSuccess(Uint8List frame) {
if (frame.length < 12) return;
final prefix = pubKeyToHex(frame.sublist(2, 8));
final ts = ByteData.sublistView(frame, 8, 12).getUint32(0, Endian.little);
if (ts == 0) return;
_repeaterLoginClocks[prefix] = DateTime.fromMillisecondsSinceEpoch(
ts * 1000,
isUtc: true,
);
notifyListeners();
}
void _handleCustomVars(Uint8List frame) {
final buf = BufferReader(frame.sublist(1));
try {
_currentCustomVars = _parseKeyValueString(buf.readCString());
// Reflect current GPS state in the polling timer (handles initial
// device state on connect as well as external CLI/USB toggles).
if (_currentCustomVars?['gps'] == '1') {
_startGpsLocationPolling();
} else {
_stopGpsLocationPolling();
}
} catch (e) {
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
}
@@ -5816,6 +5893,7 @@ class MeshCoreConnector extends ChangeNotifier {
_notifyListenersTimer?.cancel();
_reconnectTimer?.cancel();
_batteryPollTimer?.cancel();
_gpsLocationPollTimer?.cancel();
_radioStatsPollTimer?.cancel();
radioStatsNotifier.dispose();
_receivedFramesController.close();