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:
+112
-45
@@ -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) {
|
||||
|
||||
// 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) {
|
||||
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 });
|
||||
});
|
||||
|
||||
// автоподключение при загрузке (опционально)
|
||||
|
||||
Reference in New Issue
Block a user