Merge PR #2107 (aepod) into release integration

This commit is contained in:
ruv committed 2026-10-05 11:44:29 -04:00
commit 7955af5bc0
4 files changed
+122 -9

No files matched your search

+4 -1
View File
@@ -141,8 +141,11 @@ RuView turns ordinary WiFi into a contactless sensor. A $9 ESP32 board reads the
```bash
# Option 1: Docker (simulated data, no hardware needed)
docker pull ruvnet/wifi-densepose:latest
docker run -p 3000:3000 ruvnet/wifi-densepose:latest
export RUVIEW_API_TOKEN=$(openssl rand -hex 32) # required; the container exits 64 without it
docker run -p 127.0.0.1:3000:3000 -e RUVIEW_API_TOKEN ruvnet/wifi-densepose:latest
# Open http://localhost:3000
# ESP32 nodes in Docker also need a UDP source guard (RUVIEW_UDP_ALLOW);
# see docs/user-guide.md "Receiving ESP32 frames in Docker".
# Option 2a: Live sensing with ESP32-S3 hardware ($9)
# Flash firmware, provision WiFi, and start sensing:
+25
View File
@@ -37,6 +37,31 @@ services:
# volumes: ["/path/to/models:/app/models"]
# MODELS_DIR=/app/models
- MODELS_DIR=${MODELS_DIR:-data/models}
# Auth (issue #864). The entrypoint refuses to start (exit 64) unless
# RUVIEW_API_TOKEN is set. Export it before `docker compose up`:
# export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
# Bare names pass the host value through only when it is set.
- RUVIEW_API_TOKEN
# ESP32 UDP ingest (ADR-296, issue #2089). The UDP receiver defaults to
# 127.0.0.1, which cannot receive from the published 5005/udp port.
# Set ONE source guard and the entrypoint binds 0.0.0.0:
# RUVIEW_UDP_ALLOW=<node-ip-or-cidr> restrict sources (preferred)
# RUVIEW_UDP_INSECURE_LAN=true accept the spoofing risk
# Docker Desktop and OrbStack on macOS rewrite the source address to the
# bridge gateway, so a LAN-subnet allowlist drops every frame. There,
# allow the gateway instead:
# docker network inspect <project>_default \
# --format '{{(index .IPAM.Config 0).Gateway}}'
# RUVIEW_UDP_ALLOW=<gateway>/32
# The gateway admits anything that reaches the published port, so keep
# 5005/udp off untrusted networks.
- RUVIEW_UDP_ALLOW
- RUVIEW_UDP_INSECURE_LAN
- RUVIEW_UDP_BIND
# Browsing the UI from another machine also needs the host name or IP
# in SENSING_ALLOWED_HOSTS (otherwise 421), and the TCP ports above
# published beyond 127.0.0.1.
- SENSING_ALLOWED_HOSTS
security_opt:
- no-new-privileges:true
cap_drop:
+46
View File
@@ -22,6 +22,10 @@
# Default is `auto`. Set CSI_SOURCE=simulated when you want
# fake data tagged as such; never set it implicitly.
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
# RUVIEW_UDP_ALLOW / RUVIEW_UDP_INSECURE_LAN — ADR-296 UDP source guard.
# Setting either makes the entrypoint bind UDP on 0.0.0.0 so
# ESP32 frames on the published 5005/udp port can arrive.
# RUVIEW_UDP_BIND — explicit UDP bind; overrides the choice above.
set -e
# ── Issue #864: fail-closed on default posture ───────────────────────────────
@@ -102,6 +106,48 @@ esac
# server binary so users can just pass flags:
# docker run <image> --source esp32 --tick-ms 500
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
# ── Issue #2089: UDP CSI ingest posture inside a container ───────────────
# ADR-296 defaults the UDP receiver to 127.0.0.1. Inside a container that
# bind can never receive from a published port, so no node ever appears.
# We bind 0.0.0.0 only when the operator has configured an ADR-296 guard
# (RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN); without one the server
# would refuse a routable bind, so we keep loopback and say why instead.
# An explicit RUVIEW_UDP_BIND or --udp-bind always wins.
case "${RUVIEW_UDP_INSECURE_LAN:-}" in
1|yes|YES|on|ON|TRUE|True) export RUVIEW_UDP_INSECURE_LAN=true ;;
0|no|NO|off|OFF|FALSE|False) export RUVIEW_UDP_INSECURE_LAN=false ;;
esac
__udp_bind_arg=0
__udp_guard_arg=0
for __arg in "$@"; do
case "$__arg" in
--udp-bind|--udp-bind=*) __udp_bind_arg=1 ;;
--udp-allow|--udp-allow=*|--udp-insecure-lan) __udp_guard_arg=1 ;;
esac
done
if [ -z "${RUVIEW_UDP_BIND:-}" ] && [ "$__udp_bind_arg" = 0 ]; then
if [ -n "${RUVIEW_UDP_ALLOW:-}" ] || [ "${RUVIEW_UDP_INSECURE_LAN:-}" = true ] \
|| [ "$__udp_guard_arg" = 1 ]; then
export RUVIEW_UDP_BIND=0.0.0.0
echo "[entrypoint] UDP CSI receiver: binding 0.0.0.0 inside the container" >&2
echo "[entrypoint] (a source guard is set: RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN)." >&2
else
case "${CSI_SOURCE:-auto}" in
simulated|simulate) ;;
*)
echo "[entrypoint] NOTE: UDP CSI receiver stays on 127.0.0.1 (ADR-296 default)." >&2
echo "[entrypoint] Inside a container that bind cannot receive ESP32 frames from" >&2
echo "[entrypoint] a published 5005/udp port, so no node will appear. To ingest:" >&2
echo "[entrypoint] -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> (restrict sources), or" >&2
echo "[entrypoint] -e RUVIEW_UDP_INSECURE_LAN=true (accept spoofing risk)" >&2
echo "[entrypoint] Docker Desktop/OrbStack on macOS rewrite the source to the bridge" >&2
echo "[entrypoint] gateway, so the allowlist must name <gateway>/32, not the LAN subnet." >&2
echo "[entrypoint] See https://github.com/ruvnet/RuView/issues/2089" >&2
;;
esac
fi
fi
set -- /app/sensing-server \
--source "${CSI_SOURCE:-auto}" \
--tick-ms 100 \
+47 -8
View File
@@ -109,7 +109,7 @@ Multi-architecture image (amd64 + arm64). Works on Intel/AMD and Apple Silicon M
| `simulated` | Generate synthetic CSI frames (no hardware required) |
| `wifi` | Host Wi-Fi RSSI (not available inside containers) |
Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wifi-densepose:latest`
Example: `docker run -e RUVIEW_API_TOKEN -e CSI_SOURCE=esp32 -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> -p 127.0.0.1:3000:3000 -p 5005:5005/udp ruvnet/wifi-densepose:latest`. The container exits with code 64 unless `RUVIEW_API_TOKEN` is set, and receives no ESP32 frames until a UDP source guard is set; see [Receiving ESP32 frames in Docker](#receiving-esp32-frames-in-docker).
### From Source (Rust)
@@ -261,8 +261,9 @@ Non-interactive:
### 30-Second Demo (Docker)
```bash
# Pull and run
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
# Pull and run (the container exits with code 64 without an API token)
export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
docker run -p 127.0.0.1:3000:3000 -p 127.0.0.1:3001:3001 -e RUVIEW_API_TOKEN ruvnet/wifi-densepose:latest
# Open the UI in your browser
# http://localhost:3000
@@ -365,11 +366,39 @@ Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for po
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
docker run -p 127.0.0.1:3000:3000 -p 127.0.0.1:3001:3001 -p 5005:5005/udp \
-e RUVIEW_API_TOKEN -e CSI_SOURCE=esp32 -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> \
ruvnet/wifi-densepose:latest
```
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
#### Receiving ESP32 frames in Docker
The sensing server binds its UDP receiver to `127.0.0.1` by default (ADR-296). Inside a container that address cannot receive anything from a published `5005/udp` port, so no node ever appears. The image's entrypoint binds UDP on `0.0.0.0` only when you set a source guard, and prints a note at startup when you have not:
| Variable | Effect |
|----------|--------|
| `RUVIEW_API_TOKEN` | Required. Without it (or `RUVIEW_ALLOW_UNAUTHENTICATED=1`) the container exits with code 64. |
| `RUVIEW_UDP_ALLOW=<ip-or-cidr>` | Accept frames only from these sources. Preferred. |
| `RUVIEW_UDP_INSECURE_LAN=true` | Accept frames from any source. The UDP data plane is not authenticated, so only use this on a trusted network. |
| `RUVIEW_UDP_BIND` | Explicit UDP bind address. Overrides the entrypoint's choice. |
| `SENSING_ALLOWED_HOSTS=<host-ip>` | Needed to open the UI from another machine; otherwise the server answers 421. That also requires publishing the TCP ports beyond `127.0.0.1`. |
**macOS (Docker Desktop, OrbStack):** datagrams reach the container from the Docker bridge gateway, not from the node's LAN address, so an allowlist that names your LAN subnet drops every frame. Allow the gateway instead:
```bash
export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
GW=$(docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}')
docker run --rm -d --name ruview \
-p 127.0.0.1:3000:3000 -p 127.0.0.1:3001:3001 -p 5005:5005/udp \
-e RUVIEW_API_TOKEN -e CSI_SOURCE=esp32 -e RUVIEW_UDP_ALLOW="$GW/32" \
ruvnet/wifi-densepose:latest
curl -s -H "Authorization: Bearer $RUVIEW_API_TOKEN" localhost:3000/api/v1/nodes
```
Allowing the gateway admits anything that can reach the published port, so keep `5005/udp` off untrusted networks. Node identity still comes from the frame payload. With Docker Compose the service runs on the project network (`<project>_default`), so inspect that network's gateway rather than `bridge`. Docker Desktop on Windows has a related source-address problem; see [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
### ESP32 Multistatic Mesh (Advanced)
For higher accuracy with through-wall tracking, deploy 3-6 ESP32-S3 nodes in a **multistatic mesh** configuration. Each node acts as both transmitter and receiver, creating multiple sensing paths through the environment.
@@ -407,13 +436,17 @@ cargo run -p wifi-densepose-sensing-server -- \
# Docker
docker run --rm \
-e RUVIEW_API_TOKEN \
-e CSI_SOURCE=esp32 \
-p 3000:3000 \
-p 3001:3001 \
-e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> \
-p 127.0.0.1:3000:3000 \
-p 127.0.0.1:3001:3001 \
-p 5005:5005/udp \
ruvnet/wifi-densepose:latest
```
On macOS Docker the allowlist must name the bridge gateway; see [Receiving ESP32 frames in Docker](#receiving-esp32-frames-in-docker).
Open the UI from the sensing server, not from a local file:
| View | URL |
@@ -2047,8 +2080,10 @@ Binary size: 990 KB (8MB flash, 52% free) or 773 KB (4MB flash). v0.5.0 adds mmW
# From source
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
# Docker (see "Receiving ESP32 frames in Docker" for the UDP source guard)
docker run -p 127.0.0.1:3000:3000 -p 127.0.0.1:3001:3001 -p 5005:5005/udp \
-e RUVIEW_API_TOKEN -e CSI_SOURCE=esp32 -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> \
ruvnet/wifi-densepose:latest
```
See [ADR-018](../docs/adr/ADR-018-esp32-dev-implementation.md), [ADR-029](../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md), and [Tutorial #34](https://github.com/ruvnet/RuView/issues/34).
@@ -2377,6 +2412,8 @@ For production deployments with both Rust and Python services:
```bash
cd docker
export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
export RUVIEW_UDP_ALLOW=<node-ip-or-cidr> # macOS Docker: <gateway>/32
docker compose up
```
@@ -2384,6 +2421,8 @@ This starts:
- Rust sensing server on ports 3000 (HTTP), 3001 (WS), 5005 (UDP)
- Python legacy server on ports 8080 (HTTP), 8765 (WS)
The sensing server exits with code 64 when `RUVIEW_API_TOKEN` is unset. Without `RUVIEW_UDP_ALLOW` or `RUVIEW_UDP_INSECURE_LAN=true` it starts but receives no ESP32 frames. `docker/docker-compose.yml` passes these variables through from your shell, along with `RUVIEW_UDP_BIND` and `SENSING_ALLOWED_HOSTS`. See [Receiving ESP32 frames in Docker](#receiving-esp32-frames-in-docker).
---
## Testing Firmware Without Hardware (QEMU)