Add Network + Bluetooth config in Web UI

- src/system/network.py: WiFi scan/connect/hotspot via nmcli + iwlist fallback
- src/system/bluetooth.py: BT scan/pair/connect/MIDI via bluetoothctl
- REST endpoints: /api/wifi/* and /api/bluetooth/* (16 endpoints)
- Web UI: tabbed settings with Network + Bluetooth panels
- network.js: WiFi scan, connect modal, saved nets, hotspot
- bluetooth.js: BT scan, pair/unpair, connect, MIDI service
- style.css: tabs, network list, BT device list, signal indicators
- scripts/setup-bt-midi.sh: BT MIDI systemd bridge service
- tests/test_network.py: 22 tests (nmcli/iwlist, fallback, hotspot)
- tests/test_bt.py: 24 tests (status, scan, pair, MIDI, edge cases)
- _gather_state includes wifi + bluetooth sub-objects
- All 93 tests pass
This commit is contained in:
2026-06-09 01:14:41 -04:00
parent c73b1ffa6c
commit b4237f2f1d
10 changed files with 3305 additions and 186 deletions
+295 -80
View File
@@ -6,103 +6,318 @@
<h1>Settings</h1>
</div>
<div class="card">
<div class="card-header">
<h2>Connection</h2>
</div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Hostname</div>
<div class="setting-value"><code id="hostname">pedal.local</code></div>
</div>
<div class="setting-row">
<div class="setting-label">Web Server</div>
<div class="setting-value"><code id="web-url">http://pedal.local:8080</code></div>
</div>
<div class="setting-row">
<div class="setting-label">Connected Clients</div>
<div class="setting-value"><span id="ws-count">0</span></div>
</div>
</div>
<!-- Tab navigation -->
<div class="settings-tabs">
<button class="settings-tab active" data-tab="connection" onclick="switchTab('connection')">Connection</button>
<button class="settings-tab" data-tab="audio" onclick="switchTab('audio')">Audio</button>
<button class="settings-tab" data-tab="routing" onclick="switchTab('routing')">4CM Routing</button>
<button class="settings-tab" data-tab="network" onclick="switchTab('network')">Network</button>
<button class="settings-tab" data-tab="bluetooth" onclick="switchTab('bluetooth')">Bluetooth</button>
<button class="settings-tab" data-tab="about" onclick="switchTab('about')">About</button>
</div>
<div class="card">
<div class="card-header">
<h2>Audio</h2>
</div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Master Volume</div>
<div class="setting-value">
<input type="range" class="slider" id="settings-volume"
min="0" max="100" value="{{ (master_volume * 100)|int }}"
oninput="setVolume(this.value)">
<span id="settings-volume-text">{{ (master_volume * 100)|int }}%</span>
<!-- Tab: Connection -->
<div class="settings-panel active" id="tab-connection">
<div class="card">
<div class="card-header"><h2>Connection</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Hostname</div>
<div class="setting-value"><code id="hostname">pedal.local</code></div>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Tuner</div>
<div class="setting-value">
<button class="btn toggle-btn {% if tuner_enabled %}active{% endif %}"
id="settings-tuner-btn" onclick="toggleTuner()">
{% if tuner_enabled %}ON{% else %}OFF{% endif %}
</button>
<div class="setting-row">
<div class="setting-label">Web Server</div>
<div class="setting-value"><code id="web-url">http://pedal.local:8080</code></div>
</div>
<div class="setting-row">
<div class="setting-label">Connected Clients</div>
<div class="setting-value"><span id="ws-count">0</span></div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>4-Cable Method (4CM)</h2>
</div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">4CM Mode</div>
<div class="setting-value">
<button class="btn toggle-btn {% if routing_mode == '4cm' %}active{% endif %}"
id="4cm-toggle-btn" onclick="toggle4cm()">
{% if routing_mode == '4cm' %}ON{% else %}OFF{% endif %}
</button>
<!-- Tab: Audio -->
<div class="settings-panel" id="tab-audio">
<div class="card">
<div class="card-header"><h2>Audio</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Master Volume</div>
<div class="setting-value">
<input type="range" class="slider" id="settings-volume"
min="0" max="100" value="{{ (master_volume * 100)|int }}"
oninput="setVolume(this.value)">
<span id="settings-volume-text">{{ (master_volume * 100)|int }}%</span>
</div>
</div>
</div>
<div class="setting-row" id="breakpoint-row"
{% if routing_mode != '4cm' %}style="opacity: 0.4; pointer-events: none;"{% endif %}>
<div class="setting-label">Breakpoint</div>
<div class="setting-value">
<input type="range" class="slider" id="4cm-breakpoint"
min="0" max="16" value="{{ routing_breakpoint }}"
oninput="set4cmBreakpoint(this.value)">
<span id="4cm-breakpoint-text">{{ routing_breakpoint }}</span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Routing</div>
<div class="setting-value">
<span id="4cm-routing-desc" class="routing-desc">
{% if routing_mode == '4cm' %}
Input 1 (Guitar) → Pre blocks [0..{{ routing_breakpoint }}) → Send
&nbsp;|&nbsp; Input 2 (Return) → Post blocks [{{ routing_breakpoint }}..] → Output
{% else %}
Mono: Guitar → Full chain → Output
{% endif %}
</span>
<div class="setting-row">
<div class="setting-label">Tuner</div>
<div class="setting-value">
<button class="btn toggle-btn {% if tuner_enabled %}active{% endif %}"
id="settings-tuner-btn" onclick="toggleTuner()">
{% if tuner_enabled %}ON{% else %}OFF{% endif %}
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>About</h2>
<!-- Tab: 4CM Routing -->
<div class="settings-panel" id="tab-routing">
<div class="card">
<div class="card-header"><h2>4-Cable Method (4CM)</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">4CM Mode</div>
<div class="setting-value">
<button class="btn toggle-btn {% if routing_mode == '4cm' %}active{% endif %}"
id="4cm-toggle-btn" onclick="toggle4cm()">
{% if routing_mode == '4cm' %}ON{% else %}OFF{% endif %}
</button>
</div>
</div>
<div class="setting-row" id="breakpoint-row"
{% if routing_mode != '4cm' %}style="opacity: 0.4; pointer-events: none;"{% endif %}>
<div class="setting-label">Breakpoint</div>
<div class="setting-value">
<input type="range" class="slider" id="4cm-breakpoint"
min="0" max="16" value="{{ routing_breakpoint }}"
oninput="set4cmBreakpoint(this.value)">
<span id="4cm-breakpoint-text">{{ routing_breakpoint }}</span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Routing</div>
<div class="setting-value">
<span id="4cm-routing-desc" class="routing-desc">
{% if routing_mode == '4cm' %}
Input 1 (Guitar) → Pre blocks [0..{{ routing_breakpoint }}) → Send
&nbsp;|&nbsp; Input 2 (Return) → Post blocks [{{ routing_breakpoint }}..] → Output
{% else %}
Mono: Guitar → Full chain → Output
{% endif %}
</span>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="about-info">
<p><strong>Pi Multi-FX Pedal</strong> v0.1.0</p>
<p>Raspberry Pi 4B — Python 3.11 — JACK Audio</p>
<p><a href="https://gitea.ourpad.casa/shawn/pi-multifx-pedal" target="_blank">Gitea Repository</a></p>
</div>
<!-- Tab: Network -->
<div class="settings-panel" id="tab-network">
<!-- WiFi Client Status -->
<div class="card" id="wifi-status-card">
<div class="card-header"><h2>WiFi Status</h2></div>
<div class="card-body">
<div class="network-status-row">
<div class="network-status-icon" id="wifi-icon">📶</div>
<div class="network-status-info">
<div class="network-ssid" id="wifi-ssid"></div>
<div class="network-ip" id="wifi-ip"></div>
<div class="network-signal"><span id="wifi-signal-text"></span></div>
</div>
<button class="btn btn-secondary" onclick="refreshWiFiStatus()">Refresh</button>
</div>
</div>
</div>
<!-- WiFi Scan & Connect -->
<div class="card">
<div class="card-header">
<h2>WiFi Networks</h2>
<button class="btn btn-secondary" onclick="scanWiFi()" id="wifi-scan-btn">Scan</button>
</div>
<div class="card-body" id="wifi-scan-results">
<div class="hint">Click Scan to find available WiFi networks.</div>
</div>
</div>
<!-- Known Networks -->
<div class="card" id="wifi-saved-card">
<div class="card-header"><h2>Known Networks</h2></div>
<div class="card-body" id="wifi-saved-list">
<div class="loading">Loading...</div>
</div>
</div>
<!-- Hotspot -->
<div class="card">
<div class="card-header"><h2>WiFi Hotspot</h2></div>
<div class="card-body">
<div class="hotspot-controls">
<div class="setting-row">
<div class="setting-label">Hotspot Status</div>
<div class="setting-value">
<span id="hotspot-status-indicator" class="hotspot-indicator hotspot-off">Off</span>
</div>
</div>
<div class="setting-row" id="hotspot-ssid-row">
<div class="setting-label">SSID</div>
<div class="setting-value">
<input type="text" class="network-input" id="hotspot-ssid" value="Pi-Pedal" maxlength="32">
</div>
</div>
<div class="setting-row" id="hotspot-psk-row">
<div class="setting-label">Password</div>
<div class="setting-value">
<input type="password" class="network-input" id="hotspot-psk" value="pedal1234" maxlength="63">
</div>
</div>
<div class="setting-row">
<div class="setting-label">Connected Clients</div>
<div class="setting-value"><span id="hotspot-clients">0</span></div>
</div>
<div class="hotspot-actions">
<button class="btn btn-primary" id="hotspot-enable-btn" onclick="enableHotspot()">Enable Hotspot</button>
<button class="btn btn-danger" id="hotspot-disable-btn" onclick="disableHotspot()">Disable Hotspot</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Bluetooth -->
<div class="settings-panel" id="tab-bluetooth">
<!-- BT Adapter Status -->
<div class="card">
<div class="card-header"><h2>Bluetooth</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Bluetooth</div>
<div class="setting-value">
<button class="btn toggle-btn" id="bt-power-btn" onclick="toggleBTPower()">OFF</button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Discoverable</div>
<div class="setting-value">
<button class="btn toggle-btn" id="bt-discoverable-btn" onclick="toggleBTDiscoverable()">OFF</button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Adapter Name</div>
<div class="setting-value"><span id="bt-adapter-name"></span></div>
</div>
<div class="setting-row">
<div class="setting-label">Address</div>
<div class="setting-value"><code id="bt-address"></code></div>
</div>
</div>
</div>
<!-- BT Scan -->
<div class="card">
<div class="card-header">
<h2>Discover Devices</h2>
<button class="btn btn-secondary" onclick="scanBT()" id="bt-scan-btn">Scan</button>
</div>
<div class="card-body" id="bt-scan-results">
<div class="hint">Click Scan to find discoverable Bluetooth devices.</div>
</div>
</div>
<!-- Paired Devices -->
<div class="card">
<div class="card-header"><h2>Paired Devices</h2></div>
<div class="card-body" id="bt-paired-list">
<div class="loading">Loading...</div>
</div>
</div>
<!-- BT MIDI Service -->
<div class="card">
<div class="card-header"><h2>Bluetooth MIDI</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">BT MIDI Service</div>
<div class="setting-value">
<span class="bt-midi-indicator" id="bt-midi-indicator">Off</span>
</div>
</div>
<div class="bt-midi-actions">
<button class="btn btn-primary" id="bt-midi-enable-btn" onclick="enableBTMIDI()">Enable</button>
<button class="btn btn-danger" id="bt-midi-disable-btn" onclick="disableBTMIDI()">Disable</button>
</div>
<div class="setting-row mt-8">
<div class="setting-label">Connected MIDI Devices</div>
<div class="setting-value" id="bt-midi-devices">
<span class="hint">None connected</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: About -->
<div class="settings-panel" id="tab-about">
<div class="card">
<div class="card-header"><h2>About</h2></div>
<div class="card-body">
<div class="about-info">
<p><strong>Pi Multi-FX Pedal</strong> v0.1.0</p>
<p>Raspberry Pi 4B — Python 3.11 — JACK Audio</p>
<p><a href="https://gitea.ourpad.casa/shawn/pi-multifx-pedal" target="_blank">Gitea Repository</a></p>
</div>
</div>
</div>
</div>
<!-- WiFi connect modal -->
<div class="modal-overlay" id="wifi-connect-modal" style="display:none;" onclick="if(event.target===this)closeWiFiModal()">
<div class="modal">
<div class="modal-header">
<h2>Connect to <span id="wifi-modal-ssid"></span></h2>
<button class="btn-close" onclick="closeWiFiModal()">&times;</button>
</div>
<div class="modal-body">
<div class="setting-row">
<div class="setting-label">Password</div>
<div class="setting-value">
<input type="password" class="network-input" id="wifi-modal-password" placeholder="Enter WiFi password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="connectWiFi()">Connect</button>
<button class="btn btn-secondary" onclick="closeWiFiModal()">Cancel</button>
</div>
<div id="wifi-modal-error" class="network-error mt-8"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/network.js"></script>
<script src="/static/bluetooth.js"></script>
<script>
/* ── Tab switching ────────────────────────────────────────────────── */
function switchTab(tabId) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`.settings-tab[data-tab="${tabId}"]`).classList.add('active');
document.getElementById(`tab-${tabId}`).classList.add('active');
// Refresh tab data when switching to it
if (tabId === 'network') {
refreshWiFiStatus();
loadSavedNetworks();
} else if (tabId === 'bluetooth') {
refreshBTStatus();
refreshBTMIDIStatus();
loadPairedDevices();
}
}
/* ── Initial load on settings page ───────────────────────────────── */
document.addEventListener('DOMContentLoaded', () => {
// Check if we should auto-switch based on URL hash
const hash = window.location.hash.replace('#', '');
if (['network', 'bluetooth', 'audio', 'routing', 'about'].includes(hash)) {
switchTab(hash);
}
});
</script>
{% endblock %}