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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Wyoming Satellite WebSocket</title>
|
<title>Wyoming Satellite Control</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
padding: 20px;
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
background: #1a1a2e;
|
|
||||||
color: #eee;
|
color: #eee;
|
||||||
margin: 0;
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
h1 { color: #00d9ff; margin-bottom: 10px; }
|
|
||||||
#status {
|
.container {
|
||||||
padding: 8px 12px;
|
max-width: 1200px;
|
||||||
border-radius: 4px;
|
margin: 0 auto;
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
#status.connected { background: #2d5a3d; color: #7fff7f; }
|
|
||||||
#status.disconnected { background: #5a2d2d; color: #ff7f7f; }
|
/* Header */
|
||||||
#status.connecting { background: #5a5a2d; color: #ffff7f; }
|
.header {
|
||||||
#log {
|
display: flex;
|
||||||
background: #0f0f1a;
|
justify-content: space-between;
|
||||||
border: 1px solid #333;
|
align-items: center;
|
||||||
border-radius: 4px;
|
margin-bottom: 20px;
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
height: calc(100vh - 180px);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
overflow-y: auto;
|
border-radius: 12px;
|
||||||
white-space: pre-wrap;
|
backdrop-filter: blur(10px);
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
.event {
|
|
||||||
border-left: 3px solid #00d9ff;
|
.header h1 {
|
||||||
padding-left: 10px;
|
font-size: 24px;
|
||||||
margin: 10px 0;
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
background: rgba(0, 217, 255, 0.05);
|
-webkit-background-clip: text;
|
||||||
padding: 10px;
|
-webkit-text-fill-color: transparent;
|
||||||
border-radius: 0 4px 4px 0;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.event-type { color: #00d9ff; font-weight: bold; }
|
|
||||||
.event-data { color: #aaa; }
|
.status {
|
||||||
.clear-btn {
|
|
||||||
background: #00d9ff;
|
|
||||||
color: #1a1a2e;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
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;
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(90deg, #00d9ff 0%, #00ff88 100%);
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
cursor: pointer;
|
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;
|
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;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="container">
|
||||||
<div>
|
<!-- Header -->
|
||||||
<h1>🛰️ Wyoming Satellite WebSocket</h1>
|
<div class="header">
|
||||||
<div id="status" class="connecting">Connecting...</div>
|
<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>
|
||||||
<div>
|
|
||||||
<button class="clear-btn" onclick="toggleSettings()">⚙️ Settings</button>
|
<!-- Settings -->
|
||||||
<button class="clear-btn" onclick="clearLog()">Clear Log</button>
|
<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>
|
</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>
|
<script>
|
||||||
|
// State
|
||||||
|
let ws = null;
|
||||||
|
let eventCount = 0;
|
||||||
|
let currentRms = 0;
|
||||||
|
let rmsTimeout = null;
|
||||||
|
|
||||||
|
// Elements
|
||||||
const log = document.getElementById('log');
|
const log = document.getElementById('log');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const settingsPanel = document.getElementById('settings');
|
const settingsPanel = document.getElementById('settingsPanel');
|
||||||
const wsUrlInput = document.getElementById('wsUrl');
|
const wsUrlInput = document.getElementById('wsUrl');
|
||||||
const autoReconnectCheckbox = document.getElementById('autoReconnect');
|
const autoReconnectCheckbox = document.getElementById('autoReconnect');
|
||||||
let eventCount = 0;
|
const rmsFill = document.getElementById('rmsFill');
|
||||||
let ws = null;
|
const rmsValue = document.getElementById('rmsValue');
|
||||||
let autoReconnect = true;
|
|
||||||
|
|
||||||
function toggleSettings() {
|
// Auto-detect WebSocket URL
|
||||||
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
|
window.addEventListener('load', () => {
|
||||||
}
|
const detectedUrl = `ws://${window.location.hostname}:8675`;
|
||||||
|
wsUrlInput.value = detectedUrl;
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
function connect() {
|
function connect() {
|
||||||
const wsUrl = wsUrlInput.value.trim();
|
const wsUrl = wsUrlInput.value.trim();
|
||||||
|
|
||||||
if (!wsUrl) {
|
if (!wsUrl) {
|
||||||
addEvent('SYSTEM', { error: 'Please enter a WebSocket URL' });
|
addEvent('SYSTEM', { error: 'Please enter a WebSocket URL' });
|
||||||
return;
|
return;
|
||||||
@@ -108,26 +496,32 @@
|
|||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatus('connecting', 'Connecting...');
|
||||||
|
addEvent('SYSTEM', { status: `Connecting to ${wsUrl}...` });
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
status.textContent = 'Connected to ' + wsUrl;
|
setStatus('connected', `Connected`);
|
||||||
status.className = 'connected';
|
addEvent('SYSTEM', { status: `Connected to ${wsUrl}` });
|
||||||
addEvent('SYSTEM', { status: 'Connected to ' + wsUrl });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
addEvent(data.type, data.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) {
|
} catch (e) {
|
||||||
addEvent('RAW', { raw: event.data });
|
addEvent('RAW', { raw: event.data });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
status.textContent = 'Disconnected';
|
setStatus('disconnected', 'Disconnected');
|
||||||
status.className = 'disconnected';
|
|
||||||
addEvent('SYSTEM', { status: 'Connection closed' });
|
addEvent('SYSTEM', { status: 'Connection closed' });
|
||||||
|
|
||||||
// Auto-reconnect
|
// Auto-reconnect
|
||||||
@@ -140,39 +534,117 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
ws.onerror = (err) => {
|
||||||
status.textContent = 'Error';
|
setStatus('disconnected', 'Error');
|
||||||
status.className = 'disconnected';
|
|
||||||
addEvent('SYSTEM', { error: 'WebSocket error', details: err.toString() });
|
addEvent('SYSTEM', { error: 'WebSocket error', details: err.toString() });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-detect WebSocket URL from current page location on first load
|
// Set status
|
||||||
window.addEventListener('load', () => {
|
function setStatus(state, text) {
|
||||||
const detectedUrl = `ws://${window.location.hostname}:8675`;
|
status.className = `status ${state}`;
|
||||||
wsUrlInput.value = detectedUrl;
|
status.textContent = text;
|
||||||
connect();
|
}
|
||||||
|
|
||||||
|
// 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) {
|
function addEvent(type, data) {
|
||||||
eventCount++;
|
eventCount++;
|
||||||
const time = new Date().toLocaleTimeString();
|
const time = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'event';
|
div.className = `log-entry ${type.toLowerCase().replace('_', '-')}`;
|
||||||
div.innerHTML = `<span class="event-type">[${time}] #${eventCount} ${escapeHtml(type)}</span>\n<span class="event-data">${escapeHtml(JSON.stringify(data, null, 2))}</span>`;
|
|
||||||
|
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.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) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLog() {
|
|
||||||
log.innerHTML = '';
|
|
||||||
eventCount = 0;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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