Compare commits

...

16 Commits

Author SHA1 Message Date
Winston Lowe f870e77e98 Merge branch 'main' into dev-dbDevicePrefix 2026-03-12 09:37:44 -07:00
Winston Lowe e6658a6026 Migrate storage keys to scoped keys across multiple store classes 2026-03-12 09:29:46 -07:00
Winston Lowe c81791cf1e Migrate legacy storage keys to scoped keys in various store classes (#289) 2026-03-12 08:39:17 -07:00
Winston Lowe 6da54e13c3 Migrate legacy storage keys to scoped keys in various store classes 2026-03-12 08:29:56 -07:00
Winston Lowe 1fba5312a2 Refactor storage classes to include companion's public key (#277)
* Refactor storage classes to include public key handling and improve data loading/saving logic

* Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method

* Remove unused app_logger import from ContactDiscoveryStore

* Add warning log for empty publicKeyHex in saveChannelMessages method

* Add warning log for empty publicKeyHex in clearMessages method

* Migrate legacy storage keys to scoped keys across multiple stores

* Remove legacy unscoped keys during migration in storage classes

* Update lib/storage/contact_store.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 00:14:48 -07:00
zjs81 a1b77bb29b Merge pull request #269 from zjs81/dev-latLonFix
Changed contacts latitude and longitude fields to be null until parsed and set
2026-03-07 13:53:09 -07:00
zjs81 4eecfc92dc Merge pull request #252 from just-stuff-tm/feature/usb
Feature/usb
2026-03-07 13:16:39 -07:00
zjs81 90c8cf5f3e Add TODO to switch flserial to official repo 2026-03-07 13:12:45 -07:00
zjs81 06fa176367 Narrow macOS sandbox entitlement to /dev/cu. and /dev/tty. only
The /dev/ prefix granted read/write to all device nodes. The app only
needs access to serial port devices (/dev/cu.* and /dev/tty.*) for USB
LoRa communication.
2026-03-07 13:10:42 -07:00
zjs81 e4285774a0 Merge branch 'main' into feature/usb 2026-03-07 13:03:15 -07:00
zjs81 b2da695102 Run dart format 2026-03-07 13:01:27 -07:00
zjs81 e1327a93c7 Fix contact sync fallback when channel 0 never arrives
On web BLE, contact sync is deferred until channel 0 arrives via
_handleChannelInfo. If channel 0 times out or channel sync completes
without it, _pendingInitialContactsSync stays true and contacts never
load. Add fallback in _cleanupChannelSync to trigger getContacts() if
the flag is still set when channel sync ends.
2026-03-07 13:00:23 -07:00
zjs81 421bc71bb7 Enhance USB port opening and reading logic with improved error handling and debug logging 2026-03-07 12:55:15 -07:00
Winston Lowe 84ec139ce6 Add latitude and longitude fields to contact handling in MeshCoreConnector 2026-03-07 11:02:47 -08:00
Winston Lowe b748b96237 Enhance contact handling logic in MeshCoreConnector to support conditional addition based on auto-add settings (#268) 2026-03-07 01:45:53 -08:00
Winston Lowe c2671ac2ae Refactor data handling of contacts (#267)
* Refactor data handling in MeshCoreConnector and BufferReader for improved readability and efficiency

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix pointer tracking in BufferReader's readCString method

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 01:23:46 -08:00
24 changed files with 597 additions and 164 deletions
+78 -41
View File
@@ -291,6 +291,7 @@ class MeshCoreConnector extends ChangeNotifier {
bool get isLoadingChannels => _isLoadingChannels;
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
Uint8List? get selfPublicKey => _selfPublicKey;
String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0));
String? get selfName => _selfName;
double? get selfLatitude => _selfLatitude;
double? get selfLongitude => _selfLongitude;
@@ -663,6 +664,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Initialize notification service
_notificationService.initialize();
_loadChannelOrder();
_loadDiscoveredContactCache();
// Initialize retry service callbacks
_retryService?.initialize(
@@ -691,7 +693,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> loadDiscoveredContactCache() async {
Future<void> _loadDiscoveredContactCache() async {
final cached = await _discoveryContactStore.loadContacts();
_discoveredContacts
..clear()
@@ -906,10 +908,7 @@ class MeshCoreConnector extends ChangeNotifier {
try {
await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null;
_appDebugLogService?.info(
'connectUsb: opening serial port…',
tag: 'USB',
);
_appDebugLogService?.info('connectUsb: opening serial port…', tag: 'USB');
await _usbManager.connect(portName: portName, baudRate: baudRate);
_appDebugLogService?.info(
'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}',
@@ -1196,7 +1195,6 @@ class MeshCoreConnector extends ChangeNotifier {
await _requestDeviceInfo();
_startBatteryPolling();
unawaited(loadDiscoveredContactCache());
final gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
@@ -1952,6 +1950,8 @@ class MeshCoreConnector extends ChangeNotifier {
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: DateTime.now(),
),
);
@@ -2250,6 +2250,14 @@ class MeshCoreConnector extends ChangeNotifier {
_hasLoadedChannels = true;
_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
}
@@ -2306,7 +2314,7 @@ class MeshCoreConnector extends ChangeNotifier {
case pushCodeNewAdvert:
debugPrint('Got New CONTACT');
// It's the same format as respCodeContact, so we can reuse the handler
_handleContact(frame);
_handleContact(frame, isContact: false);
break;
case respCodeContact:
debugPrint('Got CONTACT');
@@ -2482,6 +2490,27 @@ class MeshCoreConnector extends ChangeNotifier {
selfName.isNotEmpty) {
_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;
_selfInfoRetryTimer?.cancel();
_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);
if (contact != null) {
if (contact.type == advTypeRepeater) {
@@ -2694,11 +2723,23 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector',
);
} else {
_contacts.add(contact);
appLogger.info(
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
tag: 'Connector',
);
if ((_autoAddUsers && contact.type == advTypeChat) ||
(_autoAddRepeaters && contact.type == advTypeRepeater) ||
(_autoAddRoomServers && contact.type == advTypeRoom) ||
(_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);
_loadMessagesForContact(contact.publicKeyHex);
@@ -4270,44 +4311,40 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleRxData(Uint8List frame) {
final packet = BufferReader(frame);
double snr = 0.0;
int routeType = 0;
int payloadType = 0;
Uint8List pathBytes = Uint8List(0);
Uint8List payload = Uint8List(0);
try {
packet.skipBytes(1); // Skip frame type byte
snr = packet.readInt8() / 4.0;
final snr = packet.readInt8() / 4.0;
packet.skipBytes(1); // Skip RSSI byte
//final rssi = packet.readByte();
final header = packet.readByte();
routeType = header & 0x03;
payloadType = (header >> 2) & 0x0F;
final routeType = header & 0x03;
final payloadType = (header >> 2) & 0x0F;
if (routeType == _routeTransportFlood ||
routeType == _routeTransportDirect) {
packet.skipBytes(4); // Skip transport-specific bytes
}
//final payloadVer = (header >> 6) & 0x03;
final pathLen = packet.readByte();
pathBytes = packet.readBytes(pathLen);
payload = packet.readBytes(packet.remaining);
final pathBytes = packet.readBytes(pathLen);
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) {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
final rawPacket = frame.sublist(3);
switch (payloadType) {
case payloadTypeADVERT:
_handlePayloadAdvertReceived(
rawPacket,
payload,
pathBytes,
routeType,
snr,
);
break;
default:
}
}
void importContact(Uint8List frame) {
@@ -4332,8 +4369,8 @@ class MeshCoreConnector extends ChangeNotifier {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
double latitude = 0.0;
double longitude = 0.0;
double? latitude;
double? longitude;
String name = '';
Uint8List publicKey = Uint8List(0);
int type = 0;
@@ -4374,7 +4411,7 @@ class MeshCoreConnector extends ChangeNotifier {
publicKey: publicKey,
name: name,
type: type,
pathLength: pathBytes.length,
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,
path: Uint8List.fromList(
pathBytes.reversed.toList(),
), // Store path in reverse for easier use in outgoing messages
@@ -4393,8 +4430,8 @@ class MeshCoreConnector extends ChangeNotifier {
double snr,
) {
final advert = BufferReader(payload);
double latitude = 0.0;
double longitude = 0.0;
double? latitude;
double? longitude;
String name = '';
String contactKeyHex = '';
Uint8List publicKey = Uint8List(0);
+5 -3
View File
@@ -24,8 +24,7 @@ class MeshCoreUsbManager {
// --- Configuration ---
Future<List<String>> listPorts() => _service.listPorts();
void setRequestPortLabel(String label) =>
_service.setRequestPortLabel(label);
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
void setFallbackDeviceName(String label) =>
_service.setFallbackDeviceName(label);
@@ -36,7 +35,10 @@ class MeshCoreUsbManager {
}
// --- Connection lifecycle ---
Future<void> connect({required String portName, int baudRate = 115200}) async {
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
_debugLog?.info(
'UsbManager.connect: portName=$portName baud=$baudRate',
tag: 'USB',
+27 -1
View File
@@ -4,6 +4,7 @@ import 'dart:typed_data';
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
int _lastPointer = 0;
final Uint8List _buffer;
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
@@ -13,6 +14,7 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'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) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'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);
String readString() {
_lastPointer = _pointer;
final value = readRemainingBytes();
try {
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 bytes = readBytes(maxLength);
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 readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() =>
@@ -78,6 +101,9 @@ class BufferReader {
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
+20 -19
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class Contact {
@@ -166,28 +168,27 @@ class Contact {
static Contact? fromFrame(Uint8List data) {
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
final reader = BufferReader(data);
try {
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final respCode = reader.readByte();
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastModOffset);
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
final lastMod = reader.readUInt32LE();
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
@@ -198,14 +199,14 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
);
} catch (e) {
// If parsing fails, return null
appLogger.error('Failed to parse contact frame: $e');
return null;
}
}
+4
View File
@@ -54,6 +54,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
void initState() {
super.initState();
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels();
_loadCommunities();
@@ -106,7 +108,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
_isProcessing = true;
});
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
bool addPublicChannel,
) async {
// Save community to local storage
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
+8 -6
View File
@@ -107,7 +107,8 @@ class _UsbScreenState extends State<UsbScreen> {
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isLoading = _isLoadingPorts;
final showBle = PlatformInfo.isWeb ||
final showBle =
PlatformInfo.isWeb ||
PlatformInfo.isAndroid ||
PlatformInfo.isIOS;
@@ -238,7 +239,7 @@ class _UsbScreenState extends State<UsbScreen> {
final isConnecting =
connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.usb;
connector.activeTransport == MeshCoreTransportType.usb;
return ListView.separated(
padding: const EdgeInsets.all(8),
@@ -259,8 +260,7 @@ class _UsbScreenState extends State<UsbScreen> {
),
subtitle: showRawName ? Text(rawName) : null,
trailing: ElevatedButton(
onPressed:
isConnecting ? null : () => _connectPort(port),
onPressed: isConnecting ? null : () => _connectPort(port),
child: Text(l10n.common_connect),
),
onTap: isConnecting ? null : () => _connectPort(port),
@@ -329,8 +329,10 @@ class _UsbScreenState extends State<UsbScreen> {
if (_connector.state != MeshCoreConnectionState.disconnected) return;
final rawPortName = normalizeUsbPortName(port);
appLogger.info('Connect tapped for $port (raw: $rawPortName)',
tag: 'UsbScreen');
appLogger.info(
'Connect tapped for $port (raw: $rawPortName)',
tag: 'UsbScreen',
);
try {
await _connector.connectUsb(portName: rawPortName);
+1 -2
View File
@@ -101,8 +101,7 @@ class NotificationService {
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
if (addr != null && addr.isNotEmpty) return true;
// Fallback: check the default socket for the current user.
final uid = Platform.environment['UID'] ??
Platform.environment['EUID'];
final uid = Platform.environment['UID'] ?? Platform.environment['EUID'];
final path = '/run/user/${uid ?? '1000'}/bus';
return File(path).existsSync();
}
+31 -8
View File
@@ -118,10 +118,7 @@ class UsbSerialService {
tag: 'USB Serial',
);
} catch (error) {
_debugLogService?.error(
'Web connect failed: $error',
tag: 'USB Serial',
);
_debugLogService?.error('Web connect failed: $error', tag: 'USB Serial');
await _cleanupFailedConnect();
_status = UsbSerialStatus.disconnected;
_connectedPortName = null;
@@ -268,9 +265,23 @@ class UsbSerialService {
return null;
}
Future<void> _openPort(JSObject port, int baudRate) {
final options = JSObject()..['baudRate'] = baudRate.toJS;
return port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
Future<void> _openPort(JSObject port, int baudRate) async {
final options = JSObject()
..['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 {
@@ -324,8 +335,12 @@ class UsbSerialService {
Future<void> _pumpReads() async {
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 {
while (_status == UsbSerialStatus.connected &&
identical(reader, _reader)) {
@@ -333,6 +348,7 @@ class UsbSerialService {
.callMethod<JSPromise<JSAny?>>('read'.toJS)
.toDart;
if (result == null) {
_debugLogService?.warn('_pumpReads: null result', tag: 'USB Serial');
break;
}
final resultObject = result as JSObject;
@@ -340,20 +356,27 @@ class UsbSerialService {
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
final done = doneValue != null && doneValue.dartify() == true;
if (done) {
_debugLogService?.info('_pumpReads: done=true', tag: 'USB Serial');
break;
}
final value = resultObject.getProperty<JSAny?>('value'.toJS);
final bytes = _coerceBytes(value);
if (bytes != null && bytes.isNotEmpty) {
_debugLogService?.info(
'USB RX raw: ${bytes.length} byte(s)',
tag: 'USB Serial',
);
_ingestRawBytes(bytes);
}
}
} catch (error, stackTrace) {
_debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial');
if (_status == UsbSerialStatus.connected) {
_addFrameError(error, stackTrace);
}
} finally {
_debugLogService?.info('_pumpReads: ended', tag: 'USB Serial');
_releaseLock(reader);
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
_addFrameError(StateError('USB serial connection closed'));
+44 -7
View File
@@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@@ -7,13 +9,25 @@ import 'prefs_manager.dart';
class ChannelMessageStore {
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
Future<void> saveChannelMessages(
int channelIndex,
List<ChannelMessage> messages,
) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save channel messages.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
// Convert messages to JSON
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
@@ -24,12 +38,35 @@ class ChannelMessageStore {
/// Load messages for a specific channel
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 key = '$_keyPrefix$channelIndex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
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 {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList.map((json) => _messageFromJson(json)).toList();
@@ -42,14 +79,14 @@ class ChannelMessageStore {
/// Clear messages for a specific channel
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
await prefs.remove(key);
}
/// Clear all channel messages
Future<void> clearAllChannelMessages() async {
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) {
await prefs.remove(key);
}
+36 -6
View File
@@ -1,20 +1,50 @@
import 'dart:convert';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save channel order.');
return;
}
final prefs = PrefsManager.instance;
await prefs.setString(_key, jsonEncode(order));
await prefs.setString(keyFor, jsonEncode(order));
}
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 raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
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 channel order from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final decoded = jsonDecode(raw);
final decoded = jsonDecode(jsonString);
if (decoded is List) {
return decoded
.map((value) => value is int ? value : int.tryParse('$value'))
@@ -24,7 +54,7 @@ class ChannelOrderStore {
} catch (_) {
// fall through to legacy parse
}
return raw
return jsonString
.split(',')
.map((value) => int.tryParse(value))
.whereType<int>()
+35 -3
View File
@@ -1,17 +1,49 @@
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load channel settings.',
);
return false;
}
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;
}
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 key = '$_smazKeyPrefix$channelIndex';
final key = '$keyFor$channelIndex';
await prefs.setBool(key, enabled);
}
}
+34 -5
View File
@@ -2,18 +2,43 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/channel.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load channels.');
return [];
}
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
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 channel messages from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +48,13 @@ class ChannelStore {
}
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 jsonList = channels.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(Channel channel) {
+34 -3
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import '../models/community.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
/// 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
/// be considered sensitive (though device encryption handles security).
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
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 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) {
return [];
}
@@ -32,9 +59,13 @@ class CommunityStore {
/// Save all communities to storage
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 jsonList = communities.map((c) => c.toJson()).toList();
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
/// Add a new community
+3 -3
View File
@@ -5,11 +5,11 @@ import '../models/discovery_contact.dart';
import 'prefs_manager.dart';
class ContactDiscoveryStore {
static const String _key = 'discovered_contacts';
static const String _keyPrefix = 'discovered_contacts';
Future<List<DiscoveryContact>> loadContacts() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
final jsonStr = prefs.getString(_keyPrefix);
if (jsonStr == null) return [];
try {
@@ -25,7 +25,7 @@ class ContactDiscoveryStore {
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(DiscoveryContact contact) {
+37 -5
View File
@@ -1,17 +1,45 @@
import 'dart:convert';
import '../models/contact_group.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load contact groups.');
return [];
}
final prefs = PrefsManager.instance;
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
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 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 {
final decoded = jsonDecode(raw);
final decoded = jsonDecode(jsonString);
if (decoded is List) {
return decoded
.whereType<Map<String, dynamic>>()
@@ -25,8 +53,12 @@ class ContactGroupStore {
}
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 encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
await prefs.setString(_key, encoded);
await prefs.setString(keyFor, encoded);
}
}
+35 -3
View File
@@ -1,17 +1,49 @@
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load contact settings.',
);
return false;
}
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;
}
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 key = '$_smazKeyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
await prefs.setBool(key, enabled);
}
}
+37 -5
View File
@@ -2,18 +2,46 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/contact.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
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 {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load contacts.');
return [];
}
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
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 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 {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
@@ -23,9 +51,13 @@ class ContactStore {
}
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 jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
await prefs.setString(keyFor, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(Contact contact) {
+43 -5
View File
@@ -2,26 +2,60 @@ import 'dart:convert';
import 'dart:typed_data';
import '../models/message.dart';
import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class MessageStore {
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(
String contactKeyHex,
List<Message> messages,
) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save messages.');
return;
}
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
final jsonList = messages.map(_messageToJson).toList();
await prefs.setString(key, jsonEncode(jsonList));
}
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 key = '$_keyPrefix$contactKeyHex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
final key = '$keyFor$contactKeyHex';
final oldKey = '$_keyPrefix$contactKeyHex';
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 {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
@@ -32,8 +66,12 @@ class MessageStore {
}
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 key = '$_keyPrefix$contactKeyHex';
final key = '$keyFor$contactKeyHex';
await prefs.remove(key);
}
+37 -5
View File
@@ -1,11 +1,18 @@
import 'dart:async';
import 'dart:convert';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
/// Storage for unread message tracking with debounced writes to reduce I/O.
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
Timer? _contactUnreadSaveTimer;
@@ -20,12 +27,33 @@ class UnreadStore {
}
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 jsonStr = prefs.getString(_contactUnreadCountKey);
if (jsonStr == null) return {};
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 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 {
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));
} catch (_) {
return {};
@@ -33,6 +61,10 @@ class UnreadStore {
}
void saveContactUnreadCount(Map<String, int> counts) {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save unread counts.');
return;
}
_pendingContactUnreadCount = counts;
_contactUnreadSaveTimer?.cancel();
@@ -49,7 +81,7 @@ class UnreadStore {
final prefs = PrefsManager.instance;
final jsonStr = jsonEncode(_pendingContactUnreadCount);
await prefs.setString(_contactUnreadCountKey, jsonStr);
await prefs.setString(keyFor, jsonStr);
_pendingContactUnreadCount = null;
}
+3 -1
View File
@@ -14,9 +14,11 @@
<true/>
<key>com.apple.security.device.usb</key>
<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>
<array>
<string>/dev/</string>
<string>/dev/cu.</string>
<string>/dev/tty.</string>
</array>
<key>com.apple.security.device.camera</key>
<true/>
+3 -1
View File
@@ -10,9 +10,11 @@
<true/>
<key>com.apple.security.device.usb</key>
<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>
<array>
<string>/dev/</string>
<string>/dev/cu.</string>
<string>/dev/tty.</string>
</array>
<key>com.apple.security.device.camera</key>
<true/>
+1
View File
@@ -38,6 +38,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_blue_plus: ^2.1.0
# TODO: Switch to official flserial repo once changes are upstreamed
flserial:
git:
url: https://github.com/MeshEnvy/flserial.git
+36 -32
View File
@@ -116,10 +116,12 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
));
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump();
expect(connector.connectUsbCalls, 0);
@@ -131,28 +133,29 @@ void main() {
},
);
testWidgets(
'UsbScreen sends raw port name when tapping Connect',
(tester) async {
final connector = _FakeMeshCoreConnector(
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
testWidgets('UsbScreen sends raw port name when tapping Connect', (
tester,
) async {
final connector = _FakeMeshCoreConnector(
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.tap(find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
));
await tester.pump();
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pump();
expect(connector.connectUsbCalls, 1);
expect(connector.lastConnectPortName, 'COM6');
},
);
expect(connector.connectUsbCalls, 1);
expect(connector.lastConnectPortName, 'COM6');
});
testWidgets('ScannerScreen USB action reflects platform support', (
tester,
@@ -177,8 +180,9 @@ void main() {
});
group('Error Handling', () {
testWidgets('shows error SnackBar when listing ports fails',
(tester) async {
testWidgets('shows error SnackBar when listing ports fails', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
connector.listUsbPortsImpl = () async {
throw PlatformException(
@@ -195,9 +199,7 @@ void main() {
expect(find.text('USB permission was denied.'), findsOneWidget);
});
testWidgets('connection failure shows SnackBar error', (
tester,
) async {
testWidgets('connection failure shows SnackBar error', (tester) async {
final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
var connectAttempted = false;
connector.connectUsbImpl = ({required String portName}) async {
@@ -210,10 +212,12 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
));
await tester.tap(
find.ancestor(
of: find.text('Connect'),
matching: find.bySubtype<ElevatedButton>(),
),
);
await tester.pumpAndSettle();
expect(connectAttempted, isTrue);