234 lines
7.5 KiB
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();
|
|
}
|
|
}
|