Skip to content
Open
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
3 changes: 2 additions & 1 deletion lib/services/agent_health_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,9 @@ class AgentHealthService {
// Primary: server relay (Twilio / Edge) — no Android SEND_SMS required.
final relayUrl = dotenv.env['SMS_DISPATCH_URL']?.trim() ?? '';
final relayKey = dotenv.env['SMS_DISPATCH_ANON_KEY']?.trim() ?? '';
if (relayUrl.isNotEmpty && relayKey.isNotEmpty)
if (relayUrl.isNotEmpty && relayKey.isNotEmpty) {
return AgentReadiness.ready;
}

// Fallback: open SMS app intent (no permission). If relay isn't configured,
// we mark this as degraded (still usable, but requires user interaction).
Expand Down
6 changes: 4 additions & 2 deletions lib/services/family_tracking_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ class FamilyTrackingService {
final digits = raw.replaceAll(RegExp(r'\D'), '');
if (digits.length < 10) return null;
if (digits.length == 10) return digits;
if (digits.length == 11 && digits.startsWith('0'))
if (digits.length == 11 && digits.startsWith('0')) {
return digits.substring(1);
if (digits.length >= 12 && digits.startsWith('91'))
}
if (digits.length >= 12 && digits.startsWith('91')) {
return digits.substring(digits.length - 10);
}
return digits.length > 10 ? digits.substring(digits.length - 10) : digits;
}

Expand Down
15 changes: 10 additions & 5 deletions lib/services/gemma_model_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ class GemmaModelManager {
static const String hfTokenUrl = 'https://huggingface.co/settings/tokens';

/// SharedPreferences keys.
static const String prefAutoDownloadOptOut =
'gemma_auto_download_opt_out_v1';
static const String prefAutoDownloadOptOut = 'gemma_auto_download_opt_out_v1';
static const String prefAutoDownloadInFlight =
'gemma_auto_download_in_flight_v1';

Expand Down Expand Up @@ -147,7 +146,9 @@ class GemmaModelManager {
e.statusCode != 429) {
rethrow;
}
appLog.w('[GemmaModel] $url failed (${e.statusCode}); trying next mirror');
appLog.w(
'[GemmaModel] $url failed (${e.statusCode}); trying next mirror',
);
}
}
throw lastErr ??
Expand Down Expand Up @@ -233,7 +234,9 @@ class GemmaModelManager {
try {
await for (final chunk in response.stream) {
if (cancelToken?.isCancelled ?? false) {
appLog.i('[GemmaModel] Download cancelled — partial file kept for resume');
appLog.i(
'[GemmaModel] Download cancelled — partial file kept for resume',
);
await sink.flush();
await sink.close();
return;
Expand All @@ -260,7 +263,9 @@ class GemmaModelManager {

// Atomic rename: .download → final path.
await tmpFile.rename(path);
appLog.i('[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path');
appLog.i(
'[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path',
);
} finally {
client.close();
}
Expand Down
7 changes: 2 additions & 5 deletions lib/services/government_facility_seed_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,11 @@ class GovernmentFacilitySeedService {
}

if (batchParameters.isNotEmpty) {
await db.executeBatch(
'''
await db.executeBatch('''
INSERT OR REPLACE INTO emergency_facilities
(id, name, type, latitude, longitude, contact_number, capabilities, data_source, state_code, district)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''',
batchParameters,
);
''', batchParameters);
}

await prefs.setInt(_prefsKeyImportedVersion, v);
Expand Down
12 changes: 8 additions & 4 deletions lib/services/roadsos_assistant_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,21 +195,25 @@ class RoadSosAssistantService extends StateNotifier<AssistantState> {
/// into the generic five-question script when offline.
String _detectSceneContextLocal(String input) {
final lower = input.toLowerCase();
if (lower.contains('pedestrian') || lower.contains('पैदल'))
if (lower.contains('pedestrian') || lower.contains('पैदल')) {
return 'pedestrian_hit';
}
if (lower.contains('rollover') ||
lower.contains('पलटा') ||
lower.contains('overturned'))
lower.contains('overturned')) {
return 'rollover';
}
if (lower.contains('fire') ||
lower.contains('आग') ||
lower.contains('smoke') ||
lower.contains('धुआँ'))
lower.contains('धुआँ')) {
return 'fire_hazard';
}
if (lower.contains('collision') ||
lower.contains('टक्कर') ||
lower.contains('crash'))
lower.contains('crash')) {
return 'vehicle_collision';
}
return 'unknown';
}

Expand Down
24 changes: 16 additions & 8 deletions lib/services/tier2_local_triage_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ class Tier2LocalTriageModel {
var score = 0;

// Life-threatening indicators.
if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest']))
if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest'])) {
score += 8;
if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive']))
}
if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive'])) {
score += 6;
if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting']))
}
if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting'])) {
score += 6;
}
if (_hasAny(t, const ['trapped', 'pinned', 'stuck in vehicle'])) score += 6;

// Major trauma / dangerous context.
Expand All @@ -34,8 +37,9 @@ class Tier2LocalTriageModel {
if (_hasAny(t, const ['fire', 'smoke', 'burning', 'explosion'])) score += 5;

// Moderate indicators.
if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident']))
if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident'])) {
score += 2;
}

// Hint from crash detection (1-5): amplify but do not let it dominate fully.
score += (h - 1) * 2;
Expand Down Expand Up @@ -93,17 +97,21 @@ class Tier2LocalTriageModel {
}

String _firstAidQueryFromText(String t) {
if (t.contains('bleed'))
if (t.contains('bleed')) {
return 'severe bleeding wound management tourniquet';
if (_hasAny(t, const ['burn', 'smoke']))
}
if (_hasAny(t, const ['burn', 'smoke'])) {
return 'burn wound first aid cool water';
}
if (_hasAny(t, const ['not breathing', 'cpr', 'choking'])) {
return 'CPR rescue breathing Heimlich';
}
if (_hasAny(t, const ['fracture', 'broken']))
if (_hasAny(t, const ['fracture', 'broken'])) {
return 'fracture immobilization splint';
if (_hasAny(t, const ['head injury', 'concussion']))
}
if (_hasAny(t, const ['head injury', 'concussion'])) {
return 'head injury concussion protocol';
}
return 'general road accident first aid emergency response';
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/services/vital_signs_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ class VitalSignsLogger extends StateNotifier<VitalSigns?> {
if (respiratoryRate >= 24) flags.add('tachypnoea (rapid breathing)');
if (respiratoryRate <= 8) flags.add('bradypnoea (slow breathing)');
if (bloodOxygen < 90) flags.add('hypoxia (SpO2 <90% — CRITICAL)');
if (bloodOxygen >= 90 && bloodOxygen < 94)
if (bloodOxygen >= 90 && bloodOxygen < 94) {
flags.add('borderline oxygen (SpO2 90-93%)');
}

if (flags.isEmpty) {
return 'No immediately critical vital signs from bystander observation.';
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/gemma_model_download_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ class _GemmaModelDownloadScreenState extends State<GemmaModelDownloadScreen> {

Future<void> _openUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri))
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}

// ── Build ─────────────────────────────────────────────────────────────────
Expand Down
6 changes: 4 additions & 2 deletions lib/ui/map_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ class _RoadSosMapState extends State<RoadSosMap> with TickerProviderStateMixin {
// - If custom provider fails (keys/restrictions), fall back to Carto.
// - If Carto fails (blocked network / DNS / captive portal), fall back to
// OSM tile CDN for demo resilience (do not ship high-volume traffic there).
if (!t.contains('basemaps.cartocdn.com'))
if (!t.contains('basemaps.cartocdn.com')) {
return MapTileConfig.cartoDarkMatter;
}
return MapTileConfig.tileOpenstreetmapOrgViolatesPolicyAtScale;
}

Expand Down Expand Up @@ -303,8 +304,9 @@ class _RoadSosMapState extends State<RoadSosMap> with TickerProviderStateMixin {
}

List<Marker> _buildIncidentMarkers() {
if (widget.state.incidentId == null || widget.state.location == null)
if (widget.state.incidentId == null || widget.state.location == null) {
return [];
}
if (widget.state.location!.source == 'unknown') return [];

return [
Expand Down
19 changes: 11 additions & 8 deletions test/dispatch_status_panel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:roadsos/models/dispatch_channel_status.dart';
import 'package:roadsos/ui/dispatch_status_panel.dart';

void main() {
testWidgets('DispatchStatusPanel shows channel titles and lifecycle detail', (tester) async {
testWidgets('DispatchStatusPanel shows channel titles and lifecycle detail', (
tester,
) async {
const channels = [
DispatchChannelRow(
id: 'sms',
Expand All @@ -22,10 +24,11 @@ void main() {

await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
home: const Scaffold(
body: DispatchStatusPanel(channels: channels),
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
home: const Scaffold(body: DispatchStatusPanel(channels: channels)),
),
);

Expand All @@ -36,12 +39,12 @@ void main() {
expect(find.text('No peers in range.'), findsOneWidget);
});

testWidgets('DispatchStatusPanel renders nothing for empty channels', (tester) async {
testWidgets('DispatchStatusPanel renders nothing for empty channels', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DispatchStatusPanel(channels: const []),
),
home: Scaffold(body: DispatchStatusPanel(channels: const [])),
),
);

Expand Down
58 changes: 32 additions & 26 deletions test/india_emergency_routing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,38 @@ void main() {
expect(r?.stateCode, 'IN-KA');
});

test('Tamil Nadu box bounds (8.0–13.6N, 76.0–80.6E) excluding KA overlap', () {
// TN SW corner
var r = resolveIndiaEmergencyRoute(8.0, 76.0);
expect(r?.stateCode, 'IN-TN');

// TN NW corner (Note: 13.6, 76.0 overlaps with KA box. The code checks KA first,
// so it will resolve to IN-KA. We test a point just east of KA box for TN NW corner.)
r = resolveIndiaEmergencyRoute(13.6, 79.0);
expect(r?.stateCode, 'IN-TN');

// TN NE corner
r = resolveIndiaEmergencyRoute(13.6, 80.6);
expect(r?.stateCode, 'IN-TN');

// TN SE corner
r = resolveIndiaEmergencyRoute(8.0, 80.6);
expect(r?.stateCode, 'IN-TN');
});

test('Tamil Nadu bounds that overlap with Karnataka resolve to Karnataka', () {
// The point (12.0, 77.0) is in both TN box (8.0-13.6, 76.0-80.6)
// and KA box (11.5-18.6, 74.0-78.9).
// Since KA is checked first in _resolveByCoarseBoxes, it must resolve to KA.
final r = resolveIndiaEmergencyRoute(12.0, 77.0);
expect(r?.stateCode, 'IN-KA');
});
test(
'Tamil Nadu box bounds (8.0–13.6N, 76.0–80.6E) excluding KA overlap',
() {
// TN SW corner
var r = resolveIndiaEmergencyRoute(8.0, 76.0);
expect(r?.stateCode, 'IN-TN');

// TN NW corner (Note: 13.6, 76.0 overlaps with KA box. The code checks KA first,
// so it will resolve to IN-KA. We test a point just east of KA box for TN NW corner.)
r = resolveIndiaEmergencyRoute(13.6, 79.0);
expect(r?.stateCode, 'IN-TN');

// TN NE corner
r = resolveIndiaEmergencyRoute(13.6, 80.6);
expect(r?.stateCode, 'IN-TN');

// TN SE corner
r = resolveIndiaEmergencyRoute(8.0, 80.6);
expect(r?.stateCode, 'IN-TN');
},
);

test(
'Tamil Nadu bounds that overlap with Karnataka resolve to Karnataka',
() {
// The point (12.0, 77.0) is in both TN box (8.0-13.6, 76.0-80.6)
// and KA box (11.5-18.6, 74.0-78.9).
// Since KA is checked first in _resolveByCoarseBoxes, it must resolve to KA.
final r = resolveIndiaEmergencyRoute(12.0, 77.0);
expect(r?.stateCode, 'IN-KA');
},
);
});

group('just outside coarse boxes', () {
Expand Down
5 changes: 4 additions & 1 deletion test/services/crash_confidence_engine_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ void main() {
final result = CrashConfidenceEngine.score(signals);

expect(result.tier, equals(CrashConfidenceTier.high));
expect(result.incidentLabel, equals('Detected incident — possible emergency'));
expect(
result.incidentLabel,
equals('Detected incident — possible emergency'),
);
expect(result.score, equals(1.0));
});

Expand Down
Loading
Loading