Skip to content
Draft
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
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---
Expand All @@ -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)
```

Expand All @@ -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
Expand All @@ -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?"*
Expand Down Expand Up @@ -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
Expand Down
131 changes: 101 additions & 30 deletions lib/services/emergency_orchestrator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class SOSState {
final String? incidentId;
final List<Facility> nearbyFacilities;
final bool isBystander;
final bool isDemo;
final List<DispatchChannelRow> dispatchChannels;
final bool isBeaconActive;

Expand All @@ -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,
Expand All @@ -105,6 +107,7 @@ class SOSState {
String? incidentId,
List<Facility>? nearbyFacilities,
bool? isBystander,
bool? isDemo,
List<DispatchChannelRow>? dispatchChannels,
bool? isBeaconActive,
bool? wasInDrivingMode,
Expand All @@ -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,
Expand Down Expand Up @@ -156,8 +160,6 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
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.
Expand Down Expand Up @@ -188,7 +190,7 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
appLog.d('🚒 [ORCHESTRATOR] $message');
}

Future<void> startSos({bool isBystander = false}) async {
Future<void> startSos({bool isBystander = false, bool isDemo = false}) async {
if (state.phase != SOSPhase.idle) return;

final isDriving = _ref.read(drivingModeProvider) == DrivingMode.driving;
Expand All @@ -197,23 +199,24 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
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'));

Expand All @@ -236,11 +239,17 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
state = state.copyWith(countdownSeconds: state.countdownSeconds - 1);
} else {
timer.cancel();
_executeEmergencyPipeline();
if (isDemo) {
_executeDemoPipeline();
} else {
_executeEmergencyPipeline();
}
}
});
}

Future<void> startDemoSos() => startSos(isBystander: true, isDemo: true);

void cancelSos() {
_countdownTimer?.cancel();
state = const SOSState();
Expand Down Expand Up @@ -314,6 +323,11 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
_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,
Expand Down Expand Up @@ -431,7 +445,11 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
_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());
Expand Down Expand Up @@ -570,31 +588,13 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
return family;
});

final nearbyFuture = guard<bool>(
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<Object?> results;
try {
results = await Future.wait([
meshFuture,
smsFuture,
persistedFuture,
familyFuture,
nearbyFuture,
]).timeout(_dispatchChannelTimeout + const Duration(seconds: 1));
} catch (e, st) {
// Absolute guard: never hang in dispatching.
Expand Down Expand Up @@ -661,6 +661,77 @@ class EmergencyOrchestrator extends StateNotifier<SOSState> {
}
}

Future<void> _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<void>.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<void>.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<SmsDispatchOutcome> _dispatchSmsWithRetry(
String payload, {
double? lat,
Expand Down
8 changes: 3 additions & 5 deletions lib/ui/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -904,11 +904,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen>
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.
Expand Down
27 changes: 27 additions & 0 deletions test/orchestrator_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:roadsos/services/emergency_orchestrator.dart';

Expand All @@ -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<void> _runDemoMode'));

expect(demoSection, contains('startDemoSos()'));
expect(demoSection, isNot(contains('startSos(isBystander: true)')));
});
});
}
Loading