Add region management

This adds region management: the user can manage a list of available regions
and for each channel pick a region from that list to apply to messages.

Region discovery from nearby repeaters will be done in a separate PR.

This is a part of the work needed for #120.
This commit is contained in:
Stephan Rodemeier
2026-04-05 21:35:39 +02:00
parent 0757c8e53a
commit 0e074fd806
33 changed files with 1653 additions and 68 deletions
+47 -12
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:math' as math;
import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/storage/region_store.dart';
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
@@ -34,6 +35,7 @@ import 'meshcore_connector_tcp.dart';
import '../storage/channel_message_store.dart';
import '../storage/channel_order_store.dart';
import '../storage/channel_settings_store.dart';
import '../storage/channel_region_store.dart';
import '../storage/channel_store.dart';
import '../storage/contact_discovery_store.dart';
import '../storage/contact_settings_store.dart';
@@ -276,6 +278,7 @@ class MeshCoreConnector extends ChangeNotifier {
final MessageStore _messageStore = MessageStore();
final ChannelOrderStore _channelOrderStore = ChannelOrderStore();
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
final ChannelRegionStore _channelRegionStore = ChannelRegionStore();
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
final ContactStore _contactStore = ContactStore();
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
@@ -283,6 +286,7 @@ class MeshCoreConnector extends ChangeNotifier {
final UnreadStore _unreadStore = UnreadStore();
List<Channel> _cachedChannels = [];
final Map<int, bool> _channelSmazEnabled = {};
final Map<int, Region> _channelRegions = {};
bool _lastSentWasCliCommand =
false; // Track if last sent message was a CLI command
final Map<String, bool> _contactSmazEnabled = {};
@@ -603,6 +607,14 @@ class MeshCoreConnector extends ChangeNotifier {
return _contactSmazEnabled[contactKeyHex] ?? false;
}
bool hasChannelRegion(int channelIndex) {
return _channelRegions[channelIndex] != '';
}
Region getChannelRegion(int channelIndex) {
return _channelRegions[channelIndex] ?? '';
}
void ensureContactSmazSettingLoaded(String contactKeyHex) {
_ensureContactSmazSettingLoaded(contactKeyHex);
}
@@ -692,6 +704,14 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
Future<void> setChannelRegion(int channelIndex, String region) async {
_channelRegions[channelIndex] = await _channelRegionStore.saveRegion(
channelIndex,
region,
);
notifyListeners();
}
Future<void> _loadChannelOrder() async {
_channelOrder = await _channelOrderStore.loadChannelOrder();
_applyChannelOrder();
@@ -840,9 +860,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> loadChannelSettings({int? maxChannels}) async {
_channelSmazEnabled.clear();
_channelRegions.clear();
final channelCount = maxChannels ?? _maxChannels;
for (int i = 0; i < channelCount; i++) {
_channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i);
_channelRegions[i] = await _channelRegionStore.loadRegion(i);
}
}
@@ -2973,12 +2995,19 @@ class MeshCoreConnector extends ChangeNotifier {
// Send the reaction to the device (don't add as a visible message)
final reactionQueueId = _nextReactionSendQueueId();
_pendingChannelSentQueue.add(reactionQueueId);
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, text),
channelSendQueueId: reactionQueueId,
expectsGenericAck: true,
);
try {
await sendFrame(
buildSetFloodScopeFrame(getChannelRegion(channel.index)),
);
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, text),
channelSendQueueId: reactionQueueId,
expectsGenericAck: true,
);
} finally {
await sendFrame(buildSetFloodScopeFrame(''));
}
return;
}
@@ -3001,12 +3030,17 @@ class MeshCoreConnector extends ChangeNotifier {
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
? Smaz.encodeIfSmaller(text)
: text;
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText),
channelSendQueueId: message.messageId,
expectsGenericAck: true,
);
try {
await sendFrame(buildSetFloodScopeFrame(getChannelRegion(channel.index)));
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText),
channelSendQueueId: message.messageId,
expectsGenericAck: true,
);
} finally {
await sendFrame(buildSetFloodScopeFrame(''));
}
}
Future<void> removeContact(Contact contact) async {
@@ -3680,6 +3714,7 @@ class MeshCoreConnector extends ChangeNotifier {
_messageStore.setPublicKeyHex = selfPublicKeyHex;
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_channelRegionStore.setPublicKeyHex = selfPublicKeyHex;
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
_contactStore.setPublicKeyHex = selfPublicKeyHex;
_channelStore.setPublicKeyHex = selfPublicKeyHex;
+17
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking
@@ -211,6 +212,7 @@ const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
const int cmdSetPathHashMode = 61;
const int cmdSetFloodScope = 54;
// Text message types
const int txtTypePlain = 0;
@@ -955,3 +957,18 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
}
return writer.toBytes();
}
//Build CMD_SET_FLOOD_SCOPE
// Format: [cmd][scope]
Uint8List buildSetFloodScopeFrame(String region) {
if (region == '') {
// reset scope
return Uint8List.fromList([cmdSetFloodScope, 0]);
}
final name = region.startsWith('#') ? region : '#$region';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
final scope = Uint8List.fromList(hash.sublist(0, 16));
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
}