initial commit

This commit is contained in:
Janez T
2025-10-13 22:28:20 +02:00
commit 823a163123
175 changed files with 12697 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
import 'dart:typed_data';
import 'dart:convert';
/// Buffer reader for parsing MeshCore protocol binary data
class BufferReader {
final Uint8List _buffer;
int _offset = 0;
BufferReader(this._buffer);
/// Get remaining bytes count
int get remainingBytesCount => _buffer.length - _offset;
/// Check if there are bytes remaining
bool get hasRemaining => _offset < _buffer.length;
/// Get current offset
int get offset => _offset;
/// Set offset
set offset(int value) => _offset = value;
/// Read a single byte (uint8)
int readByte() {
if (_offset >= _buffer.length) {
throw Exception('Buffer overflow: attempting to read beyond buffer length');
}
return _buffer[_offset++];
}
/// Read a signed byte (int8)
int readInt8() {
final value = readByte();
return value > 127 ? value - 256 : value;
}
/// Read unsigned 16-bit integer (little-endian)
int readUInt16LE() {
if (_offset + 2 > _buffer.length) {
throw Exception('Buffer overflow: attempting to read beyond buffer length');
}
final value = _buffer[_offset] | (_buffer[_offset + 1] << 8);
_offset += 2;
return value;
}
/// Read signed 16-bit integer (little-endian)
int readInt16LE() {
final value = readUInt16LE();
return value > 32767 ? value - 65536 : value;
}
/// Read unsigned 32-bit integer (little-endian)
int readUInt32LE() {
if (_offset + 4 > _buffer.length) {
throw Exception('Buffer overflow: attempting to read beyond buffer length');
}
final value = _buffer[_offset] |
(_buffer[_offset + 1] << 8) |
(_buffer[_offset + 2] << 16) |
(_buffer[_offset + 3] << 24);
_offset += 4;
return value;
}
/// Read signed 32-bit integer (little-endian)
int readInt32LE() {
final value = readUInt32LE();
return value > 2147483647 ? value - 4294967296 : value;
}
/// Read a fixed number of bytes
Uint8List readBytes(int length) {
if (_offset + length > _buffer.length) {
throw Exception('Buffer overflow: attempting to read beyond buffer length');
}
final bytes = _buffer.sublist(_offset, _offset + length);
_offset += length;
return bytes;
}
/// Read remaining bytes
Uint8List readRemainingBytes() {
final bytes = _buffer.sublist(_offset);
_offset = _buffer.length;
return bytes;
}
/// Read null-terminated string (C-string) with max length
String readCString(int maxLength) {
if (_offset + maxLength > _buffer.length) {
throw Exception('Buffer overflow: attempting to read beyond buffer length');
}
final bytes = _buffer.sublist(_offset, _offset + maxLength);
_offset += maxLength;
// Find null terminator
int nullIndex = bytes.indexOf(0);
if (nullIndex == -1) {
nullIndex = maxLength;
}
// Decode string up to null terminator
return utf8.decode(bytes.sublist(0, nullIndex));
}
/// Read length-prefixed string (remaining bytes as UTF-8)
String readString() {
final bytes = readRemainingBytes();
return utf8.decode(bytes);
}
/// Peek at next byte without advancing offset
int peekByte() {
if (_offset >= _buffer.length) {
throw Exception('Buffer overflow: attempting to peek beyond buffer length');
}
return _buffer[_offset];
}
/// Skip bytes
void skip(int count) {
if (_offset + count > _buffer.length) {
throw Exception('Buffer overflow: attempting to skip beyond buffer length');
}
_offset += count;
}
/// Reset offset to beginning
void reset() {
_offset = 0;
}
@override
String toString() {
return 'BufferReader(length: ${_buffer.length}, offset: $_offset, remaining: $remainingBytesCount)';
}
}

View File

@@ -0,0 +1,129 @@
import 'dart:typed_data';
import 'dart:convert';
/// Buffer writer for creating MeshCore protocol binary data
class BufferWriter {
final List<int> _buffer = [];
/// Get current buffer length
int get length => _buffer.length;
/// Write a single byte (uint8)
void writeByte(int value) {
if (value < 0 || value > 255) {
throw ArgumentError('Byte value must be between 0 and 255');
}
_buffer.add(value);
}
/// Write a signed byte (int8)
void writeInt8(int value) {
if (value < -128 || value > 127) {
throw ArgumentError('Int8 value must be between -128 and 127');
}
_buffer.add(value < 0 ? value + 256 : value);
}
/// Write unsigned 16-bit integer (little-endian)
void writeUInt16LE(int value) {
if (value < 0 || value > 65535) {
throw ArgumentError('UInt16 value must be between 0 and 65535');
}
_buffer.add(value & 0xFF);
_buffer.add((value >> 8) & 0xFF);
}
/// Write signed 16-bit integer (little-endian)
void writeInt16LE(int value) {
if (value < -32768 || value > 32767) {
throw ArgumentError('Int16 value must be between -32768 and 32767');
}
final unsigned = value < 0 ? value + 65536 : value;
writeUInt16LE(unsigned);
}
/// Write unsigned 32-bit integer (little-endian)
void writeUInt32LE(int value) {
if (value < 0 || value > 4294967295) {
throw ArgumentError('UInt32 value must be between 0 and 4294967295');
}
_buffer.add(value & 0xFF);
_buffer.add((value >> 8) & 0xFF);
_buffer.add((value >> 16) & 0xFF);
_buffer.add((value >> 24) & 0xFF);
}
/// Write signed 32-bit integer (little-endian)
void writeInt32LE(int value) {
if (value < -2147483648 || value > 2147483647) {
throw ArgumentError('Int32 value must be between -2147483648 and 2147483647');
}
final unsigned = value < 0 ? value + 4294967296 : value;
writeUInt32LE(unsigned);
}
/// Write bytes from Uint8List
void writeBytes(Uint8List bytes) {
_buffer.addAll(bytes);
}
/// Write bytes from List<int>
void writeBytesFromList(List<int> bytes) {
_buffer.addAll(bytes);
}
/// Write null-terminated string (C-string) with fixed length
/// Pads with zeros if string is shorter than maxLength
void writeCString(String str, int maxLength) {
final bytes = utf8.encode(str);
// Ensure we don't exceed max length
final length = bytes.length < maxLength ? bytes.length : maxLength;
// Write string bytes
for (int i = 0; i < length; i++) {
_buffer.add(bytes[i]);
}
// Pad with zeros
for (int i = length; i < maxLength; i++) {
_buffer.add(0);
}
}
/// Write length-prefixed string
void writeString(String str) {
final bytes = utf8.encode(str);
_buffer.addAll(bytes);
}
/// Write string with length prefix (1 byte)
void writeLengthPrefixedString(String str) {
final bytes = utf8.encode(str);
if (bytes.length > 255) {
throw ArgumentError('String too long for length-prefixed format (max 255 bytes)');
}
writeByte(bytes.length);
_buffer.addAll(bytes);
}
/// Get buffer as Uint8List
Uint8List toBytes() {
return Uint8List.fromList(_buffer);
}
/// Clear the buffer
void clear() {
_buffer.clear();
}
/// Get buffer as hex string (for debugging)
String toHexString() {
return _buffer.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
}
@override
String toString() {
return 'BufferWriter(length: $length, hex: ${toHexString()})';
}
}

View File

@@ -0,0 +1,186 @@
import 'dart:typed_data';
import 'package:latlong2/latlong.dart';
import '../models/contact_telemetry.dart';
import 'buffer_reader.dart';
import 'meshcore_constants.dart';
/// Cayenne LPP (Low Power Payload) data parser
/// Used for decoding telemetry sensor data from MeshCore devices
class CayenneLppParser {
/// Parse Cayenne LPP data into ContactTelemetry
static ContactTelemetry parse(Uint8List data) {
final reader = BufferReader(data);
LatLng? gpsLocation;
double? batteryPercentage;
double? batteryMilliVolts;
double? temperature;
double? humidity;
double? pressure;
final extraSensorData = <String, dynamic>{};
while (reader.hasRemaining) {
try {
final channel = reader.readByte();
final type = reader.readByte();
switch (type) {
case MeshCoreConstants.lppDigitalInput:
final value = reader.readByte();
extraSensorData['digital_input_$channel'] = value;
break;
case MeshCoreConstants.lppDigitalOutput:
final value = reader.readByte();
extraSensorData['digital_output_$channel'] = value;
break;
case MeshCoreConstants.lppAnalogInput:
final value = reader.readInt16LE() / 100.0;
extraSensorData['analog_input_$channel'] = value;
// If this is a battery reading
if (channel == 0 || channel == 1) {
batteryMilliVolts = value * 1000;
batteryPercentage = _calculateBatteryPercentage(value);
}
break;
case MeshCoreConstants.lppAnalogOutput:
final value = reader.readInt16LE() / 100.0;
extraSensorData['analog_output_$channel'] = value;
break;
case MeshCoreConstants.lppIlluminanceSensor:
final value = reader.readUInt16LE();
extraSensorData['illuminance_$channel'] = value;
break;
case MeshCoreConstants.lppPresenceSensor:
final value = reader.readByte();
extraSensorData['presence_$channel'] = value;
break;
case MeshCoreConstants.lppTemperatureSensor:
temperature = reader.readInt16LE() / 10.0;
break;
case MeshCoreConstants.lppHumiditySensor:
humidity = reader.readByte() / 2.0;
break;
case MeshCoreConstants.lppAccelerometer:
final x = reader.readInt16LE() / 1000.0;
final y = reader.readInt16LE() / 1000.0;
final z = reader.readInt16LE() / 1000.0;
extraSensorData['accelerometer_$channel'] = {'x': x, 'y': y, 'z': z};
break;
case MeshCoreConstants.lppBarometer:
pressure = reader.readUInt16LE() / 10.0;
break;
case MeshCoreConstants.lppGyrometer:
final x = reader.readInt16LE() / 100.0;
final y = reader.readInt16LE() / 100.0;
final z = reader.readInt16LE() / 100.0;
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;
gpsLocation = LatLng(lat, lon);
extraSensorData['altitude_$channel'] = alt;
break;
default:
// Unknown type, skip remaining to avoid parsing errors
reader.skip(reader.remainingBytesCount);
break;
}
} catch (e) {
// If we encounter a parsing error, break and return what we have
break;
}
}
return ContactTelemetry(
gpsLocation: gpsLocation,
batteryPercentage: batteryPercentage,
batteryMilliVolts: batteryMilliVolts,
temperature: temperature,
humidity: humidity,
pressure: pressure,
timestamp: DateTime.now(),
extraSensorData: extraSensorData.isNotEmpty ? extraSensorData : null,
);
}
/// Calculate battery percentage from voltage (V)
static double _calculateBatteryPercentage(double voltage) {
// Standard lithium battery curve: 3.0V = 0%, 4.2V = 100%
if (voltage <= 3.0) return 0.0;
if (voltage >= 4.2) return 100.0;
return ((voltage - 3.0) / 1.2) * 100.0;
}
/// Create Cayenne LPP data for GPS location
static Uint8List createGpsData({
required double latitude,
required double longitude,
double altitude = 0.0,
int channel = 0,
}) {
final buffer = <int>[];
buffer.add(channel);
buffer.add(MeshCoreConstants.lppGps);
// Latitude (3 bytes, signed, 0.0001° precision)
final lat = (latitude * 10000).round();
buffer.add((lat >> 16) & 0xFF);
buffer.add((lat >> 8) & 0xFF);
buffer.add(lat & 0xFF);
// Longitude (3 bytes, signed, 0.0001° precision)
final lon = (longitude * 10000).round();
buffer.add((lon >> 16) & 0xFF);
buffer.add((lon >> 8) & 0xFF);
buffer.add(lon & 0xFF);
// Altitude (3 bytes, signed, 0.01m precision)
final alt = (altitude * 100).round();
buffer.add((alt >> 16) & 0xFF);
buffer.add((alt >> 8) & 0xFF);
buffer.add(alt & 0xFF);
return Uint8List.fromList(buffer);
}
/// Create Cayenne LPP data for temperature
static Uint8List createTemperatureData(double celsius, {int channel = 0}) {
final buffer = <int>[];
buffer.add(channel);
buffer.add(MeshCoreConstants.lppTemperatureSensor);
final temp = (celsius * 10).round();
buffer.add((temp >> 8) & 0xFF);
buffer.add(temp & 0xFF);
return Uint8List.fromList(buffer);
}
/// Create Cayenne LPP data for battery voltage
static Uint8List createBatteryData(double voltage, {int channel = 0}) {
final buffer = <int>[];
buffer.add(channel);
buffer.add(MeshCoreConstants.lppAnalogInput);
final volts = (voltage * 100).round();
buffer.add((volts >> 8) & 0xFF);
buffer.add(volts & 0xFF);
return Uint8List.fromList(buffer);
}
}

View File

@@ -0,0 +1,394 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../models/contact.dart';
import '../models/contact_telemetry.dart';
import '../models/message.dart';
import 'buffer_reader.dart';
import 'buffer_writer.dart';
import 'meshcore_constants.dart';
/// Callback types for MeshCore events
typedef OnContactCallback = void Function(Contact contact);
typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
typedef OnMessageCallback = void Function(Message message);
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
typedef OnErrorCallback = void Function(String error);
typedef OnConnectionStateCallback = void Function(bool isConnected);
/// MeshCore BLE Service - handles all BLE communication
class MeshCoreBleService {
BluetoothDevice? _device;
BluetoothCharacteristic? _rxCharacteristic;
BluetoothCharacteristic? _txCharacteristic;
StreamSubscription? _txSubscription;
// Event callbacks
OnConnectionStateCallback? onConnectionStateChanged;
OnContactCallback? onContactReceived;
OnContactsCompleteCallback? onContactsComplete;
OnMessageCallback? onMessageReceived;
OnTelemetryCallback? onTelemetryReceived;
OnErrorCallback? onError;
// Internal state
final List<Contact> _pendingContacts = [];
bool _isConnected = false;
bool get isConnected => _isConnected;
/// Scan for MeshCore devices
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
try {
// Start scanning
await FlutterBluePlus.startScan(
timeout: timeout,
withServices: [Guid(MeshCoreConstants.bleServiceUuid)],
);
// Listen to scan results
await for (final scanResult in FlutterBluePlus.scanResults) {
for (final result in scanResult) {
if (result.advertisementData.serviceUuids
.contains(Guid(MeshCoreConstants.bleServiceUuid))) {
yield result.device;
}
}
}
} catch (e) {
onError?.call('Scan error: $e');
}
}
/// Connect to a MeshCore device
Future<bool> connect(BluetoothDevice device) async {
try {
_device = device;
// Connect to device
await device.connect(
license: License.free,
timeout: const Duration(seconds: 15),
mtu: 512,
);
// Discover services
final services = await device.discoverServices();
// Find MeshCore service
BluetoothService? meshCoreService;
for (final service in services) {
if (service.uuid.toString().toLowerCase() ==
MeshCoreConstants.bleServiceUuid.toLowerCase()) {
meshCoreService = service;
break;
}
}
if (meshCoreService == null) {
throw Exception('MeshCore service not found');
}
// Find RX and TX characteristics
for (final characteristic in meshCoreService.characteristics) {
final uuid = characteristic.uuid.toString().toLowerCase();
if (uuid == MeshCoreConstants.bleCharacteristicRxUuid.toLowerCase()) {
_rxCharacteristic = characteristic;
} else if (uuid ==
MeshCoreConstants.bleCharacteristicTxUuid.toLowerCase()) {
_txCharacteristic = characteristic;
}
}
if (_rxCharacteristic == null || _txCharacteristic == null) {
throw Exception('Required characteristics not found');
}
// Enable notifications on TX characteristic
await _txCharacteristic!.setNotifyValue(true);
// Listen to TX characteristic
_txSubscription = _txCharacteristic!.lastValueStream.listen(
_onDataReceived,
onError: (error) => onError?.call('TX notification error: $error'),
);
_isConnected = true;
onConnectionStateChanged?.call(true);
// Send initial device query
await _sendDeviceQuery();
return true;
} catch (e) {
onError?.call('Connection error: $e');
_isConnected = false;
onConnectionStateChanged?.call(false);
return false;
}
}
/// Disconnect from device
Future<void> disconnect() async {
try {
await _txSubscription?.cancel();
await _device?.disconnect();
_isConnected = false;
_device = null;
_rxCharacteristic = null;
_txCharacteristic = null;
onConnectionStateChanged?.call(false);
} catch (e) {
onError?.call('Disconnect error: $e');
}
}
/// Write data to RX characteristic
Future<void> _writeData(Uint8List data) async {
if (_rxCharacteristic == null) {
throw Exception('Not connected');
}
try {
await _rxCharacteristic!.write(data, withoutResponse: true);
} catch (e) {
onError?.call('Write error: $e');
rethrow;
}
}
/// Handle incoming data from TX characteristic
void _onDataReceived(List<int> data) {
try {
final reader = BufferReader(Uint8List.fromList(data));
final responseCode = reader.readByte();
switch (responseCode) {
case MeshCoreConstants.respContactsStart:
_handleContactsStart(reader);
break;
case MeshCoreConstants.respContact:
_handleContact(reader);
break;
case MeshCoreConstants.respEndOfContacts:
_handleEndOfContacts(reader);
break;
case MeshCoreConstants.respContactMsgRecv:
_handleContactMessage(reader);
break;
case MeshCoreConstants.respChannelMsgRecv:
_handleChannelMessage(reader);
break;
case MeshCoreConstants.pushTelemetryResponse:
_handleTelemetryResponse(reader);
break;
case MeshCoreConstants.respOk:
case MeshCoreConstants.respErr:
// Handle OK/Error responses if needed
break;
default:
// Unknown response code
break;
}
} catch (e) {
onError?.call('Data parsing error: $e');
}
}
/// Handle ContactsStart response
void _handleContactsStart(BufferReader reader) {
_pendingContacts.clear();
final count = reader.readUInt32LE();
// Optional: notify about expected count
}
/// Handle Contact response
void _handleContact(BufferReader reader) {
try {
final publicKey = reader.readBytes(32);
final type = ContactType.fromValue(reader.readByte());
final flags = reader.readByte();
final outPathLen = reader.readInt8();
final outPath = reader.readBytes(64);
final advName = reader.readCString(32);
final lastAdvert = reader.readUInt32LE();
final advLat = reader.readInt32LE();
final advLon = reader.readInt32LE();
final lastMod = reader.readUInt32LE();
final contact = Contact(
publicKey: publicKey,
type: type,
flags: flags,
outPathLen: outPathLen,
outPath: outPath,
advName: advName,
lastAdvert: lastAdvert,
advLat: advLat,
advLon: advLon,
lastMod: lastMod,
);
_pendingContacts.add(contact);
onContactReceived?.call(contact);
} catch (e) {
onError?.call('Contact parsing error: $e');
}
}
/// Handle EndOfContacts response
void _handleEndOfContacts(BufferReader reader) {
onContactsComplete?.call(List.from(_pendingContacts));
_pendingContacts.clear();
}
/// Handle ContactMsgRecv response
void _handleContactMessage(BufferReader reader) {
try {
final pubKeyPrefix = reader.readBytes(6);
final pathLen = reader.readByte();
final txtType = MessageTextType.fromValue(reader.readByte());
final senderTimestamp = reader.readUInt32LE();
final text = reader.readString();
final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_${pubKeyPrefix.map((b) => b.toRadixString(16)).join()}',
messageType: MessageType.contact,
senderPublicKeyPrefix: pubKeyPrefix,
pathLen: pathLen,
textType: txtType,
senderTimestamp: senderTimestamp,
text: text,
receivedAt: DateTime.now(),
);
onMessageReceived?.call(message);
} catch (e) {
onError?.call('Contact message parsing error: $e');
}
}
/// Handle ChannelMsgRecv response
void _handleChannelMessage(BufferReader reader) {
try {
final channelIdx = reader.readInt8();
final pathLen = reader.readByte();
final txtType = MessageTextType.fromValue(reader.readByte());
final senderTimestamp = reader.readUInt32LE();
final text = reader.readString();
final message = Message(
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
messageType: MessageType.channel,
channelIdx: channelIdx,
pathLen: pathLen,
textType: txtType,
senderTimestamp: senderTimestamp,
text: text,
receivedAt: DateTime.now(),
);
onMessageReceived?.call(message);
} catch (e) {
onError?.call('Channel message parsing error: $e');
}
}
/// Handle TelemetryResponse push
void _handleTelemetryResponse(BufferReader reader) {
try {
reader.readByte(); // reserved
final pubKeyPrefix = reader.readBytes(6);
final lppSensorData = reader.readRemainingBytes();
onTelemetryReceived?.call(pubKeyPrefix, lppSensorData);
} catch (e) {
onError?.call('Telemetry parsing error: $e');
}
}
/// Send AppStart command
Future<void> _sendAppStart() async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdAppStart);
writer.writeByte(1); // appVer
writer.writeBytes(Uint8List(6)); // reserved
writer.writeString('MeshCore SAR'); // appName
await _writeData(writer.toBytes());
}
/// Send DeviceQuery command
Future<void> _sendDeviceQuery() async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdDeviceQuery);
writer.writeByte(MeshCoreConstants.supportedCompanionProtocolVersion);
await _writeData(writer.toBytes());
await _sendAppStart();
}
/// Get contacts from device
Future<void> getContacts() async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdGetContacts);
await _writeData(writer.toBytes());
}
/// Send text message to contact
Future<void> sendTextMessage({
required Uint8List contactPublicKey,
required String text,
}) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendTxtMsg);
writer.writeByte(MeshCoreConstants.txtTypePlain);
writer.writeByte(0); // attempt
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
writer.writeBytes(contactPublicKey.sublist(0, 6));
writer.writeString(text);
await _writeData(writer.toBytes());
}
/// Send channel text message
Future<void> sendChannelMessage({
required int channelIdx,
required String text,
}) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendChannelTxtMsg);
writer.writeByte(MeshCoreConstants.txtTypePlain);
writer.writeByte(channelIdx);
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
writer.writeString(text);
await _writeData(writer.toBytes());
}
/// Request telemetry from contact
Future<void> requestTelemetry(Uint8List contactPublicKey) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSendTelemetryReq);
writer.writeByte(0); // reserved
writer.writeByte(0); // reserved
writer.writeByte(0); // reserved
writer.writeBytes(contactPublicKey);
await _writeData(writer.toBytes());
}
/// Get battery voltage
Future<void> getBatteryVoltage() async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdGetBatteryVoltage);
await _writeData(writer.toBytes());
}
/// Set device time
Future<void> setDeviceTime() async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetDeviceTime);
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
await _writeData(writer.toBytes());
}
/// Dispose resources
void dispose() {
_txSubscription?.cancel();
_pendingContacts.clear();
}
}

View File

@@ -0,0 +1,138 @@
/// MeshCore BLE and Protocol Constants
class MeshCoreConstants {
// Supported protocol version
static const int supportedCompanionProtocolVersion = 1;
// BLE Service and Characteristic UUIDs
static const String bleServiceUuid =
'6E400001-B5A3-F393-E0A9-E50E24DCCA9E';
static const String bleCharacteristicRxUuid =
'6E400002-B5A3-F393-E0A9-E50E24DCCA9E'; // Write
static const String bleCharacteristicTxUuid =
'6E400003-B5A3-F393-E0A9-E50E24DCCA9E'; // Notify
// Command Codes (App -> Device)
static const int cmdAppStart = 1;
static const int cmdSendTxtMsg = 2;
static const int cmdSendChannelTxtMsg = 3;
static const int cmdGetContacts = 4;
static const int cmdGetDeviceTime = 5;
static const int cmdSetDeviceTime = 6;
static const int cmdSendSelfAdvert = 7;
static const int cmdSetAdvertName = 8;
static const int cmdAddUpdateContact = 9;
static const int cmdSyncNextMessage = 10;
static const int cmdSetRadioParams = 11;
static const int cmdSetTxPower = 12;
static const int cmdResetPath = 13;
static const int cmdSetAdvertLatLon = 14;
static const int cmdRemoveContact = 15;
static const int cmdShareContact = 16;
static const int cmdExportContact = 17;
static const int cmdImportContact = 18;
static const int cmdReboot = 19;
static const int cmdGetBatteryVoltage = 20;
static const int cmdSetTuningParams = 21;
static const int cmdDeviceQuery = 22;
static const int cmdExportPrivateKey = 23;
static const int cmdImportPrivateKey = 24;
static const int cmdSendRawData = 25;
static const int cmdSendLogin = 26;
static const int cmdSendStatusReq = 27;
static const int cmdGetChannel = 31;
static const int cmdSetChannel = 32;
static const int cmdSignStart = 33;
static const int cmdSignData = 34;
static const int cmdSignFinish = 35;
static const int cmdSendTracePath = 36;
static const int cmdSetOtherParams = 38;
static const int cmdSendTelemetryReq = 39;
static const int cmdSendBinaryReq = 50;
// Response Codes (Device -> App)
static const int respOk = 0;
static const int respErr = 1;
static const int respContactsStart = 2;
static const int respContact = 3;
static const int respEndOfContacts = 4;
static const int respSelfInfo = 5;
static const int respSent = 6;
static const int respContactMsgRecv = 7;
static const int respChannelMsgRecv = 8;
static const int respCurrTime = 9;
static const int respNoMoreMessages = 10;
static const int respExportContact = 11;
static const int respBatteryVoltage = 12;
static const int respDeviceInfo = 13;
static const int respPrivateKey = 14;
static const int respDisabled = 15;
static const int respChannelInfo = 18;
static const int respSignStart = 19;
static const int respSignature = 20;
// Push Codes (Device -> App, unsolicited)
static const int pushAdvert = 0x80;
static const int pushPathUpdated = 0x81;
static const int pushSendConfirmed = 0x82;
static const int pushMsgWaiting = 0x83;
static const int pushRawData = 0x84;
static const int pushLoginSuccess = 0x85;
static const int pushLoginFail = 0x86;
static const int pushStatusResponse = 0x87;
static const int pushLogRxData = 0x88;
static const int pushTraceData = 0x89;
static const int pushNewAdvert = 0x8A;
static const int pushTelemetryResponse = 0x8B;
static const int pushBinaryResponse = 0x8C;
// Error Codes
static const int errUnsupportedCmd = 1;
static const int errNotFound = 2;
static const int errTableFull = 3;
static const int errBadState = 4;
static const int errFileIoError = 5;
static const int errIllegalArg = 6;
// Advert Types
static const int advTypeNone = 0;
static const int advTypeChat = 1;
static const int advTypeRepeater = 2;
static const int advTypeRoom = 3;
// Self Advert Types
static const int selfAdvertZeroHop = 0;
static const int selfAdvertFlood = 1;
// Text Types
static const int txtTypePlain = 0;
static const int txtTypeCliData = 1;
static const int txtTypeSignedPlain = 2;
// Binary Request Types
static const int binaryReqGetTelemetryData = 0x03;
static const int binaryReqGetAvgMinMax = 0x04;
static const int binaryReqGetAccessList = 0x05;
static const int binaryReqGetNeighbours = 0x06;
// Cayenne LPP Data Types
static const int lppDigitalInput = 0;
static const int lppDigitalOutput = 1;
static const int lppAnalogInput = 2;
static const int lppAnalogOutput = 3;
static const int lppIlluminanceSensor = 101;
static const int lppPresenceSensor = 102;
static const int lppTemperatureSensor = 103;
static const int lppHumiditySensor = 104;
static const int lppAccelerometer = 113;
static const int lppBarometer = 115;
static const int lppGyrometer = 134;
static const int lppGps = 136;
// MTU and timing
static const int maxMtuSize = 512;
static const int defaultTimeout = 5000; // 5 seconds
static const int reconnectDelay = 2000; // 2 seconds
static const int telemetryUpdateInterval = 300000; // 5 minutes
MeshCoreConstants._(); // Private constructor to prevent instantiation
}

View File

@@ -0,0 +1,170 @@
import 'dart:io';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
import 'package:flutter_map_tile_caching/custom_backend_api.dart';
import 'package:path_provider/path_provider.dart';
import '../models/map_layer.dart';
class TileCacheService {
static const String _storeName = 'meshcore_sar_tiles';
late final FMTCStore _store;
bool _isInitialized = false;
bool _isDownloading = false;
Future<void> initialize() async {
if (_isInitialized) return;
try {
await FMTCObjectBoxBackend().initialise();
_store = FMTCStore(_storeName);
await _store.manage.create();
_isInitialized = true;
} catch (e) {
// Store might already exist
_store = FMTCStore(_storeName);
_isInitialized = true;
}
}
FMTCTileProvider getTileProvider(MapLayer layer) {
if (!_isInitialized) {
throw StateError('TileCacheService not initialized. Call initialize() first.');
}
return _store.getTileProvider(
loadingStrategy: BrowseLoadingStrategy.cacheFirst,
cachedValidDuration: const Duration(days: 30),
);
}
Future<void> downloadRegion({
required MapLayer layer,
required LatLngBounds bounds,
required int minZoom,
required int maxZoom,
Function(double progress)? onProgress,
}) async {
if (!_isInitialized) {
throw StateError('TileCacheService not initialized. Call initialize() first.');
}
if (_isDownloading) {
throw StateError('A download is already in progress. Cancel it first.');
}
_isDownloading = true;
try {
final region = RectangleRegion(bounds);
final downloadable = region.toDownloadable(
minZoom: minZoom,
maxZoom: maxZoom,
options: TileLayer(
urlTemplate: layer.urlTemplate,
),
);
final download = _store.download.startForeground(
region: downloadable,
);
await for (final progress in download.downloadProgress) {
if (onProgress != null && progress.maxTilesCount > 0) {
// Use attemptedTilesCount instead of successfulTilesCount
// attemptedTilesCount includes successful + buffered + skipped tiles
final percentage = progress.percentageProgress;
print('Download progress: ${progress.attemptedTilesCount}/${progress.maxTilesCount} = ${percentage.toStringAsFixed(1)}% (successful: ${progress.successfulTilesCount}, buffered: ${progress.bufferedTilesCount}, skipped: ${progress.skippedTilesCount})');
onProgress(percentage);
}
}
} finally {
_isDownloading = false;
}
}
Future<void> cancelDownload() async {
if (!_isInitialized) return;
await _store.download.cancel();
}
Future<void> clearCache() async {
if (!_isInitialized) return;
await _store.manage.delete();
await _store.manage.create();
}
Future<int> getCachedTileCount() async {
if (!_isInitialized) return 0;
final stats = await _store.stats.length;
return stats;
}
Future<double> getCacheSizeMB() async {
if (!_isInitialized) return 0.0;
final stats = await _store.stats.size;
return stats / (1024 * 1024);
}
Future<String> exportCache() async {
if (!_isInitialized) {
throw StateError('TileCacheService not initialized. Call initialize() first.');
}
final directory = await getApplicationDocumentsDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
final exportPath = '${directory.path}/meshcore_maps_$timestamp.fmtc';
// Export using FMTCBackendAccess
await FMTCBackendAccess.internal.exportStores(
storeNames: [_storeName],
path: exportPath,
);
return exportPath;
}
Future<void> importCache(String filePath) async {
if (!_isInitialized) {
throw StateError('TileCacheService not initialized. Call initialize() first.');
}
final file = File(filePath);
if (!await file.exists()) {
throw Exception('Import file not found: $filePath');
}
// Import using FMTCBackendAccess
await FMTCBackendAccess.internal.importStores(
storeNames: [_storeName],
path: filePath,
strategy: ImportConflictStrategy.rename,
);
}
Future<List<String>> getAvailableStores() async {
if (!_isInitialized) {
throw StateError('TileCacheService not initialized. Call initialize() first.');
}
final stores = await FMTCRoot.stats.storesAvailable;
return stores.map((store) => store.storeName).toList();
}
Future<Map<String, dynamic>> getStoreStats() async {
if (!_isInitialized) return {};
final length = await _store.stats.length;
final size = await _store.stats.size;
return {
'tileCount': length,
'sizeMB': size / (1024 * 1024),
'storeName': _storeName,
};
}
void dispose() {
_isInitialized = false;
}
}