Add comprehensive tests for DrawingMessageParser and SarMessageParser

- Implement critical string formatting tests for DrawingMessageParser, ensuring correct message creation, parsing, and validation.
- Validate JSON structure, coordinate formatting, and color index preservation in drawing messages.
- Introduce tests for various drawing types, including LineDrawing and RectangleDrawing, with emphasis on output consistency and error handling.
- Add tests for SarMessageParser, covering message creation, parsing, and format validation, including backward compatibility with old formats.
- Ensure proper handling of special characters, empty notes, and extreme coordinate values in SAR messages.
- Validate that messages conform to the CLAUDE.md specification and maintain compactness and efficiency.
This commit is contained in:
Janez T
2025-10-26 16:51:38 +01:00
parent cc4d666a94
commit 3a20b756fd
14 changed files with 1520 additions and 27 deletions

View File

@@ -24,7 +24,7 @@ class AppProvider with ChangeNotifier {
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
bool _isSimpleMode = false;
bool _isSimpleMode = true;
bool get isSimpleMode => _isSimpleMode;
AppProvider({
@@ -64,7 +64,7 @@ class AppProvider with ChangeNotifier {
Future<void> _loadSimpleMode() async {
try {
final prefs = await SharedPreferences.getInstance();
_isSimpleMode = prefs.getBool('simple_mode') ?? false;
_isSimpleMode = prefs.getBool('simple_mode') ?? true;
notifyListeners();
} catch (e) {
debugPrint('Error loading simple mode setting: $e');

View File

@@ -440,7 +440,7 @@ class _MessagesTabState extends State<MessagesTab> {
// New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<name>
// Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
final sarMessage =
'S:$emoji:$colorIndex:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
'S:$emoji:${colorIndex.toString()}:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';
if (sendToChannel) {
// Create message ID

View File

@@ -148,16 +148,32 @@ class CayenneLppParser {
break;
case MeshCoreConstants.lppGps:
// MeshCore GPS format: int32 LE (4 bytes each) × 10000 for lat/lon, × 100 for alt
// See MESHCORE_BLE_PROTOCOL.md lines 336-337, 434-435, 977-978
final rawLat = reader.readInt32LE();
final rawLon = reader.readInt32LE();
final rawAlt = reader.readInt32LE();
final lat = rawLat / 1000000.0;
final lon = rawLon / 1000000.0;
// Decode: divide by scaling factors
final lat = rawLat / 10000.0; // Fixed: was 1000000.0 (100x error!)
final lon = rawLon / 10000.0; // Fixed: was 1000000.0 (100x error!)
final alt = rawAlt / 100.0;
debugPrint(
' GPS Location (raw): lat=$rawLat, lon=$rawLon, alt=$rawAlt',
' GPS Location (raw int32 LE): lat=$rawLat (0x${rawLat.toRadixString(16)}), lon=$rawLon (0x${rawLon.toRadixString(16)}), alt=$rawAlt (0x${rawAlt.toRadixString(16)})',
);
debugPrint(' GPS Location: $lat°, $lon°, altitude=${alt}m');
debugPrint(
' GPS Location (decoded): ${lat.toStringAsFixed(6)}°, ${lon.toStringAsFixed(6)}°, altitude=${alt.toStringAsFixed(2)}m',
);
// Validate coordinates are in valid range
if (lat < -90.0 || lat > 90.0) {
debugPrint(' ⚠️ WARNING: Latitude out of range: $lat°');
}
if (lon < -180.0 || lon > 180.0) {
debugPrint(' ⚠️ WARNING: Longitude out of range: $lon°');
}
gpsLocation = LatLng(lat, lon);
extraSensorData['altitude_$channel'] = alt;
break;
@@ -224,6 +240,7 @@ class CayenneLppParser {
}
/// Create Cayenne LPP data for GPS location
/// MeshCore GPS format: int32 LE (4 bytes each) × 10000 for lat/lon, × 100 for alt
static Uint8List createGpsData({
required double latitude,
required double longitude,
@@ -235,23 +252,26 @@ class CayenneLppParser {
buffer.add(channel);
buffer.add(MeshCoreConstants.lppGps);
// Latitude (3 bytes, signed, 0.0001° precision)
// Latitude (int32 LE, 4 bytes, signed, 0.0001° precision)
final lat = (latitude * 10000).round();
buffer.add((lat >> 16) & 0xFF);
buffer.add((lat >> 8) & 0xFF);
buffer.add(lat & 0xFF);
buffer.add(lat & 0xFF); // Byte 0 (LSB)
buffer.add((lat >> 8) & 0xFF); // Byte 1
buffer.add((lat >> 16) & 0xFF); // Byte 2
buffer.add((lat >> 24) & 0xFF); // Byte 3 (MSB)
// Longitude (3 bytes, signed, 0.0001° precision)
// Longitude (int32 LE, 4 bytes, signed, 0.0001° precision)
final lon = (longitude * 10000).round();
buffer.add((lon >> 16) & 0xFF);
buffer.add((lon >> 8) & 0xFF);
buffer.add(lon & 0xFF);
buffer.add(lon & 0xFF); // Byte 0 (LSB)
buffer.add((lon >> 8) & 0xFF); // Byte 1
buffer.add((lon >> 16) & 0xFF); // Byte 2
buffer.add((lon >> 24) & 0xFF); // Byte 3 (MSB)
// Altitude (3 bytes, signed, 0.01m precision)
// Altitude (int32 LE, 4 bytes, signed, 0.01m precision)
final alt = (altitude * 100).round();
buffer.add((alt >> 16) & 0xFF);
buffer.add((alt >> 8) & 0xFF);
buffer.add(alt & 0xFF);
buffer.add(alt & 0xFF); // Byte 0 (LSB)
buffer.add((alt >> 8) & 0xFF); // Byte 1
buffer.add((alt >> 16) & 0xFF); // Byte 2
buffer.add((alt >> 24) & 0xFF); // Byte 3 (MSB)
return Uint8List.fromList(buffer);
}

View File

@@ -46,7 +46,7 @@ class DrawingMessageParser {
/// Sender will be determined from packet metadata on receiving end
static String createDrawingMessage(MapDrawing drawing) {
final json = drawing.toNetworkJson();
final jsonStr = jsonEncode(json);
final jsonStr = jsonEncode(json).toString();
return '$prefix$jsonStr';
}

View File

@@ -133,7 +133,7 @@ class SarMessageParser {
}) {
// New format: S:emoji:colorIndex:lat,lon:notes
final colorIdx = colorIndex ?? 0; // Default to red if not specified
final text = 'S:${type.emoji}:$colorIdx:${location.latitude},${location.longitude}';
final text = 'S:${type.emoji}:$colorIdx:${location.latitude.toString()},${location.longitude.toString()}';
if (notes != null && notes.isNotEmpty) {
// Use colon-separated format for inline message
return '$text:$notes';

View File

@@ -182,7 +182,25 @@ class ContactTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
// Simple mode: Only show location and distance
// Simple mode: Show last update time
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: contact.isRecentlySeen
? Colors.green
: Colors.grey,
),
const SizedBox(width: 4),
Text(
'${AppLocalizations.of(context)!.lastSeen}: ${contact.timeSinceLastSeen}',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
const SizedBox(height: 4),
// Simple mode: Show location and distance
if (location != null) ...[
Row(
children: [