Merge branch 'pr-392' into dev

This commit is contained in:
Rightup committed 2026-08-18 08:33:11 +01:00
commit e840c10ed9
6 files changed
+113 -1

No files matched your search

+50
View File
@@ -457,6 +457,10 @@ services:
# USB devices. Uncomment/change only if needed.
# - /dev/bus/usb/002:/dev/bus/usb/002
# USB serial modem. See "USB serial modems" below before using a
# /dev/serial/by-id/ path here.
# - /dev/openhop-modem:/dev/openhop-modem
cap_add:
- SYS_RAWIO
@@ -475,6 +479,52 @@ volumes:
openhop-repeater-data:
```
### USB serial modems
A `/dev/serial/by-id/` path is the stable way to name a USB modem, but it cannot
always be handed to a container. ESP32-S3 boards that use the chip's native
USB-Serial-JTAG peripheral report their factory MAC address as the USB serial
number, colons included, so udev produces a name like:
```
/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_60:55:F9:C0:27:18-if00
```
Docker parses `devices:` entries as `host:container[:permissions]` and has no way
to escape a colon inside the path, so that name is rejected. `/dev/serial/by-path/`
does not help either, since those contain PCI addresses with colons of their own.
Two further things to know: `/dev/serial/by-id` is built by udev on the host, and
udev does not run inside the container, so the directory is not visible in there
unless you bind-mount it. And a bare `/dev/ttyACM0` is not stable across re-plugs
or reboots.
Install the udev rule shipped with openHop Core, which creates a colon-free
symlink that survives re-plugs:
```bash
sudo cp 99-openhop-modem.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty --action=change
```
Then map the symlink and point the config at the same path:
```yaml
devices:
- /dev/openhop-modem:/dev/openhop-modem
```
```yaml
kiss:
port: "/dev/openhop-modem"
```
The rule matches USB ID `303a:1001`, which every ESP32-S3 board using native USB
shares. With more than one attached, pin the rule to a single board's serial
number — the rules file has a commented example and the `udevadm` command to read
it.
## Roadmap
- [ ] **Public map integration**: submit repeater location and details to a
+4 -1
View File
@@ -528,7 +528,10 @@ radio:
# KISS modem (when radio_type: kiss). Requires openhop-core with KISS support.
# kiss:
# port: "/dev/ttyUSB0"
# port: "/dev/ttyUSB0" # 99-openhop-modem.rules symlinks to /dev/openhop-modem.
# # Prefer that over /dev/serial/by-id/ in Docker: an
# # ESP32-S3 by-id name contains colons and Docker reads
# # those as its host:container separator.
# baud_rate: 9600
# # Optional KISS key-up / CSMA tuning, forwarded to the modem firmware.
# # Omit to keep the wrapper/firmware defaults. For a host-managed repeater the
+6
View File
@@ -11,6 +11,12 @@ services:
- /dev/gpiochip0
# USB DEVICES (Your path may differ)
- /dev/bus/usb/002:/dev/bus/usb/002
# USB SERIAL MODEM (KISS / openHop USB). Do NOT use a /dev/serial/by-id/
# path here: ESP32-S3 boards report their MAC as the USB serial number, so
# the name contains colons and Docker reads those as the host:container
# separator. Install 99-openhop-modem.rules on the host and map the
# symlink it creates instead. See README, "USB serial modems".
# - /dev/openhop-modem:/dev/openhop-modem
# SPI DEVICES PERMISSIONS
cap_add:
- SYS_RAWIO
+15
View File
@@ -651,6 +651,13 @@ install_repeater() {
udevadm trigger 2>/dev/null || true
fi
echo "59"; echo "# Installing udev rules for USB modems..."
if [ -f "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" ]; then
cp "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" /etc/udev/rules.d/99-openhop-modem.rules
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=tty --action=change 2>/dev/null || true
fi
echo "65"; echo "# Setting permissions..."
# Venv stays root-owned (pip runs as root); service user only needs read+execute
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
@@ -1148,6 +1155,14 @@ upgrade_repeater() {
elif [ -f /etc/udev/rules.d/99-ch341.rules ]; then
echo " ✓ CH341 udev rules already present"
fi
if [ -f "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" ]; then
cp "$SCRIPT_DIR/../openhop-core/99-openhop-modem.rules" /etc/udev/rules.d/99-openhop-modem.rules
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=tty --action=change 2>/dev/null || true
echo " ✓ USB modem udev rules updated"
elif [ -f /etc/udev/rules.d/99-openhop-modem.rules ]; then
echo " ✓ USB modem udev rules already present"
fi
echo " ✓ User groups updated"
echo "[7/9] Fixing permissions and helper files..."
+5
View File
@@ -1436,6 +1436,11 @@ class APIEndpoints:
for item in devices:
dev = item.get("device")
if dev and dev not in dedup:
# Docker reads `devices:` entries as host:container[:perms] and
# cannot escape a colon inside the path, so a /dev/serial/by-id/
# name carrying an ESP32-S3 MAC serial is unusable in a
# container. Flag it so the UI can offer a symlink instead.
item["docker_safe"] = ":" not in dev
dedup[dev] = item
sorted_devices = sorted(dedup.values(), key=lambda x: x["device"])
+33
View File
@@ -535,6 +535,39 @@ def test_serial_ports_dedupes_duplicate_devices(cherrypy_ctx):
assert "first" in result["data"][0]["description"]
def test_serial_ports_flags_paths_docker_cannot_map(cherrypy_ctx):
del cherrypy_ctx
api = _make_api()
# ESP32-S3 boards on the native USB-Serial-JTAG peripheral report their MAC
# as the USB serial number, colons included. Docker reads `devices:` as
# host:container, so this path cannot be passed into a container.
by_id = SimpleNamespace(
device="/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_60:55:F9:C0:27:18-if00",
description="USB JTAG/serial debug unit",
hwid="USB VID:PID=303A:1001",
)
with patch("serial.tools.list_ports.comports", return_value=[by_id]):
result = api.serial_ports()
assert result["success"] is True
assert result["data"][0]["docker_safe"] is False
def test_serial_ports_marks_ordinary_paths_docker_safe(cherrypy_ctx):
del cherrypy_ctx
api = _make_api()
p1 = SimpleNamespace(device="/dev/ttyACM0", description="USB CDC", hwid="n/a")
with patch("serial.tools.list_ports.comports", return_value=[p1]):
result = api.serial_ports()
assert result["success"] is True
assert result["data"][0]["docker_safe"] is True
def test_config_export_redacts_secrets_and_identity_keys(cherrypy_ctx):
request, _ = cherrypy_ctx
request.method = "GET"