mymuseum-visitapp/lib/Components/GlassesDebugPanel.dart

372 lines
14 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:meta_wearables_dat/meta_wearables_dat.dart';
import 'package:mymuseum_visitapp/Services/Glasses/glasses_orchestrator.dart';
import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart';
import 'package:mymuseum_visitapp/constants.dart';
/// Panneau de debug pour l'intégration Ray-Ban Meta.
/// À ouvrir via un bouton discret dans l'app (ex: appui long sur le logo).
/// Ne pas inclure en production.
class GlassesDebugPanel extends StatefulWidget {
const GlassesDebugPanel({super.key});
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.grey[900],
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (_) => const GlassesDebugPanel(),
);
}
@override
State<GlassesDebugPanel> createState() => _GlassesDebugPanelState();
}
class _GlassesDebugPanelState extends State<GlassesDebugPanel> {
String _log = '';
bool _busy = false;
bool _monitoring = false;
final List<StreamSubscription> _subs = [];
@override
void dispose() {
for (final s in _subs) { s.cancel(); }
super.dispose();
}
void _addLog(String msg) {
if (!mounted) return;
setState(() => _log = '${DateTime.now().toIso8601String().substring(11, 19)} $msg\n$_log');
}
void _toggleMonitor() {
if (_monitoring) {
for (final s in _subs) { s.cancel(); }
_subs.clear();
setState(() => _monitoring = false);
_addLog('⏹ Monitor arrêté');
return;
}
_subs.add(Wearables.instance.registrationStateStream.listen(
(s) => _addLog('📋 registration: ${s.state} err=${s.error}'),
onError: (e) => _addLog('📋 registration error: $e'),
));
_subs.add(Wearables.instance.devicesStream.listen(
(d) => _addLog('📱 devices: $d'),
onError: (e) => _addLog('📱 devices error: $e'),
));
_subs.add(Wearables.instance.streamStateStream.listen(
(s) => _addLog('🎥 streamState: $s'),
onError: (e) => _addLog('🎥 streamState error: $e'),
));
_subs.add(Wearables.instance.videoFramesStream.listen(
(f) => _addLog('🖼 videoFrame: ${f.length} bytes'),
onError: (e) => _addLog('🖼 videoFrame error: $e'),
));
setState(() => _monitoring = true);
_addLog('▶ Monitor démarré — interagis avec les lunettes');
}
Future<void> _run(String label, Future<void> Function() fn) async {
if (_busy) return;
setState(() => _busy = true);
_addLog('$label');
try {
await fn();
_addLog('$label');
} catch (e) {
_addLog('$label: $e');
} finally {
setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.75,
maxChildSize: 0.95,
builder: (_, scroll) => Column(
children: [
// Handle
Container(
margin: const EdgeInsets.symmetric(vertical: 8),
width: 40, height: 4,
decoration: BoxDecoration(
color: Colors.grey[600],
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.bug_report, color: Colors.amber, size: 18),
const SizedBox(width: 8),
const Text('Glasses Debug',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const Spacer(),
// État en temps réel
ValueListenableBuilder<GlassesState>(
valueListenable: MetaGlassesService.instance.state,
builder: (_, state, __) {
final color = state == GlassesState.streaming
? Colors.green
: state == GlassesState.connected
? Colors.lightGreen
: state == GlassesState.connecting
? Colors.orange
: Colors.red;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
border: Border.all(color: color),
borderRadius: BorderRadius.circular(12),
),
child: Text(
state.name.toUpperCase(),
style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold),
),
);
},
),
],
),
),
const Divider(color: Colors.grey),
// Boutons actions
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
spacing: 8, runSpacing: 8,
children: [
_ActionButton(
label: 'Activer caméra',
icon: Icons.camera_alt,
onTap: () => _run('requestCameraPermission + startStream', () async {
await Wearables.instance.requestCameraPermission();
await Wearables.instance.startStream(
videoQuality: 'MEDIUM',
frameRate: 24,
);
}),
),
_ActionButton(
label: 'Start stream',
icon: Icons.videocam,
onTap: () => _run('startStream (direct)', () async {
await Wearables.instance.startStream(
videoQuality: 'MEDIUM',
frameRate: 24,
);
}),
),
_ActionButton(
label: 'Capture photo',
icon: Icons.photo_camera,
onTap: () => _run('capturePhoto', () async {
await MetaGlassesService.instance.requestPhotoCapture();
}),
),
_ActionButton(
label: 'Test TTS',
icon: Icons.volume_up,
onTap: () => _run('TTS test', () async {
final o = activeOrchestrator;
if (o == null) {
_addLog('⚠ Orchestrateur non initialisé');
return;
}
// Stoppe l'écoute wake word pendant le TTS pour éviter le conflit audio focus
await o.wakeWordEngine.stop();
try {
await o.ttsEngine.speak(
'Bonjour. Je suis votre assistant de visite. Bienvenue au musée.',
languageCode: 'fr-FR',
);
} finally {
await o.wakeWordEngine.start(
onDetected: () => o.triggerConversation(),
onDetectedWithCommand: (cmd) => o.triggerConversation(),
);
}
}),
),
_ActionButton(
label: _monitoring ? 'Stop monitor' : 'Event monitor',
icon: _monitoring ? Icons.sensors_off : Icons.sensors,
color: _monitoring ? Colors.orange : Colors.purple,
onTap: _toggleMonitor,
),
_ActionButton(
label: 'Stop stream',
icon: Icons.stop,
color: Colors.red,
onTap: () => _run('stopStream', () async {
await Wearables.instance.stopStream();
}),
),
_ActionButton(
label: 'Reconnect',
icon: Icons.refresh,
onTap: () => _run('disconnect + connect', () async {
await MetaGlassesService.instance.disconnect();
await Future.delayed(const Duration(milliseconds: 500));
await MetaGlassesService.instance.connect();
}),
),
],
),
),
const Divider(color: Colors.grey),
// Transcription en direct
if (activeOrchestrator != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: ValueListenableBuilder<bool>(
valueListenable: activeOrchestrator!.isListeningForCommand,
builder: (_, listening, __) => ValueListenableBuilder<String>(
valueListenable: activeOrchestrator!.lastTranscription,
builder: (_, text, __) => Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: listening
? Colors.green.withValues(alpha: 0.15)
: Colors.grey[850],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: listening ? Colors.green : Colors.grey[700]!,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(
listening ? Icons.mic : Icons.mic_none,
color: listening ? Colors.green : Colors.grey,
size: 14,
),
const SizedBox(width: 6),
Text(
listening ? 'Écoute en cours...' : 'Dernière transcription',
style: TextStyle(
color: listening ? Colors.green : Colors.grey,
fontSize: 11,
),
),
]),
if (text.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'"$text"',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
),
),
),
// Dernier texte TTS
if (activeOrchestrator != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: ValueListenableBuilder<String>(
valueListenable: activeOrchestrator!.lastTtsText,
builder: (_, text, __) => text.isEmpty
? const SizedBox.shrink()
: Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withValues(alpha: 0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
const Icon(Icons.volume_up, color: Colors.blue, size: 14),
const SizedBox(width: 6),
const Text('Dernier TTS',
style: TextStyle(color: Colors.blue, fontSize: 11)),
]),
const SizedBox(height: 4),
Text(
text,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
),
),
const Divider(color: Colors.grey),
// Log
Expanded(
child: SingleChildScrollView(
controller: scroll,
padding: const EdgeInsets.all(12),
child: _log.isEmpty
? const Text('Logs apparaîtront ici...',
style: TextStyle(color: Colors.grey, fontSize: 12))
: Text(
_log,
style: const TextStyle(
color: Colors.greenAccent,
fontSize: 11,
fontFamily: 'monospace',
),
),
),
),
],
),
);
}
}
class _ActionButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onTap;
final Color color;
const _ActionButton({
required this.label,
required this.icon,
required this.onTap,
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: color.withValues(alpha: 0.15),
foregroundColor: color,
side: BorderSide(color: color.withValues(alpha: 0.4)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
icon: Icon(icon, size: 16),
label: Text(label, style: const TextStyle(fontSize: 12)),
onPressed: onTap,
);
}
}