mymuseum-visitapp/lib/Services/geo_beacon_trigger_service.dart

234 lines
7.5 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:beacon_scanner/beacon_scanner.dart';
import 'package:flutter/foundation.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Services/assistantService.dart';
import 'package:mymuseum_visitapp/Services/Glasses/glasses_orchestrator.dart';
import 'package:mymuseum_visitapp/constants.dart';
/// Point de déclenchement géographique pour le TTS automatique.
class GeoTriggerPoint {
final String id;
final double latitude;
final double longitude;
final double radiusMeters;
const GeoTriggerPoint({
required this.id,
required this.latitude,
required this.longitude,
this.radiusMeters = 20.0,
});
}
/// Déclenche automatiquement une réponse TTS de l'assistant quand :
/// - Le visiteur entre dans le rayon d'un GeoTriggerPoint (GPS)
/// - Un beacon BLE est détecté à proximité (< [beaconProximityMeters])
///
/// Le mode proactif doit être activé dans [VisitAppContext.proactiveModeEnabled]
/// pour que les déclenchements soient actifs.
class GeoBeaconTriggerService {
GeoBeaconTriggerService._();
static final GeoBeaconTriggerService instance = GeoBeaconTriggerService._();
static const double beaconProximityMeters = 3.0;
static const int cooldownMillis = 30000; // 30s entre deux triggers du même point
VisitAppContext? _visitAppContext;
AssistantService? _assistantService;
StreamSubscription<Position>? _geoSub;
StreamSubscription<ScanResult>? _beaconSub;
final List<GeoTriggerPoint> _geoPoints = [];
final Set<String> _triggeredIds = {};
final Map<String, int> _lastTriggerTime = {};
bool _running = false;
/// Lance le service.
/// [geoPoints] : points de déclenchement GPS (à peupler depuis la configuration).
Future<void> start({
required VisitAppContext visitAppContext,
List<GeoTriggerPoint> geoPoints = const [],
}) async {
if (_running) return;
if (!Platform.isAndroid && !Platform.isIOS) return;
_visitAppContext = visitAppContext;
_assistantService = AssistantService(visitAppContext: visitAppContext);
_geoPoints
..clear()
..addAll(geoPoints);
_running = true;
await _startGeoTracking();
await _startBeaconTracking();
debugPrint('[GeoBeaconTriggerService] Started — ${_geoPoints.length} geo points, beacons active');
}
Future<void> stop() async {
await _geoSub?.cancel();
await _beaconSub?.cancel();
_geoSub = null;
_beaconSub = null;
_running = false;
debugPrint('[GeoBeaconTriggerService] Stopped');
}
/// Recharge les GeoTriggerPoints (appeler après un changement de configuration).
void updateGeoPoints(List<GeoTriggerPoint> points) {
_geoPoints
..clear()
..addAll(points);
_triggeredIds.clear();
}
// ── GPS ─────────────────────────────────────────────────────────────────────
Future<void> _startGeoTracking() async {
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
debugPrint('[GeoBeaconTriggerService] Location permission not granted');
return;
}
_geoSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5,
),
).listen(_onPosition);
}
void _onPosition(Position position) {
if (!_isProactiveModeActive()) return;
for (final point in _geoPoints) {
final dist = Geolocator.distanceBetween(
position.latitude,
position.longitude,
point.latitude,
point.longitude,
);
if (dist <= point.radiusMeters && _canTrigger(point.id)) {
_trigger(
triggerId: point.id,
prompt: 'Tu es un guide audio de musée. '
'Le visiteur vient d\'entrer dans la zone "${point.id}". '
'Accueille-le et donne-lui une brève information de bienvenue en 2 phrases.',
);
}
}
}
// ── Beacons ──────────────────────────────────────────────────────────────────
Future<void> _startBeaconTracking() async {
final ctx = _visitAppContext;
if (ctx?.beaconSections == null || ctx!.beaconSections!.isEmpty) return;
final regions = <Region>[
if (Platform.isIOS)
Region(
identifier: 'GlassesBeacon',
beaconId: IBeaconId(proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825'),
)
else
Region(identifier: 'GlassesBeacon'),
];
try {
_beaconSub = BeaconScanner.instance
.ranging(regions)
.listen((ScanResult result) => _onBeaconResult(result));
} catch (e) {
debugPrint('[GeoBeaconTriggerService] Beacon scan error: $e');
}
}
void _onBeaconResult(ScanResult result) {
if (!_isProactiveModeActive()) return;
final ctx = _visitAppContext;
if (ctx?.beaconSections == null) return;
for (final beacon in result.beacons) {
if (beacon.accuracy > beaconProximityMeters) continue;
final match = ctx!.beaconSections!.firstWhere(
(bs) =>
bs != null &&
bs.minorBeaconId == beacon.id.minorId &&
bs.configurationId == ctx.configuration?.id,
orElse: () => null,
);
if (match?.sectionId != null && _canTrigger(match!.sectionId!)) {
_trigger(
triggerId: match.sectionId!,
prompt: 'Tu es un guide audio de musée. '
'Le visiteur est juste devant l\'œuvre ou l\'espace lié au point d\'intérêt "${match.sectionId}". '
'Donne une présentation courte et engageante en 2-3 phrases.',
);
}
}
}
// ── Déclenchement commun ─────────────────────────────────────────────────────
bool _canTrigger(String id) {
final now = DateTime.now().millisecondsSinceEpoch;
final last = _lastTriggerTime[id] ?? 0;
return (now - last) >= cooldownMillis;
}
bool _isProactiveModeActive() {
return _visitAppContext?.proactiveModeEnabled == true &&
_visitAppContext?.glassesEnabled == true;
}
Future<void> _trigger({
required String triggerId,
required String prompt,
}) async {
_lastTriggerTime[triggerId] = DateTime.now().millisecondsSinceEpoch;
debugPrint('[GeoBeaconTriggerService] Trigger: $triggerId');
try {
final response = await _assistantService!.chat(
message: prompt,
configurationId: _visitAppContext?.configuration?.id,
);
if (response.reply.isNotEmpty) {
final lang = _visitAppContext?.language ?? 'FR';
await activeOrchestrator?.ttsEngine.speak(
response.reply,
languageCode: _toLangCode(lang),
);
}
} catch (e) {
debugPrint('[GeoBeaconTriggerService] Trigger error for $triggerId: $e');
}
}
String _toLangCode(String lang) {
switch (lang.toUpperCase()) {
case 'FR': return 'fr-FR';
case 'NL': return 'nl-NL';
case 'EN': return 'en-US';
case 'DE': return 'de-DE';
default: return 'fr-FR';
}
}
void dispose() {
stop();
}
}