Skip to content

Commit a71d7d8

Browse files
committed
****Hybrid Mode Implemented******
1 parent f75ed6f commit a71d7d8

File tree

9 files changed

+322
-113
lines changed

9 files changed

+322
-113
lines changed

android/local.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
33
flutter.buildMode=release
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1770265889
5+
flutter.versionCode=1770351949

ios/Flutter/Generated.xcconfig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
FLUTTER_ROOT=/opt/homebrew/share/flutter
33
FLUTTER_APPLICATION_PATH=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App
44
COCOAPODS_PARALLEL_CODE_SIGN=true
5-
FLUTTER_TARGET=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/lib/main.dart
5+
FLUTTER_TARGET=lib/main.dart
66
FLUTTER_BUILD_DIR=build
77
FLUTTER_BUILD_NAME=1.0.0
8-
FLUTTER_BUILD_NUMBER=1
8+
FLUTTER_BUILD_NUMBER=1770351949
99
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
1010
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
11-
DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44
11+
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NzAzNTE5NDk=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44
1212
DART_OBFUSCATION=false
13-
TRACK_WIDGET_CREATION=true
14-
TREE_SHAKE_ICONS=false
13+
TRACK_WIDGET_CREATION=false
14+
TREE_SHAKE_ICONS=true
1515
PACKAGE_CONFIG=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/.dart_tool/package_config.json

ios/Flutter/flutter_export_environment.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
export "FLUTTER_ROOT=/opt/homebrew/share/flutter"
44
export "FLUTTER_APPLICATION_PATH=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App"
55
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
6-
export "FLUTTER_TARGET=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/lib/main.dart"
6+
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=1"
10-
export "DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44"
9+
export "FLUTTER_BUILD_NUMBER=1770351949"
10+
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NzAzNTE5NDk=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44"
1111
export "DART_OBFUSCATION=false"
12-
export "TRACK_WIDGET_CREATION=true"
13-
export "TREE_SHAKE_ICONS=false"
12+
export "TRACK_WIDGET_CREATION=false"
13+
export "TREE_SHAKE_ICONS=true"
1414
export "PACKAGE_CONFIG=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/.dart_tool/package_config.json"

lib/models/user_preferences.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class UserPreferences {
5252
/// Unit system for distances (metric, imperial)
5353
final String unitSystem;
5454

55+
/// Hybrid mode enabled (alternates Active + Discovery pings)
56+
final bool hybridModeEnabled;
57+
5558
const UserPreferences({
5659
this.powerLevel = 0.3,
5760
this.txPower = 22,
@@ -70,6 +73,7 @@ class UserPreferences {
7073
this.closeAppAfterDisconnect = false,
7174
this.themeMode = 'dark',
7275
this.unitSystem = 'metric',
76+
this.hybridModeEnabled = false,
7377
});
7478

7579
/// Create from JSON (for persistence)
@@ -92,6 +96,7 @@ class UserPreferences {
9296
closeAppAfterDisconnect: (json['closeAppAfterDisconnect'] as bool?) ?? false,
9397
themeMode: (json['themeMode'] as String?) ?? 'dark',
9498
unitSystem: (json['unitSystem'] as String?) ?? 'metric',
99+
hybridModeEnabled: (json['hybridModeEnabled'] as bool?) ?? false,
95100
);
96101
}
97102

@@ -115,6 +120,7 @@ class UserPreferences {
115120
'closeAppAfterDisconnect': closeAppAfterDisconnect,
116121
'themeMode': themeMode,
117122
'unitSystem': unitSystem,
123+
'hybridModeEnabled': hybridModeEnabled,
118124
};
119125
}
120126

@@ -137,6 +143,7 @@ class UserPreferences {
137143
bool? closeAppAfterDisconnect,
138144
String? themeMode,
139145
String? unitSystem,
146+
bool? hybridModeEnabled,
140147
}) {
141148
return UserPreferences(
142149
powerLevel: powerLevel ?? this.powerLevel,
@@ -156,6 +163,7 @@ class UserPreferences {
156163
closeAppAfterDisconnect: closeAppAfterDisconnect ?? this.closeAppAfterDisconnect,
157164
themeMode: themeMode ?? this.themeMode,
158165
unitSystem: unitSystem ?? this.unitSystem,
166+
hybridModeEnabled: hybridModeEnabled ?? this.hybridModeEnabled,
159167
);
160168
}
161169

@@ -200,7 +208,8 @@ class UserPreferences {
200208
other.mapStyle == mapStyle &&
201209
other.closeAppAfterDisconnect == closeAppAfterDisconnect &&
202210
other.themeMode == themeMode &&
203-
other.unitSystem == unitSystem;
211+
other.unitSystem == unitSystem &&
212+
other.hybridModeEnabled == hybridModeEnabled;
204213
}
205214

206215
@override
@@ -222,6 +231,7 @@ class UserPreferences {
222231
closeAppAfterDisconnect,
223232
themeMode,
224233
unitSystem,
234+
hybridModeEnabled,
225235
);
226236
}
227237

lib/providers/app_state_provider.dart

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ enum AutoMode {
4646
active,
4747
/// Passive Mode: Listening only (no transmit)
4848
passive,
49+
/// Hybrid Mode: Alternates Discovery + Active pings each interval
50+
hybrid,
4951
}
5052

5153
/// Result of uploading an offline session
@@ -263,6 +265,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
263265
bool get isDiscoveryListening => _pingService?.isDiscoveryListening ?? false; // True during discovery listening window (for Passive Mode)
264266
/// Check if auto-ping disable is pending (waiting for RX window)
265267
bool get isPendingDisable => _pingService?.pendingDisable ?? false;
268+
/// True when running any mode that does TX (Active or Hybrid)
269+
bool get isTxModeRunning => _autoPingEnabled && (_autoMode == AutoMode.active || _autoMode == AutoMode.hybrid);
266270
int get queueSize => _queueSize;
267271
int? get currentNoiseFloor => _currentNoiseFloor;
268272
int? get currentBatteryPercent => _currentBatteryPercent;
@@ -1937,12 +1941,14 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
19371941
return true;
19381942
}
19391943

1940-
/// Toggle auto-ping mode (Active or Passive)
1941-
/// Returns false if blocked by cooldown (Active Mode only - Passive Mode ignores cooldown)
1944+
/// Toggle auto-ping mode (Active, Passive, or Hybrid)
1945+
/// Returns false if blocked by cooldown (Active/Hybrid Mode only - Passive Mode ignores cooldown)
19421946
Future<bool> toggleAutoPing(AutoMode mode) async {
19431947
if (_pingService == null) return false;
19441948

19451949
final isPassive = mode == AutoMode.passive;
1950+
final isHybrid = mode == AutoMode.hybrid;
1951+
final isTxMode = !isPassive; // Active and Hybrid both do TX
19461952

19471953
// If currently running the same mode, stop it (always allow stopping)
19481954
if (_autoPingEnabled && _autoMode == mode) {
@@ -1988,11 +1994,11 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
19881994

19891995
_autoPingEnabled = false;
19901996

1991-
// Start 7-second shared cooldown ONLY for Active Mode (not Passive Mode)
1997+
// Start 7-second shared cooldown for TX modes (Active/Hybrid), not Passive Mode
19921998
// Passive Mode is listening only, no cooldown needed
1993-
if (!isPassive) {
1999+
if (isTxMode) {
19942000
_cooldownTimer.start(7000);
1995-
debugLog('[ACTIVE MODE] Shared cooldown started (7s) - blocks TX Ping and Active Mode');
2001+
debugLog('[${mode.name.toUpperCase()} MODE] Shared cooldown started (7s) - blocks TX Ping and TX modes');
19962002
} else {
19972003
debugLog('[PASSIVE MODE] Stopped - no cooldown (listen-only mode)');
19982004
}
@@ -2003,10 +2009,10 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20032009
if (!sessionCheck) return false;
20042010
}
20052011

2006-
// Block starting if shared cooldown is active (Active Mode only)
2012+
// Block starting if shared cooldown is active (TX modes only)
20072013
// Passive Mode is listening only and can start during cooldown
2008-
if (!isPassive && _cooldownTimer.isRunning) {
2009-
debugLog('[ACTIVE MODE] Start blocked by shared cooldown');
2014+
if (isTxMode && _cooldownTimer.isRunning) {
2015+
debugLog('[${mode.name.toUpperCase()} MODE] Start blocked by shared cooldown');
20102016
return false;
20112017
}
20122018

@@ -2037,7 +2043,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20372043
_pingService!.setAutoPingInterval(intervalMs);
20382044
debugLog('[PING] Using interval from preferences: ${_preferences.autoPingInterval}s (${intervalMs}ms)');
20392045

2040-
final started = await _pingService!.enableAutoPing(passiveMode: isPassive);
2046+
final started = await _pingService!.enableAutoPing(passiveMode: isPassive, hybridMode: isHybrid);
20412047
if (!started) {
20422048
// Blocked by cooldown or already enabled
20432049
if (_pingService!.isInCooldown()) {
@@ -2047,16 +2053,17 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20472053
}
20482054
return false;
20492055
}
2050-
// Start RX wardriving for both Active Mode and Passive Mode
2056+
// Start RX wardriving for all modes
20512057
// Reference: state.rxTracking.isWardriving = true in wardrive.js
20522058
_rxLogger?.startWardriving();
20532059
_autoPingEnabled = true;
20542060

20552061
// Start noise floor session for graph tracking
2056-
_startNoiseFloorSession(isPassive ? 'passive' : 'active');
2062+
final sessionLabel = isPassive ? 'passive' : isHybrid ? 'hybrid' : 'active';
2063+
_startNoiseFloorSession(sessionLabel);
20572064

20582065
// Enable heartbeat ONLY for Passive Mode (not offline mode)
2059-
// Active Mode renews session via auto-pings every 15/30/60s
2066+
// Active/Hybrid Mode renews session via auto-pings every 15/30/60s
20602067
// Manual Mode has natural 5-minute timeout
20612068
if (isPassive && !_preferences.offlineMode) {
20622069
_apiService.enableHeartbeat(
@@ -2068,12 +2075,12 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
20682075
},
20692076
);
20702077
debugLog('[HEARTBEAT] Enabled for Passive Mode');
2071-
} else if (!isPassive) {
2072-
debugLog('[HEARTBEAT] Not enabled - Active Mode renews via auto-pings');
2078+
} else if (isTxMode) {
2079+
debugLog('[HEARTBEAT] Not enabled - ${mode.name} Mode renews via auto-pings');
20732080
}
20742081

20752082
// Start background service for continuous operation
2076-
final modeName = isPassive ? 'Passive Mode' : 'Active Mode';
2083+
final modeName = isPassive ? 'Passive Mode' : isHybrid ? 'Hybrid Mode' : 'Active Mode';
20772084
await BackgroundServiceManager.startService(
20782085
mode: modeName,
20792086
txCount: _pingStats.txCount,

lib/screens/home_screen.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,7 @@ class _HomeScreenState extends State<HomeScreen> {
829829

830830
/// Show help bottom sheet explaining each control
831831
void _showControlsHelp(BuildContext context) {
832+
final prefs = Provider.of<AppStateProvider>(context, listen: false).preferences;
832833
showModalBottomSheet(
833834
context: context,
834835
useSafeArea: true,
@@ -893,12 +894,14 @@ class _HomeScreenState extends State<HomeScreen> {
893894
description: 'Send a single ping to #wardriving and track which repeaters heard it.',
894895
),
895896

896-
// Active Mode button
897+
// Active Mode / Hybrid Mode button
897898
_buildHelpItem(
898-
icon: Icons.sensors,
899+
icon: prefs.hybridModeEnabled ? Icons.compare_arrows : Icons.sensors,
899900
color: const Color(0xFF6366F1),
900-
title: 'Active Mode',
901-
description: 'Auto-pings #wardriving at your set interval, tracks repeaters from pings and received mesh traffic.',
901+
title: prefs.hybridModeEnabled ? 'Hybrid Mode' : 'Active Mode',
902+
description: prefs.hybridModeEnabled
903+
? 'Alternates between auto-pinging #wardriving and sending zero-hop discovery pings each interval, tracks repeaters from pings, nearby repeaters, and received mesh traffic.'
904+
: 'Auto-pings #wardriving at your set interval, tracks repeaters from pings and received mesh traffic.',
902905
),
903906

904907
// Passive Mode button

lib/screens/settings_screen.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
223223
onTap: isAutoMode ? null : () => _showIntervalSelector(context, appState),
224224
),
225225

226+
// Hybrid Mode Toggle
227+
SwitchListTile(
228+
secondary: const Icon(Icons.compare_arrows),
229+
title: Row(
230+
children: [
231+
const Text('Hybrid Mode'),
232+
const SizedBox(width: 4),
233+
GestureDetector(
234+
onTap: () => _showHybridModeInfo(context),
235+
child: Icon(
236+
Icons.info_outline,
237+
size: 18,
238+
color: Theme.of(context).colorScheme.onSurfaceVariant,
239+
),
240+
),
241+
],
242+
),
243+
subtitle: const Text('Combines Active and Passive modes'),
244+
value: prefs.hybridModeEnabled,
245+
onChanged: isAutoMode ? null : (value) {
246+
appState.updatePreferences(prefs.copyWith(hybridModeEnabled: value));
247+
},
248+
),
249+
226250
// Carpeater Ignore Setting
227251
SwitchListTile(
228252
secondary: const Icon(Icons.filter_alt),
@@ -858,6 +882,61 @@ class _SettingsScreenState extends State<SettingsScreen> {
858882
);
859883
}
860884

885+
void _showHybridModeInfo(BuildContext context) {
886+
showDialog(
887+
context: context,
888+
builder: (context) => AlertDialog(
889+
title: const Row(
890+
children: [
891+
Icon(Icons.compare_arrows, size: 24),
892+
SizedBox(width: 8),
893+
Text('Hybrid Mode'),
894+
],
895+
),
896+
content: const Column(
897+
mainAxisSize: MainAxisSize.min,
898+
crossAxisAlignment: CrossAxisAlignment.start,
899+
children: [
900+
Text(
901+
'Replaces Active Mode. Alternates between auto-pinging #wardriving and sending zero-hop discovery pings each interval, tracking repeaters from pings, nearby repeaters, and received mesh traffic.',
902+
style: TextStyle(fontSize: 14),
903+
),
904+
SizedBox(height: 12),
905+
Text('How it works:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
906+
SizedBox(height: 4),
907+
Text(
908+
'Discovery \u2192 wait \u2192 TX Ping \u2192 wait \u2192 Discovery \u2192 ...',
909+
style: TextStyle(fontSize: 13, fontFamily: 'monospace'),
910+
),
911+
SizedBox(height: 12),
912+
Text('Interval timing:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
913+
SizedBox(height: 4),
914+
Text(
915+
'At 15s interval, each ping type fires every 30s. Discovery\'s 30s firmware rate limit is naturally respected.',
916+
style: TextStyle(fontSize: 13),
917+
),
918+
SizedBox(height: 12),
919+
Text('When enabled:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
920+
SizedBox(height: 4),
921+
Text(
922+
'\u2022 Replaces the Active button with Hybrid\n'
923+
'\u2022 50% less TX airtime vs Active Mode\n'
924+
'\u2022 Discovery finds nearby repeaters\n'
925+
'\u2022 TX pings test coverage through them',
926+
style: TextStyle(fontSize: 13),
927+
),
928+
],
929+
),
930+
actions: [
931+
TextButton(
932+
onPressed: () => Navigator.of(context).pop(),
933+
child: const Text('Got it'),
934+
),
935+
],
936+
),
937+
);
938+
}
939+
861940
void _showIntervalSelector(BuildContext context, AppStateProvider appState) {
862941
final currentInterval = appState.preferences.autoPingInterval;
863942

0 commit comments

Comments
 (0)