diff --git a/PLATFORM.md b/PLATFORM.md new file mode 100644 index 0000000..5777350 --- /dev/null +++ b/PLATFORM.md @@ -0,0 +1,92 @@ +# Connecting the demos to the selfpatch platform + +Any demo in this repository can show up in the selfpatch platform as a robot. The connection goes through the selfpatch agent: a small container next to the demo that opens an outbound connection to the platform and lets the platform read the demo's ros2_medkit gateway. Nothing on the demo side listens for the platform, so the same steps work when the platform is in the cloud and the demo is behind NAT. + +This page is also written for AI coding assistants. Paste it into the assistant's context before asking it to run a demo against the platform or to build platform UI on top of a demo. + +## What you need + +- A running demo (`./run-demo.sh` in its directory). +- An account on the platform you want to use. +- Docker. The scripts pull the public agent image `ghcr.io/selfpatch/agent:0.1` (amd64 and arm64). `SELFPATCH_AGENT_IMAGE` picks another one. + +## Connect a demo + +1. In the platform: Devices > Add device > "Through the selfpatch agent". Copy the pairing code. It is valid for 10 minutes. +2. In the demo directory: + + ```bash + ./connect-platform.sh + ``` + + Paste the code when asked, or pass it as `PAIRING_CODE=XXXX-XXXX-XXXX-XXXX`. + +3. The device shows as online in the platform within a few seconds. + +The agent runs as the container `selfpatch-agent-` and keeps its credential in a volume of the same name. It restarts with Docker and reconnects on its own. `./connect-platform.sh --disconnect` removes it. Remove the device in the platform as well. + +Available in `sensor_diagnostics`, `ota_nav2_sensor_fix` and `turtlebot3_integration`. For any other demo run `lib/connect-platform.sh `. + +| Variable | Default | Use | +|---|---|---| +| `PLATFORM_URL` | `https://platform.selfpatch.ai` | The platform to pair with, for example a staging deployment | +| `PAIRING_CODE` | asked for | The code from the platform | +| `SELFPATCH_AGENT_IMAGE` | `ghcr.io/selfpatch/agent:0.1` | Agent image | +| `PLATFORM_INSECURE_HTTP` | `0` | `1` allows a plain http platform, for a local dev platform | +| `GATEWAY_CONTAINER` | the demo's gateway container | Which demo instance to connect | + +## A platform running on your machine + +The agent runs in a container, so `localhost` is the container itself. Point it at the host: + +```bash +PLATFORM_URL=http://host.docker.internal: PLATFORM_INSECURE_HTTP=1 ./connect-platform.sh +``` + +On Linux, `host.docker.internal` needs `--add-host=host.docker.internal:host-gateway` or the host's LAN address instead. + +## Several robots + +Every demo instance is its own device in the platform, with its own agent. The sensor demo can run several times side by side: + +```bash +cd demos/sensor_diagnostics +COMPOSE_PROJECT_NAME=robot2 DEMO_CONTAINER=robot2 GATEWAY_PORT=8082 \ + WEB_UI_CONTAINER=robot2_web_ui WEB_UI_PORT=3002 ROS_DOMAIN_ID=41 \ + docker compose up -d --build +GATEWAY_CONTAINER=robot2 ./connect-platform.sh +``` + +To stop it, disconnect the agent first. While it is attached, compose cannot remove the network: + +```bash +GATEWAY_CONTAINER=robot2 ./connect-platform.sh --disconnect +COMPOSE_PROJECT_NAME=robot2 ./stop-demo.sh +``` + +Give each instance its own `ROS_DOMAIN_ID` so the robots do not see each other's ROS graph. The fault scripts take `GATEWAY_URL`, for example `GATEWAY_URL=http://localhost:8082 ./inject-noise.sh`. + +## Making something happen + +| Demo | Faults | Back to normal | +|---|---|---| +| `sensor_diagnostics` | `./inject-noise.sh`, `./inject-drift.sh`, `./inject-nan.sh`, `./inject-failure.sh`, `./inject-fault-scenario.sh` (all sensors at once) | `./restore-normal.sh` | +| `ota_nav2_sensor_fix` | `./send-goal.sh`: the robot drives into the phantom obstacle of the broken lidar, Nav2 aborts | `./publish-fix.sh`, `./apply-fix.sh`, `./clear-fault.sh`, `./send-goal.sh` again | +| `turtlebot3_integration` | `./inject-nav-failure.sh`, `./inject-localization-failure.sh` | `./restore-normal.sh` | + +## What the gateway exposes + +The platform shows what the demo's gateway returns. The full list of routes is in the gateway's own OpenAPI document: `curl http://localhost:8080/api/v1/docs`. The parts a platform view usually needs: + +- Entity tree: `/areas`, `/components`, `/apps`, `/functions`, each with `/{id}` and relations such as `/components/{id}/hosts`. +- Faults: `/faults` (all), `/faults/stream` (server-sent events), `/{entity}/faults`, `/{entity}/faults/{fault_code}` with freeze frames and the recording reference. +- Recordings: `/{entity}/bulk-data` and `/{entity}/bulk-data/rosbags/{id}` (MCAP). +- Live values: `/{entity}/data` and `/{entity}/data/{data_id}`. +- Logs: `/{entity}/logs`. +- Health and version: `/health`, `/version-info`. + +Through the platform the agent forwards read requests (GET) only. Operations, scripts, configuration writes and fault clearing work on the gateway directly (`http://localhost:8080`) but not through the platform. + +## Building platform UI on a demo + +Build against what the gateway actually returns, not against mocked data. Before adding a view, call the endpoint on a running demo and use that response. If a view needs data the gateway does not expose, that is a finding, not something to mock: open an issue in [ros2_medkit](https://github.com/selfpatch/ros2_medkit/issues) with the view you want, the endpoint you tried and its response. Some gaps are by design and some are missing features, and the issue is where that gets decided. diff --git a/README.md b/README.md index 6494bd1..d846283 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,18 @@ cd demos/sensor_diagnostics # or turtlebot3_integration, moveit_pick_place, mul # Follow the README.md in that directory ``` +## Connect a Demo to the selfpatch Platform + +`sensor_diagnostics`, `ota_nav2_sensor_fix` and `turtlebot3_integration` can show up in the selfpatch platform as robots through the selfpatch agent: + +```bash +cd demos/sensor_diagnostics +./run-demo.sh +./connect-platform.sh # paste the pairing code from the platform +``` + +Several robots side by side, a platform on your own machine and what the gateway exposes to the platform: see [PLATFORM.md](PLATFORM.md). + ## Example API Usage All demos expose a SOVD-compliant REST API. Here are some common operations: diff --git a/demos/ota_nav2_sensor_fix/connect-platform.sh b/demos/ota_nav2_sensor_fix/connect-platform.sh new file mode 100755 index 0000000..a3d0d9d --- /dev/null +++ b/demos/ota_nav2_sensor_fix/connect-platform.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Connect this demo to the selfpatch platform through the selfpatch agent. +# See lib/connect-platform.sh for the environment variables. +# +# ./connect-platform.sh pair and start the agent +# ./connect-platform.sh --disconnect stop and remove the agent + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec "${SCRIPT_DIR}/../../lib/connect-platform.sh" "${GATEWAY_CONTAINER:-ota_demo_gateway}" "$@" diff --git a/demos/sensor_diagnostics/connect-platform.sh b/demos/sensor_diagnostics/connect-platform.sh new file mode 100755 index 0000000..f62e03e --- /dev/null +++ b/demos/sensor_diagnostics/connect-platform.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Connect this demo to the selfpatch platform through the selfpatch agent. +# See lib/connect-platform.sh for the environment variables. +# +# ./connect-platform.sh pair and start the agent +# ./connect-platform.sh --disconnect stop and remove the agent + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec "${SCRIPT_DIR}/../../lib/connect-platform.sh" "${GATEWAY_CONTAINER:-sensor_diagnostics_demo}" "$@" diff --git a/demos/sensor_diagnostics/docker-compose.yml b/demos/sensor_diagnostics/docker-compose.yml index a09019f..4026fcf 100644 --- a/demos/sensor_diagnostics/docker-compose.yml +++ b/demos/sensor_diagnostics/docker-compose.yml @@ -4,12 +4,14 @@ services: build: context: . dockerfile: Dockerfile - container_name: sensor_diagnostics_demo + # Names, ports and the ROS domain can be overridden to run more than one + # robot side by side, see "Several robots" in the README. + container_name: ${DEMO_CONTAINER:-sensor_diagnostics_demo} environment: - - ROS_DOMAIN_ID=40 + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-40} - BEACON_MODE=${BEACON_MODE:-none} ports: - - "8080:8080" + - "${GATEWAY_PORT:-8080}:8080" stdin_open: true tty: true # Default command launches the full demo @@ -23,9 +25,9 @@ services: # Web UI for visualization (optional) medkit-web-ui: image: ghcr.io/selfpatch/ros2_medkit_web_ui:latest - container_name: ros2_medkit_web_ui + container_name: ${WEB_UI_CONTAINER:-ros2_medkit_web_ui} ports: - - "3000:80" + - "${WEB_UI_PORT:-3000}:80" depends_on: - sensor-demo diff --git a/demos/turtlebot3_integration/connect-platform.sh b/demos/turtlebot3_integration/connect-platform.sh new file mode 100755 index 0000000..83e7b38 --- /dev/null +++ b/demos/turtlebot3_integration/connect-platform.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Connect this demo to the selfpatch platform through the selfpatch agent. +# See lib/connect-platform.sh for the environment variables. +# +# ./connect-platform.sh pair and start the agent +# ./connect-platform.sh --disconnect stop and remove the agent + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# run-demo.sh --nvidia starts the gateway under a different container name. +if [[ -z "${GATEWAY_CONTAINER:-}" ]]; then + GATEWAY_CONTAINER=turtlebot3_medkit_demo + if [[ "$(docker inspect -f '{{.State.Running}}' turtlebot3_medkit_demo_nvidia 2>/dev/null)" == "true" ]]; then + GATEWAY_CONTAINER=turtlebot3_medkit_demo_nvidia + fi +fi +exec "${SCRIPT_DIR}/../../lib/connect-platform.sh" "$GATEWAY_CONTAINER" "$@" diff --git a/lib/connect-platform.sh b/lib/connect-platform.sh new file mode 100755 index 0000000..386c16d --- /dev/null +++ b/lib/connect-platform.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Connect a running demo to the selfpatch platform through the selfpatch agent. +# Source-free: each demo's connect-platform.sh calls this with its gateway +# container name. +# +# lib/connect-platform.sh pair and start the agent +# lib/connect-platform.sh --disconnect stop and remove the agent +# +# The agent runs as its own container on the demo's Docker network, opens an +# outbound connection to the platform and lets it read the demo's gateway. It +# keeps its credential in a volume, so it survives restarts and reconnects on +# its own. Each demo instance gets its own agent and shows up in the platform +# as its own device. +# +# Environment variables: +# PLATFORM_URL - platform to pair with (default: https://platform.selfpatch.ai) +# PAIRING_CODE - pairing code from the platform (asked for when unset) +# SELFPATCH_AGENT_IMAGE - agent image (default: ghcr.io/selfpatch/agent:0.1) +# PLATFORM_INSECURE_HTTP=1 - allow a plain http platform, e.g. a local dev +# platform at http://host.docker.internal: + +set -eu + +usage() { + echo "Usage: $0 [--disconnect]" +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +GATEWAY_CONTAINER="$1" +ACTION="${2:-connect}" +PLATFORM_URL="${PLATFORM_URL:-https://platform.selfpatch.ai}" +AGENT_IMAGE="${SELFPATCH_AGENT_IMAGE:-ghcr.io/selfpatch/agent:0.1}" +AGENT_NAME="selfpatch-agent-${GATEWAY_CONTAINER}" + +if [[ "$ACTION" == "--disconnect" ]]; then + docker rm -f "$AGENT_NAME" >/dev/null 2>&1 || true + docker volume rm "$AGENT_NAME" >/dev/null 2>&1 || true + echo "Agent ${AGENT_NAME} removed. Remove the device in the platform too." + exit 0 +fi + +if [[ "$(docker inspect -f '{{.State.Running}}' "$GATEWAY_CONTAINER" 2>/dev/null)" != "true" ]]; then + echo "Container ${GATEWAY_CONTAINER} is not running. Start the demo first: ./run-demo.sh" + exit 1 +fi + +NETWORK="$(docker inspect -f '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}' "$GATEWAY_CONTAINER" | awk '{print $1}')" +if [[ -z "$NETWORK" || "$NETWORK" == "host" ]]; then + MEDKIT_URL="http://127.0.0.1:8080" + NETWORK="host" +else + MEDKIT_URL="http://${GATEWAY_CONTAINER}:8080" +fi + +if ! docker image inspect "$AGENT_IMAGE" >/dev/null 2>&1 && ! docker pull -q "$AGENT_IMAGE" >/dev/null; then + echo "Cannot pull ${AGENT_IMAGE}. Set SELFPATCH_AGENT_IMAGE to an agent image you can reach." + exit 1 +fi + +if [[ -z "${PAIRING_CODE:-}" ]]; then + echo "In the platform: Devices > Add device > 'Through the selfpatch agent', then copy the pairing code." + read -r -p "Pairing code: " PAIRING_CODE +fi + +PAIR_ARGS=(pair --platform-url "$PLATFORM_URL" --medkit-url "$MEDKIT_URL") +RUN_ARGS=(run) +if [[ "${PLATFORM_INSECURE_HTTP:-0}" == "1" ]]; then + PAIR_ARGS+=(--insecure-platform-http) + RUN_ARGS+=(--insecure-platform-http) +fi + +docker volume create "$AGENT_NAME" >/dev/null +echo "$PAIRING_CODE" | docker run --rm -i --network "$NETWORK" \ + -e SELFPATCH_AGENT_STATE=/data -v "${AGENT_NAME}:/data" \ + "$AGENT_IMAGE" "${PAIR_ARGS[@]}" + +docker rm -f "$AGENT_NAME" >/dev/null 2>&1 || true +docker run -d --name "$AGENT_NAME" --restart unless-stopped --network "$NETWORK" \ + -e SELFPATCH_AGENT_STATE=/data -v "${AGENT_NAME}:/data" \ + "$AGENT_IMAGE" "${RUN_ARGS[@]}" >/dev/null + +echo "" +echo "Agent ${AGENT_NAME} is running. The demo appears in the platform within a few seconds." +echo "Logs: docker logs -f ${AGENT_NAME}" +echo "Disconnect: GATEWAY_CONTAINER=${GATEWAY_CONTAINER} ./connect-platform.sh --disconnect (in the demo directory)"