mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 23:24:29 +10:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f870e77e98 | |||
| e6658a6026 | |||
| c81791cf1e | |||
| 6da54e13c3 | |||
| 1fba5312a2 | |||
| a1b77bb29b | |||
| 4eecfc92dc | |||
| 90c8cf5f3e | |||
| 06fa176367 | |||
| e4285774a0 | |||
| b2da695102 | |||
| e1327a93c7 | |||
| 421bc71bb7 | |||
| 84ec139ce6 | |||
| b748b96237 | |||
| c2671ac2ae |
@@ -291,6 +291,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
bool get isLoadingChannels => _isLoadingChannels;
|
bool get isLoadingChannels => _isLoadingChannels;
|
||||||
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
|
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
|
||||||
Uint8List? get selfPublicKey => _selfPublicKey;
|
Uint8List? get selfPublicKey => _selfPublicKey;
|
||||||
|
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));
|
||||||
String? get selfName => _selfName;
|
String? get selfName => _selfName;
|
||||||
double? get selfLatitude => _selfLatitude;
|
double? get selfLatitude => _selfLatitude;
|
||||||
double? get selfLongitude => _selfLongitude;
|
double? get selfLongitude => _selfLongitude;
|
||||||
@@ -663,6 +664,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Initialize notification service
|
// Initialize notification service
|
||||||
_notificationService.initialize();
|
_notificationService.initialize();
|
||||||
_loadChannelOrder();
|
_loadChannelOrder();
|
||||||
|
_loadDiscoveredContactCache();
|
||||||
|
|
||||||
// Initialize retry service callbacks
|
// Initialize retry service callbacks
|
||||||
_retryService?.initialize(
|
_retryService?.initialize(
|
||||||
@@ -691,7 +693,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadDiscoveredContactCache() async {
|
Future<void> _loadDiscoveredContactCache() async {
|
||||||
final cached = await _discoveryContactStore.loadContacts();
|
final cached = await _discoveryContactStore.loadContacts();
|
||||||
_discoveredContacts
|
_discoveredContacts
|
||||||
..clear()
|
..clear()
|
||||||
@@ -906,10 +908,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
await _usbFrameSubscription?.cancel();
|
await _usbFrameSubscription?.cancel();
|
||||||
_usbFrameSubscription = null;
|
_usbFrameSubscription = null;
|
||||||
_appDebugLogService?.info(
|
_appDebugLogService?.info('connectUsb: opening serial port…', tag: 'USB');
|
||||||
'connectUsb: opening serial port…',
|
|
||||||
tag: 'USB',
|
|
||||||
);
|
|
||||||
await _usbManager.connect(portName: portName, baudRate: baudRate);
|
await _usbManager.connect(portName: portName, baudRate: baudRate);
|
||||||
_appDebugLogService?.info(
|
_appDebugLogService?.info(
|
||||||
'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}',
|
'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}',
|
||||||
@@ -1196,7 +1195,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
await _requestDeviceInfo();
|
await _requestDeviceInfo();
|
||||||
_startBatteryPolling();
|
_startBatteryPolling();
|
||||||
unawaited(loadDiscoveredContactCache());
|
|
||||||
|
|
||||||
final gotSelfInfo = await _waitForSelfInfo(
|
final gotSelfInfo = await _waitForSelfInfo(
|
||||||
timeout: const Duration(seconds: 3),
|
timeout: const Duration(seconds: 3),
|
||||||
@@ -1952,6 +1950,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
type: contact.type,
|
type: contact.type,
|
||||||
pathLength: contact.pathLength,
|
pathLength: contact.pathLength,
|
||||||
path: contact.path,
|
path: contact.path,
|
||||||
|
latitude: contact.latitude,
|
||||||
|
longitude: contact.longitude,
|
||||||
lastSeen: DateTime.now(),
|
lastSeen: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2250,6 +2250,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_hasLoadedChannels = true;
|
_hasLoadedChannels = true;
|
||||||
_previousChannelsCache.clear();
|
_previousChannelsCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: if contact sync was deferred waiting for channel 0 but
|
||||||
|
// channel sync finished without triggering it, start contacts now.
|
||||||
|
if (_pendingInitialContactsSync && isConnected) {
|
||||||
|
_pendingInitialContactsSync = false;
|
||||||
|
unawaited(getContacts());
|
||||||
|
}
|
||||||
|
|
||||||
// Keep cache on failure/disconnection for future attempts
|
// Keep cache on failure/disconnection for future attempts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2306,7 +2314,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
case pushCodeNewAdvert:
|
case pushCodeNewAdvert:
|
||||||
debugPrint('Got New CONTACT');
|
debugPrint('Got New CONTACT');
|
||||||
// It's the same format as respCodeContact, so we can reuse the handler
|
// It's the same format as respCodeContact, so we can reuse the handler
|
||||||
_handleContact(frame);
|
_handleContact(frame, isContact: false);
|
||||||
break;
|
break;
|
||||||
case respCodeContact:
|
case respCodeContact:
|
||||||
debugPrint('Got CONTACT');
|
debugPrint('Got CONTACT');
|
||||||
@@ -2482,6 +2490,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
selfName.isNotEmpty) {
|
selfName.isNotEmpty) {
|
||||||
_usbManager.updateConnectedLabel(selfName);
|
_usbManager.updateConnectedLabel(selfName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//set all the stores' public key so they can load the correct data
|
||||||
|
_channelMessageStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_messageStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_channelOrderStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_channelSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_contactSettingsStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_contactStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_channelStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
_unreadStore.setPublicKeyHex = selfPublicKeyHex;
|
||||||
|
|
||||||
|
// Now that we have self info, we can load all the persisted data for this node
|
||||||
|
_loadChannelOrder();
|
||||||
|
loadContactCache();
|
||||||
|
loadChannelSettings();
|
||||||
|
loadCachedChannels();
|
||||||
|
|
||||||
|
// Load persisted channel messages
|
||||||
|
loadAllChannelMessages();
|
||||||
|
loadUnreadState();
|
||||||
|
|
||||||
_awaitingSelfInfo = false;
|
_awaitingSelfInfo = false;
|
||||||
_selfInfoRetryTimer?.cancel();
|
_selfInfoRetryTimer?.cancel();
|
||||||
_selfInfoRetryTimer = null;
|
_selfInfoRetryTimer = null;
|
||||||
@@ -2655,7 +2684,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleContact(Uint8List frame) {
|
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
||||||
final contact = Contact.fromFrame(frame);
|
final contact = Contact.fromFrame(frame);
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
if (contact.type == advTypeRepeater) {
|
if (contact.type == advTypeRepeater) {
|
||||||
@@ -2694,11 +2723,23 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
tag: 'Connector',
|
tag: 'Connector',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_contacts.add(contact);
|
if ((_autoAddUsers && contact.type == advTypeChat) ||
|
||||||
appLogger.info(
|
(_autoAddRepeaters && contact.type == advTypeRepeater) ||
|
||||||
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
(_autoAddRoomServers && contact.type == advTypeRoom) ||
|
||||||
tag: 'Connector',
|
(_autoAddSensors && contact.type == advTypeSensor) ||
|
||||||
);
|
isContact) {
|
||||||
|
_contacts.add(contact);
|
||||||
|
appLogger.info(
|
||||||
|
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
appLogger.info(
|
||||||
|
"Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings",
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_knownContactKeys.add(contact.publicKeyHex);
|
_knownContactKeys.add(contact.publicKeyHex);
|
||||||
_loadMessagesForContact(contact.publicKeyHex);
|
_loadMessagesForContact(contact.publicKeyHex);
|
||||||
@@ -4270,44 +4311,40 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
void _handleRxData(Uint8List frame) {
|
void _handleRxData(Uint8List frame) {
|
||||||
final packet = BufferReader(frame);
|
final packet = BufferReader(frame);
|
||||||
double snr = 0.0;
|
|
||||||
int routeType = 0;
|
|
||||||
int payloadType = 0;
|
|
||||||
Uint8List pathBytes = Uint8List(0);
|
|
||||||
Uint8List payload = Uint8List(0);
|
|
||||||
try {
|
try {
|
||||||
packet.skipBytes(1); // Skip frame type byte
|
packet.skipBytes(1); // Skip frame type byte
|
||||||
snr = packet.readInt8() / 4.0;
|
final snr = packet.readInt8() / 4.0;
|
||||||
packet.skipBytes(1); // Skip RSSI byte
|
packet.skipBytes(1); // Skip RSSI byte
|
||||||
//final rssi = packet.readByte();
|
//final rssi = packet.readByte();
|
||||||
final header = packet.readByte();
|
final header = packet.readByte();
|
||||||
routeType = header & 0x03;
|
final routeType = header & 0x03;
|
||||||
payloadType = (header >> 2) & 0x0F;
|
final payloadType = (header >> 2) & 0x0F;
|
||||||
if (routeType == _routeTransportFlood ||
|
if (routeType == _routeTransportFlood ||
|
||||||
routeType == _routeTransportDirect) {
|
routeType == _routeTransportDirect) {
|
||||||
packet.skipBytes(4); // Skip transport-specific bytes
|
packet.skipBytes(4); // Skip transport-specific bytes
|
||||||
}
|
}
|
||||||
//final payloadVer = (header >> 6) & 0x03;
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
final pathLen = packet.readByte();
|
final pathLen = packet.readByte();
|
||||||
pathBytes = packet.readBytes(pathLen);
|
final pathBytes = packet.readBytes(pathLen);
|
||||||
payload = packet.readBytes(packet.remaining);
|
final payload = packet.readBytes(packet.remaining);
|
||||||
|
|
||||||
|
final rawPacket = frame.sublist(3);
|
||||||
|
switch (payloadType) {
|
||||||
|
case payloadTypeADVERT:
|
||||||
|
_handlePayloadAdvertReceived(
|
||||||
|
rawPacket,
|
||||||
|
payload,
|
||||||
|
pathBytes,
|
||||||
|
routeType,
|
||||||
|
snr,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final rawPacket = frame.sublist(3);
|
|
||||||
switch (payloadType) {
|
|
||||||
case payloadTypeADVERT:
|
|
||||||
_handlePayloadAdvertReceived(
|
|
||||||
rawPacket,
|
|
||||||
payload,
|
|
||||||
pathBytes,
|
|
||||||
routeType,
|
|
||||||
snr,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void importContact(Uint8List frame) {
|
void importContact(Uint8List frame) {
|
||||||
@@ -4332,8 +4369,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double latitude = 0.0;
|
double? latitude;
|
||||||
double longitude = 0.0;
|
double? longitude;
|
||||||
String name = '';
|
String name = '';
|
||||||
Uint8List publicKey = Uint8List(0);
|
Uint8List publicKey = Uint8List(0);
|
||||||
int type = 0;
|
int type = 0;
|
||||||
@@ -4374,7 +4411,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
pathLength: pathBytes.length,
|
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,
|
||||||
path: Uint8List.fromList(
|
path: Uint8List.fromList(
|
||||||
pathBytes.reversed.toList(),
|
pathBytes.reversed.toList(),
|
||||||
), // Store path in reverse for easier use in outgoing messages
|
), // Store path in reverse for easier use in outgoing messages
|
||||||
@@ -4393,8 +4430,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
double snr,
|
double snr,
|
||||||
) {
|
) {
|
||||||
final advert = BufferReader(payload);
|
final advert = BufferReader(payload);
|
||||||
double latitude = 0.0;
|
double? latitude;
|
||||||
double longitude = 0.0;
|
double? longitude;
|
||||||
String name = '';
|
String name = '';
|
||||||
String contactKeyHex = '';
|
String contactKeyHex = '';
|
||||||
Uint8List publicKey = Uint8List(0);
|
Uint8List publicKey = Uint8List(0);
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ class MeshCoreUsbManager {
|
|||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
Future<List<String>> listPorts() => _service.listPorts();
|
Future<List<String>> listPorts() => _service.listPorts();
|
||||||
|
|
||||||
void setRequestPortLabel(String label) =>
|
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
|
||||||
_service.setRequestPortLabel(label);
|
|
||||||
|
|
||||||
void setFallbackDeviceName(String label) =>
|
void setFallbackDeviceName(String label) =>
|
||||||
_service.setFallbackDeviceName(label);
|
_service.setFallbackDeviceName(label);
|
||||||
@@ -36,7 +35,10 @@ class MeshCoreUsbManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Connection lifecycle ---
|
// --- Connection lifecycle ---
|
||||||
Future<void> connect({required String portName, int baudRate = 115200}) async {
|
Future<void> connect({
|
||||||
|
required String portName,
|
||||||
|
int baudRate = 115200,
|
||||||
|
}) async {
|
||||||
_debugLog?.info(
|
_debugLog?.info(
|
||||||
'UsbManager.connect: portName=$portName baud=$baudRate',
|
'UsbManager.connect: portName=$portName baud=$baudRate',
|
||||||
tag: 'USB',
|
tag: 'USB',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||||
class BufferReader {
|
class BufferReader {
|
||||||
int _pointer = 0;
|
int _pointer = 0;
|
||||||
|
int _lastPointer = 0;
|
||||||
final Uint8List _buffer;
|
final Uint8List _buffer;
|
||||||
|
|
||||||
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
|
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
|
||||||
@@ -13,6 +14,7 @@ class BufferReader {
|
|||||||
int readByte() => readBytes(1)[0];
|
int readByte() => readBytes(1)[0];
|
||||||
|
|
||||||
Uint8List readBytes(int count) {
|
Uint8List readBytes(int count) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
if (_pointer + count > _buffer.length) {
|
if (_pointer + count > _buffer.length) {
|
||||||
throw RangeError(
|
throw RangeError(
|
||||||
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
@@ -24,6 +26,7 @@ class BufferReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void skipBytes(int count) {
|
void skipBytes(int count) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
if (_pointer + count > _buffer.length) {
|
if (_pointer + count > _buffer.length) {
|
||||||
throw RangeError(
|
throw RangeError(
|
||||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
@@ -35,6 +38,7 @@ class BufferReader {
|
|||||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||||
|
|
||||||
String readString() {
|
String readString() {
|
||||||
|
_lastPointer = _pointer;
|
||||||
final value = readRemainingBytes();
|
final value = readRemainingBytes();
|
||||||
try {
|
try {
|
||||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||||
@@ -43,7 +47,8 @@ class BufferReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String readCString(int maxLength) {
|
String readCStringGreedy(int maxLength) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
final value = <int>[];
|
final value = <int>[];
|
||||||
final bytes = readBytes(maxLength);
|
final bytes = readBytes(maxLength);
|
||||||
for (final byte in bytes) {
|
for (final byte in bytes) {
|
||||||
@@ -57,6 +62,24 @@ class BufferReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String readCString(int maxLength) {
|
||||||
|
final backupPointer = _pointer;
|
||||||
|
final value = <int>[];
|
||||||
|
int counter = 0;
|
||||||
|
while (counter < maxLength) {
|
||||||
|
final byte = readByte();
|
||||||
|
if (byte == 0) break;
|
||||||
|
value.add(byte);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
_lastPointer = backupPointer;
|
||||||
|
try {
|
||||||
|
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||||
|
} catch (e) {
|
||||||
|
return String.fromCharCodes(value); // Latin-1 fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||||
int readUInt16LE() =>
|
int readUInt16LE() =>
|
||||||
@@ -78,6 +101,9 @@ class BufferReader {
|
|||||||
if ((value & 0x800000) != 0) value -= 0x1000000;
|
if ((value & 0x800000) != 0) value -= 0x1000000;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetPointer() => _pointer = 0;
|
||||||
|
void rewind() => _pointer = _lastPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer Writer - accumulating binary data builder
|
// Buffer Writer - accumulating binary data builder
|
||||||
|
|||||||
+20
-19
@@ -1,4 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
class Contact {
|
class Contact {
|
||||||
@@ -166,28 +168,27 @@ class Contact {
|
|||||||
|
|
||||||
static Contact? fromFrame(Uint8List data) {
|
static Contact? fromFrame(Uint8List data) {
|
||||||
if (data.isEmpty) return null;
|
if (data.isEmpty) return null;
|
||||||
if (data[0] != respCodeContact) return null;
|
final reader = BufferReader(data);
|
||||||
try {
|
try {
|
||||||
final pubKey = Uint8List.fromList(
|
final respCode = reader.readByte();
|
||||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
|
||||||
);
|
return null;
|
||||||
final type = data[contactTypeOffset];
|
}
|
||||||
final flags = data[contactFlagsOffset];
|
final pubKey = reader.readBytes(pubKeySize);
|
||||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
final type = reader.readByte();
|
||||||
|
final flags = reader.readByte();
|
||||||
|
final pathLen = reader.readByte();
|
||||||
final safePathLen = pathLen > 0
|
final safePathLen = pathLen > 0
|
||||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||||
: 0;
|
: 0;
|
||||||
final pathBytes = safePathLen > 0
|
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||||
? Uint8List.fromList(
|
final name = reader.readCStringGreedy(maxNameSize);
|
||||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
|
||||||
)
|
final lastMod = reader.readUInt32LE();
|
||||||
: Uint8List(0);
|
|
||||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
|
||||||
final lastmod = readUint32LE(data, contactLastModOffset);
|
|
||||||
|
|
||||||
double? lat, lon;
|
double? lat, lon;
|
||||||
final latRaw = readInt32LE(data, contactLatOffset);
|
final latRaw = reader.readInt32LE();
|
||||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
final lonRaw = reader.readInt32LE();
|
||||||
if (latRaw != 0 || lonRaw != 0) {
|
if (latRaw != 0 || lonRaw != 0) {
|
||||||
lat = latRaw / 1e6;
|
lat = latRaw / 1e6;
|
||||||
lon = lonRaw / 1e6;
|
lon = lonRaw / 1e6;
|
||||||
@@ -198,14 +199,14 @@ class Contact {
|
|||||||
name: name.isEmpty ? 'Unknown' : name,
|
name: name.isEmpty ? 'Unknown' : name,
|
||||||
type: type,
|
type: type,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
pathLength: pathLen,
|
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
|
||||||
path: pathBytes,
|
path: pathBytes,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If parsing fails, return null
|
appLogger.error('Failed to parse contact frame: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<MeshCoreConnector>().getChannels();
|
context.read<MeshCoreConnector>().getChannels();
|
||||||
_loadCommunities();
|
_loadCommunities();
|
||||||
@@ -106,7 +108,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
|
||||||
final channelMessageStore = ChannelMessageStore();
|
final channelMessageStore = ChannelMessageStore();
|
||||||
|
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
// Auto-navigate back to scanner if disconnected
|
// Auto-navigate back to scanner if disconnected
|
||||||
if (!checkConnectionAndNavigate(connector)) {
|
if (!checkConnectionAndNavigate(connector)) {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse the community data
|
// Parse the community data
|
||||||
final community = Community.fromQrData(const Uuid().v4(), data);
|
final community = Community.fromQrData(const Uuid().v4(), data);
|
||||||
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
bool addPublicChannel,
|
bool addPublicChannel,
|
||||||
) async {
|
) async {
|
||||||
// Save community to local storage
|
// Save community to local storage
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
await _communityStore.addCommunity(community);
|
await _communityStore.addCommunity(community);
|
||||||
|
|
||||||
// Optionally add the community public channel to the device
|
// Optionally add the community public channel to the device
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, child) {
|
builder: (context, connector, child) {
|
||||||
final isLoading = _isLoadingPorts;
|
final isLoading = _isLoadingPorts;
|
||||||
final showBle = PlatformInfo.isWeb ||
|
final showBle =
|
||||||
|
PlatformInfo.isWeb ||
|
||||||
PlatformInfo.isAndroid ||
|
PlatformInfo.isAndroid ||
|
||||||
PlatformInfo.isIOS;
|
PlatformInfo.isIOS;
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
|
|
||||||
final isConnecting =
|
final isConnecting =
|
||||||
connector.state == MeshCoreConnectionState.connecting &&
|
connector.state == MeshCoreConnectionState.connecting &&
|
||||||
connector.activeTransport == MeshCoreTransportType.usb;
|
connector.activeTransport == MeshCoreTransportType.usb;
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@@ -259,8 +260,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
),
|
),
|
||||||
subtitle: showRawName ? Text(rawName) : null,
|
subtitle: showRawName ? Text(rawName) : null,
|
||||||
trailing: ElevatedButton(
|
trailing: ElevatedButton(
|
||||||
onPressed:
|
onPressed: isConnecting ? null : () => _connectPort(port),
|
||||||
isConnecting ? null : () => _connectPort(port),
|
|
||||||
child: Text(l10n.common_connect),
|
child: Text(l10n.common_connect),
|
||||||
),
|
),
|
||||||
onTap: isConnecting ? null : () => _connectPort(port),
|
onTap: isConnecting ? null : () => _connectPort(port),
|
||||||
@@ -329,8 +329,10 @@ class _UsbScreenState extends State<UsbScreen> {
|
|||||||
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
|
|
||||||
final rawPortName = normalizeUsbPortName(port);
|
final rawPortName = normalizeUsbPortName(port);
|
||||||
appLogger.info('Connect tapped for $port (raw: $rawPortName)',
|
appLogger.info(
|
||||||
tag: 'UsbScreen');
|
'Connect tapped for $port (raw: $rawPortName)',
|
||||||
|
tag: 'UsbScreen',
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _connector.connectUsb(portName: rawPortName);
|
await _connector.connectUsb(portName: rawPortName);
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ class NotificationService {
|
|||||||
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
|
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
|
||||||
if (addr != null && addr.isNotEmpty) return true;
|
if (addr != null && addr.isNotEmpty) return true;
|
||||||
// Fallback: check the default socket for the current user.
|
// Fallback: check the default socket for the current user.
|
||||||
final uid = Platform.environment['UID'] ??
|
final uid = Platform.environment['UID'] ?? Platform.environment['EUID'];
|
||||||
Platform.environment['EUID'];
|
|
||||||
final path = '/run/user/${uid ?? '1000'}/bus';
|
final path = '/run/user/${uid ?? '1000'}/bus';
|
||||||
return File(path).existsSync();
|
return File(path).existsSync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,10 +118,7 @@ class UsbSerialService {
|
|||||||
tag: 'USB Serial',
|
tag: 'USB Serial',
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_debugLogService?.error(
|
_debugLogService?.error('Web connect failed: $error', tag: 'USB Serial');
|
||||||
'Web connect failed: $error',
|
|
||||||
tag: 'USB Serial',
|
|
||||||
);
|
|
||||||
await _cleanupFailedConnect();
|
await _cleanupFailedConnect();
|
||||||
_status = UsbSerialStatus.disconnected;
|
_status = UsbSerialStatus.disconnected;
|
||||||
_connectedPortName = null;
|
_connectedPortName = null;
|
||||||
@@ -268,9 +265,23 @@ class UsbSerialService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openPort(JSObject port, int baudRate) {
|
Future<void> _openPort(JSObject port, int baudRate) async {
|
||||||
final options = JSObject()..['baudRate'] = baudRate.toJS;
|
final options = JSObject()
|
||||||
return port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
|
..['baudRate'] = baudRate.toJS
|
||||||
|
..['flowControl'] = 'none'.toJS;
|
||||||
|
await port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
|
||||||
|
|
||||||
|
// Prevent ESP32 USB-CDC reset: hold DTR=true, RTS=false after open.
|
||||||
|
try {
|
||||||
|
final signals = JSObject()
|
||||||
|
..['dataTerminalReady'] = true.toJS
|
||||||
|
..['requestToSend'] = false.toJS;
|
||||||
|
await port
|
||||||
|
.callMethod<JSPromise<JSAny?>>('setSignals'.toJS, signals)
|
||||||
|
.toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// setSignals may not be supported on all browsers/devices.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanupFailedConnect() async {
|
Future<void> _cleanupFailedConnect() async {
|
||||||
@@ -324,8 +335,12 @@ class UsbSerialService {
|
|||||||
|
|
||||||
Future<void> _pumpReads() async {
|
Future<void> _pumpReads() async {
|
||||||
final reader = _reader;
|
final reader = _reader;
|
||||||
if (reader == null) return;
|
if (reader == null) {
|
||||||
|
_debugLogService?.warn('_pumpReads: reader is null', tag: 'USB Serial');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_debugLogService?.info('_pumpReads: started', tag: 'USB Serial');
|
||||||
try {
|
try {
|
||||||
while (_status == UsbSerialStatus.connected &&
|
while (_status == UsbSerialStatus.connected &&
|
||||||
identical(reader, _reader)) {
|
identical(reader, _reader)) {
|
||||||
@@ -333,6 +348,7 @@ class UsbSerialService {
|
|||||||
.callMethod<JSPromise<JSAny?>>('read'.toJS)
|
.callMethod<JSPromise<JSAny?>>('read'.toJS)
|
||||||
.toDart;
|
.toDart;
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
_debugLogService?.warn('_pumpReads: null result', tag: 'USB Serial');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
final resultObject = result as JSObject;
|
final resultObject = result as JSObject;
|
||||||
@@ -340,20 +356,27 @@ class UsbSerialService {
|
|||||||
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
|
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
|
||||||
final done = doneValue != null && doneValue.dartify() == true;
|
final done = doneValue != null && doneValue.dartify() == true;
|
||||||
if (done) {
|
if (done) {
|
||||||
|
_debugLogService?.info('_pumpReads: done=true', tag: 'USB Serial');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final value = resultObject.getProperty<JSAny?>('value'.toJS);
|
final value = resultObject.getProperty<JSAny?>('value'.toJS);
|
||||||
final bytes = _coerceBytes(value);
|
final bytes = _coerceBytes(value);
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB RX raw: ${bytes.length} byte(s)',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
_ingestRawBytes(bytes);
|
_ingestRawBytes(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
|
_debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial');
|
||||||
if (_status == UsbSerialStatus.connected) {
|
if (_status == UsbSerialStatus.connected) {
|
||||||
_addFrameError(error, stackTrace);
|
_addFrameError(error, stackTrace);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
_debugLogService?.info('_pumpReads: ended', tag: 'USB Serial');
|
||||||
_releaseLock(reader);
|
_releaseLock(reader);
|
||||||
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
|
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
|
||||||
_addFrameError(StateError('USB serial connection closed'));
|
_addFrameError(StateError('USB serial connection closed'));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
import '../helpers/smaz.dart';
|
import '../helpers/smaz.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
@@ -7,13 +9,25 @@ import 'prefs_manager.dart';
|
|||||||
class ChannelMessageStore {
|
class ChannelMessageStore {
|
||||||
static const String _keyPrefix = 'channel_messages_';
|
static const String _keyPrefix = 'channel_messages_';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
/// Save messages for a specific channel
|
/// Save messages for a specific channel
|
||||||
Future<void> saveChannelMessages(
|
Future<void> saveChannelMessages(
|
||||||
int channelIndex,
|
int channelIndex,
|
||||||
List<ChannelMessage> messages,
|
List<ChannelMessage> messages,
|
||||||
) async {
|
) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot save channel messages.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
|
|
||||||
// Convert messages to JSON
|
// Convert messages to JSON
|
||||||
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
|
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
|
||||||
@@ -24,12 +38,35 @@ class ChannelMessageStore {
|
|||||||
|
|
||||||
/// Load messages for a specific channel
|
/// Load messages for a specific channel
|
||||||
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
|
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot load channel messages.',
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
|
final oldKey = '$_keyPrefix$channelIndex';
|
||||||
final jsonString = prefs.getString(key);
|
|
||||||
if (jsonString == null) return [];
|
|
||||||
|
|
||||||
|
String? jsonString = prefs.getString(key);
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(oldKey);
|
||||||
|
prefs.remove(oldKey);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel messages from legacy key $oldKey to scoped key $key',
|
||||||
|
);
|
||||||
|
await prefs.setString(key, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||||
return jsonList.map((json) => _messageFromJson(json)).toList();
|
return jsonList.map((json) => _messageFromJson(json)).toList();
|
||||||
@@ -42,14 +79,14 @@ class ChannelMessageStore {
|
|||||||
/// Clear messages for a specific channel
|
/// Clear messages for a specific channel
|
||||||
Future<void> clearChannelMessages(int channelIndex) async {
|
Future<void> clearChannelMessages(int channelIndex) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
await prefs.remove(key);
|
await prefs.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all channel messages
|
/// Clear all channel messages
|
||||||
Future<void> clearAllChannelMessages() async {
|
Future<void> clearAllChannelMessages() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix));
|
final keys = prefs.getKeys().where((k) => k.startsWith(keyFor));
|
||||||
for (var key in keys) {
|
for (var key in keys) {
|
||||||
await prefs.remove(key);
|
await prefs.remove(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ChannelOrderStore {
|
class ChannelOrderStore {
|
||||||
static const String _key = 'channel_order';
|
static const String _keyPrefix = 'channel_order_';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<void> saveChannelOrder(List<int> order) async {
|
Future<void> saveChannelOrder(List<int> order) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save channel order.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
await prefs.setString(_key, jsonEncode(order));
|
await prefs.setString(keyFor, jsonEncode(order));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<int>> loadChannelOrder() async {
|
Future<List<int>> loadChannelOrder() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load channel order.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final raw = prefs.getString(_key);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (raw == null || raw.isEmpty) return [];
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(raw);
|
final decoded = jsonDecode(jsonString);
|
||||||
if (decoded is List) {
|
if (decoded is List) {
|
||||||
return decoded
|
return decoded
|
||||||
.map((value) => value is int ? value : int.tryParse('$value'))
|
.map((value) => value is int ? value : int.tryParse('$value'))
|
||||||
@@ -24,7 +54,7 @@ class ChannelOrderStore {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// fall through to legacy parse
|
// fall through to legacy parse
|
||||||
}
|
}
|
||||||
return raw
|
return jsonString
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((value) => int.tryParse(value))
|
.map((value) => int.tryParse(value))
|
||||||
.whereType<int>()
|
.whereType<int>()
|
||||||
|
|||||||
@@ -1,17 +1,49 @@
|
|||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ChannelSettingsStore {
|
class ChannelSettingsStore {
|
||||||
static const String _smazKeyPrefix = 'channel_smaz_';
|
static const String _keyPrefix = 'channel_smaz_';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<bool> loadSmazEnabled(int channelIndex) async {
|
Future<bool> loadSmazEnabled(int channelIndex) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot load channel settings.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_smazKeyPrefix$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
|
final oldKey = '$_keyPrefix$channelIndex';
|
||||||
|
bool? enabled = prefs.getBool(oldKey);
|
||||||
|
if (enabled == null) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
enabled = prefs.getBool(oldKey);
|
||||||
|
prefs.remove(oldKey);
|
||||||
|
if (enabled != null) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel settings from legacy key $oldKey to scoped key $key',
|
||||||
|
);
|
||||||
|
await prefs.setBool(key, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
return prefs.getBool(key) ?? false;
|
return prefs.getBool(key) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
|
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot save channel settings.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_smazKeyPrefix$channelIndex';
|
final key = '$keyFor$channelIndex';
|
||||||
await prefs.setBool(key, enabled);
|
await prefs.setBool(key, enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,43 @@ import 'dart:convert';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ChannelStore {
|
class ChannelStore {
|
||||||
static const String _key = 'channels';
|
static const String _keyPrefix = 'channels';
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<List<Channel>> loadChannels() async {
|
Future<List<Channel>> loadChannels() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load channels.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_key);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonStr == null) return [];
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||||
return jsonList
|
return jsonList
|
||||||
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -23,9 +48,13 @@ class ChannelStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveChannels(List<Channel> channels) async {
|
Future<void> saveChannels(List<Channel> channels) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save channels.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonList = channels.map(_toJson).toList();
|
final jsonList = channels.map(_toJson).toList();
|
||||||
await prefs.setString(_key, jsonEncode(jsonList));
|
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _toJson(Channel channel) {
|
Map<String, dynamic> _toJson(Channel channel) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../models/community.dart';
|
import '../models/community.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
/// Persists communities to local storage using SharedPreferences.
|
/// Persists communities to local storage using SharedPreferences.
|
||||||
@@ -9,12 +10,38 @@ import 'prefs_manager.dart';
|
|||||||
/// Each community contains its secret K, so this data should
|
/// Each community contains its secret K, so this data should
|
||||||
/// be considered sensitive (though device encryption handles security).
|
/// be considered sensitive (though device encryption handles security).
|
||||||
class CommunityStore {
|
class CommunityStore {
|
||||||
static const String _communitiesKey = 'communities_v1';
|
static const String _keyPrefix = 'communities_v1';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
/// Load all communities from storage
|
/// Load all communities from storage
|
||||||
Future<List<Community>> loadCommunities() async {
|
Future<List<Community>> loadCommunities() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load communities.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonString = prefs.getString(_communitiesKey);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -32,9 +59,13 @@ class CommunityStore {
|
|||||||
|
|
||||||
/// Save all communities to storage
|
/// Save all communities to storage
|
||||||
Future<void> saveCommunities(List<Community> communities) async {
|
Future<void> saveCommunities(List<Community> communities) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save communities.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonList = communities.map((c) => c.toJson()).toList();
|
final jsonList = communities.map((c) => c.toJson()).toList();
|
||||||
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
|
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new community
|
/// Add a new community
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import '../models/discovery_contact.dart';
|
|||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ContactDiscoveryStore {
|
class ContactDiscoveryStore {
|
||||||
static const String _key = 'discovered_contacts';
|
static const String _keyPrefix = 'discovered_contacts';
|
||||||
|
|
||||||
Future<List<DiscoveryContact>> loadContacts() async {
|
Future<List<DiscoveryContact>> loadContacts() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_key);
|
final jsonStr = prefs.getString(_keyPrefix);
|
||||||
if (jsonStr == null) return [];
|
if (jsonStr == null) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -25,7 +25,7 @@ class ContactDiscoveryStore {
|
|||||||
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
|
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonList = contacts.map(_toJson).toList();
|
final jsonList = contacts.map(_toJson).toList();
|
||||||
await prefs.setString(_key, jsonEncode(jsonList));
|
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _toJson(DiscoveryContact contact) {
|
Map<String, dynamic> _toJson(DiscoveryContact contact) {
|
||||||
|
|||||||
@@ -1,17 +1,45 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import '../models/contact_group.dart';
|
import '../models/contact_group.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ContactGroupStore {
|
class ContactGroupStore {
|
||||||
static const String _key = 'contact_groups';
|
static const String _keyPrefix = 'contact_groups';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<List<ContactGroup>> loadGroups() async {
|
Future<List<ContactGroup>> loadGroups() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load contact groups.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final raw = prefs.getString(_key);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (raw == null || raw.isEmpty) return [];
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(raw);
|
final decoded = jsonDecode(jsonString);
|
||||||
if (decoded is List) {
|
if (decoded is List) {
|
||||||
return decoded
|
return decoded
|
||||||
.whereType<Map<String, dynamic>>()
|
.whereType<Map<String, dynamic>>()
|
||||||
@@ -25,8 +53,12 @@ class ContactGroupStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveGroups(List<ContactGroup> groups) async {
|
Future<void> saveGroups(List<ContactGroup> groups) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save contact groups.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
|
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
|
||||||
await prefs.setString(_key, encoded);
|
await prefs.setString(keyFor, encoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,49 @@
|
|||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ContactSettingsStore {
|
class ContactSettingsStore {
|
||||||
static const String _smazKeyPrefix = 'contact_smaz_';
|
static const String _keyPrefix = 'contact_smaz_';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<bool> loadSmazEnabled(String contactKeyHex) async {
|
Future<bool> loadSmazEnabled(String contactKeyHex) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot load contact settings.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_smazKeyPrefix$contactKeyHex';
|
final key = '$keyFor$contactKeyHex';
|
||||||
|
final oldKey = '$_keyPrefix$contactKeyHex';
|
||||||
|
bool? enabled = prefs.getBool(key);
|
||||||
|
if (enabled == null) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
enabled = prefs.getBool(oldKey);
|
||||||
|
prefs.remove(oldKey);
|
||||||
|
if (enabled != null) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating contact settings from legacy key $oldKey to scoped key $key',
|
||||||
|
);
|
||||||
|
await prefs.setBool(key, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
return prefs.getBool(key) ?? false;
|
return prefs.getBool(key) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveSmazEnabled(String contactKeyHex, bool enabled) async {
|
Future<void> saveSmazEnabled(String contactKeyHex, bool enabled) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn(
|
||||||
|
'Public key hex is not set. Cannot save contact settings.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_smazKeyPrefix$contactKeyHex';
|
final key = '$keyFor$contactKeyHex';
|
||||||
await prefs.setBool(key, enabled);
|
await prefs.setBool(key, enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,46 @@ import 'dart:convert';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class ContactStore {
|
class ContactStore {
|
||||||
static const String _key = 'contacts';
|
static const String _keyPrefix = 'contacts';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<List<Contact>> loadContacts() async {
|
Future<List<Contact>> loadContacts() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load contacts.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_key);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonStr == null) return [];
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||||
return jsonList
|
return jsonList
|
||||||
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -23,9 +51,13 @@ class ContactStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveContacts(List<Contact> contacts) async {
|
Future<void> saveContacts(List<Contact> contacts) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save contacts.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonList = contacts.map(_toJson).toList();
|
final jsonList = contacts.map(_toJson).toList();
|
||||||
await prefs.setString(_key, jsonEncode(jsonList));
|
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _toJson(Contact contact) {
|
Map<String, dynamic> _toJson(Contact contact) {
|
||||||
|
|||||||
@@ -2,26 +2,60 @@ import 'dart:convert';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../helpers/smaz.dart';
|
import '../helpers/smaz.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
class MessageStore {
|
class MessageStore {
|
||||||
static const String _keyPrefix = 'messages_';
|
static const String _keyPrefix = 'messages_';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
Future<void> saveMessages(
|
Future<void> saveMessages(
|
||||||
String contactKeyHex,
|
String contactKeyHex,
|
||||||
List<Message> messages,
|
List<Message> messages,
|
||||||
) async {
|
) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save messages.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$contactKeyHex';
|
final key = '$keyFor$contactKeyHex';
|
||||||
final jsonList = messages.map(_messageToJson).toList();
|
final jsonList = messages.map(_messageToJson).toList();
|
||||||
await prefs.setString(key, jsonEncode(jsonList));
|
await prefs.setString(key, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Message>> loadMessages(String contactKeyHex) async {
|
Future<List<Message>> loadMessages(String contactKeyHex) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load messages.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$contactKeyHex';
|
final key = '$keyFor$contactKeyHex';
|
||||||
final jsonString = prefs.getString(key);
|
final oldKey = '$_keyPrefix$contactKeyHex';
|
||||||
if (jsonString == null) return [];
|
String? jsonString = prefs.getString(key);
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(oldKey);
|
||||||
|
prefs.remove(oldKey);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating messages from legacy key $oldKey to scoped key $key',
|
||||||
|
);
|
||||||
|
await prefs.setString(key, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||||
@@ -32,8 +66,12 @@ class MessageStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearMessages(String contactKeyHex) async {
|
Future<void> clearMessages(String contactKeyHex) async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot clear messages.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final key = '$_keyPrefix$contactKeyHex';
|
final key = '$keyFor$contactKeyHex';
|
||||||
await prefs.remove(key);
|
await prefs.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
import 'prefs_manager.dart';
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
/// Storage for unread message tracking with debounced writes to reduce I/O.
|
/// Storage for unread message tracking with debounced writes to reduce I/O.
|
||||||
class UnreadStore {
|
class UnreadStore {
|
||||||
static const String _contactUnreadCountKey = 'contact_unread_count';
|
static const String _keyPrefix = 'contact_unread_count';
|
||||||
|
|
||||||
|
String publicKeyHex = '';
|
||||||
|
set setPublicKeyHex(String value) =>
|
||||||
|
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
|
||||||
|
|
||||||
|
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||||
|
|
||||||
// Debounce timers to batch rapid writes
|
// Debounce timers to batch rapid writes
|
||||||
Timer? _contactUnreadSaveTimer;
|
Timer? _contactUnreadSaveTimer;
|
||||||
@@ -20,12 +27,33 @@ class UnreadStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, int>> loadContactUnreadCount() async {
|
Future<Map<String, int>> loadContactUnreadCount() async {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot load unread counts.');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_contactUnreadCountKey);
|
String? jsonString = prefs.getString(keyFor);
|
||||||
if (jsonStr == null) return {};
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
// Attempt migration from legacy unscoped key on first load
|
||||||
|
final legacyJsonString = prefs.getString(_keyPrefix);
|
||||||
|
prefs.remove(_keyPrefix);
|
||||||
|
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
|
||||||
|
appLogger.info(
|
||||||
|
'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||||
|
);
|
||||||
|
await prefs.setString(keyFor, legacyJsonString);
|
||||||
|
jsonString = legacyJsonString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
jsonString = prefs.getString(keyFor);
|
||||||
|
}
|
||||||
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
final json = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||||
return json.map((key, value) => MapEntry(key, value as int));
|
return json.map((key, value) => MapEntry(key, value as int));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return {};
|
return {};
|
||||||
@@ -33,6 +61,10 @@ class UnreadStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void saveContactUnreadCount(Map<String, int> counts) {
|
void saveContactUnreadCount(Map<String, int> counts) {
|
||||||
|
if (publicKeyHex.isEmpty) {
|
||||||
|
appLogger.warn('Public key hex is not set. Cannot save unread counts.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
_pendingContactUnreadCount = counts;
|
_pendingContactUnreadCount = counts;
|
||||||
|
|
||||||
_contactUnreadSaveTimer?.cancel();
|
_contactUnreadSaveTimer?.cancel();
|
||||||
@@ -49,7 +81,7 @@ class UnreadStore {
|
|||||||
|
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = jsonEncode(_pendingContactUnreadCount);
|
final jsonStr = jsonEncode(_pendingContactUnreadCount);
|
||||||
await prefs.setString(_contactUnreadCountKey, jsonStr);
|
await prefs.setString(keyFor, jsonStr);
|
||||||
_pendingContactUnreadCount = null;
|
_pendingContactUnreadCount = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.usb</key>
|
<key>com.apple.security.device.usb</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<!-- USB serial ports (/dev/cu.* and /dev/tty.*) for LoRa device communication -->
|
||||||
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
|
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/dev/</string>
|
<string>/dev/cu.</string>
|
||||||
|
<string>/dev/tty.</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.usb</key>
|
<key>com.apple.security.device.usb</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<!-- USB serial ports (/dev/cu.* and /dev/tty.*) for LoRa device communication -->
|
||||||
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
|
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/dev/</string>
|
<string>/dev/cu.</string>
|
||||||
|
<string>/dev/tty.</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_blue_plus: ^2.1.0
|
flutter_blue_plus: ^2.1.0
|
||||||
|
# TODO: Switch to official flserial repo once changes are upstreamed
|
||||||
flserial:
|
flserial:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/MeshEnvy/flserial.git
|
url: https://github.com/MeshEnvy/flserial.git
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.ancestor(
|
await tester.tap(
|
||||||
of: find.text('Connect'),
|
find.ancestor(
|
||||||
matching: find.bySubtype<ElevatedButton>(),
|
of: find.text('Connect'),
|
||||||
));
|
matching: find.bySubtype<ElevatedButton>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(connector.connectUsbCalls, 0);
|
expect(connector.connectUsbCalls, 0);
|
||||||
@@ -131,28 +133,29 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets('UsbScreen sends raw port name when tapping Connect', (
|
||||||
'UsbScreen sends raw port name when tapping Connect',
|
tester,
|
||||||
(tester) async {
|
) async {
|
||||||
final connector = _FakeMeshCoreConnector(
|
final connector = _FakeMeshCoreConnector(
|
||||||
ports: <String>['COM6 - USB Serial Device (COM6)'],
|
ports: <String>['COM6 - USB Serial Device (COM6)'],
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
_buildTestApp(connector: connector, child: const UsbScreen()),
|
_buildTestApp(connector: connector, child: const UsbScreen()),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.ancestor(
|
await tester.tap(
|
||||||
of: find.text('Connect'),
|
find.ancestor(
|
||||||
matching: find.bySubtype<ElevatedButton>(),
|
of: find.text('Connect'),
|
||||||
));
|
matching: find.bySubtype<ElevatedButton>(),
|
||||||
await tester.pump();
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
expect(connector.connectUsbCalls, 1);
|
expect(connector.connectUsbCalls, 1);
|
||||||
expect(connector.lastConnectPortName, 'COM6');
|
expect(connector.lastConnectPortName, 'COM6');
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets('ScannerScreen USB action reflects platform support', (
|
testWidgets('ScannerScreen USB action reflects platform support', (
|
||||||
tester,
|
tester,
|
||||||
@@ -177,8 +180,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('Error Handling', () {
|
group('Error Handling', () {
|
||||||
testWidgets('shows error SnackBar when listing ports fails',
|
testWidgets('shows error SnackBar when listing ports fails', (
|
||||||
(tester) async {
|
tester,
|
||||||
|
) async {
|
||||||
final connector = _FakeMeshCoreConnector();
|
final connector = _FakeMeshCoreConnector();
|
||||||
connector.listUsbPortsImpl = () async {
|
connector.listUsbPortsImpl = () async {
|
||||||
throw PlatformException(
|
throw PlatformException(
|
||||||
@@ -195,9 +199,7 @@ void main() {
|
|||||||
expect(find.text('USB permission was denied.'), findsOneWidget);
|
expect(find.text('USB permission was denied.'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('connection failure shows SnackBar error', (
|
testWidgets('connection failure shows SnackBar error', (tester) async {
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
|
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
|
||||||
var connectAttempted = false;
|
var connectAttempted = false;
|
||||||
connector.connectUsbImpl = ({required String portName}) async {
|
connector.connectUsbImpl = ({required String portName}) async {
|
||||||
@@ -210,10 +212,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.ancestor(
|
await tester.tap(
|
||||||
of: find.text('Connect'),
|
find.ancestor(
|
||||||
matching: find.bySubtype<ElevatedButton>(),
|
of: find.text('Connect'),
|
||||||
));
|
matching: find.bySubtype<ElevatedButton>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(connectAttempted, isTrue);
|
expect(connectAttempted, isTrue);
|
||||||
|
|||||||
Reference in New Issue
Block a user