mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add Packet Log Screen for BLE packet logging and exporting
- Implemented PacketLogScreen to display and filter BLE packet logs. - Added functionality to export logs as CSV and text files. - Introduced clipboard copy feature for hex data. - Implemented clear logs functionality with confirmation dialog. - Enhanced MeshCoreBleService to log TX and RX packets with descriptions. - Added BufferReader methods for reading unsigned and signed 16-bit integers (big-endian). - Updated CayenneLppParser to read values as big-endian. - Created MessageStorageService for persisting messages to local storage. - Enhanced map markers to display telemetry data including voltage, humidity, and pressure.
This commit is contained in:
@@ -50,6 +50,22 @@ class BufferReader {
|
||||
return value > 32767 ? value - 65536 : value;
|
||||
}
|
||||
|
||||
/// Read unsigned 16-bit integer (big-endian)
|
||||
int readUInt16BE() {
|
||||
if (_offset + 2 > _buffer.length) {
|
||||
throw Exception('Buffer overflow: attempting to read beyond buffer length');
|
||||
}
|
||||
final value = (_buffer[_offset] << 8) | _buffer[_offset + 1];
|
||||
_offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Read signed 16-bit integer (big-endian)
|
||||
int readInt16BE() {
|
||||
final value = readUInt16BE();
|
||||
return value > 32767 ? value - 65536 : value;
|
||||
}
|
||||
|
||||
/// Read unsigned 32-bit integer (little-endian)
|
||||
int readUInt32LE() {
|
||||
if (_offset + 4 > _buffer.length) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAnalogInput:
|
||||
final rawValue = reader.readInt16LE();
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
print(' Analog Input (raw): $rawValue');
|
||||
print(' Analog Input (volts): ${value}V');
|
||||
@@ -63,7 +63,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAnalogOutput:
|
||||
final rawValue = reader.readInt16LE();
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
print(' Analog Output (raw): $rawValue');
|
||||
print(' Analog Output (volts): ${value}V');
|
||||
@@ -71,7 +71,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppIlluminanceSensor:
|
||||
final value = reader.readUInt16LE();
|
||||
final value = reader.readUInt16BE();
|
||||
print(' Illuminance: $value lux');
|
||||
extraSensorData['illuminance_$channel'] = value;
|
||||
break;
|
||||
@@ -83,7 +83,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppTemperatureSensor:
|
||||
final rawValue = reader.readInt16LE();
|
||||
final rawValue = reader.readInt16BE();
|
||||
temperature = rawValue / 10.0;
|
||||
print(' Temperature (raw): $rawValue');
|
||||
print(' Temperature: ${temperature?.toStringAsFixed(1)}°C');
|
||||
@@ -97,22 +97,22 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
final x = reader.readInt16LE() / 1000.0;
|
||||
final y = reader.readInt16LE() / 1000.0;
|
||||
final z = reader.readInt16LE() / 1000.0;
|
||||
final x = reader.readInt16BE() / 1000.0;
|
||||
final y = reader.readInt16BE() / 1000.0;
|
||||
final z = reader.readInt16BE() / 1000.0;
|
||||
print(' Accelerometer: x=$x, y=$y, z=$z');
|
||||
extraSensorData['accelerometer_$channel'] = {'x': x, 'y': y, 'z': z};
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
final rawValue = reader.readUInt16LE();
|
||||
final rawValue = reader.readUInt16BE();
|
||||
pressure = rawValue / 10.0;
|
||||
print(' Barometer (raw): $rawValue');
|
||||
print(' Barometer: ${pressure?.toStringAsFixed(1)} hPa');
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
final rawValue = reader.readUInt16LE();
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
print(' Voltage (raw): $rawValue');
|
||||
print(' Voltage: ${value}V');
|
||||
@@ -123,9 +123,9 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
final x = reader.readInt16LE() / 100.0;
|
||||
final y = reader.readInt16LE() / 100.0;
|
||||
final z = reader.readInt16LE() / 100.0;
|
||||
final x = reader.readInt16BE() / 100.0;
|
||||
final y = reader.readInt16BE() / 100.0;
|
||||
final z = reader.readInt16BE() / 100.0;
|
||||
print(' Gyrometer: x=$x, y=$y, z=$z');
|
||||
extraSensorData['gyrometer_$channel'] = {'x': x, 'y': y, 'z': z};
|
||||
break;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/contact_telemetry.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/ble_packet_log.dart';
|
||||
import 'buffer_reader.dart';
|
||||
import 'buffer_writer.dart';
|
||||
import 'meshcore_constants.dart';
|
||||
@@ -15,6 +16,7 @@ typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
|
||||
typedef OnMessageCallback = void Function(Message message);
|
||||
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
|
||||
typedef OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
|
||||
typedef OnNoMoreMessagesCallback = void Function();
|
||||
typedef OnErrorCallback = void Function(String error);
|
||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||
|
||||
@@ -32,6 +34,7 @@ class MeshCoreBleService {
|
||||
OnMessageCallback? onMessageReceived;
|
||||
OnTelemetryCallback? onTelemetryReceived;
|
||||
OnSelfInfoCallback? onSelfInfoReceived;
|
||||
OnNoMoreMessagesCallback? onNoMoreMessages;
|
||||
OnErrorCallback? onError;
|
||||
|
||||
// Internal state
|
||||
@@ -49,6 +52,11 @@ class MeshCoreBleService {
|
||||
VoidCallback? onRxActivity;
|
||||
VoidCallback? onTxActivity;
|
||||
|
||||
// Packet logging
|
||||
final List<BlePacketLog> _packetLogs = [];
|
||||
List<BlePacketLog> get packetLogs => List.unmodifiable(_packetLogs);
|
||||
static const int _maxLogSize = 1000; // Keep last 1000 packets
|
||||
|
||||
/// Scan for MeshCore devices
|
||||
Stream<BluetoothDevice> scanForDevices({Duration timeout = const Duration(seconds: 10)}) async* {
|
||||
try {
|
||||
@@ -238,6 +246,10 @@ class MeshCoreBleService {
|
||||
throw Exception('Characteristic does not support write operations');
|
||||
}
|
||||
|
||||
// Log TX packet (extract command code from first byte)
|
||||
final commandCode = data.isNotEmpty ? data[0] : null;
|
||||
_logPacket(data, PacketDirection.tx, responseCode: commandCode);
|
||||
|
||||
// Increment TX packet counter and trigger activity indicator
|
||||
_txPacketCount++;
|
||||
onTxActivity?.call();
|
||||
@@ -261,17 +273,22 @@ class MeshCoreBleService {
|
||||
return;
|
||||
}
|
||||
|
||||
final dataBytes = Uint8List.fromList(data);
|
||||
|
||||
// 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));
|
||||
final reader = BufferReader(dataBytes);
|
||||
final responseCode = reader.readByte();
|
||||
print(' Response code: $responseCode (0x${responseCode.toRadixString(16)})');
|
||||
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||
|
||||
// Log RX packet (before processing so we capture everything)
|
||||
_logPacket(dataBytes, PacketDirection.rx, responseCode: responseCode);
|
||||
|
||||
switch (responseCode) {
|
||||
case MeshCoreConstants.respContactsStart:
|
||||
print(' → Handling ContactsStart');
|
||||
@@ -317,6 +334,14 @@ class MeshCoreBleService {
|
||||
print(' → Handling LogRxData push');
|
||||
_handleLogRxData(reader);
|
||||
break;
|
||||
case MeshCoreConstants.pushNewAdvert:
|
||||
print(' → Handling NewAdvert push');
|
||||
_handleNewAdvert(reader);
|
||||
break;
|
||||
case MeshCoreConstants.respNoMoreMessages:
|
||||
print(' → Response: No More Messages');
|
||||
onNoMoreMessages?.call();
|
||||
break;
|
||||
case MeshCoreConstants.respOk:
|
||||
print(' → Response: OK');
|
||||
break;
|
||||
@@ -708,6 +733,79 @@ class MeshCoreBleService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle NewAdvert push
|
||||
void _handleNewAdvert(BufferReader reader) {
|
||||
try {
|
||||
print(' [NewAdvert] Parsing new advertisement...');
|
||||
print(' Remaining bytes: ${reader.remainingBytesCount}');
|
||||
|
||||
// NewAdvert format is identical to Contact response:
|
||||
// - 32 bytes: public key
|
||||
// - 1 byte: type
|
||||
// - 1 byte: flags
|
||||
// - 1 byte: outPathLen
|
||||
// - 64 bytes: outPath
|
||||
// - 32 bytes: advName (null-terminated string)
|
||||
// - 4 bytes: lastAdvert (uint32)
|
||||
// - 4 bytes: advLat (int32)
|
||||
// - 4 bytes: advLon (int32)
|
||||
// - 4 bytes: lastMod (uint32)
|
||||
|
||||
final publicKey = reader.readBytes(32);
|
||||
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,
|
||||
type: type,
|
||||
flags: flags,
|
||||
outPathLen: outPathLen,
|
||||
outPath: outPath,
|
||||
advName: advName,
|
||||
lastAdvert: lastAdvert,
|
||||
advLat: advLat,
|
||||
advLon: advLon,
|
||||
lastMod: lastMod,
|
||||
);
|
||||
|
||||
print(' ✅ [NewAdvert] Parsed successfully - new contact advertised on network');
|
||||
// Call the contact received callback to add/update the contact
|
||||
onContactReceived?.call(contact);
|
||||
} catch (e) {
|
||||
print(' ❌ [NewAdvert] Parsing error: $e');
|
||||
onError?.call('NewAdvert parsing error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Send AppStart command
|
||||
Future<void> _sendAppStart() async {
|
||||
print('📤 [BLE] Preparing AppStart command...');
|
||||
@@ -780,10 +878,11 @@ class MeshCoreBleService {
|
||||
}
|
||||
|
||||
/// Request telemetry from contact
|
||||
Future<void> requestTelemetry(Uint8List contactPublicKey) async {
|
||||
/// [zeroHop] - if true, only direct connection (no mesh forwarding)
|
||||
Future<void> requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendTelemetryReq);
|
||||
writer.writeByte(0); // reserved
|
||||
writer.writeByte(zeroHop ? 0 : 255); // hop count: 0 = direct only, 255 = unlimited
|
||||
writer.writeByte(0); // reserved
|
||||
writer.writeByte(0); // reserved
|
||||
writer.writeBytes(contactPublicKey);
|
||||
@@ -797,6 +896,14 @@ class MeshCoreBleService {
|
||||
await _writeData(writer.toBytes());
|
||||
}
|
||||
|
||||
/// Sync next message from device queue
|
||||
/// Returns true if a message was retrieved, false if no more messages
|
||||
Future<void> syncNextMessage() async {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSyncNextMessage);
|
||||
await _writeData(writer.toBytes());
|
||||
}
|
||||
|
||||
/// Set device time
|
||||
Future<void> setDeviceTime() async {
|
||||
final writer = BufferWriter();
|
||||
@@ -862,6 +969,87 @@ class MeshCoreBleService {
|
||||
await _writeData(writer.toBytes());
|
||||
}
|
||||
|
||||
/// Log a packet
|
||||
void _logPacket(Uint8List data, PacketDirection direction, {int? responseCode}) {
|
||||
// Add new packet
|
||||
_packetLogs.add(BlePacketLog(
|
||||
timestamp: DateTime.now(),
|
||||
rawData: data,
|
||||
direction: direction,
|
||||
responseCode: responseCode,
|
||||
description: _getPacketDescription(responseCode, direction),
|
||||
));
|
||||
|
||||
// Limit log size to prevent memory issues
|
||||
if (_packetLogs.length > _maxLogSize) {
|
||||
_packetLogs.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get human-readable description of packet
|
||||
String? _getPacketDescription(int? code, PacketDirection direction) {
|
||||
if (direction == PacketDirection.tx) {
|
||||
// TX packets - command codes
|
||||
switch (code) {
|
||||
case MeshCoreConstants.cmdGetContacts:
|
||||
return 'Get Contacts';
|
||||
case MeshCoreConstants.cmdSendTxtMsg:
|
||||
return 'Send Text Message';
|
||||
case MeshCoreConstants.cmdSendChannelTxtMsg:
|
||||
return 'Send Channel Message';
|
||||
case MeshCoreConstants.cmdSendTelemetryReq:
|
||||
return 'Request Telemetry';
|
||||
case MeshCoreConstants.cmdDeviceQuery:
|
||||
return 'Device Query';
|
||||
case MeshCoreConstants.cmdAppStart:
|
||||
return 'App Start';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// RX packets - response codes
|
||||
switch (code) {
|
||||
case MeshCoreConstants.respContactsStart:
|
||||
return 'Contacts Start';
|
||||
case MeshCoreConstants.respContact:
|
||||
return 'Contact Info';
|
||||
case MeshCoreConstants.respEndOfContacts:
|
||||
return 'End of Contacts';
|
||||
case MeshCoreConstants.respSent:
|
||||
return 'Message Sent';
|
||||
case MeshCoreConstants.respContactMsgRecv:
|
||||
return 'Contact Message';
|
||||
case MeshCoreConstants.respChannelMsgRecv:
|
||||
return 'Channel Message';
|
||||
case MeshCoreConstants.pushTelemetryResponse:
|
||||
return 'Telemetry Data';
|
||||
case MeshCoreConstants.respDeviceInfo:
|
||||
return 'Device Info';
|
||||
case MeshCoreConstants.respSelfInfo:
|
||||
return 'Self Info';
|
||||
case MeshCoreConstants.pushAdvert:
|
||||
return 'Advertisement';
|
||||
case MeshCoreConstants.pushLogRxData:
|
||||
return 'Log RX Data';
|
||||
case MeshCoreConstants.pushNewAdvert:
|
||||
return 'New Advertisement';
|
||||
case MeshCoreConstants.respNoMoreMessages:
|
||||
return 'No More Messages';
|
||||
case MeshCoreConstants.respOk:
|
||||
return 'OK';
|
||||
case MeshCoreConstants.respErr:
|
||||
return 'ERROR';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear packet logs
|
||||
void clearPacketLogs() {
|
||||
_packetLogs.clear();
|
||||
}
|
||||
|
||||
/// Reset packet counters
|
||||
void resetCounters() {
|
||||
_rxPacketCount = 0;
|
||||
@@ -872,5 +1060,6 @@ class MeshCoreBleService {
|
||||
void dispose() {
|
||||
_txSubscription?.cancel();
|
||||
_pendingContacts.clear();
|
||||
_packetLogs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
164
lib/services/message_storage_service.dart
Normal file
164
lib/services/message_storage_service.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting messages to local storage
|
||||
class MessageStorageService {
|
||||
static const String _messagesKey = 'stored_messages';
|
||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
||||
|
||||
/// Save messages to persistent storage
|
||||
Future<void> saveMessages(List<Message> messages) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Convert messages to JSON
|
||||
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
|
||||
|
||||
// Limit to max stored messages (keep most recent)
|
||||
final limitedList = jsonList.length > _maxStoredMessages
|
||||
? jsonList.sublist(jsonList.length - _maxStoredMessages)
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_messagesKey, jsonString);
|
||||
|
||||
print('✅ [MessageStorage] Saved ${limitedList.length} messages to storage');
|
||||
} catch (e) {
|
||||
print('❌ [MessageStorage] Error saving messages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load messages from persistent storage
|
||||
Future<List<Message>> loadMessages() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
print('ℹ️ [MessageStorage] No stored messages found');
|
||||
return [];
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
final messages = jsonList
|
||||
.map((json) => _messageFromJson(json as Map<String, dynamic>))
|
||||
.where((msg) => msg != null)
|
||||
.cast<Message>()
|
||||
.toList();
|
||||
|
||||
print('✅ [MessageStorage] Loaded ${messages.length} messages from storage');
|
||||
return messages;
|
||||
} catch (e) {
|
||||
print('❌ [MessageStorage] Error loading messages: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all stored messages
|
||||
Future<void> clearMessages() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_messagesKey);
|
||||
print('✅ [MessageStorage] Cleared all stored messages');
|
||||
} catch (e) {
|
||||
print('❌ [MessageStorage] Error clearing messages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {
|
||||
'messageCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
}
|
||||
|
||||
final sizeBytes = jsonString.length;
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
|
||||
return {
|
||||
'messageCount': jsonList.length,
|
||||
'storageSizeBytes': sizeBytes,
|
||||
'storageSizeKB': (sizeBytes / 1024).toStringAsFixed(2),
|
||||
};
|
||||
} catch (e) {
|
||||
print('❌ [MessageStorage] Error getting storage stats: $e');
|
||||
return {
|
||||
'messageCount': 0,
|
||||
'storageSizeBytes': 0,
|
||||
'storageSizeKB': 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Message to JSON
|
||||
Map<String, dynamic> _messageToJson(Message message) {
|
||||
return {
|
||||
'id': message.id,
|
||||
'messageType': message.messageType.name,
|
||||
'senderPublicKeyPrefix': message.senderPublicKeyPrefix != null
|
||||
? base64Encode(message.senderPublicKeyPrefix!)
|
||||
: null,
|
||||
'channelIdx': message.channelIdx,
|
||||
'pathLen': message.pathLen,
|
||||
'textType': message.textType.value,
|
||||
'senderTimestamp': message.senderTimestamp,
|
||||
'text': message.text,
|
||||
'isSarMarker': message.isSarMarker,
|
||||
'sarMarkerType': message.sarMarkerType?.name,
|
||||
'sarGpsLat': message.sarGpsCoordinates?.latitude,
|
||||
'sarGpsLon': message.sarGpsCoordinates?.longitude,
|
||||
'receivedAtMillis': message.receivedAt.millisecondsSinceEpoch,
|
||||
'senderName': message.senderName,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert JSON to Message
|
||||
Message? _messageFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return Message(
|
||||
id: json['id'] as String,
|
||||
messageType: MessageType.values.firstWhere(
|
||||
(e) => e.name == json['messageType'],
|
||||
orElse: () => MessageType.contact,
|
||||
),
|
||||
senderPublicKeyPrefix: json['senderPublicKeyPrefix'] != null
|
||||
? Uint8List.fromList(
|
||||
base64Decode(json['senderPublicKeyPrefix'] as String))
|
||||
: null,
|
||||
channelIdx: json['channelIdx'] as int?,
|
||||
pathLen: json['pathLen'] as int,
|
||||
textType: MessageTextType.fromValue(json['textType'] as int),
|
||||
senderTimestamp: json['senderTimestamp'] as int,
|
||||
text: json['text'] as String,
|
||||
isSarMarker: json['isSarMarker'] as bool? ?? false,
|
||||
sarMarkerType: json['sarMarkerType'] != null
|
||||
? SarMarkerType.values.firstWhere(
|
||||
(e) => e.name == json['sarMarkerType'],
|
||||
orElse: () => SarMarkerType.unknown,
|
||||
)
|
||||
: null,
|
||||
sarGpsCoordinates: json['sarGpsLat'] != null &&
|
||||
json['sarGpsLon'] != null
|
||||
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
|
||||
: null,
|
||||
receivedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['receivedAtMillis'] as int),
|
||||
senderName: json['senderName'] as String?,
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ [MessageStorage] Error parsing message from JSON: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user