mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-04 06:52:59 +10:00
fixes
This commit is contained in:
@@ -613,10 +613,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
throw Exception("MeshCore characteristics not found");
|
||||
}
|
||||
|
||||
// Give the device a moment to be ready for descriptor writes
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
// Retry setNotifyValue with increasing delays
|
||||
bool notifySet = false;
|
||||
for (int attempt = 0; attempt < 3 && !notifySet; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
await Future.delayed(Duration(milliseconds: 500 * attempt));
|
||||
}
|
||||
await _txCharacteristic!.setNotifyValue(true);
|
||||
notifySet = true;
|
||||
} catch (e) {
|
||||
debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e');
|
||||
if (attempt == 2) rethrow;
|
||||
}
|
||||
}
|
||||
_notifySubscription = _txCharacteristic!.onValueReceived.listen(_handleFrame);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
@@ -1185,6 +1195,30 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(bytes);
|
||||
}
|
||||
|
||||
Future<void> setNodeName(String name) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSetAdvertNameFrame(name));
|
||||
}
|
||||
|
||||
Future<void> setNodeLocation({required double lat, required double lon}) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSetAdvertLatLonFrame(lat, lon));
|
||||
}
|
||||
|
||||
Future<void> sendSelfAdvert({bool flood = true}) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
|
||||
}
|
||||
|
||||
Future<void> rebootDevice() async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildRebootFrame());
|
||||
}
|
||||
|
||||
Future<void> setPrivacyMode(bool enabled) async {
|
||||
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
||||
}
|
||||
|
||||
Future<void> getChannels({int? maxChannels}) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
|
||||
@@ -210,6 +210,11 @@ void writeUint32LE(Uint8List data, int offset, int value) {
|
||||
data[offset + 3] = (value >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
// Helper to write int32 little-endian
|
||||
void writeInt32LE(Uint8List data, int offset, int value) {
|
||||
writeUint32LE(data, offset, value & 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
// Helper to read null-terminated UTF-8 string
|
||||
String readCString(Uint8List data, int offset, int maxLen) {
|
||||
int end = offset;
|
||||
@@ -362,6 +367,39 @@ Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Build CMD_SEND_SELF_ADVERT frame
|
||||
// Format: [cmd][flood_flag]
|
||||
Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
|
||||
return Uint8List.fromList([cmdSendSelfAdvert, flood ? 1 : 0]);
|
||||
}
|
||||
|
||||
// Build CMD_SET_ADVERT_NAME frame
|
||||
// Format: [cmd][name...]
|
||||
Uint8List buildSetAdvertNameFrame(String name) {
|
||||
final nameBytes = utf8.encode(name);
|
||||
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
|
||||
final frame = Uint8List(1 + nameLen);
|
||||
frame[0] = cmdSetAdvertName;
|
||||
frame.setRange(1, 1 + nameLen, nameBytes.sublist(0, nameLen));
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Build CMD_SET_ADVERT_LATLON frame
|
||||
// Format: [cmd][lat x4][lon x4]
|
||||
Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
|
||||
final frame = Uint8List(9);
|
||||
frame[0] = cmdSetAdvertLatLon;
|
||||
writeInt32LE(frame, 1, (lat * 1000000).round());
|
||||
writeInt32LE(frame, 5, (lon * 1000000).round());
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Build CMD_REBOOT frame
|
||||
// Format: [cmd]["reboot"]
|
||||
Uint8List buildRebootFrame() {
|
||||
return Uint8List.fromList([cmdReboot, ...utf8.encode('reboot')]);
|
||||
}
|
||||
|
||||
// Build CMD_SYNC_NEXT_MESSAGE frame
|
||||
Uint8List buildSyncNextMessageFrame() {
|
||||
return Uint8List.fromList([cmdSyncNextMessage]);
|
||||
|
||||
@@ -16,7 +16,9 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
title: const Text('App Settings'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Consumer2<AppSettingsService, MeshCoreConnector>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer2<AppSettingsService, MeshCoreConnector>(
|
||||
builder: (context, settingsService, connector, child) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -34,6 +36,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
@@ -117,6 +119,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -100,7 +100,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Column(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
@@ -161,6 +163,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_buildMessageComposer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildSummaryCard(context, observedLabel: observedLabel),
|
||||
@@ -76,6 +78,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
..._buildHopTiles(hops),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -296,7 +299,9 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
appBar: AppBar(
|
||||
title: const Text('Path Map'),
|
||||
),
|
||||
body: Stack(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: mapKey,
|
||||
@@ -351,6 +356,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
_buildLegendCard(context, hops),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -38,11 +38,16 @@ class _ChannelsScreenState extends State<ChannelsScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final allowBack = !connector.isConnected;
|
||||
|
||||
return PopScope(
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Channels'),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: !widget.hideBackButton,
|
||||
automaticallyImplyLeading: !widget.hideBackButton && allowBack,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
@@ -126,6 +131,7 @@ class _ChannelsScreenState extends State<ChannelsScreen> {
|
||||
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final allowBack = !connector.isConnected;
|
||||
|
||||
if (!connector.isConnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -93,11 +94,13 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
return PopScope(
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 16,
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: !widget.hideBackButton,
|
||||
automaticallyImplyLeading: !widget.hideBackButton && allowBack,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -234,6 +237,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,11 +130,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
center = highlightPosition;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
final allowBack = !connector.isConnected;
|
||||
|
||||
return PopScope(
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Node Map'),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: !widget.hideBackButton,
|
||||
automaticallyImplyLeading: !widget.hideBackButton && allowBack,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
@@ -238,6 +242,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
onPressed: () => _showFilterDialog(context, settingsService),
|
||||
child: const Icon(Icons.filter_list),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,7 +31,9 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Padding(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -143,6 +145,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -550,7 +550,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading && _nameController.text.isEmpty
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: _isLoading && _nameController.text.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -568,6 +570,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_buildDangerZoneCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -296,7 +296,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _loadStatus,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -309,6 +311,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ class ScannerScreen extends StatelessWidget {
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: Consumer<MeshCoreConnector>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -33,6 +35,7 @@ class ScannerScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isScanning = connector.state == MeshCoreConnectionState.scanning;
|
||||
|
||||
@@ -18,7 +18,9 @@ class SettingsScreen extends StatelessWidget {
|
||||
title: const Text('Settings'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Consumer<MeshCoreConnector>(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -38,6 +40,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,7 +247,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.sendCliCommand('set name ${controller.text}');
|
||||
await connector.setNodeName(controller.text);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -302,18 +305,33 @@ class SettingsScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
var updated = false;
|
||||
if (latController.text.isNotEmpty) {
|
||||
await connector.sendCliCommand('set lat ${latController.text}');
|
||||
updated = true;
|
||||
final latText = latController.text.trim();
|
||||
final lonText = lonController.text.trim();
|
||||
if (latText.isEmpty && lonText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (lonController.text.isNotEmpty) {
|
||||
await connector.sendCliCommand('set lon ${lonController.text}');
|
||||
updated = true;
|
||||
|
||||
final currentLat = connector.selfLatitude;
|
||||
final currentLon = connector.selfLongitude;
|
||||
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
|
||||
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
|
||||
if (lat == null || lon == null) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Enter both latitude and longitude.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (updated) {
|
||||
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid latitude or longitude.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await connector.setNodeLocation(lat: lat, lon: lon);
|
||||
await connector.refreshDeviceInfo();
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Location updated')),
|
||||
@@ -340,7 +358,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.sendCliCommand('set privacy on');
|
||||
await connector.setPrivacyMode(true);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -352,7 +370,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.sendCliCommand('set privacy off');
|
||||
await connector.setPrivacyMode(false);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -367,7 +385,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
|
||||
connector.sendCliCommand('advert');
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Advertisement sent')),
|
||||
);
|
||||
@@ -394,7 +412,7 @@ class SettingsScreen extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
connector.sendCliCommand('reboot');
|
||||
connector.rebootDevice();
|
||||
},
|
||||
child: const Text('Reboot', style: TextStyle(color: Colors.orange)),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user