feat: add BLE transport support for companion devices

Integrate meshcore library's BLE connection (via bleak) as a third
transport option alongside serial and TCP. Priority: BLE > TCP > Serial.

Config: MC_BLE_ADDRESS and MC_BLE_PIN environment variables.
Docker: bluez/dbus packages, NET_ADMIN cap, D-Bus socket mount.
UI: transport type badge in navbar, transport_type in /api/status.
Watchdog: skip USB reset for BLE connections (same as TCP).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-29 10:03:45 +02:00
parent bd59826504
commit 710f69c350
9 changed files with 95 additions and 27 deletions
+21 -15
View File
@@ -136,22 +136,26 @@ def auto_detect_usb_device() -> str:
log(f"Error during USB device auto-detection: {e}", "ERROR")
return None
def is_tcp_connection() -> bool:
"""Check if the application is configured to use a TCP connection instead of a serial port."""
def _read_env_value(key: str) -> str:
"""Read a value from the .env file. Returns empty string if not found."""
env_file = os.path.join(MCWEBUI_DIR, '.env')
if os.path.exists(env_file):
try:
with open(env_file, 'r') as f:
for line in f:
if line.startswith('MC_TCP_HOST='):
val = line.split('=', 1)[1].strip().strip('"\'')
if val:
return True
if line.startswith(f'{key}='):
return line.split('=', 1)[1].strip().strip('"\'')
except Exception as e:
log(f"Failed to read .env file for TCP host: {e}", "WARN")
log(f"Failed to read .env file for {key}: {e}", "WARN")
return ''
return False
def is_tcp_connection() -> bool:
"""Check if the application is configured to use a TCP connection instead of a serial port."""
return bool(_read_env_value('MC_TCP_HOST'))
def is_ble_connection() -> bool:
"""Check if the application is configured to use a BLE connection."""
return bool(_read_env_value('MC_BLE_ADDRESS'))
def reset_esp32_device():
"""Perform a hardware reset on ESP32/LoRa device using DTR/RTS lines via ioctl."""
@@ -433,7 +437,8 @@ def handle_unhealthy_container(container_name: str, status: dict):
restart_success = False
if container_name == 'mc-webui':
recent_restarts = count_recent_restarts(container_name, minutes=8)
if recent_restarts >= 3 and not is_tcp_connection():
uses_serial = not is_tcp_connection() and not is_ble_connection()
if recent_restarts >= 3 and uses_serial:
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. Attempting hardware USB reset.", "WARN")
# Stop the container first so it releases the serial port
run_compose_command(['stop', container_name])
@@ -442,8 +447,8 @@ def handle_unhealthy_container(container_name: str, status: dict):
time.sleep(5) # Give OS time to re-enumerate the device
restart_success = start_container(container_name)
else:
if recent_restarts >= 3 and is_tcp_connection():
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. TCP connection used, skipping hardware USB reset.", "WARN")
if recent_restarts >= 3 and not uses_serial:
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. Non-serial connection, skipping hardware USB reset.", "WARN")
restart_success = restart_container(container_name)
else:
# Restart the container
@@ -512,7 +517,8 @@ def handle_unresponsive_device(container_name: str, status: dict):
restart_success = False
if container_name == 'mc-webui':
recent_restarts = count_recent_restarts(container_name, minutes=8)
if recent_restarts >= 3 and not is_tcp_connection():
uses_serial = not is_tcp_connection() and not is_ble_connection()
if recent_restarts >= 3 and uses_serial:
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. Attempting hardware USB reset.", "WARN")
# Stop the container first so it releases the serial port
run_compose_command(['stop', container_name])
@@ -521,8 +527,8 @@ def handle_unresponsive_device(container_name: str, status: dict):
time.sleep(5) # Give OS time to re-enumerate the device
restart_success = start_container(container_name)
else:
if recent_restarts >= 3 and is_tcp_connection():
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. TCP connection used, skipping hardware USB reset.", "WARN")
if recent_restarts >= 3 and not uses_serial:
log(f"{container_name} has been restarted {recent_restarts} times in the last 8 minutes. Non-serial connection, skipping hardware USB reset.", "WARN")
restart_success = restart_container(container_name)
else:
# Restart the container