feat: Add RX/TX activity indicators and packet counters; update GPS coordinate conversion

This commit is contained in:
Janez T
2025-10-14 10:46:07 +02:00
parent 62bbb342ec
commit 373998bc7d
6 changed files with 316 additions and 33 deletions

View File

@@ -79,8 +79,8 @@ class Contact {
LatLng? get advertLocation { LatLng? get advertLocation {
if (advLat == 0 && advLon == 0) return null; if (advLat == 0 && advLon == 0) return null;
// Convert from int32 to double (degrees) // Convert from int32 to double (degrees)
final lat = advLat / 1e7; final lat = advLat / 1e6;
final lon = advLon / 1e7; final lon = advLon / 1e6;
return LatLng(lat, lon); return LatLng(lat, lon);
} }

View File

@@ -28,6 +28,19 @@ class ConnectionProvider with ChangeNotifier {
String? _error; String? _error;
String? get error => _error; String? get error => _error;
// Activity indicators (for blinking)
bool _rxActivity = false;
bool _txActivity = false;
bool get rxActivity => _rxActivity;
bool get txActivity => _txActivity;
Timer? _rxActivityTimer;
Timer? _txActivityTimer;
// Packet counters
int get rxPacketCount => _bleService.rxPacketCount;
int get txPacketCount => _bleService.txPacketCount;
// Callbacks for other providers // Callbacks for other providers
Function(Contact)? onContactReceived; Function(Contact)? onContactReceived;
Function(List<Contact>)? onContactsComplete; Function(List<Contact>)? onContactsComplete;
@@ -90,6 +103,31 @@ class ConnectionProvider with ChangeNotifier {
_bleService.onTelemetryReceived = (publicKey, lppData) { _bleService.onTelemetryReceived = (publicKey, lppData) {
onTelemetryReceived?.call(publicKey, lppData); onTelemetryReceived?.call(publicKey, lppData);
}; };
// Activity indicators
_bleService.onRxActivity = () {
_rxActivity = true;
notifyListeners();
// Reset after 100ms
_rxActivityTimer?.cancel();
_rxActivityTimer = Timer(const Duration(milliseconds: 100), () {
_rxActivity = false;
notifyListeners();
});
};
_bleService.onTxActivity = () {
_txActivity = true;
notifyListeners();
// Reset after 100ms
_txActivityTimer?.cancel();
_txActivityTimer = Timer(const Duration(milliseconds: 100), () {
_txActivity = false;
notifyListeners();
});
};
} }
/// Start scanning for MeshCore devices /// Start scanning for MeshCore devices
@@ -269,6 +307,8 @@ class ConnectionProvider with ChangeNotifier {
@override @override
void dispose() { void dispose() {
_rxActivityTimer?.cancel();
_txActivityTimer?.cancel();
_bleService.dispose(); _bleService.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -377,21 +377,67 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
) )
else else
OutlinedButton( Row(
onPressed: () async { mainAxisSize: MainAxisSize.min,
await provider.disconnect(); children: [
if (context.mounted) { // RX indicator
context.read<AppProvider>().clearAllData(); Container(
} width: 8,
}, height: 8,
style: OutlinedButton.styleFrom( decoration: BoxDecoration(
foregroundColor: Colors.white, shape: BoxShape.circle,
side: const BorderSide(color: Colors.white), color: provider.rxActivity
shape: RoundedRectangleBorder( ? Colors.green
borderRadius: BorderRadius.circular(20), : Colors.grey.withOpacity(0.3),
),
), ),
), const SizedBox(width: 4),
child: const Text('Disconnect'), Text(
'RX:${provider.rxPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
// TX indicator
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: provider.txActivity
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
const SizedBox(width: 4),
Text(
'TX:${provider.txPacketCount}',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: () async {
await provider.disconnect();
if (context.mounted) {
context.read<AppProvider>().clearAllData();
}
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
child: const Text('Disconnect', style: TextStyle(fontSize: 13)),
),
],
), ),
], ],
); );

View File

@@ -9,6 +9,10 @@ import 'meshcore_constants.dart';
class CayenneLppParser { class CayenneLppParser {
/// Parse Cayenne LPP data into ContactTelemetry /// Parse Cayenne LPP data into ContactTelemetry
static ContactTelemetry parse(Uint8List data) { static ContactTelemetry parse(Uint8List data) {
print(' [CayenneLPP] Parsing LPP data...');
print(' Data length: ${data.length} bytes');
print(' Data (hex): ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
final reader = BufferReader(data); final reader = BufferReader(data);
LatLng? gpsLocation; LatLng? gpsLocation;
@@ -19,92 +23,145 @@ class CayenneLppParser {
double? pressure; double? pressure;
final extraSensorData = <String, dynamic>{}; final extraSensorData = <String, dynamic>{};
int fieldCount = 0;
while (reader.hasRemaining) { while (reader.hasRemaining) {
try { try {
fieldCount++;
print(' [Field $fieldCount] Position: ${data.length - reader.remainingBytesCount}');
final channel = reader.readByte(); final channel = reader.readByte();
print(' Channel: $channel');
final type = reader.readByte(); final type = reader.readByte();
print(' Type: $type (0x${type.toRadixString(16).padLeft(2, '0')})');
switch (type) { switch (type) {
case MeshCoreConstants.lppDigitalInput: case MeshCoreConstants.lppDigitalInput:
final value = reader.readByte(); final value = reader.readByte();
print(' Digital Input: $value');
extraSensorData['digital_input_$channel'] = value; extraSensorData['digital_input_$channel'] = value;
break; break;
case MeshCoreConstants.lppDigitalOutput: case MeshCoreConstants.lppDigitalOutput:
final value = reader.readByte(); final value = reader.readByte();
print(' Digital Output: $value');
extraSensorData['digital_output_$channel'] = value; extraSensorData['digital_output_$channel'] = value;
break; break;
case MeshCoreConstants.lppAnalogInput: case MeshCoreConstants.lppAnalogInput:
final value = reader.readInt16LE() / 100.0; final rawValue = reader.readInt16LE();
final value = rawValue / 100.0;
print(' Analog Input (raw): $rawValue');
print(' Analog Input (volts): ${value}V');
extraSensorData['analog_input_$channel'] = value; extraSensorData['analog_input_$channel'] = value;
// If this is a battery reading // If this is a battery reading
if (channel == 0 || channel == 1) { if (channel == 0 || channel == 1) {
batteryMilliVolts = value * 1000; batteryMilliVolts = value * 1000;
batteryPercentage = _calculateBatteryPercentage(value); batteryPercentage = _calculateBatteryPercentage(value);
print(' → Battery: ${batteryPercentage?.toStringAsFixed(1)}% (${batteryMilliVolts?.toStringAsFixed(0)}mV)');
} }
break; break;
case MeshCoreConstants.lppAnalogOutput: case MeshCoreConstants.lppAnalogOutput:
final value = reader.readInt16LE() / 100.0; final rawValue = reader.readInt16LE();
final value = rawValue / 100.0;
print(' Analog Output (raw): $rawValue');
print(' Analog Output (volts): ${value}V');
extraSensorData['analog_output_$channel'] = value; extraSensorData['analog_output_$channel'] = value;
break; break;
case MeshCoreConstants.lppIlluminanceSensor: case MeshCoreConstants.lppIlluminanceSensor:
final value = reader.readUInt16LE(); final value = reader.readUInt16LE();
print(' Illuminance: $value lux');
extraSensorData['illuminance_$channel'] = value; extraSensorData['illuminance_$channel'] = value;
break; break;
case MeshCoreConstants.lppPresenceSensor: case MeshCoreConstants.lppPresenceSensor:
final value = reader.readByte(); final value = reader.readByte();
print(' Presence: $value');
extraSensorData['presence_$channel'] = value; extraSensorData['presence_$channel'] = value;
break; break;
case MeshCoreConstants.lppTemperatureSensor: case MeshCoreConstants.lppTemperatureSensor:
temperature = reader.readInt16LE() / 10.0; final rawValue = reader.readInt16LE();
temperature = rawValue / 10.0;
print(' Temperature (raw): $rawValue');
print(' Temperature: ${temperature?.toStringAsFixed(1)}°C');
break; break;
case MeshCoreConstants.lppHumiditySensor: case MeshCoreConstants.lppHumiditySensor:
humidity = reader.readByte() / 2.0; final rawValue = reader.readByte();
humidity = rawValue / 2.0;
print(' Humidity (raw): $rawValue');
print(' Humidity: ${humidity?.toStringAsFixed(1)}%');
break; break;
case MeshCoreConstants.lppAccelerometer: case MeshCoreConstants.lppAccelerometer:
final x = reader.readInt16LE() / 1000.0; final x = reader.readInt16LE() / 1000.0;
final y = reader.readInt16LE() / 1000.0; final y = reader.readInt16LE() / 1000.0;
final z = reader.readInt16LE() / 1000.0; final z = reader.readInt16LE() / 1000.0;
print(' Accelerometer: x=$x, y=$y, z=$z');
extraSensorData['accelerometer_$channel'] = {'x': x, 'y': y, 'z': z}; extraSensorData['accelerometer_$channel'] = {'x': x, 'y': y, 'z': z};
break; break;
case MeshCoreConstants.lppBarometer: case MeshCoreConstants.lppBarometer:
pressure = reader.readUInt16LE() / 10.0; final rawValue = reader.readUInt16LE();
pressure = rawValue / 10.0;
print(' Barometer (raw): $rawValue');
print(' Barometer: ${pressure?.toStringAsFixed(1)} hPa');
break;
case MeshCoreConstants.lppVoltageSensor:
final rawValue = reader.readUInt16LE();
final value = rawValue / 100.0;
print(' Voltage (raw): $rawValue');
print(' Voltage: ${value}V');
// Treat voltage sensor as battery reading
batteryMilliVolts = value * 1000;
batteryPercentage = _calculateBatteryPercentage(value);
print(' → Battery: ${batteryPercentage?.toStringAsFixed(1)}% (${batteryMilliVolts?.toStringAsFixed(0)}mV)');
break; break;
case MeshCoreConstants.lppGyrometer: case MeshCoreConstants.lppGyrometer:
final x = reader.readInt16LE() / 100.0; final x = reader.readInt16LE() / 100.0;
final y = reader.readInt16LE() / 100.0; final y = reader.readInt16LE() / 100.0;
final z = reader.readInt16LE() / 100.0; final z = reader.readInt16LE() / 100.0;
print(' Gyrometer: x=$x, y=$y, z=$z');
extraSensorData['gyrometer_$channel'] = {'x': x, 'y': y, 'z': z}; extraSensorData['gyrometer_$channel'] = {'x': x, 'y': y, 'z': z};
break; break;
case MeshCoreConstants.lppGps: case MeshCoreConstants.lppGps:
final lat = reader.readInt32LE() / 10000.0; final rawLat = reader.readInt32LE();
final lon = reader.readInt32LE() / 10000.0; final rawLon = reader.readInt32LE();
final alt = reader.readInt32LE() / 100.0; final rawAlt = reader.readInt32LE();
final lat = rawLat / 1000000.0;
final lon = rawLon / 1000000.0;
final alt = rawAlt / 100.0;
print(' GPS Location (raw): lat=$rawLat, lon=$rawLon, alt=$rawAlt');
print(' GPS Location: ${lat}°, ${lon}°, altitude=${alt}m');
gpsLocation = LatLng(lat, lon); gpsLocation = LatLng(lat, lon);
extraSensorData['altitude_$channel'] = alt; extraSensorData['altitude_$channel'] = alt;
break; break;
default: default:
print(' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes');
// Unknown type, skip remaining to avoid parsing errors // Unknown type, skip remaining to avoid parsing errors
reader.skip(reader.remainingBytesCount); reader.skip(reader.remainingBytesCount);
break; break;
} }
} catch (e) { } catch (e) {
print(' ❌ Parsing error: $e');
// If we encounter a parsing error, break and return what we have // If we encounter a parsing error, break and return what we have
break; break;
} }
} }
print(' Parsed $fieldCount fields');
print(' ✅ [CayenneLPP] Parsing complete');
print(' GPS: ${gpsLocation != null ? '${gpsLocation.latitude}°, ${gpsLocation.longitude}°' : 'none'}');
print(' Battery: ${batteryPercentage != null ? '${batteryPercentage.toStringAsFixed(1)}%' : 'none'}');
print(' Temperature: ${temperature != null ? '${temperature.toStringAsFixed(1)}°C' : 'none'}');
return ContactTelemetry( return ContactTelemetry(
gpsLocation: gpsLocation, gpsLocation: gpsLocation,
batteryPercentage: batteryPercentage, batteryPercentage: batteryPercentage,

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../models/contact_telemetry.dart'; import '../models/contact_telemetry.dart';
@@ -36,6 +37,16 @@ class MeshCoreBleService {
bool _isConnected = false; bool _isConnected = false;
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
// Packet counters
int _rxPacketCount = 0;
int _txPacketCount = 0;
int get rxPacketCount => _rxPacketCount;
int get txPacketCount => _txPacketCount;
// Activity callbacks (for blinking indicators)
VoidCallback? onRxActivity;
VoidCallback? onTxActivity;
/// Scan for MeshCore devices /// Scan for MeshCore devices
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* { Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
try { try {
@@ -225,6 +236,10 @@ class MeshCoreBleService {
throw Exception('Characteristic does not support write operations'); throw Exception('Characteristic does not support write operations');
} }
// Increment TX packet counter and trigger activity indicator
_txPacketCount++;
onTxActivity?.call();
print('✅ [BLE] Write successful'); print('✅ [BLE] Write successful');
} catch (e) { } catch (e) {
print('❌ [BLE] Write error: $e'); print('❌ [BLE] Write error: $e');
@@ -237,6 +252,17 @@ class MeshCoreBleService {
void _onDataReceived(List<int> data) { void _onDataReceived(List<int> data) {
try { try {
print('📥 [BLE] Received ${data.length} bytes from TX characteristic'); print('📥 [BLE] Received ${data.length} bytes from TX characteristic');
// Handle empty data
if (data.isEmpty) {
print(' ⚠️ Empty data received, ignoring');
return;
}
// Increment RX packet counter and trigger activity indicator
_rxPacketCount++;
onRxActivity?.call();
print(' Raw data: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}'); print(' Raw data: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
final reader = BufferReader(Uint8List.fromList(data)); final reader = BufferReader(Uint8List.fromList(data));
@@ -257,6 +283,10 @@ class MeshCoreBleService {
print(' → Handling EndOfContacts'); print(' → Handling EndOfContacts');
_handleEndOfContacts(reader); _handleEndOfContacts(reader);
break; break;
case MeshCoreConstants.respSent:
print(' → Handling Sent confirmation');
_handleSentConfirmation(reader);
break;
case MeshCoreConstants.respContactMsgRecv: case MeshCoreConstants.respContactMsgRecv:
print(' → Handling ContactMessage'); print(' → Handling ContactMessage');
_handleContactMessage(reader); _handleContactMessage(reader);
@@ -313,16 +343,41 @@ class MeshCoreBleService {
/// Handle Contact response /// Handle Contact response
void _handleContact(BufferReader reader) { void _handleContact(BufferReader reader) {
try { try {
print(' [Contact] Parsing contact...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
final publicKey = reader.readBytes(32); final publicKey = reader.readBytes(32);
final type = ContactType.fromValue(reader.readByte()); print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
final typeByte = reader.readByte();
final type = ContactType.fromValue(typeByte);
print(' Type byte: $typeByte → Type: $type');
final flags = reader.readByte(); final flags = reader.readByte();
print(' Flags: $flags (0x${flags.toRadixString(16).padLeft(2, '0')})');
final outPathLen = reader.readInt8(); final outPathLen = reader.readInt8();
print(' Out path length: $outPathLen');
final outPath = reader.readBytes(64); final outPath = reader.readBytes(64);
print(' Out path: ${outPath.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
final advName = reader.readCString(32); final advName = reader.readCString(32);
print(' Advertised name: "$advName"');
final lastAdvert = reader.readUInt32LE(); final lastAdvert = reader.readUInt32LE();
print(' Last advert timestamp: $lastAdvert');
final advLat = reader.readInt32LE(); final advLat = reader.readInt32LE();
print(' Latitude (raw int32): $advLat');
print(' Latitude (decimal): ${advLat / 1000000.0}°');
final advLon = reader.readInt32LE(); final advLon = reader.readInt32LE();
print(' Longitude (raw int32): $advLon');
print(' Longitude (decimal): ${advLon / 1000000.0}°');
final lastMod = reader.readUInt32LE(); final lastMod = reader.readUInt32LE();
print(' Last modified timestamp: $lastMod');
final contact = Contact( final contact = Contact(
publicKey: publicKey, publicKey: publicKey,
@@ -337,9 +392,11 @@ class MeshCoreBleService {
lastMod: lastMod, lastMod: lastMod,
); );
print(' ✅ [Contact] Parsed successfully');
_pendingContacts.add(contact); _pendingContacts.add(contact);
onContactReceived?.call(contact); onContactReceived?.call(contact);
} catch (e) { } catch (e) {
print(' ❌ [Contact] Parsing error: $e');
onError?.call('Contact parsing error: $e'); onError?.call('Contact parsing error: $e');
} }
} }
@@ -350,14 +407,62 @@ class MeshCoreBleService {
_pendingContacts.clear(); _pendingContacts.clear();
} }
/// Handle Sent confirmation response
void _handleSentConfirmation(BufferReader reader) {
try {
print(' [Sent] Parsing sent confirmation...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
// Sent confirmation format (from protocol):
// - 1 byte: reserved
// - 4 bytes: public key prefix (recipient)
// - 2 bytes: message ID
// - 2 bytes: reserved
if (reader.remainingBytesCount >= 9) {
final reserved1 = reader.readByte();
print(' Reserved: $reserved1');
final pubKeyPrefix = reader.readBytes(4);
print(' Recipient public key prefix: ${pubKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
final messageId = reader.readUInt16LE();
print(' Message ID: $messageId');
if (reader.remainingBytesCount >= 2) {
final reserved2 = reader.readUInt16LE();
print(' Reserved2: $reserved2');
}
}
print(' ✅ [Sent] Message sent confirmation');
} catch (e) {
print(' ❌ [Sent] Parsing error: $e');
// Don't call onError - sent confirmations are informational
}
}
/// Handle ContactMsgRecv response /// Handle ContactMsgRecv response
void _handleContactMessage(BufferReader reader) { void _handleContactMessage(BufferReader reader) {
try { try {
print(' [ContactMessage] Parsing contact message...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
final pubKeyPrefix = reader.readBytes(6); final pubKeyPrefix = reader.readBytes(6);
print(' Sender public key prefix: ${pubKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
final pathLen = reader.readByte(); final pathLen = reader.readByte();
final txtType = MessageTextType.fromValue(reader.readByte()); print(' Path length: $pathLen');
final txtTypeByte = reader.readByte();
final txtType = MessageTextType.fromValue(txtTypeByte);
print(' Text type byte: $txtTypeByte → Type: $txtType');
final senderTimestamp = reader.readUInt32LE(); final senderTimestamp = reader.readUInt32LE();
print(' Sender timestamp: $senderTimestamp (${DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000)})');
final text = reader.readString(); final text = reader.readString();
print(' Text: "$text"');
final message = Message( final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_${pubKeyPrefix.map((b) => b.toRadixString(16)).join()}', id: '${DateTime.now().millisecondsSinceEpoch}_${pubKeyPrefix.map((b) => b.toRadixString(16)).join()}',
@@ -370,8 +475,10 @@ class MeshCoreBleService {
receivedAt: DateTime.now(), receivedAt: DateTime.now(),
); );
print(' ✅ [ContactMessage] Parsed successfully');
onMessageReceived?.call(message); onMessageReceived?.call(message);
} catch (e) { } catch (e) {
print(' ❌ [ContactMessage] Parsing error: $e');
onError?.call('Contact message parsing error: $e'); onError?.call('Contact message parsing error: $e');
} }
} }
@@ -379,11 +486,24 @@ class MeshCoreBleService {
/// Handle ChannelMsgRecv response /// Handle ChannelMsgRecv response
void _handleChannelMessage(BufferReader reader) { void _handleChannelMessage(BufferReader reader) {
try { try {
print(' [ChannelMessage] Parsing channel message...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
final channelIdx = reader.readInt8(); final channelIdx = reader.readInt8();
print(' Channel index: $channelIdx');
final pathLen = reader.readByte(); final pathLen = reader.readByte();
final txtType = MessageTextType.fromValue(reader.readByte()); print(' Path length: $pathLen');
final txtTypeByte = reader.readByte();
final txtType = MessageTextType.fromValue(txtTypeByte);
print(' Text type byte: $txtTypeByte → Type: $txtType');
final senderTimestamp = reader.readUInt32LE(); final senderTimestamp = reader.readUInt32LE();
print(' Sender timestamp: $senderTimestamp (${DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000)})');
final text = reader.readString(); final text = reader.readString();
print(' Text: "$text"');
final message = Message( final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx', id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
@@ -396,8 +516,10 @@ class MeshCoreBleService {
receivedAt: DateTime.now(), receivedAt: DateTime.now(),
); );
print(' ✅ [ChannelMessage] Parsed successfully');
onMessageReceived?.call(message); onMessageReceived?.call(message);
} catch (e) { } catch (e) {
print(' ❌ [ChannelMessage] Parsing error: $e');
onError?.call('Channel message parsing error: $e'); onError?.call('Channel message parsing error: $e');
} }
} }
@@ -405,12 +527,23 @@ class MeshCoreBleService {
/// Handle TelemetryResponse push /// Handle TelemetryResponse push
void _handleTelemetryResponse(BufferReader reader) { void _handleTelemetryResponse(BufferReader reader) {
try { try {
reader.readByte(); // reserved print(' [Telemetry] Parsing telemetry response...');
final pubKeyPrefix = reader.readBytes(6); print(' Remaining bytes: ${reader.remainingBytesCount}');
final lppSensorData = reader.readRemainingBytes();
final reserved = reader.readByte();
print(' Reserved byte: $reserved');
final pubKeyPrefix = reader.readBytes(6);
print(' Public key prefix: ${pubKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
final lppSensorData = reader.readRemainingBytes();
print(' LPP sensor data length: ${lppSensorData.length} bytes');
print(' LPP sensor data (hex): ${lppSensorData.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
print(' ✅ [Telemetry] Parsed successfully');
onTelemetryReceived?.call(pubKeyPrefix, lppSensorData); onTelemetryReceived?.call(pubKeyPrefix, lppSensorData);
} catch (e) { } catch (e) {
print(' ❌ [Telemetry] Parsing error: $e');
onError?.call('Telemetry parsing error: $e'); onError?.call('Telemetry parsing error: $e');
} }
} }
@@ -500,7 +633,7 @@ class MeshCoreBleService {
print(' Device type: $deviceType'); print(' Device type: $deviceType');
print(' TX power: $txPower / $maxTxPower dBm'); print(' TX power: $txPower / $maxTxPower dBm');
print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}'); print(' Public key prefix: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
print(' Position: ${advLat / 10000.0}, ${advLon / 10000.0}'); print(' Position: ${advLat / 1000000.0}, ${advLon / 1000000.0}');
print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr'); print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr');
if (reader.hasRemaining) { if (reader.hasRemaining) {
@@ -655,11 +788,17 @@ class MeshCoreBleService {
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert); writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert);
writer.writeByte(MeshCoreConstants.selfAdvertFlood); writer.writeByte(MeshCoreConstants.selfAdvertFlood);
writer.writeInt32LE((latitude * 10000).round()); writer.writeInt32LE((latitude * 1000000).round());
writer.writeInt32LE((longitude * 10000).round()); writer.writeInt32LE((longitude * 1000000).round());
await _writeData(writer.toBytes()); await _writeData(writer.toBytes());
} }
/// Reset packet counters
void resetCounters() {
_rxPacketCount = 0;
_txPacketCount = 0;
}
/// Dispose resources /// Dispose resources
void dispose() { void dispose() {
_txSubscription?.cancel(); _txSubscription?.cancel();

View File

@@ -125,6 +125,7 @@ class MeshCoreConstants {
static const int lppHumiditySensor = 104; static const int lppHumiditySensor = 104;
static const int lppAccelerometer = 113; static const int lppAccelerometer = 113;
static const int lppBarometer = 115; static const int lppBarometer = 115;
static const int lppVoltageSensor = 116;
static const int lppGyrometer = 134; static const int lppGyrometer = 134;
static const int lppGps = 136; static const int lppGps = 136;