Regions discovery from nearby repeaters

This commit is contained in:
HDDen
2026-05-31 16:47:22 +03:00
parent b72ade1387
commit 0ca96ce5b1
24 changed files with 737 additions and 13 deletions
+79 -13
View File
@@ -264,6 +264,10 @@ class MeshCoreConnector extends ChangeNotifier {
// Serializes path operations (setContactPath/clearContactPath) to prevent
// interleaved async calls from leaving in-memory state inconsistent with device.
Future<void> _pathOpLock = Future.value();
// Flood scope is a global firmware setting, so scoped channel sends must not
// overlap or a message may inherit another channel's region.
Future<void> _channelScopedSendLock = Future.value();
static const Duration _commandAckTimeout = Duration(seconds: 5);
Map<String, String>? _currentCustomVars;
/// Maps repeater pubkey-prefix hex (12 hex chars = first 6 bytes) → the
@@ -679,7 +683,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
bool hasChannelRegion(int channelIndex) {
return _channelRegions[channelIndex] != '';
return (_channelRegions[channelIndex] ?? '').isNotEmpty;
}
Region getChannelRegion(int channelIndex) {
@@ -3217,19 +3221,14 @@ class MeshCoreConnector extends ChangeNotifier {
// Send the reaction to the device (don't add as a visible message)
final reactionQueueId = _nextReactionSendQueueId();
_pendingChannelSentQueue.add(reactionQueueId);
try {
await sendFrame(
buildSetFloodScopeFrame(getChannelRegion(channel.index)),
);
await _runScopedChannelSend(() async {
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
await _sendFrameAndWaitForCommandAck(
buildSendChannelTextMsgFrame(channel.index, text),
channelSendQueueId: reactionQueueId,
expectsGenericAck: true,
);
} finally {
await sendFrame(buildSetFloodScopeFrame(''));
}
}, region: getChannelRegion(channel.index));
return;
}
@@ -3246,16 +3245,80 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
final outboundText = prepareChannelOutboundText(channel.index, text);
try {
await sendFrame(buildSetFloodScopeFrame(getChannelRegion(channel.index)));
await _runScopedChannelSend(() async {
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
await _sendFrameAndWaitForCommandAck(
buildSendChannelTextMsgFrame(channel.index, outboundText),
channelSendQueueId: message.messageId,
expectsGenericAck: true,
);
}, region: getChannelRegion(channel.index));
}
Future<void> _runScopedChannelSend(
Future<void> Function() action, {
required String region,
}) async {
final prev = _channelScopedSendLock;
final completer = Completer<void>();
_channelScopedSendLock = completer.future;
await prev;
try {
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(region));
try {
await action();
} finally {
if (isConnected) {
await _sendFrameAndWaitForCommandAck(buildSetFloodScopeFrame(''));
}
}
} finally {
await sendFrame(buildSetFloodScopeFrame(''));
completer.complete();
}
}
Future<void> _sendFrameAndWaitForCommandAck(
Uint8List data, {
String? channelSendQueueId,
bool expectsGenericAck = false,
}) async {
final completer = Completer<void>();
late final StreamSubscription<Uint8List> subscription;
late final Timer timeout;
void complete() {
if (!completer.isCompleted) completer.complete();
}
void completeError(Object error) {
if (!completer.isCompleted) completer.completeError(error);
}
subscription = receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeOk) {
complete();
} else if (frame[0] == respCodeErr) {
final errCode = frame.length > 1 ? frame[1] : -1;
completeError(Exception('Command failed with error code $errCode'));
}
});
timeout = Timer(_commandAckTimeout, () {
completeError(TimeoutException('Command ACK timed out'));
});
try {
await sendFrame(
data,
channelSendQueueId: channelSendQueueId,
expectsGenericAck: expectsGenericAck,
);
await completer.future;
} finally {
timeout.cancel();
await subscription.cancel();
}
}
@@ -3858,6 +3921,9 @@ class MeshCoreConnector extends ChangeNotifier {
case pushCodePathUpdated:
_handlePathUpdated(frame);
break;
case pushCodeControlData:
// Optional feature-specific services listen to receivedFrames directly.
break;
case pushCodeLoginSuccess:
_handleLoginSuccess(frame);
break;
+69
View File
@@ -207,6 +207,7 @@ const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdSendControlData = 55;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
@@ -226,6 +227,12 @@ const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbors = 0x06;
const int anonReqTypeRegions = 0x01;
// Control data sub-types used by MeshCore discovery packets.
const int controlSubtypeDiscoverReq = 0x08;
const int controlSubtypeDiscoverResp = 0x09;
// Repeater response codes
const int respServerLoginOk = 0;
@@ -268,6 +275,7 @@ const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
const int pushCodeControlData = 0x8E;
// Contact/advertisement types
const int advTypeChat = 1;
@@ -857,6 +865,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
return writer.toBytes();
}
Uint8List buildSendControlDataFrame(Uint8List payload) {
final writer = BufferWriter();
writer.writeByte(cmdSendControlData);
writer.writeBytes(payload);
return writer.toBytes();
}
Uint8List buildDiscoveryRequestPayload(
int tag, {
bool prefixOnly = false,
int typeMask = 1 << advTypeRepeater,
}) {
final writer = BufferWriter();
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
// subtype 0x8, with the low bit selecting short/full public keys in replies.
writer.writeByte(
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
);
writer.writeByte(typeMask);
writer.writeUInt32LE(tag);
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
return writer.toBytes();
}
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
if (path.isEmpty) return Uint8List(0);
final width = pathHashWidth.clamp(1, 4).toInt();
if (path.length % width != 0) {
return Uint8List.fromList(path.reversed.toList());
}
final reversed = Uint8List(path.length);
final hops = path.length ~/ width;
for (var i = 0; i < hops; i++) {
final from = (hops - 1 - i) * width;
reversed.setRange(i * width, (i + 1) * width, path, from);
}
return reversed;
}
// Build CMD_SEND_ANON_REQ frame.
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
Uint8List buildSendAnonReqFrame(
Uint8List repeaterPubKey, {
required int requestType,
Uint8List? replyPath,
int replyHopCount = 0,
int pathHashWidth = pathHashSize,
}) {
final width = pathHashWidth.clamp(1, 4).toInt();
final path = replyPath ?? Uint8List(0);
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
final writer = BufferWriter();
writer.writeByte(cmdSendAnonReq);
writer.writeBytes(repeaterPubKey);
writer.writeByte(requestType);
writer.writeByte(encodedPathLen);
writer.writeBytes(_reversePathByHop(path, width));
return writer.toBytes();
}
//Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {