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
+120 -53
View File
@@ -428,66 +428,112 @@
addEventToLog(`📤 cmd: ${command}`);
}
// Обработчик входящих событий LVA
// Обработчик входящих событий от satellite
function handleIncomingMessage(ev) {
try {
const json = JSON.parse(ev.data);
if(json.event === 'snapshot') {
updateUIFromSnapshot(json.data);
addEventToLog('📸 Снэпшот получен');
const eventType = json.type || json.event;
// 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...';
document.body.classList.add('listening-active');
addEventToLog('🎤 Микрофон активен');
addEventToLog('🎤 Голос обнаружен');
}
else if(json.event === 'processing') {
else if(eventType === 'voice-stopped') {
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';
addEventToLog('🗣️ Ассистент отвечает (TTS)');
}
else if(json.event === 'tts_end') {
else if(eventType === 'audio-stop' || eventType === 'played') {
if(stateText.innerText === '🔊 SPEAKING') stateText.innerText = 'IDLE';
addEventToLog('🔇 TTS завершён');
}
else if(json.event === 'timer_ringing') {
// Timer events
else if(eventType === 'timer-started') {
addEventToLog('⏱️ Таймер запущен');
}
else if(eventType === 'timer-finished') {
addEventToLog('⏰ ТАЙМЕР СРАБОТАЛ!');
stateText.innerText = '⏰ TIMER';
}
else if(json.event === 'stt_text') {
if(json.data && json.data.text) lastSttSpan.innerText = json.data.text;
addEventToLog(`🎤 Распознано: ${json.data?.text || ''}`);
// State events
else if(eventType === 'streaming-started') {
stateText.innerText = '🎙️ STREAMING';
addEventToLog('🔄 Стриминг начался');
}
else if(json.event === 'tts_text') {
if(json.data && json.data.text) lastTtsSpan.innerText = json.data.text;
else if(eventType === 'streaming-stopped') {
if(stateText.innerText === '🎙️ STREAMING') stateText.innerText = 'IDLE';
addEventToLog('⏹️ Стриминг остановлен');
}
else if(json.event === 'state_change') {
if(json.data?.state) stateText.innerText = json.data.state.toUpperCase();
addEventToLog(`🔄 состояние: ${json.data?.state}`);
// Satellite 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 {
addEventToLog(`📡 событие: ${json.event}`);
addEventToLog(`📡 событие: ${eventType}`);
}
if(json.data?.volume !== undefined) {
currentVolume = json.data.volume;
volumeSlider.value = currentVolume;
volumeValueSpan.innerText = currentVolume.toFixed(2);
liveVolSpan.innerText = Math.round(currentVolume*100)+'%';
// Update UI from event data
if(json.data) {
if(json.data.volume !== undefined) {
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) {
mutedState = json.data.muted;
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('startListeningBtn').addEventListener('click', () => sendCommand('start_listening'));
document.getElementById('stopPipelineBtn').addEventListener('click', () => sendCommand('stop_pipeline'));
document.getElementById('stopTimerBtn').addEventListener('click', () => sendCommand('stop_timer_ringing'));
document.getElementById('muteBtn').addEventListener('click', () => sendCommand('mute_microphone'));
document.getElementById('unmuteBtn').addEventListener('click', () => sendCommand('unmute_microphone'));
document.getElementById('volUpBtn').addEventListener('click', () => sendCommand('volume_up'));
document.getElementById('volDownBtn').addEventListener('click', () => sendCommand('volume_down'));
document.getElementById('pauseMediaBtn').addEventListener('click', () => sendCommand('pause_media_player'));
document.getElementById('resumeMediaBtn').addEventListener('click', () => sendCommand('resume_media_player'));
document.getElementById('stopMediaBtn').addEventListener('click', () => sendCommand('stop_media_player'));
document.getElementById('singlePressBtn').addEventListener('click', () => sendCommand('single_press'));
document.getElementById('doublePressBtn').addEventListener('click', () => sendCommand('double_press'));
document.getElementById('triplePressBtn').addEventListener('click', () => sendCommand('triple_press'));
document.getElementById('longPressBtn').addEventListener('click', () => sendCommand('long_press'));
document.getElementById('clearEvents').addEventListener('click', () => { while(eventFeedDiv.children.length > 1) eventFeedDiv.removeChild(eventFeedDiv.lastChild); addEventToLog('🧹 Лог очищен'); });
// Volume control
document.getElementById('startListeningBtn').addEventListener('click', () => sendCommand('set_volume', { level: 1.0 }));
document.getElementById('stopPipelineBtn').addEventListener('click', () => sendCommand('mute', {}));
document.getElementById('stopTimerBtn').addEventListener('click', () => sendCommand('unmute', {}));
document.getElementById('muteBtn').addEventListener('click', () => sendCommand('mute', {}));
document.getElementById('unmuteBtn').addEventListener('click', () => sendCommand('unmute', {}));
document.getElementById('volUpBtn').addEventListener('click', () => {
const newVol = Math.min(1.0, currentVolume + 0.1);
sendCommand('set_volume', { level: newVol });
volumeSlider.value = newVol;
volumeValueSpan.innerText = newVol.toFixed(2);
});
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) => {
const val = parseFloat(e.target.value);
volumeValueSpan.innerText = val.toFixed(2);
currentVolume = val;
sendCommand('set_volume', { volume: val });
sendCommand('set_volume', { level: val });
});
// автоподключение при загрузке (опционально)