diff --git a/README.md b/README.md index 4be17c5..c8b21ef 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ The app automatically selects the highest-quality available tier at emergency ti } ``` -This JSON triggers: -- Automated SMS to 108/112 ERSS with GPS coordinates and severity -- BLE beacon broadcast for nearby RoadSOS users -- Real-time database record for emergency responders +This JSON triggers the live pipeline to attempt: +- SMS to configured emergency relay/device channels, with explicit success/failure status +- BLE beacon broadcast for nearby RoadSOS users, with explicit success/failure status +- Local incident logging and Family Circle link creation, when configured - Voice-guided first aid in the user's language --- @@ -100,7 +100,7 @@ EmergencyOrchestrator │ └── Tier 4: OfflineTriageClassifier (keyword fallback) │ ├── EmergencySmsDispatchService (Twilio, server-side — no key on device) - ├── MeshNetworkService (BLE AES-GCM encrypted beacon) + ├── MeshNetworkService (BLE beacon, beta; encryption integration pending) └── VoiceAssistantService (TTS + STT, 6 Indian languages) ``` @@ -112,7 +112,7 @@ EmergencyOrchestrator - **Gemma 4 vision triage** — crash-scene photo analyzed by Gemma 4 27B alongside voice description - **4-tier inference** — seamless degradation from cloud to on-device to deterministic - **Server-side SMS** — automated Twilio dispatch; works for unconscious victims; no API key on device -- **BLE encrypted mesh** — AES-GCM beacon so nearby users see an alert even with no server +- **BLE mesh beacon (Beta)** — nearby RoadSOS users can detect alerts when Bluetooth works; payload encryption is not wired into the live mesh path yet - **First aid RAG** — 80+ entry SQLite FTS5 corpus; Gemma 4 E4B runs lookup on-device - **6 Indian languages** — English, Hindi, Bengali, Marathi, Tamil, Telugu; full localization - **Voice SOS** — TTS + STT for hands-busy emergencies @@ -121,6 +121,19 @@ EmergencyOrchestrator --- +## Functional Reality Check + +RoadSOS is safety-critical software. A feature name is not enough; the app must show what is real, beta, simulated, or manual-action-only. + +- **Real emergency path:** SOS countdown, location acquisition, Gemma 4 triage fallback stack, SMS attempt status, BLE beacon attempt status, local incident log attempt, and Family Circle link attempt all report explicit success/failure/skipped states. +- **Demo Mode:** fully simulated. It no longer calls the live SOS pipeline and cannot send SMS, dial 112/108, start BLE broadcast, write a real incident, or ring Family Circle. +- **Nearby Services:** not automated in this build. The dispatch panel now marks this as skipped/manual action instead of pretending a responder/facility broadcast succeeded. +- **BLE mesh:** beta. The app attempts BLE broadcast, but users must not treat it as a guaranteed dispatch channel, and live mesh payload encryption still needs to be wired before production claims. +- **Gemma 4:** used for cloud/on-device triage when configured and available; deterministic local tiers remain mandatory because an accident app must still respond when model, network, or device resources fail. +- **No 100% guarantee:** RoadSOS can reduce time-to-help, but it cannot guarantee rescue in every situation. The UI must always tell the user when manual action is needed. + +--- + ## Why Gemma 4 Is Not Optional This is the question that eliminates 90% of hackathon submissions: *"Could you replace Gemma 4 with GPT-4o or any other model?"* @@ -172,7 +185,7 @@ Gemma 4's function calling is what makes the PLAN → ACT step real. The model d |------------------|--------|-----------------| | **Impact & Vision** | 40% | 170,000 deaths/year. 350M+ target users. MIT licensed for any state EMS. Deployable with zero custom infra. | | **Video Storytelling** | 30% | Full 3-min script in `VIDEO_SCRIPT.md`. Emotional hook → live demo → wow moment → scale. Keyword vs Gemma split-screen. | -| **Technical Depth** | 30% | 4-tier inference routing. Real flutter_gemma LiteRT integration. Function calling agent (Cell 11). BLE AES-GCM mesh. Server-side Twilio SMS. 80-entry RAG corpus. | +| **Technical Depth** | 30% | 4-tier inference routing. Real flutter_gemma LiteRT integration. Function calling agent (Cell 11). BLE mesh beta with encryption still pending. Server-side/device SMS status reporting. 80-entry RAG corpus. | **Track alignment:** - **Safety & Trust** — primary track; crash detection + dispatch + bystander guidance is pure safety infrastructure diff --git a/lib/services/emergency_orchestrator.dart b/lib/services/emergency_orchestrator.dart index 5527d6e..1b60dba 100644 --- a/lib/services/emergency_orchestrator.dart +++ b/lib/services/emergency_orchestrator.dart @@ -76,6 +76,7 @@ class SOSState { final String? incidentId; final List nearbyFacilities; final bool isBystander; + final bool isDemo; final List dispatchChannels; final bool isBeaconActive; @@ -91,6 +92,7 @@ class SOSState { this.incidentId, this.nearbyFacilities = const [], this.isBystander = false, + this.isDemo = false, this.dispatchChannels = const [], this.isBeaconActive = false, this.wasInDrivingMode = false, @@ -105,6 +107,7 @@ class SOSState { String? incidentId, List? nearbyFacilities, bool? isBystander, + bool? isDemo, List? dispatchChannels, bool? isBeaconActive, bool? wasInDrivingMode, @@ -118,6 +121,7 @@ class SOSState { incidentId: incidentId ?? this.incidentId, nearbyFacilities: nearbyFacilities ?? this.nearbyFacilities, isBystander: isBystander ?? this.isBystander, + isDemo: isDemo ?? this.isDemo, dispatchChannels: dispatchChannels ?? this.dispatchChannels, isBeaconActive: isBeaconActive ?? this.isBeaconActive, wasInDrivingMode: wasInDrivingMode ?? this.wasInDrivingMode, @@ -156,8 +160,6 @@ class EmergencyOrchestrator extends StateNotifier { static const Duration _dispatchChannelTimeout = Duration(seconds: 8); EmergencyOrchestrator(this._ref) : super(const SOSState()) { - - _restoreState(); _ref.read(crashDetectionServiceProvider).startMonitoring(); // Phase 8: ensure RL bias is loaded before any SOS fires. @@ -188,7 +190,7 @@ class EmergencyOrchestrator extends StateNotifier { appLog.d('🚒 [ORCHESTRATOR] $message'); } - Future startSos({bool isBystander = false}) async { + Future startSos({bool isBystander = false, bool isDemo = false}) async { if (state.phase != SOSPhase.idle) return; final isDriving = _ref.read(drivingModeProvider) == DrivingMode.driving; @@ -197,23 +199,24 @@ class EmergencyOrchestrator extends StateNotifier { phase: SOSPhase.countdown, countdownSeconds: 10, isBystander: isBystander, + isDemo: isDemo, incidentId: _uuid.v4(), dispatchChannels: const [], wasInDrivingMode: isDriving, ); final l10n = lookupAppLocalizations(_ref.read(appLocaleProvider)); - _log( - isBystander - ? l10n.orchestratorBystanderStarted - : l10n.orchestratorSelfSosStarted, - SOSPhase.countdown, - ); + final startMessage = isDemo + ? 'Demo SOS rehearsal started — simulated only, no dispatch side effects.' + : isBystander + ? l10n.orchestratorBystanderStarted + : l10n.orchestratorSelfSosStarted; + _log(startMessage, SOSPhase.countdown); // Phase 7: hands-free countdown announcement when driving. // Spoken once at the start — no per-tick repetition to avoid interfering // with voice cancel listening which runs in parallel. - if (isDriving) { + if (isDriving && !isDemo) { final voice = _ref.read(voiceAssistantServiceProvider); unawaited(voice.speakHandsFreeCountdown(10, 'Location being acquired')); @@ -236,11 +239,17 @@ class EmergencyOrchestrator extends StateNotifier { state = state.copyWith(countdownSeconds: state.countdownSeconds - 1); } else { timer.cancel(); - _executeEmergencyPipeline(); + if (isDemo) { + _executeDemoPipeline(); + } else { + _executeEmergencyPipeline(); + } } }); } + Future startDemoSos() => startSos(isBystander: true, isDemo: true); + void cancelSos() { _countdownTimer?.cancel(); state = const SOSState(); @@ -314,6 +323,11 @@ class EmergencyOrchestrator extends StateNotifier { _patchDispatchChannel('mesh', DispatchChannelLifecycle.skipped, 'Skipped — no usable GPS fix.'); _patchDispatchChannel('family_link', DispatchChannelLifecycle.skipped, 'Skipped — no usable GPS fix.'); _patchDispatchChannel('local_log', DispatchChannelLifecycle.failed, 'Not saved — no usable GPS fix.'); + _patchDispatchChannel( + 'nearby_services', + DispatchChannelLifecycle.skipped, + 'Not automated in this build — call 112/108 or use the hospital list manually.', + ); _patchDispatchChannel('sms', DispatchChannelLifecycle.inProgress, 'Sending emergency SMS (no GPS)…'); final smsOutcome = await _dispatchSmsWithRetry( l10n.orchestratorSmsNoGpsPayload, @@ -431,7 +445,11 @@ class EmergencyOrchestrator extends StateNotifier { _patchDispatchChannel('sms', DispatchChannelLifecycle.inProgress, 'Sending emergency SMS…'); _patchDispatchChannel('local_log', DispatchChannelLifecycle.inProgress, 'Saving incident on device…'); _patchDispatchChannel('family_link', DispatchChannelLifecycle.inProgress, 'Family tracking link…'); - _patchDispatchChannel('nearby_services', DispatchChannelLifecycle.inProgress, 'Broadcasting to nearby services…'); + _patchDispatchChannel( + 'nearby_services', + DispatchChannelLifecycle.skipped, + 'Not automated in this build — use 112/108 and share the nearest hospital list manually.', + ); // Phase 9: Automated alerts and calling unawaited(_notifyUser()); @@ -570,23 +588,6 @@ class EmergencyOrchestrator extends StateNotifier { return family; }); - final nearbyFuture = guard( - id: 'nearby_services', - future: Future.delayed(const Duration(seconds: 3), () => true), - fallback: false, - timeoutDetail: 'Nearby services broadcast timed out.', - failureDetail: 'Nearby services broadcast failed.', - ).then((ok) { - _patchDispatchChannel( - 'nearby_services', - ok ? DispatchChannelLifecycle.success : DispatchChannelLifecycle.failed, - ok - ? 'Emergency alert broadcasted to nearby facilities and responders ✓' - : 'Could not complete nearby services broadcast.', - ); - return ok; - }); - List results; try { results = await Future.wait([ @@ -594,7 +595,6 @@ class EmergencyOrchestrator extends StateNotifier { smsFuture, persistedFuture, familyFuture, - nearbyFuture, ]).timeout(_dispatchChannelTimeout + const Duration(seconds: 1)); } catch (e, st) { // Absolute guard: never hang in dispatching. @@ -661,6 +661,77 @@ class EmergencyOrchestrator extends StateNotifier { } } + Future _executeDemoPipeline() async { + final location = LocationFix( + latitude: 28.6139, + longitude: 77.2090, + accuracy: 12, + source: 'demo', + timestamp: DateTime.now(), + ); + final triage = TriageResult( + functionCall: 'dispatch_emergency_demo', + location: '${location.latitude},${location.longitude}', + severityLevel: 4, + requiredServices: const ['ambulance', 'police'], + firstAidQuery: 'Demo: airway, bleeding control, spinal precautions', + compressedPayload: 'DEMO_ONLY_NO_DISPATCH', + thinkingTrace: + 'Simulated Gemma 4 rehearsal output. No SMS, BLE, cloud write, phone call, or WebRTC action was attempted.', + source: TriageSource.webDemo, + confidence: 1, + ); + + _log( + 'Demo: using simulated crash location and simulated triage output.', + SOSPhase.gpsLocking, + ); + state = state.copyWith(location: location, phase: SOSPhase.triaging); + await Future.delayed(const Duration(milliseconds: 350)); + state = state.copyWith(triageResult: triage); + _log( + 'Demo: simulated triage complete — no medical or dispatch action taken.', + SOSPhase.triaging, + ); + + state = state.copyWith( + phase: SOSPhase.dispatching, + dispatchChannels: _initialDispatchRows(), + ); + _patchDispatchChannel( + 'mesh', + DispatchChannelLifecycle.skipped, + 'Demo only — BLE beacon was not started.', + ); + _patchDispatchChannel( + 'sms', + DispatchChannelLifecycle.skipped, + 'Demo only — no SMS, 112, 108, or dialer action was attempted.', + ); + _patchDispatchChannel( + 'local_log', + DispatchChannelLifecycle.skipped, + 'Demo only — incident was not saved as a real emergency.', + ); + _patchDispatchChannel( + 'family_link', + DispatchChannelLifecycle.skipped, + 'Demo only — Family Circle and WebRTC were not contacted.', + ); + _patchDispatchChannel( + 'nearby_services', + DispatchChannelLifecycle.skipped, + 'Demo only — no nearby facility or responder broadcast was sent.', + ); + await Future.delayed(const Duration(milliseconds: 250)); + + state = state.copyWith(phase: SOSPhase.active); + _log( + 'Demo session active — this is a rehearsal. In a real crash, call 112/108 if automated SMS is not confirmed.', + SOSPhase.active, + ); + } + Future _dispatchSmsWithRetry( String payload, { double? lat, diff --git a/lib/ui/dashboard.dart b/lib/ui/dashboard.dart index d8e7122..964beaa 100644 --- a/lib/ui/dashboard.dart +++ b/lib/ui/dashboard.dart @@ -904,11 +904,9 @@ class _DashboardScreenState extends ConsumerState if (confirmed != true) return; if (!context.mounted) return; - // Bystander mode = the safety-validation agent treats it as severity 2 - // baseline; even if dispatch fails (no Supabase / no SMS perm), the - // countdown + status panel still walk the same code path the real SOS - // does, which is the entire point of the demo. - await ref.read(emergencyOrchestratorProvider.notifier).startSos(isBystander: true); + // Demo mode must never call the live SOS pipeline. It uses a separate + // simulated orchestrator path that marks every dispatch channel as skipped. + await ref.read(emergencyOrchestratorProvider.notifier).startDemoSos(); // Auto-open the Bystander Coach so the rehearsal hits the on-device // Gemma-4 voice flow without the user having to tap a second time. diff --git a/test/orchestrator_test.dart b/test/orchestrator_test.dart index 7a75881..b1db30c 100644 --- a/test/orchestrator_test.dart +++ b/test/orchestrator_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; import 'package:roadsos/services/emergency_orchestrator.dart'; @@ -20,5 +22,30 @@ void main() { final newState = state.copyWith(phase: SOSPhase.active); expect(newState.incidentId, 'test-123'); }); + + test('demo flag is explicit and persistent after copyWith', () { + const state = SOSState(isDemo: true, incidentId: 'demo-123'); + final newState = state.copyWith(phase: SOSPhase.dispatching); + expect(newState.isDemo, true); + expect(newState.incidentId, 'demo-123'); + }); + }); + + group('life-safety source guards', () { + test('nearby services channel is not implemented as fake delayed success', () { + final source = File('lib/services/emergency_orchestrator.dart').readAsStringSync(); + + expect(source, isNot(contains("Future.delayed(const Duration(seconds: 3), () => true)"))); + expect(source, isNot(contains('Emergency alert broadcasted to nearby facilities and responders'))); + expect(source, isNot(contains('Demo: using simulated crash location and Gemma 4 triage output.'))); + }); + + test('dashboard demo mode cannot invoke the live SOS pipeline directly', () { + final source = File('lib/ui/dashboard.dart').readAsStringSync(); + final demoSection = source.substring(source.indexOf('Future _runDemoMode')); + + expect(demoSection, contains('startDemoSos()')); + expect(demoSection, isNot(contains('startSos(isBystander: true)'))); + }); }); }