From fcde6f51550191d73e5d77bd4bc41ad0c4046c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 30 Oct 2026 06:56:24 +0000 Subject: [PATCH] Let a job choose its AMS slot; soak test for telemetry POST /jobs takes an optional amsSlot (stored in the job metadata) which dispatch passes to the gateway. The camera e2e script gains SOAK_S. --- scripts/e2e-camera-snapshot.sh | 13 +++++++++++++ scripts/real-print-p1p.sh | 4 +++- src/dispatch/index.ts | 2 ++ src/routes/jobs.ts | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/e2e-camera-snapshot.sh b/scripts/e2e-camera-snapshot.sh index 50ae7c5..007aa6c 100755 --- a/scripts/e2e-camera-snapshot.sh +++ b/scripts/e2e-camera-snapshot.sh @@ -148,6 +148,19 @@ if grep -qi 'access_code\|accessCode' "$WORK/backend.log"; then fi ok "the backend log never mentions an access code" +# Optional soak: the first real dispatch froze the proxy ~4.5 min in (an +# MQTT publish that never completed while the fleet lock was held). Staying +# connected well past that and still receiving telemetry is the regression test. +if [ -n "${SOAK_S:-}" ]; then + echo "==> soaking for ${SOAK_S}s: telemetry must keep arriving" + sleep "$SOAK_S" + AGE="$(psql_q "SELECT extract(epoch FROM now() - last_seen_at)::int FROM printers WHERE id='$CAM_UUID'")" + [ -n "$AGE" ] && [ "$AGE" -lt 20 ] || fail "telemetry is ${AGE:-never}s old after the soak - the proxy froze" + ok "telemetry still fresh (${AGE}s old) after ${SOAK_S}s" + code="$(fetch "$WORK/after.jpg")"; [ "$code" = 200 ] || fail "no camera frame after the soak: HTTP $code" + ok "camera still answers after the soak" +fi + [ -n "$SAVE_FRAME" ] && cp "$WORK/f2.jpg" "$SAVE_FRAME" && ok "kept a frame at $SAVE_FRAME" echo echo "PASS - a P1/A1 camera frame travelled printer -> proxy -> backend -> an authenticated viewer." diff --git a/scripts/real-print-p1p.sh b/scripts/real-print-p1p.sh index 473bd17..b28ff7a 100755 --- a/scripts/real-print-p1p.sh +++ b/scripts/real-print-p1p.sh @@ -20,6 +20,8 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" PRINTER_ID="${PRINTER_ID:-p1p}" FILE="${FILE:-auto-test-001.gcode.3mf}" +# 0-based across all AMS units (AMS1 slot 4 is 3). Unset: map by the project's filament. +AMS_SLOT="${AMS_SLOT:-}" PROXY_DIR="${PROXY_DIR:-$REPO_ROOT/../continuum-proxy}" GO2RTC_BIN="${GO2RTC_BIN:-$HOME/.local/bin/go2rtc}" PORT="${PORT:-3060}"; AUTH_PORT="${AUTH_PORT:-3061}" @@ -107,7 +109,7 @@ ok "the cloud sees $PRINTER_ID idle" echo "==> queueing the job" J="$(curl -sf -X POST "$BASE/jobs" -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \ - -d "{\"farmId\":\"$FARM_ID\",\"fileName\":\"$FILE\",\"contentType\":\"application/octet-stream\"}")" + -d "{\"farmId\":\"$FARM_ID\",\"fileName\":\"$FILE\",\"contentType\":\"application/octet-stream\"${AMS_SLOT:+,\"amsSlot\":$AMS_SLOT}}")" JOB_ID="$(printf '%s' "$J" | python3 -c 'import json,sys; print(json.load(sys.stdin)["job"]["id"])')" OBJECT_KEY="$(printf '%s' "$J" | python3 -c 'import json,sys; print(json.load(sys.stdin)["job"]["r2ObjectKey"])')" UPLOAD_URL="$(printf '%s' "$J" | python3 -c 'import json,sys; print(json.load(sys.stdin)["uploadUrl"])')" diff --git a/src/dispatch/index.ts b/src/dispatch/index.ts index 858ceed..6295ab9 100644 --- a/src/dispatch/index.ts +++ b/src/dispatch/index.ts @@ -213,6 +213,8 @@ export async function dispatchTick(): Promise { job_id: job.id, file_name: job.fileName, url, + // Chosen by whoever queued the job; absent means "by the project's filament". + ...(typeof job.metadata?.amsSlot === "number" ? { ams_slot: job.metadata.amsSlot } : {}), }); if (!result.ok) { diff --git a/src/routes/jobs.ts b/src/routes/jobs.ts index c2aea02..9764ed2 100644 --- a/src/routes/jobs.ts +++ b/src/routes/jobs.ts @@ -73,6 +73,7 @@ export const jobRoutes = new Elysia({ prefix: "/jobs" }) farmId: body.farmId, fileName: body.fileName, r2ObjectKey, + metadata: body.amsSlot === undefined ? {} : { amsSlot: body.amsSlot }, // From the verified token, never from the body. It used to be a // `submittedBy` field the caller filled in, which made the audit // trail a suggestion rather than a record. @@ -89,6 +90,9 @@ export const jobRoutes = new Elysia({ prefix: "/jobs" }) farmId: t.String({ format: "uuid" }), fileName: t.String({ minLength: 1, maxLength: 500 }), contentType: t.Optional(t.String()), + // 0-based across all AMS units (AMS1 slot 4 is 3). Absent: the + // gateway maps by the project's own filament. + amsSlot: t.Optional(t.Integer({ minimum: 0, maximum: 15 })), }), detail: { tags: ["jobs"], summary: "Queue a print job and get an upload URL" }, },