Skip to content

Commit 5a31ada

Browse files
committed
### New Features
- Added a cancel button for BLE scanning - Tapping a repeater ID in TX, RX, or DISC ping results now shows a popup with the name of known repeaters - Active Mode now sends heartbeat pings to maintain session ### Bug Fixes - Fixed a bug that incorrectly dropped certain RX packets due to content filter rules - Fixed Android notification issue ### Improvements - Debug log uploads now chunk larger files to ensure successful delivery - App now remembers your last choice for centre on map, heading, and rotate lock
1 parent 0c7cdd8 commit 5a31ada

15 files changed

+1031
-351
lines changed

android/local.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
3-
flutter.buildMode=release
3+
flutter.buildMode=debug
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1770442286
5+
flutter.versionCode=1

drive_route_map_feb6.html

Lines changed: 148 additions & 0 deletions
Large diffs are not rendered by default.

lib/main.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'screens/main_scaffold.dart';
1313
import 'services/bluetooth/bluetooth_service.dart';
1414
import 'services/bluetooth/mobile_bluetooth.dart';
1515
import 'services/bluetooth/web_bluetooth.dart';
16+
import 'services/background_service.dart';
1617
import 'services/debug_file_logger.dart';
1718
import 'utils/constants.dart';
1819
import 'utils/debug_logger_io.dart';
@@ -63,6 +64,12 @@ void main() async {
6364
await _requestPermissions();
6465
}
6566

67+
// Clean up any orphaned background service from a previous session
68+
// (Android foreground service can survive app process death)
69+
if (!kIsWeb) {
70+
await BackgroundServiceManager.cleanupOrphanedService();
71+
}
72+
6673
runApp(MeshMapperApp(initialThemeMode: initialThemeMode));
6774
}
6875

lib/models/user_preferences.dart

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ class UserPreferences {
5555
/// Hybrid mode enabled (alternates Active + Discovery pings)
5656
final bool hybridModeEnabled;
5757

58+
/// Map auto-follow GPS position
59+
final bool mapAutoFollow;
60+
61+
/// Map always north up (false = rotate with heading)
62+
final bool mapAlwaysNorth;
63+
64+
/// Map rotation lock (disable rotation gestures)
65+
final bool mapRotationLocked;
66+
5867
const UserPreferences({
5968
this.powerLevel = 0.3,
6069
this.txPower = 22,
@@ -74,6 +83,9 @@ class UserPreferences {
7483
this.themeMode = 'dark',
7584
this.unitSystem = 'metric',
7685
this.hybridModeEnabled = false,
86+
this.mapAutoFollow = false,
87+
this.mapAlwaysNorth = true,
88+
this.mapRotationLocked = false,
7789
});
7890

7991
/// Create from JSON (for persistence)
@@ -97,6 +109,9 @@ class UserPreferences {
97109
themeMode: (json['themeMode'] as String?) ?? 'dark',
98110
unitSystem: (json['unitSystem'] as String?) ?? 'metric',
99111
hybridModeEnabled: (json['hybridModeEnabled'] as bool?) ?? false,
112+
mapAutoFollow: (json['mapAutoFollow'] as bool?) ?? false,
113+
mapAlwaysNorth: (json['mapAlwaysNorth'] as bool?) ?? true,
114+
mapRotationLocked: (json['mapRotationLocked'] as bool?) ?? false,
100115
);
101116
}
102117

@@ -121,6 +136,9 @@ class UserPreferences {
121136
'themeMode': themeMode,
122137
'unitSystem': unitSystem,
123138
'hybridModeEnabled': hybridModeEnabled,
139+
'mapAutoFollow': mapAutoFollow,
140+
'mapAlwaysNorth': mapAlwaysNorth,
141+
'mapRotationLocked': mapRotationLocked,
124142
};
125143
}
126144

@@ -144,6 +162,9 @@ class UserPreferences {
144162
String? themeMode,
145163
String? unitSystem,
146164
bool? hybridModeEnabled,
165+
bool? mapAutoFollow,
166+
bool? mapAlwaysNorth,
167+
bool? mapRotationLocked,
147168
}) {
148169
return UserPreferences(
149170
powerLevel: powerLevel ?? this.powerLevel,
@@ -164,6 +185,9 @@ class UserPreferences {
164185
themeMode: themeMode ?? this.themeMode,
165186
unitSystem: unitSystem ?? this.unitSystem,
166187
hybridModeEnabled: hybridModeEnabled ?? this.hybridModeEnabled,
188+
mapAutoFollow: mapAutoFollow ?? this.mapAutoFollow,
189+
mapAlwaysNorth: mapAlwaysNorth ?? this.mapAlwaysNorth,
190+
mapRotationLocked: mapRotationLocked ?? this.mapRotationLocked,
167191
);
168192
}
169193

@@ -209,7 +233,10 @@ class UserPreferences {
209233
other.closeAppAfterDisconnect == closeAppAfterDisconnect &&
210234
other.themeMode == themeMode &&
211235
other.unitSystem == unitSystem &&
212-
other.hybridModeEnabled == hybridModeEnabled;
236+
other.hybridModeEnabled == hybridModeEnabled &&
237+
other.mapAutoFollow == mapAutoFollow &&
238+
other.mapAlwaysNorth == mapAlwaysNorth &&
239+
other.mapRotationLocked == mapRotationLocked;
213240
}
214241

215242
@override
@@ -232,6 +259,9 @@ class UserPreferences {
232259
themeMode,
233260
unitSystem,
234261
hybridModeEnabled,
262+
mapAutoFollow,
263+
mapAlwaysNorth,
264+
mapRotationLocked,
235265
);
236266
}
237267

lib/providers/app_state_provider.dart

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
136136
// Discovered devices
137137
List<DiscoveredDevice> _discoveredDevices = [];
138138
bool _isScanning = false;
139+
StreamSubscription<DiscoveredDevice>? _activeScanSubscription;
139140

140141
// TX/RX markers for map
141142
final List<TxPing> _txPings = [];
@@ -433,7 +434,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
433434

434435
// Update background service notification with queue size
435436
if (_autoPingEnabled) {
436-
final modeName = _autoMode == AutoMode.passive ? 'Passive Mode' : 'Active Mode';
437+
final modeName = _autoMode == AutoMode.passive ? 'Passive Mode'
438+
: _autoMode == AutoMode.hybrid ? 'Hybrid Mode' : 'Active Mode';
437439
BackgroundServiceManager.updateNotification(
438440
mode: modeName,
439441
txCount: _pingStats.txCount,
@@ -717,31 +719,46 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
717719
_isAuthError = false;
718720
notifyListeners();
719721

720-
// Listen for discovered devices
722+
// Listen for discovered devices using subscription so stopScan() can cancel
721723
DiscoveredDevice? selectedDevice;
722-
await for (final device in _bluetoothService.scanForDevices(
724+
final completer = Completer<void>();
725+
_activeScanSubscription = _bluetoothService.scanForDevices(
723726
timeout: const Duration(seconds: 15),
724-
)) {
725-
if (!_discoveredDevices.any((d) => d.id == device.id)) {
726-
_discoveredDevices.add(device);
727-
selectedDevice = device;
728-
notifyListeners();
729-
}
730-
}
727+
).listen(
728+
(device) {
729+
if (!_discoveredDevices.any((d) => d.id == device.id)) {
730+
_discoveredDevices.add(device);
731+
selectedDevice = device;
732+
notifyListeners();
733+
}
734+
},
735+
onDone: () {
736+
if (!completer.isCompleted) completer.complete();
737+
},
738+
onError: (e) {
739+
debugError('[SCAN] Scan error: $e');
740+
if (!completer.isCompleted) completer.complete();
741+
},
742+
);
743+
await completer.future;
744+
_activeScanSubscription = null;
731745

732746
_isScanning = false;
733747
notifyListeners();
734748

735749
// On web platform, the Chrome BLE picker already handles device selection,
736750
// so auto-connect immediately after the picker returns (no second click needed)
737-
if (kIsWeb && selectedDevice != null) {
751+
final webDevice = selectedDevice;
752+
if (kIsWeb && webDevice != null) {
738753
debugLog('[APP] Web platform: auto-connecting to selected device');
739-
await connectToDevice(selectedDevice);
754+
await connectToDevice(webDevice);
740755
}
741756
}
742757

743758
/// Stop scanning for devices
744759
Future<void> stopScan() async {
760+
await _activeScanSubscription?.cancel();
761+
_activeScanSubscription = null;
745762
await _bluetoothService.stopScan();
746763
_isScanning = false;
747764
notifyListeners();
@@ -1077,7 +1094,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
10771094

10781095
// Update background service notification with current stats
10791096
if (_autoPingEnabled) {
1080-
final modeName = _autoMode == AutoMode.passive ? 'Passive Mode' : 'Active Mode';
1097+
final modeName = _autoMode == AutoMode.passive ? 'Passive Mode'
1098+
: _autoMode == AutoMode.hybrid ? 'Hybrid Mode' : 'Active Mode';
10811099
BackgroundServiceManager.updateNotification(
10821100
mode: modeName,
10831101
txCount: _pingStats.txCount,
@@ -2083,10 +2101,11 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20832101
final sessionLabel = isPassive ? 'passive' : isHybrid ? 'hybrid' : 'active';
20842102
_startNoiseFloorSession(sessionLabel);
20852103

2086-
// Enable heartbeat ONLY for Passive Mode (not offline mode)
2087-
// Active/Hybrid Mode renews session via auto-pings every 15/30/60s
2088-
// Manual Mode has natural 5-minute timeout
2089-
if (isPassive && !_preferences.offlineMode) {
2104+
// Enable heartbeat for all auto-ping modes (not offline mode)
2105+
// Heartbeat sends keepalive ~1 min before session expiry (4 min timer)
2106+
// Active/Hybrid pings renew session when moving, but heartbeat is the
2107+
// safety net when stationary (25m distance filter skips TX pings)
2108+
if (!_preferences.offlineMode) {
20902109
_apiService.enableHeartbeat(
20912110
gpsProvider: () {
20922111
// Provide current GPS coordinates for heartbeat (matching wardrive.js)
@@ -2095,9 +2114,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20952114
return (lat: pos.latitude, lon: pos.longitude);
20962115
},
20972116
);
2098-
debugLog('[HEARTBEAT] Enabled for Passive Mode');
2099-
} else if (isTxMode) {
2100-
debugLog('[HEARTBEAT] Not enabled - ${mode.name} Mode renews via auto-pings');
2117+
debugLog('[HEARTBEAT] Enabled for ${mode.name} Mode');
21012118
}
21022119

21032120
// Start background service for continuous operation
@@ -2676,6 +2693,30 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
26762693
_savePreferences();
26772694
}
26782695

2696+
/// Set map auto-follow preference and persist
2697+
void setMapAutoFollow(bool value) {
2698+
_preferences = _preferences.copyWith(mapAutoFollow: value);
2699+
debugLog('[MAP] Map auto-follow set to $value');
2700+
notifyListeners();
2701+
_savePreferences();
2702+
}
2703+
2704+
/// Set map always-north preference and persist
2705+
void setMapAlwaysNorth(bool value) {
2706+
_preferences = _preferences.copyWith(mapAlwaysNorth: value);
2707+
debugLog('[MAP] Map always-north set to $value');
2708+
notifyListeners();
2709+
_savePreferences();
2710+
}
2711+
2712+
/// Set map rotation-locked preference and persist
2713+
void setMapRotationLocked(bool value) {
2714+
_preferences = _preferences.copyWith(mapRotationLocked: value);
2715+
debugLog('[MAP] Map rotation-locked set to $value');
2716+
notifyListeners();
2717+
_savePreferences();
2718+
}
2719+
26792720
/// Toggle sound notifications on/off
26802721
Future<void> toggleSoundEnabled() async {
26812722
await _audioService.toggle();

lib/screens/connection_screen.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,26 @@ class _ConnectionScreenState extends State<ConnectionScreen> with WidgetsBinding
7676
final appState = context.watch<AppStateProvider>();
7777
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
7878

79-
// Build FAB for scanning - only show when fully disconnected and idle
79+
// Build FAB for scanning - show Cancel during scan, Scan when idle
8080
// Hide FAB entirely during maintenance mode (maintenance UI has its own buttons)
8181
// Hide FAB when Bluetooth is off (shows full-screen message instead)
8282
// Hide FAB during auto-reconnect
8383
Widget? fab;
84-
if (appState.connectionStep == ConnectionStep.disconnected &&
84+
if (appState.isScanning) {
85+
// Show Cancel FAB during active scan
86+
fab = isLandscape
87+
? FloatingActionButton.small(
88+
onPressed: () => appState.stopScan(),
89+
backgroundColor: Colors.red,
90+
child: const Icon(Icons.close),
91+
)
92+
: FloatingActionButton.extended(
93+
onPressed: () => appState.stopScan(),
94+
icon: const Icon(Icons.close),
95+
label: const Text('Cancel'),
96+
backgroundColor: Colors.red,
97+
);
98+
} else if (appState.connectionStep == ConnectionStep.disconnected &&
8599
!appState.isAutoReconnecting &&
86100
(!appState.maintenanceMode || appState.offlineMode) &&
87101
!appState.isBluetoothOff) {

0 commit comments

Comments
 (0)