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:
@@ -540,6 +540,177 @@ class WebServer:
|
||||
logger.info("Uploaded IR file: %s", dest.name)
|
||||
return {"ok": True, "filename": dest.name}
|
||||
|
||||
# ── WiFi endpoints ──────────────────────────────────────
|
||||
|
||||
@app.get("/api/wifi/status")
|
||||
async def wifi_get_status():
|
||||
"""Get current WiFi client connection status."""
|
||||
from ..system.network import wifi_status
|
||||
return wifi_status()
|
||||
|
||||
@app.get("/api/wifi/scan")
|
||||
async def wifi_scan():
|
||||
"""Scan for available WiFi networks."""
|
||||
from ..system.network import wifi_scan
|
||||
return {"networks": wifi_scan()}
|
||||
|
||||
@app.get("/api/wifi/saved")
|
||||
async def wifi_saved():
|
||||
"""List saved/known WiFi networks."""
|
||||
from ..system.network import wifi_saved_networks
|
||||
return {"networks": wifi_saved_networks()}
|
||||
|
||||
@app.post("/api/wifi/connect")
|
||||
async def wifi_connect(data: dict):
|
||||
"""Connect to a WiFi network.
|
||||
|
||||
Body: {ssid: str, password: str}
|
||||
"""
|
||||
from ..system.network import wifi_connect
|
||||
return wifi_connect(
|
||||
ssid=data.get("ssid", ""),
|
||||
password=data.get("password", ""),
|
||||
)
|
||||
|
||||
@app.post("/api/wifi/disconnect")
|
||||
async def wifi_disconnect():
|
||||
"""Disconnect from the current WiFi network."""
|
||||
from ..system.network import wifi_disconnect
|
||||
return wifi_disconnect()
|
||||
|
||||
@app.post("/api/wifi/forget")
|
||||
async def wifi_forget(data: dict):
|
||||
"""Forget a saved WiFi network.
|
||||
|
||||
Body: {ssid: str}
|
||||
"""
|
||||
from ..system.network import wifi_forget
|
||||
return wifi_forget(ssid=data.get("ssid", ""))
|
||||
|
||||
@app.get("/api/wifi/hotspot")
|
||||
async def wifi_hotspot_status():
|
||||
"""Get hotspot status."""
|
||||
from ..system.network import hotspot_status
|
||||
return hotspot_status()
|
||||
|
||||
@app.post("/api/wifi/hotspot/enable")
|
||||
async def wifi_hotspot_enable(data: dict):
|
||||
"""Enable WiFi hotspot.
|
||||
|
||||
Body: {ssid: str (optional), password: str (optional)}
|
||||
"""
|
||||
from ..system.network import hotspot_enable
|
||||
return hotspot_enable(
|
||||
ssid=data.get("ssid", "Pi-Pedal"),
|
||||
password=data.get("password", "pedal1234"),
|
||||
)
|
||||
|
||||
@app.post("/api/wifi/hotspot/disable")
|
||||
async def wifi_hotspot_disable():
|
||||
"""Disable WiFi hotspot."""
|
||||
from ..system.network import hotspot_disable
|
||||
return hotspot_disable()
|
||||
|
||||
# ── Bluetooth endpoints ──────────────────────────────────
|
||||
|
||||
@app.get("/api/bluetooth/status")
|
||||
async def bluetooth_status():
|
||||
"""Get Bluetooth adapter status."""
|
||||
from ..system.bluetooth import bt_status
|
||||
return bt_status()
|
||||
|
||||
@app.post("/api/bluetooth/power")
|
||||
async def bluetooth_power(data: dict):
|
||||
"""Power Bluetooth on or off.
|
||||
|
||||
Body: {on: bool}
|
||||
"""
|
||||
from ..system.bluetooth import bt_power
|
||||
return bt_power(on=data.get("on", True))
|
||||
|
||||
@app.post("/api/bluetooth/discoverable")
|
||||
async def bluetooth_discoverable(data: dict):
|
||||
"""Set Bluetooth discoverable.
|
||||
|
||||
Body: {on: bool}
|
||||
"""
|
||||
from ..system.bluetooth import bt_discoverable
|
||||
return bt_discoverable(on=data.get("on", True))
|
||||
|
||||
@app.post("/api/bluetooth/scan")
|
||||
async def bluetooth_scan():
|
||||
"""Scan for discoverable Bluetooth devices."""
|
||||
from ..system.bluetooth import bt_scan
|
||||
return bt_scan(timeout=12)
|
||||
|
||||
@app.get("/api/bluetooth/paired")
|
||||
async def bluetooth_paired():
|
||||
"""List paired Bluetooth devices."""
|
||||
from ..system.bluetooth import bt_paired_devices
|
||||
return {"devices": bt_paired_devices()}
|
||||
|
||||
@app.post("/api/bluetooth/pair")
|
||||
async def bluetooth_pair(data: dict):
|
||||
"""Pair with a Bluetooth device.
|
||||
|
||||
Body: {address: str}
|
||||
"""
|
||||
from ..system.bluetooth import bt_pair
|
||||
return bt_pair(address=data.get("address", ""))
|
||||
|
||||
@app.post("/api/bluetooth/unpair")
|
||||
async def bluetooth_unpair(data: dict):
|
||||
"""Unpair a Bluetooth device.
|
||||
|
||||
Body: {address: str}
|
||||
"""
|
||||
from ..system.bluetooth import bt_unpair
|
||||
return bt_unpair(address=data.get("address", ""))
|
||||
|
||||
@app.post("/api/bluetooth/connect")
|
||||
async def bluetooth_connect(data: dict):
|
||||
"""Connect to a paired Bluetooth device.
|
||||
|
||||
Body: {address: str}
|
||||
"""
|
||||
from ..system.bluetooth import bt_connect
|
||||
return bt_connect(address=data.get("address", ""))
|
||||
|
||||
@app.post("/api/bluetooth/disconnect")
|
||||
async def bluetooth_disconnect(data: dict):
|
||||
"""Disconnect a Bluetooth device.
|
||||
|
||||
Body: {address: str}
|
||||
"""
|
||||
from ..system.bluetooth import bt_disconnect
|
||||
return bt_disconnect(address=data.get("address", ""))
|
||||
|
||||
@app.get("/api/bluetooth/midi-status")
|
||||
async def bluetooth_midi_status():
|
||||
"""Get Bluetooth MIDI service status."""
|
||||
from ..system.bluetooth import bt_midi_status
|
||||
return bt_midi_status()
|
||||
|
||||
@app.post("/api/bluetooth/midi-enable")
|
||||
async def bluetooth_midi_enable():
|
||||
"""Enable and start the Bluetooth MIDI service."""
|
||||
from ..system.bluetooth import bt_midi_enable
|
||||
return bt_midi_enable()
|
||||
|
||||
@app.post("/api/bluetooth/midi-disable")
|
||||
async def bluetooth_midi_disable():
|
||||
"""Stop and disable the Bluetooth MIDI service."""
|
||||
from ..system.bluetooth import bt_midi_disable
|
||||
return bt_midi_disable()
|
||||
|
||||
@app.get("/api/bluetooth/midi-devices")
|
||||
async def bluetooth_midi_devices():
|
||||
"""List connected Bluetooth MIDI devices."""
|
||||
from ..system.bluetooth import bt_midi_connected_devices
|
||||
return {"devices": bt_midi_connected_devices()}
|
||||
|
||||
# ── FX block param schemas ───────────────────────────────
|
||||
|
||||
@app.get("/api/block-params/{fx_type}")
|
||||
async def get_block_params(fx_type: str):
|
||||
"""Return editable parameters for a given FX block type.
|
||||
@@ -581,6 +752,47 @@ class WebServer:
|
||||
|
||||
return app
|
||||
|
||||
# ── Network/BT state gathering ──────────────────────────────────
|
||||
|
||||
def _gather_wifi_state(self) -> dict:
|
||||
"""Try to gather WiFi status without hard-failing."""
|
||||
try:
|
||||
from ..system.network import wifi_status, hotspot_status
|
||||
ws = wifi_status()
|
||||
hs = hotspot_status()
|
||||
return {
|
||||
"connected": ws.get("connected", False),
|
||||
"ssid": ws.get("ssid"),
|
||||
"ip": ws.get("ip"),
|
||||
"signal": ws.get("signal", 0),
|
||||
"hotspot_active": hs.get("active", False),
|
||||
"hotspot_ssid": hs.get("ssid"),
|
||||
"hotspot_clients": hs.get("clients", 0),
|
||||
}
|
||||
except Exception:
|
||||
return {"connected": False, "ssid": None, "ip": None, "signal": 0,
|
||||
"hotspot_active": False, "hotspot_ssid": None, "hotspot_clients": 0}
|
||||
|
||||
def _gather_bt_state(self) -> dict:
|
||||
"""Try to gather Bluetooth status without hard-failing."""
|
||||
try:
|
||||
from ..system.bluetooth import bt_status, bt_midi_status
|
||||
bs = bt_status()
|
||||
ms = bt_midi_status()
|
||||
return {
|
||||
"available": bs.get("available", False),
|
||||
"powered": bs.get("powered", False),
|
||||
"name": bs.get("name"),
|
||||
"address": bs.get("address"),
|
||||
"discoverable": bs.get("discoverable", False),
|
||||
"midi_enabled": ms.get("enabled", False),
|
||||
"midi_running": ms.get("running", False),
|
||||
}
|
||||
except Exception:
|
||||
return {"available": False, "powered": False, "name": None,
|
||||
"address": None, "discoverable": False,
|
||||
"midi_enabled": False, "midi_running": False}
|
||||
|
||||
# ── State gathering ──────────────────────────────────────────────
|
||||
|
||||
def _gather_state(self) -> dict[str, Any]:
|
||||
@@ -631,6 +843,8 @@ class WebServer:
|
||||
"nam_model": current_model_name,
|
||||
"ir_loaded": bool(ir and ir.is_loaded) if ir else False,
|
||||
"ir_name": current_ir_name,
|
||||
"wifi": self._gather_wifi_state(),
|
||||
"bluetooth": self._gather_bt_state(),
|
||||
}
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user