From cc2de42283c10724297f9fd56234bee782f5adf9 Mon Sep 17 00:00:00 2001 From: Jacob Dahl Date: Fri, 26 Jun 2026 18:00:15 -0600 Subject: [PATCH] test(harness): add containerized x86 dev/test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run the four managers, the gateway, nginx and the built UI on x86 in containers, with a synthetic go2rtc camera and a pymavlink heartbeat stub standing in for the flight controller — so UI and manager changes can be exercised with no hardware and no reflash. --- .dockerignore | 21 ++++ test/Dockerfile | 56 +++++++++++ test/README.md | 62 ++++++++++++ test/bin/journalctl | 51 ++++++++++ test/bin/systemctl | 103 ++++++++++++++++++++ test/configs/mavlink-router.conf | 21 ++++ test/docker-compose.yml | 33 +++++++ test/go2rtc.test.yaml | 25 +++++ test/manifests/dds-agent.manifest.json | 6 ++ test/manifests/go2rtc.manifest.json | 6 ++ test/manifests/logloader.manifest.json | 6 ++ test/manifests/mavlink-router.manifest.json | 6 ++ test/mavlink_stub.py | 64 ++++++++++++ test/nginx.conf | 39 ++++++++ test/run.sh | 41 ++++++++ test/supervisord.conf | 67 +++++++++++++ 16 files changed, 607 insertions(+) create mode 100644 .dockerignore create mode 100644 test/Dockerfile create mode 100644 test/README.md create mode 100755 test/bin/journalctl create mode 100755 test/bin/systemctl create mode 100644 test/configs/mavlink-router.conf create mode 100644 test/docker-compose.yml create mode 100644 test/go2rtc.test.yaml create mode 100644 test/manifests/dds-agent.manifest.json create mode 100644 test/manifests/go2rtc.manifest.json create mode 100644 test/manifests/logloader.manifest.json create mode 100644 test/manifests/mavlink-router.manifest.json create mode 100644 test/mavlink_stub.py create mode 100644 test/nginx.conf create mode 100755 test/run.sh create mode 100644 test/supervisord.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cf2876b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Keeps the Docker build context lean for test/Dockerfile (context = repo root). +# The x86 services-only test image only needs the frontend, the four in-repo manager +# services, the gateway, and test/ — not the heavy native-build submodules. +.git +**/node_modules +**/__pycache__ +**/*.pyc +frontend/dist +build +out + +# Native-build service submodules — not compiled or run in the x86 test harness. +services/rtsp-server +services/mavlink-router +services/polaris +services/logloader +services/dds-agent +services/rid-transmitter +services/flight-review +services/go2rtc +services/jetson-can diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..955be59 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1 +# +# Local x86 test image for the ARK-OS userspace stack: the four Python managers + the +# Express gateway + nginx serving the built Vue UI. No camera, no flight controller, no +# systemd. Build context is the repo root (see test/docker-compose.yml). See test/README.md. + +# --- build the Vue UI -> dist/ ------------------------------------------------------ +FROM node:20-slim AS frontend +WORKDIR /app +COPY frontend/package.json ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# --- install the gateway's production node_modules ---------------------------------- +FROM node:20-slim AS gateway +WORKDIR /gw +COPY services/ark-ui-backend/package.json ./ +RUN npm install --omit=dev +COPY services/ark-ui-backend/ ./ + +# --- runtime: python managers + node gateway + nginx + supervisord ------------------ +FROM python:3.11-slim AS runtime +ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 +RUN apt-get update \ + && apt-get install -y --no-install-recommends nginx supervisor gcc python3-dev \ + && rm -rf /var/lib/apt/lists/* \ + && rm -f /etc/nginx/sites-enabled/default + +# Manager Python deps (mirrors packaging/build_venv.sh). +RUN pip install --no-cache-dir \ + pymavlink dronecan fastapi uvicorn python-multipart psutil toml pyyaml pyserial + +# Node runtime + the gateway with its installed deps. +COPY --from=gateway /usr/local/bin/node /usr/local/bin/node +COPY --from=gateway /gw /usr/lib/ark-os/ark-ui-backend + +# The four in-repo manager services. +COPY services/system-manager/ /usr/lib/ark-os/python/system-manager/ +COPY services/service-manager/ /usr/lib/ark-os/python/service-manager/ +COPY services/connection-manager/ /usr/lib/ark-os/python/connection-manager/ +COPY services/autopilot-manager/ /usr/lib/ark-os/python/autopilot-manager/ + +# Built UI. +COPY --from=frontend /app/dist /var/www/ark-ui/html + +# Harness bits: nginx site, supervisord config, fake FC, systemctl/journalctl shims. +COPY test/nginx.conf /etc/nginx/conf.d/ark-ui.conf +COPY test/supervisord.conf /etc/supervisord.conf +COPY test/mavlink_stub.py /usr/lib/ark-os/test/mavlink_stub.py +COPY test/bin/systemctl test/bin/journalctl /usr/local/bin/ +RUN chmod +x /usr/local/bin/systemctl /usr/local/bin/journalctl \ + && mkdir -p /usr/lib/ark-os/manifests /etc/ark-os /run/ark-os-test + +EXPOSE 80 +CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..8f06dda --- /dev/null +++ b/test/README.md @@ -0,0 +1,62 @@ +# Local test harness (x86, no hardware) + +Run the ARK-OS userspace stack and web UI on your dev machine — in containers, with no camera, no flight controller, and no reflash. This is for iterating on the managers and the UI; it deliberately does **not** rebuild or install the arm64 `.deb` (see *Fidelity* below). + +## What runs + +One **`arkos`** container (like the real device, everything on shared localhost): + +- the four FastAPI managers — `system` (3004), `service` (3002), `connection` (3001), `autopilot` (3003) +- the **ark-ui-backend** Express gateway (3000) +- **nginx** (:80) serving the built Vue UI and proxying `/api` → gateway, `/video/` → go2rtc +- a **fake flight controller** (`mavlink_stub.py`) feeding heartbeats + telemetry to the autopilot manager + +…plus a **`go2rtc`** sidecar whose camera is a synthetic ffmpeg test pattern, so the Video page streams over the real WebRTC path with no `/dev/video*`. + +## Run it + +Only Docker is required — nothing else touches your host. The runner script uses plain `docker` (no Compose plugin or buildx needed): + +```bash +bash test/run.sh up # build + start; UI at http://localhost:8080 +bash test/run.sh logs # follow all service logs +bash test/run.sh down # stop and remove +``` + +First run takes a few minutes (it builds the Vue UI and installs deps in the image); after that it's cached. + +If you have the Compose plugin, `cd test && docker compose up --build` does the same thing (see `docker-compose.yml`). + +## What works vs. what's faked + +| Page | Status | +|------|--------| +| **System** | Real — system-manager's generic-Linux collector (CPU/mem/temp where the container can read it). | +| **Video** | Real WebRTC path; camera is a test pattern. Edit `go2rtc.test.yaml` (or via the Services page) and restart the `go2rtc` container to change the source. | +| **Autopilot** | Connected to the fake FC — heartbeat, battery, attitude, position move. No params/commands (those need SITL — see below). | +| **Services** | Driven by `manifests/`; status/start/stop/enable/logs work via the `systemctl`/`journalctl` shims (state is faked, in-container). | +| **Connections** | **Degraded** — connection-manager shells out to `nmcli`, which isn't present, so most actions error. This page is the one thing that really wants the systemd/NetworkManager tier. | + +System-manager actions that write to the host (e.g. setting the hostname) will fail or no-op in the container — expected. + +## Fast UI iteration + +The image bakes the built UI, so a frontend change means a rebuild. For tight loops, run the Vue dev server on your host against the running backend instead: + +```bash +cd frontend && npm run serve # http://localhost:8081 with HMR +``` + +`frontend/vue.config.js` already proxies `/api` → `localhost:3000`; add a `/video` proxy to `localhost:8080` if you need the Video page in dev mode. (The gateway's `:3000` is internal to the `arkos` container — publish it in `docker-compose.yml` with `ports: ["3000:3000"]` if your dev server needs to reach it directly.) + +## Upgrading the fake FC to PX4 SITL + +The stub only emits telemetry. For real parameters/commands (so the Autopilot tabs are fully exercisable), drop in PX4 SITL and point the autopilot manager at it instead of the stub: + +1. Add a SITL service to the compose file, e.g. `jonasvautherin/px4-gazebo-headless`, exposing its MAVLink UDP. +2. In `supervisord.conf`, change `autopilot-manager`'s `--connection-string` to the SITL endpoint, and disable the `mavlink-stub` program. + +## Notes + +- A repo-root `.dockerignore` keeps the build context lean (skips `.git`, `node_modules`, and the native-build service submodules the x86 harness doesn't use). +- **Fidelity:** this runs x86-native builds of the managers/gateway, not the arm64 artifact you ship. It's for behavior/UI iteration. To validate the actual package (install, systemd units, nginx wiring) before a release, run the real `.deb` in an arm64 systemd container under qemu — a separate, heavier harness we can add when needed. diff --git a/test/bin/journalctl b/test/bin/journalctl new file mode 100755 index 0000000..677237f --- /dev/null +++ b/test/bin/journalctl @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Minimal `journalctl -o json` stand-in for the no-systemd test container. + +service-manager streams logs with `journalctl -u -b 0 -n N -f -o json` and parses +each line for __REALTIME_TIMESTAMP (microseconds), MESSAGE and PRIORITY. We emit a short +synthetic backlog and then, with -f, keep emitting a line every couple of seconds so the +log pane in the UI shows a live tail. +""" +import itertools +import json +import sys +import time + + +def emit(unit: str, message: str, priority: int = 6) -> None: + line = { + "__REALTIME_TIMESTAMP": str(int(time.time() * 1_000_000)), + "PRIORITY": str(priority), + "MESSAGE": message, + "SYSLOG_IDENTIFIER": unit, + } + sys.stdout.write(json.dumps(line) + "\n") + sys.stdout.flush() + + +def main() -> int: + args = sys.argv[1:] + unit, follow, n = "system", False, 50 + for i, a in enumerate(args): + if a == "-u" and i + 1 < len(args): + unit = args[i + 1] + elif a == "-f": + follow = True + elif a == "-n" and i + 1 < len(args): + try: + n = int(args[i + 1]) + except ValueError: + pass + unit = unit[: -len(".service")] if unit.endswith(".service") else unit + + for i in range(min(n, 20)): + emit(unit, f"[test harness] {unit}: backlog line {i + 1}") + if follow: + for k in itertools.count(1): + time.sleep(2) + emit(unit, f"[test harness] {unit}: live tail tick {k}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/bin/systemctl b/test/bin/systemctl new file mode 100755 index 0000000..38decee --- /dev/null +++ b/test/bin/systemctl @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Minimal `systemctl` stand-in for the no-systemd test container. + +service-manager shells out to systemctl to read and toggle unit state. There is no +systemd here, so this shim answers the exact subset it calls (`show -p Id -p +UnitFileState -p ActiveState ...`, `is-active`, `is-enabled`, and the +start/stop/restart/enable/disable/reset-failed actions) from a small JSON state file, +so the Services page renders and its buttons actually flip state. + +State is seeded from the installed manifests on first use. The services that genuinely +run in this harness are reported active; the rest start inactive and can be "started" +from the UI (which just flips the flag here). +""" +import json +import os +import sys + +STATE = "/run/ark-os-test/units.json" +MANIFEST_DIR = "/usr/lib/ark-os/manifests" +# Units that are real processes in this harness (or the go2rtc sidecar). +RUNNING = { + "system-manager", "service-manager", "connection-manager", + "autopilot-manager", "ark-ui-backend", "go2rtc", +} + + +def load() -> dict: + if os.path.exists(STATE): + with open(STATE) as fh: + return json.load(fh) + units = {} + if os.path.isdir(MANIFEST_DIR): + for fn in os.listdir(MANIFEST_DIR): + if fn.endswith(".manifest.json"): + name = fn[: -len(".manifest.json")] + units[name] = { + "active": "active" if name in RUNNING else "inactive", + "enabled": "enabled", + } + save(units) + return units + + +def save(units: dict) -> None: + os.makedirs(os.path.dirname(STATE), exist_ok=True) + with open(STATE, "w") as fh: + json.dump(units, fh) + + +def norm(name: str) -> str: + return name[: -len(".service")] if name.endswith(".service") else name + + +def main() -> int: + args = sys.argv[1:] + if not args: + return 0 + op = args[0] + units = load() + # Anything ending in .service (show) or matching a known unit is a target. + targets = [a for a in args[1:] if a.endswith(".service") or norm(a) in units] + + if op == "show": + blocks = [] + for tgt in targets: + name = norm(tgt) + st = units.get(name, {"active": "inactive", "enabled": "disabled"}) + blocks.append( + f"Id={name}.service\n" + f"UnitFileState={st['enabled']}\n" + f"ActiveState={st['active']}\n" + ) + sys.stdout.write("\n".join(blocks)) + return 0 + + name = norm(targets[0]) if targets else None + if op == "is-active": + state = units.get(name, {}).get("active", "inactive") + print(state) + return 0 if state == "active" else 3 + if op == "is-enabled": + state = units.get(name, {}).get("enabled", "disabled") + print(state) + return 0 if state == "enabled" else 1 + + if name is None or name not in units: + # Unknown unit: succeed quietly (service-manager already gated on manifests). + return 0 + if op in ("start", "restart"): + units[name]["active"] = "active" + elif op == "stop": + units[name]["active"] = "inactive" + elif op == "enable": + units[name]["enabled"] = "enabled" + elif op == "disable": + units[name]["enabled"] = "disabled" + # reset-failed and anything else: no-op success. + save(units) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/configs/mavlink-router.conf b/test/configs/mavlink-router.conf new file mode 100644 index 0000000..8e62019 --- /dev/null +++ b/test/configs/mavlink-router.conf @@ -0,0 +1,21 @@ +[General] +# Test-harness copy of services/mavlink-router/main.conf, editable from the Services +# page (mounted into the container at /etc/ark-os/mavlink-router.conf). mavlink-router +# itself does not run in the x86 services-only harness, so edits here just exercise the +# config editor; the fake FC telemetry comes from test/mavlink_stub.py instead. +TcpServerPort = 0 + +[UartEndpoint FCUSB] +Device = /dev/serial/by-id/usb-ARK_ARK_FMU_v6X.x_0-if00 +Baud = 2000000 +FlowControl = false + +[UdpEndpoint GCS] +Mode = Server +Address = 0.0.0.0 +Port = 14550 + +[UdpEndpoint autopilot_manager] +Mode = Normal +Address = 127.0.0.1 +Port = 14571 diff --git a/test/docker-compose.yml b/test/docker-compose.yml new file mode 100644 index 0000000..11f31df --- /dev/null +++ b/test/docker-compose.yml @@ -0,0 +1,33 @@ +name: ark-os-test + +services: + # The userspace stack in one container (managers + gateway + nginx + fake FC), built + # from the repo. Browse the UI at http://localhost:8080. + arkos: + build: + context: .. + dockerfile: test/Dockerfile + image: ark-os-test:latest + init: true + depends_on: + - go2rtc + ports: + - "8080:80" + volumes: + # Manifests drive the Services page; configs are editable from it. Mounted (not + # baked) so you can tweak them without rebuilding. + - ./manifests:/usr/lib/ark-os/manifests:ro + - ./configs/mavlink-router.conf:/etc/ark-os/mavlink-router.conf + # Same file the go2rtc sidecar reads — editing it in the UI affects the real stream. + - ./go2rtc.test.yaml:/etc/ark-os/go2rtc.yaml + + # Synthetic camera. The official image bundles ffmpeg, so the test pattern source in + # go2rtc.test.yaml works with no /dev/video* device. + go2rtc: + image: alexxit/go2rtc:latest + volumes: + - ./go2rtc.test.yaml:/config/go2rtc.yaml + ports: + # WebRTC media flows browser<->here directly; must be reachable from the host. + - "8555:8555/tcp" + - "8555:8555/udp" diff --git a/test/go2rtc.test.yaml b/test/go2rtc.test.yaml new file mode 100644 index 0000000..c9eb6ae --- /dev/null +++ b/test/go2rtc.test.yaml @@ -0,0 +1,25 @@ +# go2rtc config for the LOCAL TEST HARNESS (no camera). +# +# camera1 is a synthetic ffmpeg test pattern instead of rtsp-server's real RTSP feed, +# so the Video page streams without any /dev/video* device. Swap it for a file loop to +# see "real" footage, e.g.: +# camera1: "ffmpeg:/media/sample.mp4#video=h264#input=-stream_loop -1 -re" +streams: + camera1: "ffmpeg:virtual?video=testsrc&size=1280x720#video=h264" + +# Bound to 0.0.0.0 (not 127.0.0.1 as on-device) because nginx runs in a separate +# container here and proxies /video/ to this one over the compose network. +api: + listen: ":1984" + +# WebRTC media flows directly browser<->container over 8555 (published to the host by +# docker-compose). candidates must be the address the host browser can reach, i.e. the +# published port on localhost — auto-detection would advertise the container's internal +# IP, which the browser can't route to. +webrtc: + listen: ":8555" + candidates: + - "127.0.0.1:8555" + +log: + level: info diff --git a/test/manifests/dds-agent.manifest.json b/test/manifests/dds-agent.manifest.json new file mode 100644 index 0000000..03ba323 --- /dev/null +++ b/test/manifests/dds-agent.manifest.json @@ -0,0 +1,6 @@ +{ + "displayName": "DDS Agent", + "description": "uXRCE-DDS agent bridging PX4 uORB topics to ROS 2.", + "configFile": "", + "visible": true +} diff --git a/test/manifests/go2rtc.manifest.json b/test/manifests/go2rtc.manifest.json new file mode 100644 index 0000000..5d7dd57 --- /dev/null +++ b/test/manifests/go2rtc.manifest.json @@ -0,0 +1,6 @@ +{ + "displayName": "Video Gateway (go2rtc)", + "description": "WebRTC gateway that streams the camera to the web UI's Video page.", + "configFile": "go2rtc.yaml", + "visible": true +} diff --git a/test/manifests/logloader.manifest.json b/test/manifests/logloader.manifest.json new file mode 100644 index 0000000..f0fe783 --- /dev/null +++ b/test/manifests/logloader.manifest.json @@ -0,0 +1,6 @@ +{ + "displayName": "Log Loader", + "description": "Pulls flight logs off the autopilot and uploads them.", + "configFile": "", + "visible": true +} diff --git a/test/manifests/mavlink-router.manifest.json b/test/manifests/mavlink-router.manifest.json new file mode 100644 index 0000000..d060b65 --- /dev/null +++ b/test/manifests/mavlink-router.manifest.json @@ -0,0 +1,6 @@ +{ + "displayName": "Mavlink Router", + "description": "Routes mavlink data between endpoints.", + "configFile": "mavlink-router.conf", + "visible": true +} diff --git a/test/mavlink_stub.py b/test/mavlink_stub.py new file mode 100644 index 0000000..5f43d9e --- /dev/null +++ b/test/mavlink_stub.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Fake flight controller for the local test harness. + +autopilot-manager listens on `udpin:localhost:14571` (it normally receives the FC feed +from mavlink-router). With no real autopilot we send a steady heartbeat plus a little +telemetry straight to that port, so the Autopilot page flips to "connected" and shows +moving values. This is the simplest possible stand-in — it does not answer parameter or +command requests; for that, point autopilot-manager at PX4 SITL instead. +""" +import math +import time + +from pymavlink import mavutil + +TARGET = "udpout:127.0.0.1:14571" + + +def main() -> None: + mav = mavutil.mavlink_connection(TARGET, source_system=1, source_component=1) + print(f"[mavlink-stub] sending heartbeats to {TARGET}", flush=True) + + t0 = time.time() + while True: + t = time.time() - t0 + + mav.mav.heartbeat_send( + mavutil.mavlink.MAV_TYPE_QUADROTOR, + mavutil.mavlink.MAV_AUTOPILOT_PX4, + mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED, + 0, + mavutil.mavlink.MAV_STATE_STANDBY, + ) + + # Battery ~ slowly draining 16.8V -> shown on the UI. + voltage_mv = int(16800 - (t * 2) % 4000) + mav.mav.sys_status_send( + 0, 0, 0, 500, voltage_mv, -1, 78, 0, 0, 0, 0, 0, 0, + ) + + # A little motion so the page clearly isn't frozen. + mav.mav.attitude_send( + int(t * 1000), + 0.15 * math.sin(t), # roll + 0.15 * math.cos(t), # pitch + (t * 0.2) % (2 * math.pi), # yaw + 0.0, 0.0, 0.0, + ) + mav.mav.global_position_int_send( + int(t * 1000), + int(47.3977 * 1e7), int(8.5456 * 1e7), # Zurich-ish + int((488 + 5 * math.sin(t)) * 1000), + int(5 * math.sin(t) * 1000), + 0, 0, 0, + int((t * 10) % 36000), + ) + mav.mav.vfr_hud_send( + 5.0, 5.2, int((t * 2) % 360), 50, 488 + 5 * math.sin(t), 0.5 * math.cos(t), + ) + + time.sleep(0.5) + + +if __name__ == "__main__": + main() diff --git a/test/nginx.conf b/test/nginx.conf new file mode 100644 index 0000000..30bc8e8 --- /dev/null +++ b/test/nginx.conf @@ -0,0 +1,39 @@ +# Test-harness nginx — mirrors frontend/ark-ui.nginx, but the gateway is on localhost +# (same container) and /video/ points at the go2rtc sidecar over the compose network. +server { + listen 80; + server_name localhost; + client_max_body_size 500M; + + location / { + root /var/www/ark-ui/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + location = /index.html { + root /var/www/ark-ui/html; + add_header Cache-Control "no-cache" always; + } + + # API gateway (Express). proxy_buffering off so the managers' SSE streams + # (firmware progress, network stats) flush through immediately. + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 1h; + } + + # Video page -> go2rtc sidecar (WHEP signaling). Media itself flows directly + # browser<->go2rtc over the published 8555, not through here. + location /video/ { + proxy_pass http://go2rtc:1984/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 0000000..4825169 --- /dev/null +++ b/test/run.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Compose-free runner for the local test harness — works with a bare Docker engine +# (no compose plugin / buildx needed). Usage: bash test/run.sh {up|logs|down} +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NET=ark-os-test +APP=ark-os-test-app +CAM=ark-os-test-go2rtc +IMG=ark-os-test:latest + +case "${1:-up}" in + up) + docker build -t "$IMG" -f "$ROOT/test/Dockerfile" "$ROOT" + docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET" + docker rm -f "$CAM" "$APP" >/dev/null 2>&1 || true + + # Synthetic camera. --network-alias go2rtc so the app's nginx can proxy to it. + docker run -d --name "$CAM" --network "$NET" --network-alias go2rtc \ + -v "$ROOT/test/go2rtc.test.yaml:/config/go2rtc.yaml" \ + -p 8555:8555/tcp -p 8555:8555/udp \ + alexxit/go2rtc:latest + + # The userspace stack (managers + gateway + nginx + fake FC). + docker run -d --name "$APP" --network "$NET" --init \ + -p 8080:80 \ + -v "$ROOT/test/manifests:/usr/lib/ark-os/manifests:ro" \ + -v "$ROOT/test/configs/mavlink-router.conf:/etc/ark-os/mavlink-router.conf" \ + -v "$ROOT/test/go2rtc.test.yaml:/etc/ark-os/go2rtc.yaml" \ + "$IMG" + + echo "ARK-OS test UI -> http://localhost:8080 (logs: bash test/run.sh logs)" + ;; + logs) docker logs -f "$APP" ;; + down) + docker rm -f "$APP" "$CAM" >/dev/null 2>&1 || true + docker network rm "$NET" >/dev/null 2>&1 || true + echo "stopped" + ;; + *) echo "usage: bash test/run.sh {up|logs|down}"; exit 1 ;; +esac diff --git a/test/supervisord.conf b/test/supervisord.conf new file mode 100644 index 0000000..808cbe5 --- /dev/null +++ b/test/supervisord.conf @@ -0,0 +1,67 @@ +# Runs the ARK-OS userspace stack in one container, all on shared localhost — mirroring +# the real device, where every service binds 127.0.0.1 and nginx is the only public face. +# (system/service/connection-manager hardcode host 127.0.0.1, so they must co-reside with +# the gateway that proxies to them.) Each program's stdout is forwarded to the container +# stdout so `docker compose logs -f arkos` shows everything. +[supervisord] +nodaemon=true +user=root +logfile=/dev/null +logfile_maxbytes=0 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:ark-ui-backend] +command=/usr/local/bin/node /usr/lib/ark-os/ark-ui-backend/index.js +environment=PORT="3000",NODE_ENV="production" +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:system-manager] +command=python3 system_manager.py +directory=/usr/lib/ark-os/python/system-manager +environment=PORT="3004" +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:service-manager] +command=python3 service_manager.py +directory=/usr/lib/ark-os/python/service-manager +environment=PORT="3002" +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:connection-manager] +command=python3 connection_manager.py +directory=/usr/lib/ark-os/python/connection-manager +environment=PORT="3001" +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:autopilot-manager] +command=python3 autopilot_manager.py --host 127.0.0.1 --port 3003 --connection-string udpin:127.0.0.1:14571 +directory=/usr/lib/ark-os/python/autopilot-manager +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:mavlink-stub] +command=python3 /usr/lib/ark-os/test/mavlink_stub.py +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true