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:
@@ -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>
|
||||
</>)}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -176,6 +176,27 @@ META_TOOLS = [
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "request_location_tracking",
|
||||
"description": (
|
||||
"Bittet die App, das kontinuierliche GPS-Tracking zu aktivieren oder zu "
|
||||
"deaktivieren. Default ist AUS (Akku-Schutz). Nutze das wenn du einen "
|
||||
"GPS-basierten Watcher anlegst (z.B. `near(...)`), sonst hat die App "
|
||||
"veraltete Position und der Watcher feuert nie. Auch wieder ausschalten "
|
||||
"wenn der letzte GPS-Watcher geloescht wurde."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"on": {"type": "boolean", "description": "true = Tracking an, false = aus"},
|
||||
"reason": {"type": "string", "description": "Kurzer Grund (wird in App-Notification angezeigt)"},
|
||||
},
|
||||
"required": ["on"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -401,6 +422,15 @@ class Agent:
|
||||
return f"OK — Trigger '{arguments['name']}' geloescht."
|
||||
except ValueError as e:
|
||||
return f"FEHLER: {e}"
|
||||
if name == "request_location_tracking":
|
||||
on = bool(arguments.get("on", False))
|
||||
reason = (arguments.get("reason") or "").strip()
|
||||
self._pending_events.append({
|
||||
"type": "location_tracking",
|
||||
"on": on,
|
||||
"reason": reason,
|
||||
})
|
||||
return f"OK — Tracking-Request gesendet (on={on}). App wird in Kuerze umschalten."
|
||||
if name == "trigger_list":
|
||||
items = triggers_mod.list_triggers(active_only=False)
|
||||
if not items:
|
||||
|
||||
@@ -155,6 +155,16 @@ def build_triggers_section(
|
||||
lines.append("- **Timer** fuer einmalige Erinnerungen mit konkreter Zeit ('in 10min', 'um 14:30').")
|
||||
lines.append("- **Watcher** fuer 'wenn X passiert' (Disk voll, bestimmte Tageszeit, GPS-Naehe).")
|
||||
lines.append("- ARIA legt Trigger NUR auf Stefan-Wunsch an, nicht eigenmaechtig.")
|
||||
lines.append("")
|
||||
lines.append("### GPS-Watcher mit near()")
|
||||
lines.append(
|
||||
"Wenn du einen Watcher mit `near()` anlegst: die App sendet GPS-Position "
|
||||
"nur kontinuierlich wenn Tracking AN ist (Default: AUS, Akku-Schutz). "
|
||||
"Rufe dafuer `request_location_tracking(on=true, reason=\"...\")` auf "
|
||||
"bevor oder gleich nach dem trigger_watcher. Sonst hat current_lat/lon "
|
||||
"veraltete Werte und der Watcher feuert nie. "
|
||||
"Beim Loeschen des letzten GPS-Watchers (trigger_cancel) wieder "
|
||||
"`request_location_tracking(on=false)` aufrufen.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
@@ -1358,6 +1358,18 @@ class ARIABridge:
|
||||
})
|
||||
logger.info("[brain] ARIA hat einen Trigger angelegt: %s",
|
||||
event.get("trigger", {}).get("name"))
|
||||
elif etype == "location_tracking":
|
||||
# ARIA bittet die App das GPS-Tracking ein-/auszuschalten
|
||||
await self._send_to_rvs({
|
||||
"type": "location_tracking",
|
||||
"payload": {
|
||||
"on": bool(event.get("on")),
|
||||
"reason": event.get("reason") or "",
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
logger.info("[brain] location_tracking Request: on=%s (%s)",
|
||||
event.get("on"), event.get("reason", ""))
|
||||
|
||||
# _process_core_response uebernimmt alles weitere:
|
||||
# File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat-
|
||||
@@ -1914,6 +1926,17 @@ class ARIABridge:
|
||||
logger.warning("[rvs] file_delete_request: %s", e)
|
||||
return
|
||||
|
||||
elif msg_type == "location_update":
|
||||
# Live-GPS-Update von der App (nicht an Chat gekoppelt). Wird in
|
||||
# /shared/state/location.json geschrieben, damit Watcher-Trigger
|
||||
# near()-Conditions auswerten koennen.
|
||||
lat = payload.get("lat")
|
||||
lon = payload.get("lon") or payload.get("lng")
|
||||
if lat is not None and lon is not None:
|
||||
self._persist_location({"lat": lat, "lon": lon})
|
||||
logger.debug("[gps] location_update: %.5f, %.5f", float(lat), float(lon))
|
||||
return
|
||||
|
||||
elif msg_type == "container_restart":
|
||||
# App-Button "Container neu" — leitet generisch an Diagnostic
|
||||
# weiter. Whitelist ist im Diagnostic-Server.
|
||||
|
||||
@@ -26,6 +26,7 @@ const ALLOWED_TYPES = new Set([
|
||||
"xtts_import_voice", "xtts_voice_imported",
|
||||
"skill_created",
|
||||
"trigger_created",
|
||||
"location_update", "location_tracking",
|
||||
"chat_history_request", "chat_history_response", "chat_cleared",
|
||||
"file_delete_batch_request", "file_delete_batch_response",
|
||||
"file_zip_request", "file_zip_response",
|
||||
|
||||
Reference in New Issue
Block a user