fix(server): own capture relay runtime
This commit is contained in:
parent
164ef497f0
commit
2621f7dead
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1692,7 +1692,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1704,7 +1704,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -304,6 +304,16 @@ modern `key-int-max` property for `vah264enc` and `vah265enc`. The hardware
|
||||
smoke harness now exercises the same usable route and retains encoded streams
|
||||
plus hardware-decoded proof frames.
|
||||
|
||||
### The `0.27.9` Capture Power Fix
|
||||
|
||||
The controlled 2026-08-13 Theia reboot made the downstream outage concrete:
|
||||
`relay.service` could not import `gpiod` from an obsolete home-directory
|
||||
virtualenv, so GPIO27 never powered either GC311 capture card. The system
|
||||
`libgpiod` Python binding remained healthy. Release 0.27.9 moves the helper
|
||||
into the Lesavka tree, installs it with the server, requires `libgpiod`, and
|
||||
runs it with `/usr/bin/python` before capture discovery. This removes the
|
||||
unversioned virtualenv from the physical eye-feed power path.
|
||||
|
||||
## 7. The Current Downstream Video Failure
|
||||
|
||||
The current blank downstream feeds fail before transport or decoding.
|
||||
@ -351,6 +361,15 @@ hardware itself is healthy. If the devices remain absent after reboot, the next
|
||||
checks are the `relay.service` failure reason, capture-card power, USB
|
||||
enumeration, V4L2 nodes, udev path tags/rules, and physical card/cable state.
|
||||
|
||||
The 2026-08-13 controlled reboot completed that branch of the diagnosis. SSH
|
||||
returned, but both cards remained absent and `relay.service` failed with
|
||||
`ModuleNotFoundError: No module named 'gpiod'`. Its unit referenced a private
|
||||
Python 3.14 virtualenv that had also lost `pip`; meanwhile `/usr/bin/python`
|
||||
successfully imported the installed `libgpiod` binding. This proves the common
|
||||
downstream failure was capture-card power automation, not two simultaneous
|
||||
card failures, H.264 transport, or client decoding. Release 0.27.9 makes the
|
||||
working system binding and a versioned helper the installed service contract.
|
||||
|
||||
## 8. Recovery And Deployment Sequence
|
||||
|
||||
The safe completion sequence for this incident is:
|
||||
@ -367,7 +386,7 @@ The safe completion sequence for this incident is:
|
||||
6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
|
||||
already-attached USB gadget unless a controlled rebuild is explicitly
|
||||
required.
|
||||
7. Confirm Theia reports server version `0.27.8`, the pushed release revision,
|
||||
7. Confirm Theia reports server version `0.27.9`, the pushed release revision,
|
||||
direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
|
||||
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
|
||||
frames.
|
||||
@ -385,7 +404,7 @@ hardware contract is repeatable. The remaining work falls into five groups.
|
||||
|
||||
### A. Install And Version Parity
|
||||
|
||||
- Push and deploy `0.27.8` through the client/server install scripts.
|
||||
- Push and deploy `0.27.9` through the client/server install scripts.
|
||||
- Confirm client/server version and revision in every hardware probe artifact.
|
||||
- Eliminate the current state where a fixed client talks to an unfixed server.
|
||||
|
||||
@ -451,6 +470,6 @@ host repair:
|
||||
9. disconnect/reconnect and device changes recover without stale media; and
|
||||
10. diagnostics identify the failed physical stage when any item breaks.
|
||||
|
||||
Until that sequence passes on the installed `0.27.8` client/server pair, the
|
||||
Until that sequence passes on the installed `0.27.9` client/server pair, the
|
||||
current release should be described as a validated code correction awaiting
|
||||
hardware deployment and end-to-end acceptance, not as a completed product fix.
|
||||
|
||||
47
scripts/daemon/lesavka-relay-hold.py
Executable file
47
scripts/daemon/lesavka-relay-hold.py
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hold the capture-card power relay active until systemd stops the service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
|
||||
import gpiod
|
||||
from gpiod.line import Direction, Value
|
||||
|
||||
|
||||
CHIP = os.environ.get("LESAVKA_CAPTURE_RELAY_CHIP", "/dev/gpiochip0")
|
||||
LINE = int(os.environ.get("LESAVKA_CAPTURE_RELAY_LINE", "27"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
stop_requested = False
|
||||
|
||||
def request_stop(_signum: int, _frame: object) -> None:
|
||||
nonlocal stop_requested
|
||||
stop_requested = True
|
||||
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
|
||||
with gpiod.Chip(CHIP) as chip:
|
||||
request = chip.request_lines(
|
||||
consumer="lesavka-capture-relay",
|
||||
config={
|
||||
LINE: gpiod.LineSettings(
|
||||
direction=Direction.OUTPUT,
|
||||
output_value=Value.ACTIVE,
|
||||
)
|
||||
},
|
||||
)
|
||||
try:
|
||||
while not stop_requested:
|
||||
time.sleep(0.25)
|
||||
finally:
|
||||
request.set_value(LINE, Value.INACTIVE)
|
||||
request.release()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1435,6 +1435,42 @@ UNIT
|
||||
sudo systemctl enable lesavka-recovery-ladder.timer
|
||||
}
|
||||
|
||||
install_capture_power_relay_unit() {
|
||||
local helper_source="$SCRIPT_REPO_ROOT/scripts/daemon/lesavka-relay-hold.py"
|
||||
if [[ ! -x $helper_source ]]; then
|
||||
echo "Capture relay helper is missing or not executable: $helper_source" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install_verified_executable \
|
||||
"$helper_source" \
|
||||
/usr/local/lib/lesavka/lesavka-relay-hold.py \
|
||||
"lesavka-relay-hold.py"
|
||||
|
||||
cat <<'UNIT' | sudo tee /etc/systemd/system/relay.service >/dev/null
|
||||
[Unit]
|
||||
Description=Lesavka capture-card power relay on GPIO27
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=theia
|
||||
Group=gpio
|
||||
SupplementaryGroups=gpio
|
||||
Environment=LESAVKA_CAPTURE_RELAY_CHIP=/dev/gpiochip0
|
||||
Environment=LESAVKA_CAPTURE_RELAY_LINE=27
|
||||
ExecStart=/usr/bin/python /usr/local/lib/lesavka/lesavka-relay-hold.py
|
||||
Restart=on-failure
|
||||
RestartSec=2s
|
||||
TimeoutStopSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl reset-failed relay.service >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
CAPTURE_DISCOVERY_RELAY_PRESENT=0
|
||||
CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE=0
|
||||
CAPTURE_DISCOVERY_POWER_BORROWED=0
|
||||
@ -1498,6 +1534,7 @@ pacman_install git \
|
||||
abseil-cpp \
|
||||
gcc \
|
||||
alsa-utils \
|
||||
libgpiod \
|
||||
"${PIPEWIRE_PACKAGES[@]}" \
|
||||
tailscale \
|
||||
base-devel \
|
||||
@ -1525,6 +1562,7 @@ fi
|
||||
echo "==> 1c. GPIO permissions for relay"
|
||||
echo 'z /dev/gpiochip* 0660 root gpio -' | sudo tee /etc/tmpfiles.d/gpiochip.conf >/dev/null
|
||||
sudo systemd-tmpfiles --create /etc/tmpfiles.d/gpiochip.conf || true
|
||||
install_capture_power_relay_unit
|
||||
|
||||
echo "==> 1d. Audio permissions for diagnostics"
|
||||
if getent group audio >/dev/null 2>&1 && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
|
||||
|
||||
@ -16,7 +16,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.27.8"
|
||||
version = "0.27.9"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -9,6 +9,44 @@ const SERVER_INSTALL: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/scripts/install/server.sh"
|
||||
));
|
||||
const CAPTURE_RELAY_HELPER: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/scripts/daemon/lesavka-relay-hold.py"
|
||||
));
|
||||
|
||||
#[test]
|
||||
fn server_install_owns_capture_relay_runtime_and_uses_system_gpiod() {
|
||||
for expected in [
|
||||
"libgpiod",
|
||||
"install_capture_power_relay_unit",
|
||||
"/usr/local/lib/lesavka/lesavka-relay-hold.py",
|
||||
"ExecStart=/usr/bin/python /usr/local/lib/lesavka/lesavka-relay-hold.py",
|
||||
"Restart=on-failure",
|
||||
] {
|
||||
assert!(
|
||||
SERVER_INSTALL.contains(expected),
|
||||
"server installer should preserve the capture relay dependency: {expected}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!SERVER_INSTALL.contains("/home/theia/scripts/gpio/venv/bin/python"),
|
||||
"capture relay power must not depend on an unmaintained home-directory virtualenv"
|
||||
);
|
||||
assert!(CAPTURE_RELAY_HELPER.contains("import gpiod"));
|
||||
assert!(CAPTURE_RELAY_HELPER.contains("output_value=Value.ACTIVE"));
|
||||
assert!(CAPTURE_RELAY_HELPER.contains("request.set_value(LINE, Value.INACTIVE)"));
|
||||
|
||||
let install_position = SERVER_INSTALL
|
||||
.find("install_capture_power_relay_unit\n")
|
||||
.expect("relay unit install call");
|
||||
let discovery_position = SERVER_INSTALL
|
||||
.find("prepare_capture_power_for_discovery\n")
|
||||
.expect("capture discovery call");
|
||||
assert!(
|
||||
install_position < discovery_position,
|
||||
"the fixed relay runtime must be installed before capture discovery starts it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_install_infers_checkout_owner_when_root_wrapper_hides_sudo_user() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user