feat: модернизировать WebSocket клиент с UI управлением
Новые возможности client.html: - 🔊 Управление громкостью (слайдер 0-100%) - 🔇/🔊 Кнопки Mute/Unmute - 🔔 Тест звука (play_sound команда) - 💡 LED управление (цвет, яркость) - 📊 RMS Audio Level визуализация (реальное время) - 🎨 Современный UI (карточки, градиенты, анимации) - 📱 Адаптивный дизайн start_web_server.py: - Простой HTTP сервер для тестирования - Авто-определение WebSocket URL - Красивый вывод в консоль Запуск: cd examples/websocket-service ./start_web_server.py --port 8080 Открыть: http://localhost:8080/client.html
This commit is contained in:
@@ -1,103 +1,491 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wyoming Satellite WebSocket</title>
|
||||
<title>Wyoming Satellite Control</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 20px;
|
||||
background: #1a1a2e;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #eee;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { color: #00d9ff; margin-bottom: 10px; }
|
||||
#status {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#status.connected { background: #2d5a3d; color: #7fff7f; }
|
||||
#status.disconnected { background: #5a2d2d; color: #ff7f7f; }
|
||||
#status.connecting { background: #5a5a2d; color: #ffff7f; }
|
||||
#log {
|
||||
background: #0f0f1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.event {
|
||||
border-left: 3px solid #00d9ff;
|
||||
padding-left: 10px;
|
||||
margin: 10px 0;
|
||||
background: rgba(0, 217, 255, 0.05);
|
||||
padding: 10px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.event-type { color: #00d9ff; font-weight: bold; }
|
||||
.event-data { color: #aaa; }
|
||||
.clear-btn {
|
||||
background: #00d9ff;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
|
||||
.status {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status.connected { background: rgba(0, 255, 136, 0.2); color: #00ff88; }
|
||||
.status.connected::before { background: #00ff88; }
|
||||
.status.disconnected { background: rgba(255, 71, 87, 0.2); color: #ff4757; }
|
||||
.status.disconnected::before { background: #ff4757; animation: none; }
|
||||
.status.connecting { background: rgba(255, 193, 7, 0.2); color: #ffc107; }
|
||||
.status.connecting::before { background: #ffc107; }
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: #00d9ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.control-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Volume Slider */
|
||||
.volume-slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #00d9ff 0%, #00ff88 100%);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.volume-value {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #00d9ff;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(90deg, #ff4757, #ff6b81);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(90deg, #00ff88, #00d9ff);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(90deg, #ffc107, #ff9800);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* RMS Meter */
|
||||
.rms-meter {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.rms-bar {
|
||||
height: 30px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
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;
|
||||
}
|
||||
|
||||
.rms-value {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.rms-label {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Event Log */
|
||||
.log-container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #00d9ff;
|
||||
background: rgba(0, 217, 255, 0.05);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry.audio-level {
|
||||
border-left-color: #00ff88;
|
||||
background: rgba(0, 255, 136, 0.05);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
border-left-color: #ff4757;
|
||||
background: rgba(255, 71, 87, 0.05);
|
||||
}
|
||||
|
||||
.log-entry.system {
|
||||
border-left-color: #ffc107;
|
||||
background: rgba(255, 193, 7, 0.05);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
color: #00d9ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-data {
|
||||
color: #aaa;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.clear-btn:hover { background: #00b8d9; }
|
||||
.header { display: flex; align-items: center; justify-content: space-between; }
|
||||
|
||||
/* Settings */
|
||||
.settings-panel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-panel.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-panel input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: #eee;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-panel input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #00d9ff;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #aaa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>🛰️ Wyoming Satellite WebSocket</h1>
|
||||
<div id="status" class="connecting">Connecting...</div>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>🛰️ Wyoming Satellite Control</h1>
|
||||
<div style="color: #aaa; font-size: 14px;">WebSocket Client & Audio Level Monitor</div>
|
||||
</div>
|
||||
<div class="status" id="status">
|
||||
Connecting...
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="clear-btn" onclick="toggleSettings()">⚙️ Settings</button>
|
||||
<button class="clear-btn" onclick="clearLog()">Clear Log</button>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="settings-panel" id="settingsPanel">
|
||||
<label style="color: #00d9ff; font-weight: 600; display: block; margin-bottom: 8px;">
|
||||
WebSocket URL:
|
||||
</label>
|
||||
<input type="text" id="wsUrl" value="ws://localhost:8675">
|
||||
<div style="display: flex; gap: 10px; align-items: center; margin-top: 10px;">
|
||||
<button class="btn btn-primary" onclick="connect()">
|
||||
🔌 Connect
|
||||
</button>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="autoReconnect" checked>
|
||||
Auto-reconnect
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Control Grid -->
|
||||
<div class="grid">
|
||||
<!-- Volume Control -->
|
||||
<div class="card">
|
||||
<h2>🔊 Volume Control</h2>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Master Volume</label>
|
||||
<input type="range" class="volume-slider" id="volumeSlider"
|
||||
min="0" max="100" value="100" oninput="updateVolume(this.value)">
|
||||
<div class="volume-value" id="volumeValue">100%</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-danger" onclick="sendCommand('mute', {})">
|
||||
🔇 Mute
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="sendCommand('unmute', {})">
|
||||
🔊 Unmute
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RMS Audio Level -->
|
||||
<div class="card">
|
||||
<h2>📊 RMS Audio Level</h2>
|
||||
|
||||
<div class="rms-meter">
|
||||
<div class="rms-bar">
|
||||
<div class="rms-fill" id="rmsFill" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="rms-value" id="rmsValue">0.00</div>
|
||||
<div class="rms-label">Real-time microphone level</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-warning" onclick="sendCommand('play_sound', {path: '/home/sirmrak/projects/satellite-dev/sounds/awake.wav'})">
|
||||
🔔 Test Sound
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LED Control -->
|
||||
<div class="card">
|
||||
<h2>💡 LED Control</h2>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Brightness</label>
|
||||
<input type="range" class="volume-slider" id="ledBrightness"
|
||||
min="0" max="255" value="128" oninput="updateLedBrightness(this.value)">
|
||||
<div class="volume-value" id="ledBrightnessValue">128</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="sendCommand('set_led_color', {r: 0, g: 188, b: 212})">
|
||||
🔵 Cyan
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="sendCommand('set_led_color', {r: 255, g: 0, b: 0})">
|
||||
🔴 Red
|
||||
</button>
|
||||
<button class="btn btn-success" onclick="sendCommand('set_led_color', {r: 0, g: 255, b: 0})">
|
||||
🟢 Green
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Log -->
|
||||
<div class="card">
|
||||
<h2>📋 Event Log</h2>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||
<button class="btn btn-primary" onclick="clearLog()">
|
||||
🗑️ Clear Log
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="toggleSettings()">
|
||||
⚙️ Settings
|
||||
</button>
|
||||
<label class="checkbox-label" style="margin-left: auto;">
|
||||
<input type="checkbox" id="autoScroll" checked>
|
||||
Auto-scroll
|
||||
</label>
|
||||
</div>
|
||||
<div class="log-container" id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings" style="display:none; background:#0f0f1a; padding:15px; border-radius:4px; margin-bottom:15px;">
|
||||
<label for="wsUrl" style="color:#00d9ff; font-weight:bold;">WebSocket URL:</label>
|
||||
<input type="text" id="wsUrl" value="ws://localhost:8675"
|
||||
style="width:100%; padding:8px; margin:5px 0; background:#1a1a2e; color:#eee; border:1px solid #333; border-radius:4px;">
|
||||
<button class="clear-btn" onclick="connect()">Connect</button>
|
||||
<label style="margin-left:10px; color:#aaa;">
|
||||
<input type="checkbox" id="autoReconnect" checked> Auto-reconnect
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let ws = null;
|
||||
let eventCount = 0;
|
||||
let currentRms = 0;
|
||||
let rmsTimeout = null;
|
||||
|
||||
// Elements
|
||||
const log = document.getElementById('log');
|
||||
const status = document.getElementById('status');
|
||||
const settingsPanel = document.getElementById('settings');
|
||||
const settingsPanel = document.getElementById('settingsPanel');
|
||||
const wsUrlInput = document.getElementById('wsUrl');
|
||||
const autoReconnectCheckbox = document.getElementById('autoReconnect');
|
||||
let eventCount = 0;
|
||||
let ws = null;
|
||||
let autoReconnect = true;
|
||||
const rmsFill = document.getElementById('rmsFill');
|
||||
const rmsValue = document.getElementById('rmsValue');
|
||||
|
||||
function toggleSettings() {
|
||||
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
// Auto-detect WebSocket URL
|
||||
window.addEventListener('load', () => {
|
||||
const detectedUrl = `ws://${window.location.hostname}:8675`;
|
||||
wsUrlInput.value = detectedUrl;
|
||||
connect();
|
||||
});
|
||||
|
||||
// Connect to WebSocket
|
||||
function connect() {
|
||||
const wsUrl = wsUrlInput.value.trim();
|
||||
|
||||
if (!wsUrl) {
|
||||
addEvent('SYSTEM', { error: 'Please enter a WebSocket URL' });
|
||||
return;
|
||||
@@ -108,28 +496,34 @@
|
||||
ws.close();
|
||||
}
|
||||
|
||||
setStatus('connecting', 'Connecting...');
|
||||
addEvent('SYSTEM', { status: `Connecting to ${wsUrl}...` });
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
status.textContent = 'Connected to ' + wsUrl;
|
||||
status.className = 'connected';
|
||||
addEvent('SYSTEM', { status: 'Connected to ' + wsUrl });
|
||||
setStatus('connected', `Connected`);
|
||||
addEvent('SYSTEM', { status: `Connected to ${wsUrl}` });
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
addEvent(data.type, data.data);
|
||||
|
||||
// Handle audio-level events
|
||||
if (data.type === 'audio-level' && data.data && data.data.level !== undefined) {
|
||||
updateRmsMeter(data.data.level);
|
||||
}
|
||||
} catch (e) {
|
||||
addEvent('RAW', { raw: event.data });
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
status.textContent = 'Disconnected';
|
||||
status.className = 'disconnected';
|
||||
setStatus('disconnected', 'Disconnected');
|
||||
addEvent('SYSTEM', { status: 'Connection closed' });
|
||||
|
||||
|
||||
// Auto-reconnect
|
||||
if (autoReconnectCheckbox.checked) {
|
||||
setTimeout(() => {
|
||||
@@ -140,39 +534,117 @@
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
status.textContent = 'Error';
|
||||
status.className = 'disconnected';
|
||||
setStatus('disconnected', 'Error');
|
||||
addEvent('SYSTEM', { error: 'WebSocket error', details: err.toString() });
|
||||
};
|
||||
}
|
||||
|
||||
// Auto-detect WebSocket URL from current page location on first load
|
||||
window.addEventListener('load', () => {
|
||||
const detectedUrl = `ws://${window.location.hostname}:8675`;
|
||||
wsUrlInput.value = detectedUrl;
|
||||
connect();
|
||||
// Set status
|
||||
function setStatus(state, text) {
|
||||
status.className = `status ${state}`;
|
||||
status.textContent = text;
|
||||
}
|
||||
|
||||
// Send command
|
||||
function sendCommand(command, data) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addEvent('SYSTEM', { error: 'Not connected to WebSocket server' });
|
||||
return;
|
||||
}
|
||||
|
||||
const message = { command, data };
|
||||
ws.send(JSON.stringify(message));
|
||||
addEvent('COMMAND', { command, data });
|
||||
}
|
||||
|
||||
// Update volume
|
||||
function updateVolume(value) {
|
||||
document.getElementById('volumeValue').textContent = `${value}%`;
|
||||
}
|
||||
|
||||
// Send volume command
|
||||
document.getElementById('volumeSlider').addEventListener('change', function() {
|
||||
const level = parseInt(this.value) / 100;
|
||||
sendCommand('set_volume', { level });
|
||||
});
|
||||
|
||||
// Update LED brightness
|
||||
function updateLedBrightness(value) {
|
||||
document.getElementById('ledBrightnessValue').textContent = value;
|
||||
}
|
||||
|
||||
// Send LED brightness command
|
||||
document.getElementById('ledBrightness').addEventListener('change', function() {
|
||||
const brightness = parseInt(this.value);
|
||||
sendCommand('set_led_brightness', { brightness });
|
||||
});
|
||||
|
||||
// Update RMS meter
|
||||
function updateRmsMeter(level) {
|
||||
currentRms = level;
|
||||
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(() => {
|
||||
currentRms = 0;
|
||||
rmsFill.style.width = '0%';
|
||||
rmsValue.textContent = '0.00';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Toggle settings
|
||||
function toggleSettings() {
|
||||
settingsPanel.classList.toggle('show');
|
||||
}
|
||||
|
||||
// Add event to log
|
||||
function addEvent(type, data) {
|
||||
eventCount++;
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'event';
|
||||
div.innerHTML = `<span class="event-type">[${time}] #${eventCount} ${escapeHtml(type)}</span>\n<span class="event-data">${escapeHtml(JSON.stringify(data, null, 2))}</span>`;
|
||||
div.className = `log-entry ${type.toLowerCase().replace('_', '-')}`;
|
||||
|
||||
let dataStr = '';
|
||||
if (data.error) {
|
||||
dataStr = `<span style="color: #ff4757;">${escapeHtml(data.error)}</span>`;
|
||||
} else if (data.status) {
|
||||
dataStr = `<span style="color: #00ff88;">${escapeHtml(data.status)}</span>`;
|
||||
} else if (data.command) {
|
||||
dataStr = `<span style="color: #ffc107;">→ ${escapeHtml(data.command)}</span> ${escapeHtml(JSON.stringify(data.data))}`;
|
||||
} else {
|
||||
dataStr = escapeHtml(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
div.innerHTML = `
|
||||
<span class="log-time">[${time}]</span>
|
||||
<span class="log-type">#${eventCount} ${escapeHtml(type)}</span>
|
||||
<span class="log-data">${dataStr}</span>
|
||||
`;
|
||||
|
||||
log.appendChild(div);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
// Auto-scroll
|
||||
if (document.getElementById('autoScroll').checked) {
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear log
|
||||
function clearLog() {
|
||||
log.innerHTML = '';
|
||||
eventCount = 0;
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
log.innerHTML = '';
|
||||
eventCount = 0;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple HTTP server for WebSocket client testing.
|
||||
|
||||
Usage:
|
||||
python3 start_web_server.py [--port PORT]
|
||||
|
||||
Open in browser:
|
||||
http://localhost:PORT/client.html
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import http.server
|
||||
import socketserver
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
PORT = 8080
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
|
||||
|
||||
class CustomHandler(http.server.SimpleHTTPRequestHandler):
|
||||
"""Custom handler with better logging."""
|
||||
|
||||
def do_GET(self):
|
||||
super().do_GET()
|
||||
self.log_message(f"✅ Served: {self.path}")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"[WebServer] {args[0]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='WebSocket Client Web Server')
|
||||
parser.add_argument('--port', type=int, default=PORT, help=f'Port (default: {PORT})')
|
||||
parser.add_argument('--host', default='0.0.0.0', help='Host (default: 0.0.0.0)')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Change to websocket-service directory
|
||||
os.chdir(SCRIPT_DIR)
|
||||
|
||||
print(f"""
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ 🛰️ Wyoming Satellite WebSocket Client Server ║
|
||||
╠══════════════════════════════════════════════════════════╣
|
||||
║ Serving: {str(SCRIPT_DIR):<45} ║
|
||||
║ URL: http://localhost:{args.port:<36} ║
|
||||
║ ║
|
||||
║ Press Ctrl+C to stop ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
with socketserver.TCPServer((args.host, args.port), CustomHandler) as httpd:
|
||||
try:
|
||||
print(f"🚀 Server started on http://{args.host}:{args.port}")
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Shutting down...")
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user