372 lines
14 KiB
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,
|
|
);
|
|
}
|
|
}
|