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

@@ -9,6 +9,10 @@ import 'meshcore_constants.dart';
class CayenneLppParser {
/// Parse Cayenne LPP data into ContactTelemetry
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);
LatLng? gpsLocation;
@@ -19,92 +23,145 @@ class CayenneLppParser {
double? pressure;
final extraSensorData = <String, dynamic>{};
int fieldCount = 0;
while (reader.hasRemaining) {
try {
fieldCount++;
print(' [Field $fieldCount] Position: ${data.length - reader.remainingBytesCount}');
final channel = reader.readByte();
print(' Channel: $channel');
final type = reader.readByte();
print(' Type: $type (0x${type.toRadixString(16).padLeft(2, '0')})');
switch (type) {
case MeshCoreConstants.lppDigitalInput:
final value = reader.readByte();
print(' Digital Input: $value');
extraSensorData['digital_input_$channel'] = value;
break;
case MeshCoreConstants.lppDigitalOutput:
final value = reader.readByte();
print(' Digital Output: $value');
extraSensorData['digital_output_$channel'] = value;
break;
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;
// If this is a battery reading
if (channel == 0 || channel == 1) {
batteryMilliVolts = value * 1000;
batteryPercentage = _calculateBatteryPercentage(value);
print(' → Battery: ${batteryPercentage?.toStringAsFixed(1)}% (${batteryMilliVolts?.toStringAsFixed(0)}mV)');
}
break;
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;
break;
case MeshCoreConstants.lppIlluminanceSensor:
final value = reader.readUInt16LE();
print(' Illuminance: $value lux');
extraSensorData['illuminance_$channel'] = value;
break;
case MeshCoreConstants.lppPresenceSensor:
final value = reader.readByte();
print(' Presence: $value');
extraSensorData['presence_$channel'] = value;
break;
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;
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;
case MeshCoreConstants.lppAccelerometer:
final x = reader.readInt16LE() / 1000.0;
final y = 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};
break;
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;
case MeshCoreConstants.lppGyrometer:
final x = reader.readInt16LE() / 100.0;
final y = 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};
break;
case MeshCoreConstants.lppGps:
final lat = reader.readInt32LE() / 10000.0;
final lon = reader.readInt32LE() / 10000.0;
final alt = reader.readInt32LE() / 100.0;
final rawLat = reader.readInt32LE();
final rawLon = reader.readInt32LE();
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);
extraSensorData['altitude_$channel'] = alt;
break;
default:
print(' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes');
// Unknown type, skip remaining to avoid parsing errors
reader.skip(reader.remainingBytesCount);
break;
}
} catch (e) {
print(' ❌ Parsing error: $e');
// If we encounter a parsing error, break and return what we have
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(
gpsLocation: gpsLocation,
batteryPercentage: batteryPercentage,

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../models/contact.dart';
import '../models/contact_telemetry.dart';
@@ -36,6 +37,16 @@ class MeshCoreBleService {
bool _isConnected = false;
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
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
try {
@@ -225,6 +236,10 @@ class MeshCoreBleService {
throw Exception('Characteristic does not support write operations');
}
// Increment TX packet counter and trigger activity indicator
_txPacketCount++;
onTxActivity?.call();
print('✅ [BLE] Write successful');
} catch (e) {
print('❌ [BLE] Write error: $e');
@@ -237,6 +252,17 @@ class MeshCoreBleService {
void _onDataReceived(List<int> data) {
try {
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(' ')}');
final reader = BufferReader(Uint8List.fromList(data));
@@ -257,6 +283,10 @@ class MeshCoreBleService {
print(' → Handling EndOfContacts');
_handleEndOfContacts(reader);
break;
case MeshCoreConstants.respSent:
print(' → Handling Sent confirmation');
_handleSentConfirmation(reader);
break;
case MeshCoreConstants.respContactMsgRecv:
print(' → Handling ContactMessage');
_handleContactMessage(reader);
@@ -313,16 +343,41 @@ class MeshCoreBleService {
/// Handle Contact response
void _handleContact(BufferReader reader) {
try {
print(' [Contact] Parsing contact...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
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();
print(' Flags: $flags (0x${flags.toRadixString(16).padLeft(2, '0')})');
final outPathLen = reader.readInt8();
print(' Out path length: $outPathLen');
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);
print(' Advertised name: "$advName"');
final lastAdvert = reader.readUInt32LE();
print(' Last advert timestamp: $lastAdvert');
final advLat = reader.readInt32LE();
print(' Latitude (raw int32): $advLat');
print(' Latitude (decimal): ${advLat / 1000000.0}°');
final advLon = reader.readInt32LE();
print(' Longitude (raw int32): $advLon');
print(' Longitude (decimal): ${advLon / 1000000.0}°');
final lastMod = reader.readUInt32LE();
print(' Last modified timestamp: $lastMod');
final contact = Contact(
publicKey: publicKey,
@@ -337,9 +392,11 @@ class MeshCoreBleService {
lastMod: lastMod,
);
print(' ✅ [Contact] Parsed successfully');
_pendingContacts.add(contact);
onContactReceived?.call(contact);
} catch (e) {
print(' ❌ [Contact] Parsing error: $e');
onError?.call('Contact parsing error: $e');
}
}
@@ -350,14 +407,62 @@ class MeshCoreBleService {
_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
void _handleContactMessage(BufferReader reader) {
try {
print(' [ContactMessage] Parsing contact message...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
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 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();
print(' Sender timestamp: $senderTimestamp (${DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000)})');
final text = reader.readString();
print(' Text: "$text"');
final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_${pubKeyPrefix.map((b) => b.toRadixString(16)).join()}',
@@ -370,8 +475,10 @@ class MeshCoreBleService {
receivedAt: DateTime.now(),
);
print(' ✅ [ContactMessage] Parsed successfully');
onMessageReceived?.call(message);
} catch (e) {
print(' ❌ [ContactMessage] Parsing error: $e');
onError?.call('Contact message parsing error: $e');
}
}
@@ -379,11 +486,24 @@ class MeshCoreBleService {
/// Handle ChannelMsgRecv response
void _handleChannelMessage(BufferReader reader) {
try {
print(' [ChannelMessage] Parsing channel message...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
final channelIdx = reader.readInt8();
print(' Channel index: $channelIdx');
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();
print(' Sender timestamp: $senderTimestamp (${DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000)})');
final text = reader.readString();
print(' Text: "$text"');
final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
@@ -396,8 +516,10 @@ class MeshCoreBleService {
receivedAt: DateTime.now(),
);
print(' ✅ [ChannelMessage] Parsed successfully');
onMessageReceived?.call(message);
} catch (e) {
print(' ❌ [ChannelMessage] Parsing error: $e');
onError?.call('Channel message parsing error: $e');
}
}
@@ -405,12 +527,23 @@ class MeshCoreBleService {
/// Handle TelemetryResponse push
void _handleTelemetryResponse(BufferReader reader) {
try {
reader.readByte(); // reserved
final pubKeyPrefix = reader.readBytes(6);
final lppSensorData = reader.readRemainingBytes();
print(' [Telemetry] Parsing telemetry response...');
print(' Remaining bytes: ${reader.remainingBytesCount}');
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);
} catch (e) {
print(' ❌ [Telemetry] Parsing error: $e');
onError?.call('Telemetry parsing error: $e');
}
}
@@ -500,7 +633,7 @@ class MeshCoreBleService {
print(' Device type: $deviceType');
print(' TX power: $txPower / $maxTxPower dBm');
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');
if (reader.hasRemaining) {
@@ -655,11 +788,17 @@ class MeshCoreBleService {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert);
writer.writeByte(MeshCoreConstants.selfAdvertFlood);
writer.writeInt32LE((latitude * 10000).round());
writer.writeInt32LE((longitude * 10000).round());
writer.writeInt32LE((latitude * 1000000).round());
writer.writeInt32LE((longitude * 1000000).round());
await _writeData(writer.toBytes());
}
/// Reset packet counters
void resetCounters() {
_rxPacketCount = 0;
_txPacketCount = 0;
}
/// Dispose resources
void dispose() {
_txSubscription?.cancel();

View File

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