Files
satellite/web-client/client.html
T
sirmrak c17743d829 fix: Web Client — правильное масштабирование RMS
Проблема:
- При RMS=0 столбики 30% (слишком много)
- При RMS=0.3 столбики 2% (слишком мало)

Решение:
- Масштаб: RMS 0 → 10%, RMS 0.15 → 100%
- Формула: avg = 10 + (RMS / 0.15) * 90
- Clamp 10-100
- Симуляция тишины: 10 + random*5 (маленькие колебания)

Теперь:
- Тишина (RMS=0) → 10% высоты
- Говор (RMS=0.1) → ~70% высоты
- Крик (RMS=0.15+) → 100% высоты
2026-05-21 19:47:20 +03:00

745 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>NOVA: LVA Cognitive Interface</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none; /* более чистый вид на панелях */
}
body {
background: radial-gradient(circle at 20% 30%, #0a0f1e, #03060c);
font-family: 'Segoe UI', 'Poppins', 'Inter', system-ui, -apple-system, 'Orbitron', monospace;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
backdrop-filter: blur(2px);
}
/* ГЛАВНАЯ КАРТА – голографический неон */
.glass-panel {
max-width: 1400px;
width: 100%;
background: rgba(12, 20, 35, 0.45);
backdrop-filter: blur(20px);
border-radius: 4rem;
border: 1px solid rgba(0, 255, 255, 0.25);
box-shadow: 0 30px 50px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1);
padding: 2rem 2rem 2rem 2rem;
transition: all 0.3s ease;
}
/* GRID LAYOUT */
.dashboard {
display: grid;
grid-template-columns: 1fr 0.9fr 1.2fr;
gap: 1.8rem;
}
/* общие стили блоков */
.card {
background: rgba(8, 15, 26, 0.6);
backdrop-filter: blur(12px);
border-radius: 2rem;
border: 1px solid rgba(0, 255, 255, 0.2);
padding: 1.4rem 1.5rem;
transition: all 0.2s;
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.card-header {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 3px;
font-weight: 500;
color: #7f9fcf;
border-left: 3px solid #0ff;
padding-left: 12px;
margin-bottom: 1.2rem;
display: flex;
align-items: center;
gap: 8px;
}
/* статус-индикатор */
.connection-status {
display: flex;
align-items: center;
gap: 12px;
background: rgba(0,0,0,0.4);
padding: 10px 18px;
border-radius: 60px;
width: fit-content;
margin-bottom: 20px;
}
.led {
width: 14px;
height: 14px;
border-radius: 50%;
background: #ff4d4d;
box-shadow: 0 0 6px #ff4d4d;
transition: all 0.2s;
}
.led.connected {
background: #0ff;
box-shadow: 0 0 12px #0ff, 0 0 6px #0ff;
}
.status-text {
font-weight: 500;
font-family: monospace;
color: #b0e0ff;
}
/* голосовая сфера */
.avatar-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.orb {
width: 160px;
height: 160px;
background: radial-gradient(circle at 30% 30%, #0ff, #0066aa);
border-radius: 50%;
margin: 20px auto;
box-shadow: 0 0 40px rgba(0, 255, 255, 0.5);
transition: all 0.2s cubic-bezier(0.2, 0.9, 0.4, 1.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: bold;
color: white;
text-shadow: 0 0 12px cyan;
}
.listening-active .orb {
animation: pulse-glow 1.2s infinite alternate;
transform: scale(1.02);
box-shadow: 0 0 70px rgba(0, 255, 200, 0.8);
}
@keyframes pulse-glow {
0% { box-shadow: 0 0 20px rgba(0,255,200,0.4); background: radial-gradient(circle at 30% 30%, #0ff, #0055aa);}
100% { box-shadow: 0 0 65px rgba(0,255,230,0.9); background: radial-gradient(circle at 30% 30%, #3effff, #0088dd);}
}
.state-label {
font-size: 1rem;
font-weight: 600;
background: rgba(0,255,255,0.15);
padding: 6px 20px;
border-radius: 40px;
backdrop-filter: blur(4px);
margin-top: 10px;
letter-spacing: 1px;
}
/* визуализатор аудио */
.viz-wrapper {
margin: 15px 0;
background: #03101755;
border-radius: 40px;
padding: 12px;
}
canvas {
width: 100%;
height: 80px;
display: block;
border-radius: 30px;
}
/* команды кнопки – неоновые */
.command-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 20px 0 10px;
}
.neo-btn {
background: rgba(16, 28, 48, 0.7);
border: 1px solid rgba(0, 255, 255, 0.6);
padding: 8px 16px;
border-radius: 40px;
color: #b3f0ff;
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
transition: 0.15s;
backdrop-filter: blur(4px);
display: inline-flex;
align-items: center;
gap: 8px;
letter-spacing: 0.5px;
}
.neo-btn:hover {
background: #0ff22a20;
border-color: #0ff;
color: white;
box-shadow: 0 0 10px #0ff;
transform: translateY(-2px);
}
.danger-btn {
border-color: #ff44aa;
color: #ffb0e0;
}
.danger-btn:hover {
border-color: #ff66cc;
background: #ff336620;
}
/* лента событий */
.event-feed {
background: #010a1480;
border-radius: 1.5rem;
padding: 0.8rem;
height: 260px;
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
}
.event-item {
border-bottom: 1px solid #2a4c6e50;
padding: 8px 10px;
color: #cae9ff;
display: flex;
gap: 12px;
}
.event-time {
color: #4effb5;
font-weight: 400;
}
.transcript-block {
background: #0a1222;
border-radius: 1.2rem;
padding: 12px;
margin-top: 12px;
}
.quote {
font-style: italic;
color: #eef5ff;
font-size: 0.9rem;
}
hr {
border-color: #0ff22a30;
margin: 10px 0;
}
input.volume-slider {
width: 100%;
accent-color: #0ff;
}
/* RMS Meter */
.rms-bar {
height: 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.rms-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88 0%, #ffc107 70%, #ff4757 100%);
transition: width 0.1s ease;
border-radius: 4px;
}
@media (max-width: 1000px) {
.dashboard { grid-template-columns: 1fr; gap: 1.5rem; }
.glass-panel { padding: 1.5rem; }
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.microphone-btn {
background: #ff20;
border-radius: 60px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.6rem;
cursor: pointer;
border: 1px solid cyan;
transition: 0.1s;
}
.microphone-btn:active { transform: scale(0.96);}
</style>
</head>
<body>
<div class="glass-panel">
<!-- заголовок -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<div style="font-family: 'Orbitron'; font-weight: 700; font-size: 1.6rem; letter-spacing: 2px; background: linear-gradient(135deg, #aaffff, #3b8eff); -webkit-background-clip: text; background-clip: text; color: transparent;">
⚡ Wyoming Droid Assistant ⚡
</div>
<div class="connection-status">
<div class="led" id="statusLed"></div>
<div class="status-text" id="statusLabel">disconnected</div>
<button id="connectBtn" class="neo-btn" style="margin-left: 12px;">⟳ CONNECT</button>
</div>
</div>
<div class="dashboard">
<!-- ЛЕВАЯ КОЛОНКА: Аватар + Аудиовизуализация и Состояние -->
<div class="card avatar-area">
<div class="card-header">
<span>🎙️ NEURAL CORE</span>
</div>
<div class="orb" id="orbCore">
🔊
</div>
<div class="state-label" id="stateText">IDLE</div>
<div class="viz-wrapper">
<canvas id="audioCanvas" width="400" height="60" style="width:100%; height: 70px"></canvas>
</div>
<!-- RMS Level Meter -->
<div style="margin-top: 15px;">
<div class="flex-between" style="margin-bottom: 5px;">
<span style="color: #aaa; font-size: 12px;">📊 RMS LEVEL</span>
<span id="rmsValue" style="color:#0ff; font-family: monospace; font-size: 14px;">0.00</span>
</div>
<div class="rms-bar">
<div class="rms-fill" id="rmsFill" style="width: 0%;"></div>
</div>
</div>
<div class="flex-between" style="width:100%; margin-top: 8px;">
<span>🔊 VOLUME</span>
<span id="volumeValue" style="color:#0ff">0.80</span>
</div>
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="0.8" class="volume-slider">
<div class="command-group" style="justify-content: center;">
<button id="muteBtn" class="neo-btn">🔇 MUTE</button>
<button id="unmuteBtn" class="neo-btn">🎤 UNMUTE</button>
</div>
</div>
<!-- ЦЕНТР: команды и транскрипция -->
<div class="card">
<div class="card-header">
<span>🕹️ УПРАВЛЕНИЕ • ЖЕСТЫ</span>
</div>
<div class="command-group">
<button id="startListeningBtn" class="neo-btn">🎧 START LISTEN</button>
<button id="stopPipelineBtn" class="neo-btn danger-btn">⏹️ STOP PIPELINE</button>
<button id="stopTimerBtn" class="neo-btn">⏲️ STOP TIMER</button>
</div>
<div class="command-group">
<button id="volUpBtn" class="neo-btn">➕ VOL+</button>
<button id="volDownBtn" class="neo-btn">➖ VOL-</button>
<button id="requestRmsBtn" class="neo-btn">📊 REQUEST RMS</button>
</div>
<div class="command-group">
<button id="singlePressBtn" class="neo-btn">🔘 SINGLE</button>
<button id="doublePressBtn" class="neo-btn">🔘🔘 DOUBLE</button>
<button id="triplePressBtn" class="neo-btn">🔘🔘🔘 TRIPLE</button>
<button id="longPressBtn" class="neo-btn">⏺️ LONG</button>
</div>
<hr>
<div>
<div class="card-header" style="margin-bottom: 8px;">🗣️ ПОСЛЕДНИЕ ФРАЗЫ</div>
<div class="transcript-block">
<div style="color:#88ccff;">YOU SAID:</div>
<div id="lastStt" class="quote">—</div>
<div style="color:#88ccff; margin-top: 10px;">ASSISTANT SAID:</div>
<div id="lastTts" class="quote">—</div>
</div>
</div>
</div>
<!-- ПРАВАЯ КОЛОНКА: ЛОГИ + HA СТАТУС -->
<div class="card">
<div class="card-header">
<span>📡 ТЕЛЕМЕТРИЯ & СОБЫТИЯ</span>
</div>
<div style="display: flex; gap: 16px; margin-bottom: 18px;">
<div><span style="color:#aaa;">🔊 MUTED: </span><strong id="mutedStatus">—</strong></div>
<div><span style="color:#aaa;">🏠 HA: </span><strong id="haStatus">—</strong></div>
<div><span style="color:#aaa;">🎚️ VOL: </span><strong id="liveVol">—</strong></div>
</div>
<div class="event-feed" id="eventFeed">
<div class="event-item"><span class="event-time">⬤</span> <span>готов к подключению...</span></div>
</div>
<button id="clearEvents" class="neo-btn" style="margin-top: 14px; width: 100%;">🧹 ОЧИСТИТЬ ЛОГ</button>
</div>
</div>
</div>
<script>
// ------------------ WEBSOCKET & STATE ------------------
let ws = null;
let isConnected = false;
let currentVolume = 0.8;
let mutedState = false;
let haConnected = false;
let animationFrame = null;
let audioCtx = null;
let analyser = null;
let source = null;
let mediaStream = null;
let rmsTimeout = null;
const canvas = document.getElementById('audioCanvas');
const ctx = canvas.getContext('2d');
let width = canvas.parentElement.clientWidth - 24;
let height = 70;
canvas.width = width;
canvas.height = height;
// DOM элементы
const statusLed = document.getElementById('statusLed');
const statusLabel = document.getElementById('statusLabel');
const connectBtn = document.getElementById('connectBtn');
const orb = document.getElementById('orbCore');
const stateText = document.getElementById('stateText');
const volumeSlider = document.getElementById('volumeSlider');
const volumeValueSpan = document.getElementById('volumeValue');
const lastSttSpan = document.getElementById('lastStt');
const lastTtsSpan = document.getElementById('lastTts');
const mutedStatusSpan = document.getElementById('mutedStatus');
const haStatusSpan = document.getElementById('haStatus');
const liveVolSpan = document.getElementById('liveVol');
const eventFeedDiv = document.getElementById('eventFeed');
const rmsFill = document.getElementById('rmsFill');
const rmsValue = document.getElementById('rmsValue');
function addEventToLog(message, type = 'info') {
const time = new Date().toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
const div = document.createElement('div');
div.className = 'event-item';
div.innerHTML = `<span class="event-time">${time}</span> <span>${message}</span>`;
eventFeedDiv.prepend(div);
if(eventFeedDiv.children.length > 45) eventFeedDiv.removeChild(eventFeedDiv.lastChild);
}
function updateUIFromSnapshot(data) {
if(data.volume !== undefined) {
currentVolume = data.volume;
volumeSlider.value = currentVolume;
volumeValueSpan.innerText = currentVolume.toFixed(2);
liveVolSpan.innerText = Math.round(currentVolume*100)+'%';
}
if(data.muted !== undefined) {
mutedState = data.muted;
mutedStatusSpan.innerText = mutedState ? '✅ muted' : '❌ unmuted';
}
if(data.ha_connected !== undefined) {
haConnected = data.ha_connected;
haStatusSpan.innerText = haConnected ? '⚡ connected' : '⚠️ offline';
}
if(data.last_stt_text) {
lastSttSpan.innerText = data.last_stt_text.substring(0, 100);
}
if(data.last_tts_text) {
lastTtsSpan.innerText = data.last_tts_text.substring(0, 100);
}
}
// Update RMS meter
function updateRmsMeter(level) {
if (!rmsFill || !rmsValue) return; // Проверка на null
const percent = Math.min(100, Math.max(0, level * 100));
rmsFill.style.width = percent + '%';
rmsValue.textContent = level.toFixed(2);
// Reset after 1 second of no audio
clearTimeout(rmsTimeout);
rmsTimeout = setTimeout(() => {
if (rmsFill) rmsFill.style.width = '0%';
if (rmsValue) rmsValue.textContent = '0.00';
}, 1000);
}
function sendCommand(command, payload = {}) {
if(!ws || ws.readyState !== WebSocket.OPEN) {
addEventToLog(`❌ Не удалось отправить: ${command} — нет соединения`);
return;
}
const msg = JSON.stringify({ command, data: payload });
ws.send(msg);
addEventToLog(`📤 cmd: ${command}`);
}
// Обработчик входящих событий от satellite
function handleIncomingMessage(ev) {
try {
const json = JSON.parse(ev.data);
const eventType = json.type || json.event;
// Audio level events
if(eventType === 'audio-level') {
if(json.data && json.data.level !== undefined) {
const level = json.data.level;
currentRms = level; // ✅ Обновляем currentRms для визуализации
updateRmsMeter(level);
}
}
// Voice events
else if(eventType === 'voice-started') {
stateText.innerText = '🎙️ LISTENING...';
document.body.classList.add('listening-active');
addEventToLog('🎤 Голос обнаружен');
}
else if(eventType === 'voice-stopped') {
stateText.innerText = '🧠 PROCESSING';
document.body.classList.remove('listening-active');
addEventToLog('💭 Обработка речи...');
}
// 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(eventType === 'audio-stop' || eventType === 'played') {
if(stateText.innerText === '🔊 SPEAKING') stateText.innerText = 'IDLE';
addEventToLog('🔇 TTS завершён');
}
// Timer events
else if(eventType === 'timer-started') {
addEventToLog('⏱️ Таймер запущен');
}
else if(eventType === 'timer-finished') {
addEventToLog('⏰ ТАЙМЕР СРАБОТАЛ!');
stateText.innerText = '⏰ TIMER';
}
// State events
else if(eventType === 'streaming-started') {
stateText.innerText = '🎙️ STREAMING';
addEventToLog('🔄 Стриминг начался');
}
else if(eventType === 'streaming-stopped') {
if(stateText.innerText === '🎙️ STREAMING') stateText.innerText = 'IDLE';
addEventToLog('⏹️ Стриминг остановлен');
}
// 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(`📡 событие: ${eventType}`);
}
// 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';
}
}
} catch(e) {
console.warn('Parse error:', e);
addEventToLog(`⚠️ Ошибка парсинга: ${e.message}`);
}
}
// Визуализация на canvas — RMS от satellite с оригинальной анимацией
let satelliteRmsHistory = new Array(42).fill(0);
let rmsSmoothed = 0;
function drawSatelliteVisualization() {
requestAnimationFrame(drawSatelliteVisualization);
if(!isConnected) {
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#0a1222';
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#0ff3';
ctx.font = '10px monospace';
ctx.fillText('⚡ idle', canvas.width-50, 20);
return;
}
// Масштабирование RMS: 0 → 10%, 0.15 → 100%
// Формула: avg = 10 + (RMS / 0.15) * 90
let avg = 10 + (currentRms / 0.15) * 90;
avg = Math.min(100, Math.max(10, avg)); // Clamp 10-100
// Сглаживание как в оригинале
rmsSmoothed = rmsSmoothed * 0.7 + avg * 0.3;
avg = rmsSmoothed;
// Если satellite RMS недоступен (совсем тихо) – симуляция
if(currentRms < 0.001) {
avg = 10 + Math.random() * 5; // Маленькие колебания на базовом уровне
}
// Отрисовка 42 неоновых столбцов (баров)
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#03151f';
ctx.fillRect(0,0,canvas.width, canvas.height);
const barWidth = canvas.width / 42;
const numBars = 42;
for(let i=0; i<numBars; i++) {
let barHeight = (avg * (Math.sin(i*0.5)+1)/2 + 5) * (0.5 + Math.random()*0.3);
// Увеличиваем высоту если громко (как в оригинале)
if(stateText.innerText.includes('SPEAK'))
barHeight = avg * 1.6 * (Math.sin(i*0.3)+1.2);
barHeight = Math.min(barHeight, canvas.height-10);
const x = i * (barWidth+2);
// Градиент как в оригинале
const grad = ctx.createLinearGradient(x, canvas.height-barHeight, x, canvas.height);
grad.addColorStop(0, '#0ff');
grad.addColorStop(1, '#3f87ff');
ctx.fillStyle = grad;
ctx.fillRect(x, canvas.height-barHeight, barWidth, barHeight);
}
}
drawSatelliteVisualization();
// Подключение микрофона для визуализации (опционально) — УДАЛЕНО
// Теперь используем RMS от satellite вместо локального микрофона
// WebSocket подключение
function connectWebSocket() {
const hostInput = prompt("Введите адрес LVA WebSocket (по умолчанию ws://192.168.200.111:8675):", "ws://192.168.200.111:8675");
if(!hostInput) return;
if(ws && ws.readyState === WebSocket.OPEN) ws.close();
ws = new WebSocket(hostInput);
ws.onopen = () => {
isConnected = true;
statusLed.classList.add('connected');
statusLabel.innerText = 'connected';
addEventToLog(`✅ Соединение установлено с ${hostInput}`);
stateText.innerText = 'ONLINE';
orb.style.animation = 'pulse-glow 0.8s';
setTimeout(()=> orb.style.animation='',700);
// initMicVisualizer() — удалено, используем satellite RMS
};
ws.onmessage = handleIncomingMessage;
ws.onerror = (err) => { addEventToLog(`⚠️ WebSocket ошибка`); };
ws.onclose = () => {
isConnected = false;
statusLed.classList.remove('connected');
statusLabel.innerText = 'disconnected';
addEventToLog(`🔌 Отключено`);
stateText.innerText = 'OFFLINE';
document.body.classList.remove('listening-active');
};
}
// Кнопки управления
document.getElementById('connectBtn').addEventListener('click', connectWebSocket);
// 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);
currentVolume = newVol; // ✅ Обновляем currentVolume
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);
currentVolume = newVol; // ✅ Обновляем currentVolume
sendCommand('set_volume', { level: newVol });
volumeSlider.value = newVol;
volumeValueSpan.innerText = newVol.toFixed(2);
});
// Request RMS button
document.getElementById('requestRmsBtn').addEventListener('click', () => {
// Запросить обновление audio-level (сателит отправляет их автоматически)
// Отправляем dummy команду для триггера
sendCommand('trigger_event', { event_type: 'request_audio_level', data: {} });
addEventToLog('📊 Запрошено обновление RMS');
});
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', { level: val });
});
// автоподключение при загрузке (опционально)
window.addEventListener('load', () => {
addEventToLog('🤖 Интерфейс LVA NEXUS • готов к подключению');
// Инициализация RMS элементов после загрузки DOM
const rmsFillEl = document.getElementById('rmsFill');
const rmsValueEl = document.getElementById('rmsValue');
if (rmsFillEl && rmsValueEl) {
console.log('RMS meter initialized');
}
});
</script>
</body>
</html>