Skip to content

Commit 3ffca1a

Browse files
committed
Add developer mode functionality, enhance connection error handling, and update heartbeat management
1 parent cfbbf0b commit 3ffca1a

File tree

9 files changed

+219
-69
lines changed

9 files changed

+219
-69
lines changed

ios/Flutter/Generated.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ COCOAPODS_PARALLEL_CODE_SIGN=true
55
FLUTTER_TARGET=lib/main.dart
66
FLUTTER_BUILD_DIR=build
77
FLUTTER_BUILD_NAME=1.0.0
8-
FLUTTER_BUILD_NUMBER=1769040952
8+
FLUTTER_BUILD_NUMBER=1769054467
99
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
1010
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
11-
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NjkwNDA5NTI=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
11+
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NjkwNTQ0Njc=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
1212
DART_OBFUSCATION=false
1313
TRACK_WIDGET_CREATION=false
1414
TREE_SHAKE_ICONS=true

ios/Flutter/flutter_export_environment.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export "COCOAPODS_PARALLEL_CODE_SIGN=true"
66
export "FLUTTER_TARGET=lib/main.dart"
77
export "FLUTTER_BUILD_DIR=build"
88
export "FLUTTER_BUILD_NAME=1.0.0"
9-
export "FLUTTER_BUILD_NUMBER=1769040952"
10-
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NjkwNDA5NTI=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
9+
export "FLUTTER_BUILD_NUMBER=1769054467"
10+
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NjkwNTQ0Njc=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
1111
export "DART_OBFUSCATION=false"
1212
export "TRACK_WIDGET_CREATION=false"
1313
export "TREE_SHAKE_ICONS=true"

lib/models/user_preferences.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class UserPreferences {
3737
/// Background mode enabled (requests "Always" location permission on iOS)
3838
final bool backgroundModeEnabled;
3939

40+
/// Developer mode enabled (unlocked by tapping version 7 times)
41+
final bool developerModeEnabled;
42+
4043
const UserPreferences({
4144
this.powerLevel = 0.3,
4245
this.txPower = 22,
@@ -50,6 +53,7 @@ class UserPreferences {
5053
this.offlineMode = false,
5154
this.iataCode = 'YOW',
5255
this.backgroundModeEnabled = false,
56+
this.developerModeEnabled = false,
5357
});
5458

5559
/// Create from JSON (for persistence)
@@ -67,6 +71,7 @@ class UserPreferences {
6771
offlineMode: false, // Never persist - always off by default
6872
iataCode: (json['iataCode'] as String?) ?? 'YOW',
6973
backgroundModeEnabled: (json['backgroundModeEnabled'] as bool?) ?? false,
74+
developerModeEnabled: (json['developerModeEnabled'] as bool?) ?? false,
7075
);
7176
}
7277

@@ -85,6 +90,7 @@ class UserPreferences {
8590
// offlineMode intentionally not persisted - always off on app start
8691
'iataCode': iataCode,
8792
'backgroundModeEnabled': backgroundModeEnabled,
93+
'developerModeEnabled': developerModeEnabled,
8894
};
8995
}
9096

@@ -102,6 +108,7 @@ class UserPreferences {
102108
bool? offlineMode,
103109
String? iataCode,
104110
bool? backgroundModeEnabled,
111+
bool? developerModeEnabled,
105112
}) {
106113
return UserPreferences(
107114
powerLevel: powerLevel ?? this.powerLevel,
@@ -116,6 +123,7 @@ class UserPreferences {
116123
offlineMode: offlineMode ?? this.offlineMode,
117124
iataCode: iataCode ?? this.iataCode,
118125
backgroundModeEnabled: backgroundModeEnabled ?? this.backgroundModeEnabled,
126+
developerModeEnabled: developerModeEnabled ?? this.developerModeEnabled,
119127
);
120128
}
121129

@@ -155,7 +163,8 @@ class UserPreferences {
155163
other.autoPowerSet == autoPowerSet &&
156164
other.offlineMode == offlineMode &&
157165
other.iataCode == iataCode &&
158-
other.backgroundModeEnabled == backgroundModeEnabled;
166+
other.backgroundModeEnabled == backgroundModeEnabled &&
167+
other.developerModeEnabled == developerModeEnabled;
159168
}
160169

161170
@override
@@ -172,6 +181,7 @@ class UserPreferences {
172181
offlineMode,
173182
iataCode,
174183
backgroundModeEnabled,
184+
developerModeEnabled,
175185
);
176186
}
177187
}

lib/providers/app_state_provider.dart

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class AppStateProvider extends ChangeNotifier {
8787
ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
8888
ConnectionStep _connectionStep = ConnectionStep.disconnected;
8989
String? _connectionError;
90+
bool _isAuthError = false; // Track if connection failed due to auth
9091

9192
// GPS state
9293
GpsStatus _gpsStatus = GpsStatus.permissionDenied;
@@ -174,6 +175,7 @@ class AppStateProvider extends ChangeNotifier {
174175
ConnectionStatus get connectionStatus => _connectionStatus;
175176
ConnectionStep get connectionStep => _connectionStep;
176177
String? get connectionError => _connectionError;
178+
bool get isAuthError => _isAuthError;
177179
GpsStatus get gpsStatus => _gpsStatus;
178180
Position? get currentPosition => _currentPosition;
179181
DeviceModel? get deviceModel => _deviceModel;
@@ -236,6 +238,9 @@ class AppStateProvider extends ChangeNotifier {
236238
// Offline mode
237239
bool get offlineMode => _preferences.offlineMode;
238240
List<OfflineSession> get offlineSessions => _offlineSessionService.sessions;
241+
242+
// Developer mode
243+
bool get developerModeEnabled => _preferences.developerModeEnabled;
239244
int get offlinePingCount => _apiQueueService.offlinePingCount;
240245
OfflineSessionService get offlineSessionService => _offlineSessionService;
241246

@@ -432,6 +437,7 @@ class AppStateProvider extends ChangeNotifier {
432437
_isScanning = true;
433438
_discoveredDevices = [];
434439
_connectionError = null;
440+
_isAuthError = false;
435441
notifyListeners();
436442

437443
// Listen for discovered devices
@@ -472,6 +478,7 @@ class AppStateProvider extends ChangeNotifier {
472478
Future<void> connectToDevice(DiscoveredDevice device) async {
473479
try {
474480
_connectionError = null;
481+
_isAuthError = false;
475482

476483
// Clean up any previous connection first
477484
if (_meshCoreConnection != null) {
@@ -795,7 +802,23 @@ class AppStateProvider extends ChangeNotifier {
795802
}
796803

797804
} catch (e) {
798-
_connectionError = e.toString();
805+
// Parse auth failure errors for clean display
806+
final errorStr = e.toString();
807+
if (errorStr.contains('AUTH_FAILED:')) {
808+
// Format: "Exception: AUTH_FAILED:reason:message"
809+
_isAuthError = true;
810+
final parts = errorStr.split('AUTH_FAILED:');
811+
if (parts.length > 1) {
812+
final errorParts = parts[1].split(':');
813+
final reason = errorParts.isNotEmpty ? errorParts[0] : 'unknown';
814+
_connectionError = _getErrorMessage(reason, null);
815+
} else {
816+
_connectionError = 'Authentication failed';
817+
}
818+
} else {
819+
_isAuthError = false;
820+
_connectionError = errorStr.replaceFirst('Exception: ', '');
821+
}
799822
_connectionStep = ConnectionStep.error;
800823
notifyListeners();
801824
}
@@ -1003,6 +1026,9 @@ class AppStateProvider extends ChangeNotifier {
10031026

10041027
/// Disconnect from current device
10051028
Future<void> disconnect() async {
1029+
// Disable heartbeat immediately on disconnect
1030+
_apiService.disableHeartbeat();
1031+
10061032
// Stop auto-ping if running (before releasing session)
10071033
if (_autoPingEnabled) {
10081034
await _pingService?.forceDisableAutoPing();
@@ -1129,6 +1155,9 @@ class AppStateProvider extends ChangeNotifier {
11291155
await _saveOfflineSession();
11301156
}
11311157

1158+
// Disable heartbeat when stopping auto mode
1159+
_apiService.disableHeartbeat();
1160+
11321161
_autoPingEnabled = false;
11331162

11341163
// Start 7-second shared cooldown ONLY for TX/RX Auto (not RX Auto)
@@ -1187,6 +1216,12 @@ class AppStateProvider extends ChangeNotifier {
11871216
_rxLogger?.startWardriving();
11881217
_autoPingEnabled = true;
11891218

1219+
// Enable heartbeat if not in offline mode
1220+
// Heartbeat fires after 3 minutes of API inactivity to keep session alive
1221+
if (!_preferences.offlineMode) {
1222+
_apiService.enableHeartbeat();
1223+
}
1224+
11901225
// Start background service for continuous operation
11911226
final modeName = isRxOnly ? 'RX Auto' : 'TX/RX Auto';
11921227
await BackgroundServiceManager.startService(
@@ -1415,6 +1450,9 @@ class AppStateProvider extends ChangeNotifier {
14151450

14161451
debugLog('[APP] Offline upload authenticated, session: ${authResult['session_id']}');
14171452

1453+
// Delay after auth before posting
1454+
await Future.delayed(const Duration(seconds: 1));
1455+
14181456
// 4. Upload pings in batches of 50
14191457
const batchSize = 50;
14201458
var uploadedCount = 0;
@@ -1432,6 +1470,9 @@ class AppStateProvider extends ChangeNotifier {
14321470
}
14331471
}
14341472

1473+
// Delay after posting before disconnect
1474+
await Future.delayed(const Duration(seconds: 1));
1475+
14351476
// 5. Release API session
14361477
await _apiService.requestAuth(
14371478
reason: 'disconnect',
@@ -1483,6 +1524,14 @@ class AppStateProvider extends ChangeNotifier {
14831524
_savePreferences();
14841525
}
14851526

1527+
/// Set developer mode (unlocked by tapping version 7 times)
1528+
void setDeveloperMode(bool enabled) {
1529+
_preferences = _preferences.copyWith(developerModeEnabled: enabled);
1530+
debugLog('[APP] Developer mode ${enabled ? 'enabled' : 'disabled'}');
1531+
notifyListeners();
1532+
_savePreferences();
1533+
}
1534+
14861535
/// Navigate to coordinates on map (triggered from log entries)
14871536
void navigateToMapCoordinates(double latitude, double longitude) {
14881537
_mapNavigationTarget = (lat: latitude, lon: longitude);
@@ -1510,7 +1559,7 @@ class AppStateProvider extends ChangeNotifier {
15101559
String _getErrorMessage(String? reason, String? serverMessage) {
15111560
switch (reason) {
15121561
case 'unknown_device':
1513-
return 'Device not registered. Please advertise yourself on the mesh first.';
1562+
return 'Unknown device. Please advertise yourself on the mesh using the official MeshCore app.';
15141563
case 'outside_zone':
15151564
return 'Not in any wardriving zone. Move closer to a zone and try again.';
15161565
case 'zone_disabled':

lib/screens/connection_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ class ConnectionScreen extends StatelessWidget {
503503
),
504504
const SizedBox(height: 16),
505505
Text(
506-
'Connection Failed',
506+
appState.isAuthError ? 'Authentication Failed' : 'Connection Failed',
507507
style: Theme.of(context).textTheme.titleLarge,
508508
),
509509
const SizedBox(height: 8),

lib/screens/settings_screen.dart

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,60 @@ import '../services/offline_session_service.dart';
1919
import '../utils/constants.dart';
2020

2121
/// Settings screen for user preferences and API configuration
22-
class SettingsScreen extends StatelessWidget {
22+
class SettingsScreen extends StatefulWidget {
2323
const SettingsScreen({super.key});
2424

25+
@override
26+
State<SettingsScreen> createState() => _SettingsScreenState();
27+
}
28+
29+
class _SettingsScreenState extends State<SettingsScreen> {
30+
// Developer mode tap counter state
31+
int _versionTapCount = 0;
32+
DateTime? _lastVersionTap;
33+
34+
void _onVersionTap(AppStateProvider appState) {
35+
final now = DateTime.now();
36+
37+
// Reset if last tap was more than 2 seconds ago
38+
if (_lastVersionTap != null &&
39+
now.difference(_lastVersionTap!).inSeconds > 2) {
40+
_versionTapCount = 0;
41+
}
42+
43+
_lastVersionTap = now;
44+
_versionTapCount++;
45+
46+
if (appState.developerModeEnabled) {
47+
ScaffoldMessenger.of(context).showSnackBar(
48+
const SnackBar(
49+
content: Text('Developer mode already enabled'),
50+
duration: Duration(milliseconds: 1500),
51+
),
52+
);
53+
return;
54+
}
55+
56+
if (_versionTapCount >= 7) {
57+
appState.setDeveloperMode(true);
58+
ScaffoldMessenger.of(context).showSnackBar(
59+
const SnackBar(
60+
content: Text('Developer mode enabled!'),
61+
duration: Duration(seconds: 2),
62+
),
63+
);
64+
_versionTapCount = 0;
65+
} else if (_versionTapCount >= 3) {
66+
final remaining = 7 - _versionTapCount;
67+
ScaffoldMessenger.of(context).showSnackBar(
68+
SnackBar(
69+
content: Text('$remaining taps to enable developer mode'),
70+
duration: const Duration(milliseconds: 500),
71+
),
72+
);
73+
}
74+
}
75+
2576
@override
2677
Widget build(BuildContext context) {
2778
final appState = context.watch<AppStateProvider>();
@@ -208,10 +259,13 @@ class SettingsScreen extends StatelessWidget {
208259
leading: const Icon(Icons.info_outline),
209260
title: Text(AppConstants.appName),
210261
),
211-
ListTile(
212-
leading: const Icon(Icons.new_releases_outlined),
213-
title: const Text('Version'),
214-
subtitle: Text(AppConstants.appVersion),
262+
GestureDetector(
263+
onTap: () => _onVersionTap(appState),
264+
child: ListTile(
265+
leading: const Icon(Icons.new_releases_outlined),
266+
title: const Text('Version'),
267+
subtitle: Text(AppConstants.appVersion),
268+
),
215269
),
216270
ListTile(
217271
leading: const Icon(Icons.bug_report),
@@ -220,12 +274,24 @@ class SettingsScreen extends StatelessWidget {
220274
onTap: () => _launchUrl('https://github.com/MeshMapper/MeshMapper_Project'),
221275
),
222276

223-
const Divider(),
277+
// Developer Tools section - only visible when developer mode is enabled
278+
if (appState.developerModeEnabled) ...[
279+
const Divider(),
280+
281+
_buildSectionHeader(context, 'Developer Tools'),
224282

225-
// Debug / Testing section
226-
_buildSectionHeader(context, 'Debug / Testing'),
283+
// Developer mode toggle (to disable)
284+
SwitchListTile(
285+
secondary: const Icon(Icons.developer_mode),
286+
title: const Text('Developer Mode'),
287+
subtitle: const Text('Disable to hide developer tools'),
288+
value: appState.developerModeEnabled,
289+
onChanged: (value) {
290+
appState.setDeveloperMode(value);
291+
},
292+
),
227293

228-
// GPS Simulator Toggle
294+
// GPS Simulator Toggle
229295
SwitchListTile(
230296
secondary: Icon(
231297
Icons.gps_fixed,
@@ -386,10 +452,13 @@ class SettingsScreen extends StatelessWidget {
386452
),
387453
),
388454
],
455+
], // Close developerModeEnabled conditional
389456

390-
// Debug Logs Toggle (mobile only)
457+
// Debug section (always visible on mobile)
391458
if (!kIsWeb) ...[
392-
const Divider(height: 32, thickness: 1),
459+
const Divider(),
460+
461+
_buildSectionHeader(context, 'Debug'),
393462

394463
SwitchListTile(
395464
secondary: Icon(
@@ -817,7 +886,7 @@ class SettingsScreen extends StatelessWidget {
817886
backgroundColor = Colors.red;
818887
break;
819888
case OfflineUploadResult.authFailed:
820-
message = 'Authentication failed - check device credentials';
889+
message = 'Authentication failed - Advert your device on the mesh';
821890
backgroundColor = Colors.red;
822891
break;
823892
case OfflineUploadResult.partialFailure:

0 commit comments

Comments
 (0)