Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ jobs:

- run: flutter pub get
- run: dart run build_runner build --delete-conflicting-outputs
- run: dart format --output=none --set-exit-if-changed .
- run: flutter analyze
- run: flutter test
#- run: flutter test
# no tests yet, fails without ./test directory
42 changes: 32 additions & 10 deletions bin/test_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,22 @@ class PayloadType {
class CryptoService {
/// Fixed key for "Public" channel
static final Uint8List publicChannelFixedKey = Uint8List.fromList([
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
0x8b,
0x33,
0x87,
0xe9,
0xc5,
0xcd,
0xea,
0x6a,
0xc9,
0xe5,
0xed,
0xba,
0xa1,
0x15,
0xcd,
0x72,
]);

/// Derive a 16-byte channel key from a hashtag channel name using SHA-256
Expand Down Expand Up @@ -228,8 +242,10 @@ class PacketMetadata {

final int header = raw[0];
final int routeType = header & PacketHeader.routeMask;
final int payloadType = (header >> PacketHeader.typeShift) & PacketHeader.typeMask;
final int protocolVersion = (header >> PacketHeader.verShift) & PacketHeader.verMask;
final int payloadType =
(header >> PacketHeader.typeShift) & PacketHeader.typeMask;
final int protocolVersion =
(header >> PacketHeader.verShift) & PacketHeader.verMask;

// Calculate offset for Path Length based on route type
int pathLengthOffset = 1;
Expand Down Expand Up @@ -427,9 +443,12 @@ void main(List<String> arguments) {

// Print packet metadata
print('PACKET METADATA');
print(' Header: 0x${metadata.header.toRadixString(16).padLeft(2, '0').toUpperCase()}');
print(' Route Type: ${RouteType.getName(metadata.routeType)} (0x${metadata.routeType.toRadixString(16).padLeft(2, '0')})');
print(' Payload Type: ${PayloadType.getName(metadata.payloadType)} (0x${metadata.payloadType.toRadixString(16).padLeft(2, '0')})');
print(
' Header: 0x${metadata.header.toRadixString(16).padLeft(2, '0').toUpperCase()}');
print(
' Route Type: ${RouteType.getName(metadata.routeType)} (0x${metadata.routeType.toRadixString(16).padLeft(2, '0')})');
print(
' Payload Type: ${PayloadType.getName(metadata.payloadType)} (0x${metadata.payloadType.toRadixString(16).padLeft(2, '0')})');
print(' Protocol Version: ${metadata.protocolVersion}');
print(' Path Length: ${metadata.pathLength} bytes');

Expand All @@ -444,10 +463,12 @@ void main(List<String> arguments) {
print(' Path: (empty)');
}

print(' Encrypted Payload: ${formatHex(metadata.encryptedPayload)} (${metadata.encryptedPayload.length} bytes)');
print(
' Encrypted Payload: ${formatHex(metadata.encryptedPayload)} (${metadata.encryptedPayload.length} bytes)');

if (metadata.channelHash != null) {
print(' Channel Hash: 0x${metadata.channelHash!.toRadixString(16).padLeft(2, '0').toUpperCase()}');
print(
' Channel Hash: 0x${metadata.channelHash!.toRadixString(16).padLeft(2, '0').toUpperCase()}');
}
print('');

Expand Down Expand Up @@ -514,7 +535,8 @@ void main(List<String> arguments) {
print('');
print(' Known channel hashes:');
for (final entry in channels.entries) {
print(' 0x${entry.key.toRadixString(16).padLeft(2, '0').toUpperCase()} -> ${entry.value.channelName}');
print(
' 0x${entry.key.toRadixString(16).padLeft(2, '0').toUpperCase()} -> ${entry.value.channelName}');
}
printValidationResults(steps, false, 'Unknown channel hash');
return;
Expand Down
49 changes: 27 additions & 22 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ Future<void> _requestPermissions() async {
Future<void> _requestiOSPermissions() async {
// Note: Location permission is now requested AFTER showing the prominent disclosure
// dialog in MainScaffold (required for Google Play compliance)
debugLog('[APP] iOS: Skipping location permission (handled after disclosure)');
debugLog(
'[APP] iOS: Skipping location permission (handled after disclosure)');

// Trigger Core Bluetooth authorization by checking adapter state
// This will cause iOS to show the Bluetooth permission prompt if not already granted
Expand All @@ -132,7 +133,8 @@ Future<void> _requestiOSPermissions() async {
.where((state) => state == fbp.BluetoothAdapterState.on)
.first
.timeout(const Duration(seconds: 3), onTimeout: () {
debugLog('[APP] iOS: Bluetooth authorization timeout (user may have denied or BT is off)');
debugLog(
'[APP] iOS: Bluetooth authorization timeout (user may have denied or BT is off)');
return fbp.BluetoothAdapterState.off;
});
}
Expand Down Expand Up @@ -165,36 +167,39 @@ Future<void> _requestAndroidPermissions() async {

// Dark theme - Tailwind Slate palette
const darkColorScheme = ColorScheme.dark(
primary: Color(0xFF059669), // emerald-600 (main actions)
primary: Color(0xFF059669), // emerald-600 (main actions)
onPrimary: Colors.white,
secondary: Color(0xFF0284C7), // sky-600 (TX ping)
secondary: Color(0xFF0284C7), // sky-600 (TX ping)
onSecondary: Colors.white,
tertiary: Color(0xFF4F46E5), // indigo-600 (auto modes)
tertiary: Color(0xFF4F46E5), // indigo-600 (auto modes)
onTertiary: Colors.white,
surface: Color(0xFF1E293B), // slate-800 (cards/panels)
onSurface: Color(0xFFF1F5F9), // slate-100 (primary text)
onSurfaceVariant: Color(0xFFCBD5E1), // slate-300 (muted text, brighter for contrast)
surface: Color(0xFF1E293B), // slate-800 (cards/panels)
onSurface: Color(0xFFF1F5F9), // slate-100 (primary text)
onSurfaceVariant:
Color(0xFFCBD5E1), // slate-300 (muted text, brighter for contrast)
surfaceContainerHighest: Color(0xFF0F172A), // slate-900 (main bg)
outline: Color(0xFF334155), // slate-700 (borders)
error: Color(0xFFF87171), // red-400
outline: Color(0xFF334155), // slate-700 (borders)
error: Color(0xFFF87171), // red-400
onError: Colors.white,
);

// Light theme - Tailwind Slate palette (inverted)
// Note: Using darker grays for better text contrast
const lightColorScheme = ColorScheme.light(
primary: Color(0xFF059669), // emerald-600
primary: Color(0xFF059669), // emerald-600
onPrimary: Colors.white,
secondary: Color(0xFF0284C7), // sky-600
secondary: Color(0xFF0284C7), // sky-600
onSecondary: Colors.white,
tertiary: Color(0xFF4F46E5), // indigo-600
tertiary: Color(0xFF4F46E5), // indigo-600
onTertiary: Colors.white,
surface: Color(0xFFF8FAFC), // slate-50 (cards/panels)
onSurface: Color(0xFF0F172A), // slate-900 (primary text - darker for contrast)
onSurfaceVariant: Color(0xFF475569), // slate-600 (muted text - darker for readability)
surface: Color(0xFFF8FAFC), // slate-50 (cards/panels)
onSurface:
Color(0xFF0F172A), // slate-900 (primary text - darker for contrast)
onSurfaceVariant:
Color(0xFF475569), // slate-600 (muted text - darker for readability)
surfaceContainerHighest: Color(0xFFFFFFFF), // white (main bg)
outline: Color(0xFFCBD5E1), // slate-300 (borders)
error: Color(0xFFDC2626), // red-600
outline: Color(0xFFCBD5E1), // slate-300 (borders)
error: Color(0xFFDC2626), // red-600
onError: Colors.white,
);

Expand All @@ -206,9 +211,8 @@ class MeshMapperApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Create platform-appropriate Bluetooth service
final BluetoothService bluetoothService = kIsWeb
? WebBluetoothService()
: MobileBluetoothService();
final BluetoothService bluetoothService =
kIsWeb ? WebBluetoothService() : MobileBluetoothService();

return MultiProvider(
providers: [
Expand Down Expand Up @@ -260,7 +264,8 @@ class _ThemedAppState extends State<_ThemedApp> {
scaffoldBackgroundColor: const Color(0xFFF1F5F9), // slate-100
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF8FAFC), // slate-50
foregroundColor: Color(0xFF0F172A), // slate-900 (darker for contrast)
foregroundColor:
Color(0xFF0F172A), // slate-900 (darker for contrast)
),
cardTheme: CardThemeData(
color: const Color(0xFFF8FAFC), // slate-50
Expand Down
21 changes: 14 additions & 7 deletions lib/models/api_queue_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ class ApiQueueItem extends HiveObject {
longitude: longitude,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
heardRepeats: heardRepeats,
canUploadAfter: DateTime.now().millisecondsSinceEpoch, // Immediate — flush timer controls upload timing
canUploadAfter: DateTime.now()
.millisecondsSinceEpoch, // Immediate — flush timer controls upload timing
externalAntenna: externalAntenna,
noiseFloor: noiseFloor,
power: power,
Expand Down Expand Up @@ -135,7 +136,8 @@ class ApiQueueItem extends HiveObject {
double? power,
}) {
// Format: "repeaterId:nodeType:localSnr:localRssi:remoteSnr:pubkeyFull"
final heardRepeats = '$repeaterId:$nodeType:${localSnr.toStringAsFixed(2)}:$localRssi:${remoteSnr.toStringAsFixed(2)}:$pubkeyFull';
final heardRepeats =
'$repeaterId:$nodeType:${localSnr.toStringAsFixed(2)}:$localRssi:${remoteSnr.toStringAsFixed(2)}:$pubkeyFull';
return ApiQueueItem(
type: 'DISC',
latitude: latitude,
Expand Down Expand Up @@ -163,7 +165,8 @@ class ApiQueueItem extends HiveObject {
int? noiseFloor,
double? power,
}) {
final heardRepeats = '$repeaterId:${localSnr.toStringAsFixed(2)}:$localRssi:${remoteSnr.toStringAsFixed(2)}';
final heardRepeats =
'$repeaterId:${localSnr.toStringAsFixed(2)}:$localRssi:${remoteSnr.toStringAsFixed(2)}';
return ApiQueueItem(
type: 'TRACE',
latitude: latitude,
Expand Down Expand Up @@ -249,7 +252,8 @@ class ApiQueueItem extends HiveObject {
'local_rssi': parts.length > 3 ? int.tryParse(parts[3]) ?? 0 : 0,
'remote_snr': parts.length > 4 ? double.tryParse(parts[4]) ?? 0.0 : 0.0,
'public_key': parts.length > 5 ? parts[5] : '',
'timestamp': timestamp.millisecondsSinceEpoch ~/ 1000, // Unix timestamp in seconds
'timestamp': timestamp.millisecondsSinceEpoch ~/
1000, // Unix timestamp in seconds
'external_antenna': externalAntenna,
'power': power != null ? '${power!.toStringAsFixed(1)}w' : null,
};
Expand All @@ -261,7 +265,8 @@ class ApiQueueItem extends HiveObject {
'lon': longitude,
'noisefloor': noiseFloor,
'heard_repeats': heardRepeats,
'timestamp': timestamp.millisecondsSinceEpoch ~/ 1000, // Unix timestamp in seconds
'timestamp':
timestamp.millisecondsSinceEpoch ~/ 1000, // Unix timestamp in seconds
'external_antenna': externalAntenna,
'power': power != null ? '${power!.toStringAsFixed(1)}w' : null,
};
Expand All @@ -281,7 +286,8 @@ class ApiQueueItem extends HiveObject {
}

/// Check if item is eligible for upload based on canUploadAfter
bool get isUploadEligible => DateTime.now().millisecondsSinceEpoch >= canUploadAfter;
bool get isUploadEligible =>
DateTime.now().millisecondsSinceEpoch >= canUploadAfter;

/// Mark as retried
void markRetried() {
Expand All @@ -291,5 +297,6 @@ class ApiQueueItem extends HiveObject {
}

@override
String toString() => 'ApiQueueItem($type, $latitude, $longitude, retries=$retryCount)';
String toString() =>
'ApiQueueItem($type, $latitude, $longitude, retries=$retryCount)';
}
32 changes: 16 additions & 16 deletions lib/models/connection_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
enum ConnectionStatus {
/// Not connected to any device
disconnected,

/// Currently scanning for devices
scanning,

/// Connecting to device
connecting,

/// Connected and ready
connected,

/// Connection error occurred
error,
}
Expand All @@ -27,31 +27,31 @@ enum ConnectionStep {

/// Step 1: BLE GATT connect
bleConnecting,

/// Step 2: Protocol handshake
protocolHandshake,

/// Step 3: Device info query
deviceQuery,

/// Step 4: Device identification (match device model for display/reporting)
powerConfiguration,

/// Step 5: Time synchronization
timeSync,

/// Step 6: API slot acquisition
slotAcquisition,

/// Step 7: Channel setup (#wardriving)
channelSetup,

/// Step 8: GPS initialization
gpsInit,

/// Step 9: Fully connected and ready
connected,

/// Error state
error,
}
Expand All @@ -60,13 +60,13 @@ enum ConnectionStep {
enum GpsStatus {
/// GPS permissions not granted
permissionDenied,

/// GPS is disabled on device
disabled,

/// Searching for GPS signal
searching,

/// GPS lock acquired
locked,

Expand Down
15 changes: 8 additions & 7 deletions lib/models/device_model.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
/// Represents a MeshCore device model with its power configuration.
///
///
/// This maps to the device-models.json database from the WebClient repo.
/// Power configuration is critical for PA amplifier models to prevent hardware damage.
class DeviceModel {
/// Full manufacturer string reported by device (e.g., "Ikoka Stick-E22-30dBm (Xiao_nrf52)")
final String manufacturer;

/// Short display name (e.g., "Ikoka Stick")
final String shortName;

/// Power setting for wardrive.js (0.3, 0.6, 1.0, 2.0)
/// CRITICAL: PA amplifier models require exact values
final double power;

/// Hardware platform (nrf52, esp32, esp32-s3, etc.)
final String platform;

/// Firmware TX power setting in dBm
final int txPower;

/// Additional notes about the device
final String notes;

Expand Down Expand Up @@ -55,7 +55,8 @@ class DeviceModel {
}

@override
String toString() => 'DeviceModel($shortName, power=$power, txPower=$txPower)';
String toString() =>
'DeviceModel($shortName, power=$power, txPower=$txPower)';
}

/// Container for the full device models database
Expand Down
Loading
Loading