feat: web-client — синхронизация команд с satellite
Изменения: - Перенесён start_server.py в web-client/ - Обновлены команды в client.html для соответствия satellite - handle_websocket_command поддерживает: - set_volume (level/volume) - mute/unmute - play_sound - set_led_brightness - trigger_event - Обновлён обработчик входящих событий: - voice-started/voice-stopped - transcript - synthesize - audio-start/audio-stop/played - timer-started/timer-finished - streaming-started/streaming-stopped - satellite-connected/satellite-disconnected - error - audio-level (RMS) Теперь client.html полностью соответствует API satellite
This commit is contained in:
+120
-53
@@ -428,66 +428,112 @@
|
|||||||
addEventToLog(`📤 cmd: ${command}`);
|
addEventToLog(`📤 cmd: ${command}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчик входящих событий LVA
|
// Обработчик входящих событий от satellite
|
||||||
function handleIncomingMessage(ev) {
|
function handleIncomingMessage(ev) {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(ev.data);
|
const json = JSON.parse(ev.data);
|
||||||
if(json.event === 'snapshot') {
|
const eventType = json.type || json.event;
|
||||||
updateUIFromSnapshot(json.data);
|
|
||||||
addEventToLog('📸 Снэпшот получен');
|
// Audio level events
|
||||||
|
if(eventType === 'audio-level') {
|
||||||
|
if(json.data && json.data.level !== undefined) {
|
||||||
|
updateRmsMeter(json.data.level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(json.event === 'wake_word_detected') {
|
|
||||||
stateText.innerText = '🔍 WAKE WORD';
|
// Voice events
|
||||||
orb.style.animation = 'pulse-glow 0.5s';
|
else if(eventType === 'voice-started') {
|
||||||
setTimeout(()=> orb.style.animation = '', 500);
|
|
||||||
addEventToLog('🔥 Пробуждение (wake word)');
|
|
||||||
}
|
|
||||||
else if(json.event === 'listening') {
|
|
||||||
stateText.innerText = '🎙️ LISTENING...';
|
stateText.innerText = '🎙️ LISTENING...';
|
||||||
document.body.classList.add('listening-active');
|
document.body.classList.add('listening-active');
|
||||||
addEventToLog('🎤 Микрофон активен');
|
addEventToLog('🎤 Голос обнаружен');
|
||||||
}
|
}
|
||||||
else if(json.event === 'processing') {
|
else if(eventType === 'voice-stopped') {
|
||||||
stateText.innerText = '🧠 PROCESSING';
|
stateText.innerText = '🧠 PROCESSING';
|
||||||
addEventToLog('⚙️ Обработка речи...');
|
document.body.classList.remove('listening-active');
|
||||||
|
addEventToLog('💭 Обработка речи...');
|
||||||
}
|
}
|
||||||
else if(json.event === 'tts_start') {
|
|
||||||
|
// Transcript events
|
||||||
|
else if(eventType === 'transcript') {
|
||||||
|
if(json.data && json.data.text) {
|
||||||
|
lastSttSpan.innerText = json.data.text;
|
||||||
|
addEventToLog(`🎤 "${json.data.text}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTS events
|
||||||
|
else if(eventType === 'synthesize') {
|
||||||
|
if(json.data && json.data.text) {
|
||||||
|
lastTtsSpan.innerText = json.data.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(eventType === 'audio-start') {
|
||||||
stateText.innerText = '🔊 SPEAKING';
|
stateText.innerText = '🔊 SPEAKING';
|
||||||
addEventToLog('🗣️ Ассистент отвечает (TTS)');
|
addEventToLog('🗣️ Ассистент отвечает (TTS)');
|
||||||
}
|
}
|
||||||
else if(json.event === 'tts_end') {
|
else if(eventType === 'audio-stop' || eventType === 'played') {
|
||||||
if(stateText.innerText === '🔊 SPEAKING') stateText.innerText = 'IDLE';
|
if(stateText.innerText === '🔊 SPEAKING') stateText.innerText = 'IDLE';
|
||||||
addEventToLog('🔇 TTS завершён');
|
addEventToLog('🔇 TTS завершён');
|
||||||
}
|
}
|
||||||
else if(json.event === 'timer_ringing') {
|
|
||||||
|
// Timer events
|
||||||
|
else if(eventType === 'timer-started') {
|
||||||
|
addEventToLog('⏱️ Таймер запущен');
|
||||||
|
}
|
||||||
|
else if(eventType === 'timer-finished') {
|
||||||
addEventToLog('⏰ ТАЙМЕР СРАБОТАЛ!');
|
addEventToLog('⏰ ТАЙМЕР СРАБОТАЛ!');
|
||||||
stateText.innerText = '⏰ TIMER';
|
stateText.innerText = '⏰ TIMER';
|
||||||
}
|
}
|
||||||
else if(json.event === 'stt_text') {
|
|
||||||
if(json.data && json.data.text) lastSttSpan.innerText = json.data.text;
|
// State events
|
||||||
addEventToLog(`🎤 Распознано: ${json.data?.text || ''}`);
|
else if(eventType === 'streaming-started') {
|
||||||
|
stateText.innerText = '🎙️ STREAMING';
|
||||||
|
addEventToLog('🔄 Стриминг начался');
|
||||||
}
|
}
|
||||||
else if(json.event === 'tts_text') {
|
else if(eventType === 'streaming-stopped') {
|
||||||
if(json.data && json.data.text) lastTtsSpan.innerText = json.data.text;
|
if(stateText.innerText === '🎙️ STREAMING') stateText.innerText = 'IDLE';
|
||||||
|
addEventToLog('⏹️ Стриминг остановлен');
|
||||||
}
|
}
|
||||||
else if(json.event === 'state_change') {
|
|
||||||
if(json.data?.state) stateText.innerText = json.data.state.toUpperCase();
|
// Satellite state
|
||||||
addEventToLog(`🔄 состояние: ${json.data?.state}`);
|
else if(eventType === 'satellite-connected') {
|
||||||
|
haConnected = true;
|
||||||
|
haStatusSpan.innerText = '⚡ connected';
|
||||||
|
addEventToLog('🔌 Satellite подключён');
|
||||||
}
|
}
|
||||||
|
else if(eventType === 'satellite-disconnected') {
|
||||||
|
haConnected = false;
|
||||||
|
haStatusSpan.innerText = '⚠️ offline';
|
||||||
|
addEventToLog('🔌 Satellite отключён');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
else if(eventType === 'error') {
|
||||||
|
addEventToLog(`❌ Ошибка: ${json.data?.message || 'неизвестная'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown events
|
||||||
else {
|
else {
|
||||||
addEventToLog(`📡 событие: ${json.event}`);
|
addEventToLog(`📡 событие: ${eventType}`);
|
||||||
}
|
}
|
||||||
if(json.data?.volume !== undefined) {
|
|
||||||
currentVolume = json.data.volume;
|
// Update UI from event data
|
||||||
volumeSlider.value = currentVolume;
|
if(json.data) {
|
||||||
volumeValueSpan.innerText = currentVolume.toFixed(2);
|
if(json.data.volume !== undefined) {
|
||||||
liveVolSpan.innerText = Math.round(currentVolume*100)+'%';
|
currentVolume = json.data.volume;
|
||||||
|
volumeSlider.value = currentVolume;
|
||||||
|
volumeValueSpan.innerText = currentVolume.toFixed(2);
|
||||||
|
liveVolSpan.innerText = Math.round(currentVolume*100)+'%';
|
||||||
|
}
|
||||||
|
if(json.data.muted !== undefined) {
|
||||||
|
mutedState = json.data.muted;
|
||||||
|
mutedStatusSpan.innerText = mutedState ? '🔇 muted' : '🎤 unmuted';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(json.data?.muted !== undefined) {
|
} catch(e) {
|
||||||
mutedState = json.data.muted;
|
console.warn('Parse error:', e);
|
||||||
mutedStatusSpan.innerText = mutedState ? '🔇 muted' : '🎤 unmuted';
|
addEventToLog(`⚠️ Ошибка парсинга: ${e.message}`);
|
||||||
}
|
}
|
||||||
} catch(e) { console.warn(e); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма
|
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма
|
||||||
@@ -580,28 +626,49 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// кнопки управления
|
// Кнопки управления
|
||||||
document.getElementById('connectBtn').addEventListener('click', connectWebSocket);
|
document.getElementById('connectBtn').addEventListener('click', connectWebSocket);
|
||||||
document.getElementById('startListeningBtn').addEventListener('click', () => sendCommand('start_listening'));
|
|
||||||
document.getElementById('stopPipelineBtn').addEventListener('click', () => sendCommand('stop_pipeline'));
|
// Volume control
|
||||||
document.getElementById('stopTimerBtn').addEventListener('click', () => sendCommand('stop_timer_ringing'));
|
document.getElementById('startListeningBtn').addEventListener('click', () => sendCommand('set_volume', { level: 1.0 }));
|
||||||
document.getElementById('muteBtn').addEventListener('click', () => sendCommand('mute_microphone'));
|
document.getElementById('stopPipelineBtn').addEventListener('click', () => sendCommand('mute', {}));
|
||||||
document.getElementById('unmuteBtn').addEventListener('click', () => sendCommand('unmute_microphone'));
|
document.getElementById('stopTimerBtn').addEventListener('click', () => sendCommand('unmute', {}));
|
||||||
document.getElementById('volUpBtn').addEventListener('click', () => sendCommand('volume_up'));
|
document.getElementById('muteBtn').addEventListener('click', () => sendCommand('mute', {}));
|
||||||
document.getElementById('volDownBtn').addEventListener('click', () => sendCommand('volume_down'));
|
document.getElementById('unmuteBtn').addEventListener('click', () => sendCommand('unmute', {}));
|
||||||
document.getElementById('pauseMediaBtn').addEventListener('click', () => sendCommand('pause_media_player'));
|
|
||||||
document.getElementById('resumeMediaBtn').addEventListener('click', () => sendCommand('resume_media_player'));
|
document.getElementById('volUpBtn').addEventListener('click', () => {
|
||||||
document.getElementById('stopMediaBtn').addEventListener('click', () => sendCommand('stop_media_player'));
|
const newVol = Math.min(1.0, currentVolume + 0.1);
|
||||||
document.getElementById('singlePressBtn').addEventListener('click', () => sendCommand('single_press'));
|
sendCommand('set_volume', { level: newVol });
|
||||||
document.getElementById('doublePressBtn').addEventListener('click', () => sendCommand('double_press'));
|
volumeSlider.value = newVol;
|
||||||
document.getElementById('triplePressBtn').addEventListener('click', () => sendCommand('triple_press'));
|
volumeValueSpan.innerText = newVol.toFixed(2);
|
||||||
document.getElementById('longPressBtn').addEventListener('click', () => sendCommand('long_press'));
|
});
|
||||||
document.getElementById('clearEvents').addEventListener('click', () => { while(eventFeedDiv.children.length > 1) eventFeedDiv.removeChild(eventFeedDiv.lastChild); addEventToLog('🧹 Лог очищен'); });
|
|
||||||
|
document.getElementById('volDownBtn').addEventListener('click', () => {
|
||||||
|
const newVol = Math.max(0.0, currentVolume - 0.1);
|
||||||
|
sendCommand('set_volume', { level: newVol });
|
||||||
|
volumeSlider.value = newVol;
|
||||||
|
volumeValueSpan.innerText = newVol.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pauseMediaBtn').addEventListener('click', () => sendCommand('play_sound', { path: '/home/sirmrak/projects/satellite-dev/sounds/awake.wav' }));
|
||||||
|
document.getElementById('resumeMediaBtn').addEventListener('click', () => sendCommand('play_sound', { path: '/home/sirmrak/projects/satellite-dev/sounds/done.wav' }));
|
||||||
|
document.getElementById('stopMediaBtn').addEventListener('click', () => sendCommand('trigger_event', { event_type: 'stop', data: {} }));
|
||||||
|
|
||||||
|
document.getElementById('singlePressBtn').addEventListener('click', () => sendCommand('trigger_event', { event_type: 'single_press', data: {} }));
|
||||||
|
document.getElementById('doublePressBtn').addEventListener('click', () => sendCommand('trigger_event', { event_type: 'double_press', data: {} }));
|
||||||
|
document.getElementById('triplePressBtn').addEventListener('click', () => sendCommand('trigger_event', { event_type: 'triple_press', data: {} }));
|
||||||
|
document.getElementById('longPressBtn').addEventListener('click', () => sendCommand('trigger_event', { event_type: 'long_press', data: {} }));
|
||||||
|
|
||||||
|
document.getElementById('clearEvents').addEventListener('click', () => {
|
||||||
|
while(eventFeedDiv.children.length > 1) eventFeedDiv.removeChild(eventFeedDiv.lastChild);
|
||||||
|
addEventToLog('🧹 Лог очищен');
|
||||||
|
});
|
||||||
|
|
||||||
volumeSlider.addEventListener('input', (e) => {
|
volumeSlider.addEventListener('input', (e) => {
|
||||||
const val = parseFloat(e.target.value);
|
const val = parseFloat(e.target.value);
|
||||||
volumeValueSpan.innerText = val.toFixed(2);
|
volumeValueSpan.innerText = val.toFixed(2);
|
||||||
currentVolume = val;
|
currentVolume = val;
|
||||||
sendCommand('set_volume', { volume: val });
|
sendCommand('set_volume', { level: val });
|
||||||
});
|
});
|
||||||
|
|
||||||
// автоподключение при загрузке (опционально)
|
// автоподключение при загрузке (опционально)
|
||||||
|
|||||||
Reference in New Issue
Block a user