mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add RX/TX activity indicators and packet counters; update GPS coordinate conversion
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user