feat(gps): kontinuierliches GPS-Tracking — Blitzer-Warner-Pipeline komplett

ARIA kann jetzt GPS-Watcher mit near() effektiv nutzen: die App liefert
kontinuierliche Position, Brain wertet sie in den Background-Triggers aus.

rvs/server.js
  ALLOWED_TYPES: location_update (App→Bridge) + location_tracking (Brain→App).

bridge/aria_bridge.py
  location_update Handler: persistiert {lat, lon} via _persist_location in
  /shared/state/location.json — selber Pfad wie chat/audio-events, aber als
  eigenes Event ohne Chat-Overhead.

aria-brain/agent.py
  Neues Meta-Tool request_location_tracking(on, reason). Dispatcher fuegt
  {type: "location_tracking", on, reason} zu _pending_events hinzu →
  Bridge forwarded als RVS-Message zur App.

aria-brain/prompts.py
  Trigger-Section bekam neuen Block "GPS-Watcher mit near()": ARIA wird
  angewiesen request_location_tracking(on=true) zu rufen wenn sie einen
  near()-Watcher anlegt, und wieder false beim Loeschen des letzten.

android/src/services/gpsTracking.ts (NEU)
  Singleton-Service. start(reason) → Geolocation.watchPosition mit
  distanceFilter 30m + interval 15s, sendet location_update an RVS.
  stop(reason) → clearWatch. Persistiert Status in 'aria_gps_tracking',
  restoreFromStorage() beim Settings-Mount. Permission-Request fuer
  ACCESS_FINE_LOCATION + Toast-Benachrichtigung bei An/Aus.

android/src/screens/SettingsScreen.tsx
  Neuer Switch im "Standort"-Block: "GPS-Tracking (kontinuierlich)" mit
  Hinweis-Text. Subscribe auf gpsTrackingService.onChange damit Toggle
  reflektiert wenn ARIA das per Tool umschaltet.
  RVS-Handler: location_tracking → gpsTrackingService.start/stop mit
  Reason aus Brain-Tool.

Ablauf Stefan→ARIA→Blitzer:
  1. Stefan: "Warn mich vor Blitzern auf Route nach Rhauderfehn"
  2. ARIA: skill_create("blitzer-warner") falls noch nicht da
  3. ARIA: run_blitzer-warner → Liste {lat,lon,name}
  4. ARIA: pro Eintrag trigger_watcher mit near(lat,lon,500)
  5. ARIA: request_location_tracking(on=true, reason="Blitzer-Warner aktiv")
  6. App: GPS-Tracking startet, sendet alle 15s location_update
  7. Bridge: /shared/state/location.json wird aktuell gehalten
  8. Brain-Background-Loop: alle 30s near()-Check pro Trigger
  9. Bei Erfolg: ARIA spricht "Blitzer A31 km 12 in 500m"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 01:02:05 +02:00
parent 07c761fc72
commit fa47068d6d
6 changed files with 248 additions and 0 deletions
+46
View File
@@ -51,6 +51,7 @@ import {
TTS_SPEED_STORAGE_KEY,
} from '../services/audio';
import audioService from '../services/audio';
import gpsTrackingService from '../services/gpsTracking';
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
import {
isWakeReadySoundEnabled,
@@ -121,6 +122,7 @@ const SettingsScreen: React.FC = () => {
const [manualPort, setManualPort] = useState('8765');
const [currentMode, setCurrentMode] = useState('normal');
const [gpsEnabled, setGpsEnabled] = useState(false);
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
const [scannerVisible, setScannerVisible] = useState(false);
const [logTab, setLogTab] = useState<LogTab>('live');
const [logs, setLogs] = useState<LogEntry[]>([]);
@@ -188,6 +190,11 @@ const SettingsScreen: React.FC = () => {
AsyncStorage.getItem('aria_gps_enabled').then(saved => {
if (saved !== null) setGpsEnabled(saved === 'true');
});
// gpsTrackingService status syncen + auf Aenderungen lauschen
setGpsTracking(gpsTrackingService.isActive());
const offGps = gpsTrackingService.onChange(setGpsTracking);
// Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?)
gpsTrackingService.restoreFromStorage().catch(() => {});
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
if (saved != null) {
const n = parseFloat(saved);
@@ -245,6 +252,10 @@ const SettingsScreen: React.FC = () => {
});
// Voice-Liste vom XTTS-Server holen (via RVS)
rvs.send('xtts_list_voices' as any, {});
return () => {
// gpsTrackingService-Listener abmelden (Variable offGps oben definiert)
try { offGps(); } catch {}
};
}, []);
// Speichergroesse berechnen
@@ -407,6 +418,18 @@ const SettingsScreen: React.FC = () => {
}
}
// ARIA bittet um GPS-Tracking An/Aus (Tool request_location_tracking)
if (message.type === ('location_tracking' as any)) {
const p: any = message.payload || {};
const on = !!p.on;
const reason = (p.reason as string) || 'ARIA';
if (on) {
gpsTrackingService.start(reason).catch(() => {});
} else {
gpsTrackingService.stop(reason);
}
}
// Datei-Manager: ZIP-Response (Multi-Download)
if (message.type === ('file_zip_response' as any)) {
const p: any = message.payload || {};
@@ -1004,6 +1027,29 @@ const SettingsScreen: React.FC = () => {
thumbColor={gpsEnabled ? '#FFFFFF' : '#666680'}
/>
</View>
{/* GPS-Tracking (kontinuierlich) — fuer near()-Watcher */}
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>GPS-Tracking (kontinuierlich)</Text>
<Text style={styles.toggleHint}>
Sendet alle ~15s deine Position an ARIA (wenn du dich {'>'}30m bewegt
hast). Nur noetig fuer GPS-basierte Trigger wie Blitzer-Warner
(near()-Conditions). ARIA kann das auch selbst an-/abschalten wenn
sie einen GPS-Watcher anlegt. Akku-Verbrauch erhoeht — bei langer
Fahrt einplanen.
</Text>
</View>
<Switch
value={gpsTracking}
onValueChange={(v) => {
if (v) gpsTrackingService.start('manuell').catch(() => {});
else gpsTrackingService.stop('manuell');
}}
trackColor={{ false: '#2A2A3E', true: '#FF9500' }}
thumbColor={gpsTracking ? '#FFFFFF' : '#666680'}
/>
</View>
</View>
</>)}
+138
View File
@@ -0,0 +1,138 @@
/**
* GPS-Tracking-Service.
*
* Wenn aktiv: pushed alle paar Sekunden die aktuelle Position als
* `location_update {lat, lon}` an den RVS-Server, damit Brain-Watcher
* mit `near()`-Conditions etwas zum Vergleichen haben.
*
* Default: AUS. Wird entweder vom User manuell in Settings angeschaltet
* oder von ARIA via location_tracking-RVS-Message (Brain-Tool
* `request_location_tracking`).
*
* Energie-Schutz: distanceFilter 30m, interval 15s. Echte Fahrt-Updates
* (Geschwindigkeit) kommen sauber durch, stationaer wird kaum gesendet.
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
import Geolocation from '@react-native-community/geolocation';
import rvs from './rvs';
type Listener = (active: boolean) => void;
class GpsTrackingService {
private watchId: number | null = null;
private active = false;
private listeners: Set<Listener> = new Set();
// Defensive: nicht zu schnell oeffentlich togglen
private lastChangeAt = 0;
isActive(): boolean {
return this.active;
}
onChange(cb: Listener): () => void {
this.listeners.add(cb);
return () => { this.listeners.delete(cb); };
}
private notify() {
for (const cb of this.listeners) {
try { cb(this.active); } catch {}
}
}
/** Beim App-Start: gespeicherten Zustand wiederherstellen (Default off). */
async restoreFromStorage(): Promise<void> {
try {
const v = await AsyncStorage.getItem('aria_gps_tracking');
if (v === 'true') {
console.log('[gps-track] Restore: war an, starte wieder');
this.start('Beim Start wiederhergestellt');
}
} catch {}
}
private async ensurePermission(): Promise<boolean> {
if (Platform.OS !== 'android') return true;
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'GPS-Tracking',
message: 'ARIA braucht laufende Standort-Updates damit GPS-Watcher (Blitzer-Warner, near()) funktionieren.',
buttonPositive: 'Erlauben',
buttonNegative: 'Abbrechen',
},
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (e) {
console.warn('[gps-track] Permission-Fehler:', e);
return false;
}
}
async start(reason: string = ''): Promise<boolean> {
if (this.active) return true;
const ok = await this.ensurePermission();
if (!ok) {
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
return false;
}
try {
this.watchId = Geolocation.watchPosition(
(pos) => {
const lat = pos.coords.latitude;
const lon = pos.coords.longitude;
rvs.send('location_update' as any, { lat, lon });
},
(err) => {
console.warn('[gps-track] watchPosition error:', err?.code, err?.message);
},
{
enableHighAccuracy: true,
distanceFilter: 30, // erst senden wenn 30m gewandert
interval: 15000, // (Android) gewuenschte Frequenz
fastestInterval: 10000, // (Android) max Frequenz
} as any,
);
this.active = true;
this.lastChangeAt = Date.now();
this.notify();
AsyncStorage.setItem('aria_gps_tracking', 'true').catch(() => {});
ToastAndroid.show(
reason ? `GPS-Tracking aktiv (${reason})` : 'GPS-Tracking aktiv',
ToastAndroid.SHORT,
);
console.log('[gps-track] gestartet', reason ? `(${reason})` : '');
return true;
} catch (e: any) {
console.warn('[gps-track] start fehlgeschlagen:', e?.message);
return false;
}
}
stop(reason: string = ''): void {
if (!this.active) return;
if (this.watchId !== null) {
try { Geolocation.clearWatch(this.watchId); } catch {}
this.watchId = null;
}
this.active = false;
this.lastChangeAt = Date.now();
this.notify();
AsyncStorage.setItem('aria_gps_tracking', 'false').catch(() => {});
ToastAndroid.show(
reason ? `GPS-Tracking aus (${reason})` : 'GPS-Tracking aus',
ToastAndroid.SHORT,
);
console.log('[gps-track] gestoppt', reason ? `(${reason})` : '');
}
async toggle(reason: string = ''): Promise<void> {
if (this.active) this.stop(reason);
else await this.start(reason);
}
}
export default new GpsTrackingService();