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:
2026-05-21 13:14:22 +03:00
parent c7bf061611
commit 0776d839b9
2 changed files with 120 additions and 53 deletions
+112 -45
View File
@@ -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';
orb.style.animation = 'pulse-glow 0.5s';
setTimeout(()=> orb.style.animation = '', 500);
addEventToLog('🔥 Пробуждение (wake word)');
} }
else if(json.event === 'listening') {
// Voice events
else if(eventType === 'voice-started') {
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) {
// Update UI from event data
if(json.data) {
if(json.data.volume !== undefined) {
currentVolume = json.data.volume; currentVolume = json.data.volume;
volumeSlider.value = currentVolume; volumeSlider.value = currentVolume;
volumeValueSpan.innerText = currentVolume.toFixed(2); volumeValueSpan.innerText = currentVolume.toFixed(2);
liveVolSpan.innerText = Math.round(currentVolume*100)+'%'; liveVolSpan.innerText = Math.round(currentVolume*100)+'%';
} }
if(json.data?.muted !== undefined) { if(json.data.muted !== undefined) {
mutedState = json.data.muted; mutedState = json.data.muted;
mutedStatusSpan.innerText = mutedState ? '🔇 muted' : '🎤 unmuted'; mutedStatusSpan.innerText = mutedState ? '🔇 muted' : '🎤 unmuted';
} }
} catch(e) { console.warn(e); } }
} catch(e) {
console.warn('Parse error:', e);
addEventToLog(`⚠️ Ошибка парсинга: ${e.message}`);
}
} }
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма // Визуализация микрофона (если доступен) — рисуем абстракцию ритма
@@ -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 });
}); });
// автоподключение при загрузке (опционально) // автоподключение при загрузке (опционально)