From af92cd698b396023e43ecd061bec6a8e4bc3a750 Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:02:04 -0400 Subject: [PATCH 1/7] CV: overhead ArUco localization panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global dashboard panel for detecting DICT_4X4_50 markers via an external overhead camera. Uses getUserMedia + js-aruco2 (ARUCO_4X4_50 dictionary) to match the object-tracking project PDFs. One-shot scan captures a frame, detects all markers, and overlays polygon + heading arrow annotations on the result canvas with a per-marker results table (id, pixel position, heading). Phase 1 — pixel-space only; metric pose is Phase 2. Co-Authored-By: Claude Sonnet 4.6 --- public/app.js | 2 + public/cv-localize.js | 207 ++++++++++++++++++++++++++++++++++++++++++ public/index.html | 17 ++++ public/styles.css | 41 +++++++++ 4 files changed, 267 insertions(+) create mode 100644 public/cv-localize.js diff --git a/public/app.js b/public/app.js index 96d29cd1..fadd975f 100644 --- a/public/app.js +++ b/public/app.js @@ -27,6 +27,7 @@ import { initPhones, broadcastTargetInfo, sendArucoStatus } from "./phones.js"; // (local-llm imports moved to assistant.js where the /install slash lives) import { initHelpers, setHelpersRobotRenderer, renderHelpers } from "./helpers.js"; import { startTracking as startArucoTracking, stopTracking as stopArucoTracking } from "./aruco.js"; +import { initCvLocalize } from "./cv-localize.js"; import { setupServiceWorker, wireInstallMenuItem, wireCheckUpdatesMenuItem, wireHardRefresh, wireDiagnosticsMenuItem, setReportIssueLink, readSwVersion, @@ -1726,6 +1727,7 @@ document.addEventListener("DOMContentLoaded", () => { try { initAssistant(); } catch (err) { console.error("[pip] init failed:", err); } initPhones(); initHelpers(); + try { initCvLocalize(); } catch (err) { console.error("[cv-localize] init failed:", err); } initRobotPresence(); // Lazy-load prepare.js on first click — it's ~230 LOC and touches the File diff --git a/public/cv-localize.js b/public/cv-localize.js new file mode 100644 index 00000000..9b725976 --- /dev/null +++ b/public/cv-localize.js @@ -0,0 +1,207 @@ +// Overhead camera localization via ARUCO_4X4_50 marker detection. +// +// Uses the same js-aruco2 library as aruco.js but with the 4×4 dictionary, +// which matches the DICT_4X4_50 PDFs from the object-tracking project. +// Separate detector instance — no conflict with the per-robot ARUCO tracker. +// +// Phase 1: pixel-space position + heading only. Metric pose (cm/mm via +// known marker size + camera calibration) is Phase 2. + +const CDN = "https://cdn.jsdelivr.net/gh/damianofalcioni/js-aruco2@master/src"; +const SCRIPTS = ["cv.js", "aruco.js"]; +const DICTIONARY = "ARUCO_4X4_50"; + +let _detector = null; +let _detectorPromise = null; +let _stream = null; + +function loadScript(url) { + return new Promise((resolve, reject) => { + if (document.querySelector(`script[data-aruco-src="${url}"]`)) { resolve(); return; } + const s = document.createElement("script"); + s.src = url; + s.dataset.arucoSrc = url; + s.onload = resolve; + s.onerror = () => reject(new Error(`failed to load ${url}`)); + document.head.appendChild(s); + }); +} + +async function ensureDetector() { + if (_detector) return _detector; + if (_detectorPromise) return _detectorPromise; + _detectorPromise = (async () => { + for (const f of SCRIPTS) await loadScript(`${CDN}/${f}`); + if (!window.AR?.Detector) throw new Error("AR.Detector not available after script load"); + _detector = new window.AR.Detector({ dictionaryName: DICTIONARY }); + return _detector; + })(); + try { return await _detectorPromise; } + catch (err) { _detectorPromise = null; throw err; } +} + +async function enumerateCameras() { + if (!navigator.mediaDevices?.enumerateDevices) return []; + const devs = await navigator.mediaDevices.enumerateDevices(); + return devs + .filter(d => d.kind === "videoinput") + .map((d, i) => ({ id: d.deviceId, label: d.label || `Camera ${i + 1}` })); +} + +function drawOverlay(canvasEl, markers) { + const ctx = canvasEl.getContext("2d"); + const minDim = Math.min(canvasEl.width, canvasEl.height); + const arrowLen = minDim * 0.07; + const fontSize = Math.max(12, Math.round(minDim * 0.03)); + for (const m of markers) { + const c = m.corners; + ctx.strokeStyle = "#00e87a"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(c[0].x, c[0].y); + for (let i = 1; i < 4; i++) ctx.lineTo(c[i].x, c[i].y); + ctx.closePath(); + ctx.stroke(); + ctx.strokeStyle = "#ff4466"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(m.cx, m.cy); + ctx.lineTo(m.cx + Math.cos(m.headingRad) * arrowLen, m.cy + Math.sin(m.headingRad) * arrowLen); + ctx.stroke(); + ctx.fillStyle = "#ff4466"; + ctx.beginPath(); + ctx.arc(m.cx, m.cy, 3, 0, Math.PI * 2); + ctx.fill(); + ctx.font = `bold ${fontSize}px sans-serif`; + ctx.fillStyle = "#00e87a"; + ctx.fillText(`id ${m.id}`, m.cx + 6, m.cy - 6); + } +} + +export async function initCvLocalize() { + const panel = document.getElementById("cv-localize-panel"); + if (!panel) return; + + const videoEl = document.getElementById("cv-video"); + const canvasEl = document.getElementById("cv-canvas"); + const selectEl = document.getElementById("cv-camera-select"); + const refreshBtn = document.getElementById("cv-refresh-btn"); + const startBtn = document.getElementById("cv-start-btn"); + const stopBtn = document.getElementById("cv-stop-btn"); + const scanBtn = document.getElementById("cv-scan-btn"); + const statusEl = document.getElementById("cv-status"); + const resultsEl = document.getElementById("cv-results"); + + function setStatus(msg) { statusEl.textContent = msg; } + + async function populateSelect() { + const cameras = await enumerateCameras(); + if (cameras.length === 0) { + selectEl.innerHTML = ``; + startBtn.disabled = true; + return; + } + const current = selectEl.value; + selectEl.innerHTML = cameras + .map(c => ``) + .join(""); + startBtn.disabled = false; + } + + function setRunning(on) { + videoEl.hidden = !on; + startBtn.hidden = on; + stopBtn.hidden = !on; + scanBtn.disabled = !on; + selectEl.disabled = on; + refreshBtn.disabled = on; + if (!on) { + canvasEl.hidden = true; + resultsEl.innerHTML = ""; + } + } + + refreshBtn.addEventListener("click", async () => { + refreshBtn.disabled = true; + await populateSelect(); + refreshBtn.disabled = false; + }); + + startBtn.addEventListener("click", async () => { + startBtn.disabled = true; + setStatus("Starting camera…"); + try { + const deviceId = selectEl.value; + const constraints = { video: deviceId ? { deviceId: { exact: deviceId } } : true }; + _stream = await navigator.mediaDevices.getUserMedia(constraints); + videoEl.srcObject = _stream; + await new Promise(res => { videoEl.onloadedmetadata = res; }); + videoEl.play(); + await populateSelect(); + setRunning(true); + setStatus("Camera ready · click Scan"); + } catch (err) { + startBtn.disabled = false; + setStatus(`Camera error: ${err.message}`); + } + }); + + stopBtn.addEventListener("click", () => { + if (_stream) { _stream.getTracks().forEach(t => t.stop()); _stream = null; } + videoEl.srcObject = null; + setRunning(false); + setStatus("Camera stopped · select a camera and click Start"); + }); + + scanBtn.addEventListener("click", async () => { + scanBtn.disabled = true; + setStatus("Scanning…"); + try { + const detector = await ensureDetector(); + const w = videoEl.videoWidth; + const h = videoEl.videoHeight; + if (!w || !h) throw new Error("no video frame — camera not ready"); + + canvasEl.width = w; + canvasEl.height = h; + const ctx = canvasEl.getContext("2d"); + ctx.drawImage(videoEl, 0, 0, w, h); + const imageData = ctx.getImageData(0, 0, w, h); + + const raw = detector.detect(imageData); + const markers = raw.map(m => { + const c = m.corners; + const cx = (c[0].x + c[1].x + c[2].x + c[3].x) / 4; + const cy = (c[0].y + c[1].y + c[2].y + c[3].y) / 4; + const headingRad = Math.atan2(c[1].y - c[0].y, c[1].x - c[0].x); + return { id: m.id, cx, cy, headingRad, corners: c }; + }); + + drawOverlay(canvasEl, markers); + canvasEl.hidden = false; + + if (markers.length === 0) { + resultsEl.innerHTML = `
No markers detected · try repositioning the camera or improving lighting
`; + } else { + resultsEl.innerHTML = markers.map(m => { + const deg = Math.round(m.headingRad * 180 / Math.PI); + return `
+ id ${m.id} + (${Math.round(m.cx)}, ${Math.round(m.cy)}) px + ${deg >= 0 ? "+" : ""}${deg}° +
`; + }).join(""); + } + + const t = new Date().toLocaleTimeString(); + setStatus(`${markers.length} marker${markers.length === 1 ? "" : "s"} · ${t}`); + } catch (err) { + setStatus(`Scan failed: ${err.message}`); + } finally { + scanBtn.disabled = false; + } + }); + + await populateSelect(); + setStatus("Select a camera and click Start."); +} diff --git a/public/index.html b/public/index.html index cb02fd5c..d7cf3486 100644 --- a/public/index.html +++ b/public/index.html @@ -100,6 +100,23 @@

+
+
+
Overhead localization
+
+ + + + + +
+
+ + +
+
+
+
Log diff --git a/public/styles.css b/public/styles.css index 299644e8..396e47d7 100644 --- a/public/styles.css +++ b/public/styles.css @@ -2771,3 +2771,44 @@ body.phone textarea { .label-modal::backdrop { display: none; } .modal-header button, .modal-footer { display: none; } } + +/* CV Localization panel */ +#cv-localize-panel { max-width: 440px; } +#cv-localize-panel > .row { flex-wrap: wrap; gap: 10px; } +.cv-controls { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } +.cv-select { + padding: 4px 6px; + border: 1px solid var(--border-default); + border-radius: var(--radius); + background: var(--bg); + color: var(--ink); + font-size: 13px; + max-width: 160px; +} +#cv-video { + width: 100%; + max-height: 200px; + object-fit: contain; + display: block; + margin-top: 12px; + border-radius: 4px; + background: #000; +} +#cv-canvas { + width: 100%; + display: block; + margin-top: 12px; + border-radius: 4px; +} +#cv-results { margin-top: 8px; } +.cv-result-row { + display: flex; + align-items: center; + gap: 12px; + padding: 4px 0; + border-bottom: 1px solid var(--border-default); +} +.cv-result-row:last-child { border-bottom: none; } +.cv-marker-id { font-weight: 500; min-width: 36px; } +.cv-no-markers { padding: 4px 0; } +.cv-status { margin-top: 6px; } From e66182a35bdd2b145fc42ce30b45e9bb1642d03e Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:04:11 -0400 Subject: [PATCH 2/7] CV: camera calibration script Captures chessboard frames interactively (SPACE to grab, Q to calibrate) and writes camera_matrix + dist_coeffs to scripts/camera_calibration.json. Board size and square size are configurable at the top of the file. Output feeds Phase 2 metric pose in the overhead localization panel. Co-Authored-By: Claude Sonnet 4.6 --- scripts/calibrate_camera.py | 144 ++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 scripts/calibrate_camera.py diff --git a/scripts/calibrate_camera.py b/scripts/calibrate_camera.py new file mode 100644 index 00000000..5c012ebc --- /dev/null +++ b/scripts/calibrate_camera.py @@ -0,0 +1,144 @@ +""" +Camera calibration for the overhead CV localization system. + +Prints a chessboard pattern (see assets/chessboard.pdf or generate one at +https://calib.io/pages/camera-calibration-pattern-generator), holds it at +various angles in front of the camera, and collects frames. Once you have +enough frames the script computes camera_matrix and dist_coeffs and writes +them to scripts/camera_calibration.json — used by the CV panel for Phase 2 +metric pose estimation. + +Usage: + python scripts/calibrate_camera.py + +Controls: + SPACE — capture the current frame (only saved if corners are found) + Q — quit and run calibration with collected frames + ESC — abort without saving + +Options (edit below): + CAMERA_INDEX — which camera to use (0 = default) + BOARD_COLS — inner corner count along the long edge of the board + BOARD_ROWS — inner corner count along the short edge + SQUARE_SIZE_MM — physical size of one square in millimeters + MIN_FRAMES — minimum captures before calibration is allowed + OUT_FILE — where to write the calibration JSON +""" + +import cv2 +import numpy as np +import json +import os +import sys + +CAMERA_INDEX = 0 +BOARD_COLS = 9 # inner corners, long axis +BOARD_ROWS = 6 # inner corners, short axis +SQUARE_SIZE_MM = 25 # measure your printed square — accuracy matters here +MIN_FRAMES = 15 +OUT_FILE = os.path.join(os.path.dirname(__file__), "camera_calibration.json") + +BOARD_SIZE = (BOARD_COLS, BOARD_ROWS) + +# 3-D object points for one board: (0,0,0), (1,0,0), … in square units, +# then scaled by SQUARE_SIZE_MM so tvecs come out in mm. +objp = np.zeros((BOARD_COLS * BOARD_ROWS, 3), np.float32) +objp[:, :2] = np.mgrid[0:BOARD_COLS, 0:BOARD_ROWS].T.reshape(-1, 2) +objp *= SQUARE_SIZE_MM + +obj_points = [] # 3-D world coords per accepted frame +img_points = [] # 2-D image coords per accepted frame + +cap = cv2.VideoCapture(CAMERA_INDEX) +if not cap.isOpened(): + print(f"ERROR: could not open camera {CAMERA_INDEX}") + sys.exit(1) + +print(f"Camera {CAMERA_INDEX} opened.") +print(f"Board: {BOARD_COLS}×{BOARD_ROWS} inner corners, {SQUARE_SIZE_MM} mm squares.") +print("Hold the chessboard at different angles and distances.") +print(" SPACE — capture frame Q — finish & calibrate ESC — abort") +print() + +criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) + +while True: + ret, frame = cap.read() + if not ret: + print("ERROR: failed to read frame") + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + found, corners = cv2.findChessboardCorners(gray, BOARD_SIZE, None) + + display = frame.copy() + if found: + cv2.drawChessboardCorners(display, BOARD_SIZE, corners, found) + + n = len(obj_points) + color = (0, 200, 0) if found else (0, 80, 200) + label = f"{'Board found' if found else 'No board'} | {n} frame{'s' if n != 1 else ''} captured" + if n < MIN_FRAMES: + label += f" (need {MIN_FRAMES - n} more)" + cv2.putText(display, label, (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2, cv2.LINE_AA) + if n >= MIN_FRAMES: + cv2.putText(display, "Press Q to calibrate", (10, 56), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 1, cv2.LINE_AA) + + cv2.imshow("Calibration", display) + key = cv2.waitKey(1) & 0xFF + + if key == ord("q") or key == ord("Q"): + if n < MIN_FRAMES: + print(f"Need at least {MIN_FRAMES} frames, only have {n}. Keep capturing.") + else: + break + + elif key == 27: # ESC + print("Aborted — no file written.") + cap.release() + cv2.destroyAllWindows() + sys.exit(0) + + elif key == ord(" "): + if not found: + print(" No corners found in this frame — skipped.") + continue + refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) + obj_points.append(objp) + img_points.append(refined) + print(f" Frame {len(obj_points)} captured.") + +cap.release() +cv2.destroyAllWindows() + +print(f"\nCalibrating with {len(obj_points)} frames…") +h, w = gray.shape +rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( + obj_points, img_points, (w, h), None, None +) +print(f"RMS reprojection error: {rms:.4f} px (< 1.0 is good, < 0.5 is excellent)") + +result = { + "camera_matrix": camera_matrix.tolist(), + "dist_coeffs": dist_coeffs.tolist(), + "image_size": [w, h], + "rms_error": round(rms, 6), + "board": { + "cols": BOARD_COLS, + "rows": BOARD_ROWS, + "square_size_mm": SQUARE_SIZE_MM, + }, + "frames_used": len(obj_points), +} + +with open(OUT_FILE, "w") as f: + json.dump(result, f, indent=2) + +print(f"Saved to {OUT_FILE}") +print() +print("camera_matrix:") +print(np.array(result["camera_matrix"])) +print() +print("dist_coeffs:") +print(np.array(result["dist_coeffs"])) From 36c20f70576943b0a2ee018d42d93b28c151fc40 Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:07:44 -0400 Subject: [PATCH 3/7] CV: replace calibration with marker-size pose estimation Drop the chessboard calibration script. Instead, the user enters the printed marker size in mm and POS.Posit estimates 3D pose using a focal length derived from the image dimensions (no calibration file needed). Scan results now show approximate mm floor position alongside heading. Co-Authored-By: Claude Sonnet 4.6 --- public/cv-localize.js | 64 ++++++++++++---- public/index.html | 1 + public/styles.css | 17 +++++ scripts/calibrate_camera.py | 144 ------------------------------------ 4 files changed, 68 insertions(+), 158 deletions(-) delete mode 100644 scripts/calibrate_camera.py diff --git a/public/cv-localize.js b/public/cv-localize.js index 9b725976..902cb6ce 100644 --- a/public/cv-localize.js +++ b/public/cv-localize.js @@ -4,11 +4,13 @@ // which matches the DICT_4X4_50 PDFs from the object-tracking project. // Separate detector instance — no conflict with the per-robot ARUCO tracker. // -// Phase 1: pixel-space position + heading only. Metric pose (cm/mm via -// known marker size + camera calibration) is Phase 2. +// Metric pose is computed via POS.Posit using the known printed marker size. +// Focal length is estimated from the image dimensions (assumes a typical +// 60-70° FOV webcam). No calibration file needed — accuracy is ~5-15%, +// which is sufficient for robot position correction. const CDN = "https://cdn.jsdelivr.net/gh/damianofalcioni/js-aruco2@master/src"; -const SCRIPTS = ["cv.js", "aruco.js"]; +const SCRIPTS = ["cv.js", "aruco.js", "posit1.js"]; const DICTIONARY = "ARUCO_4X4_50"; let _detector = null; @@ -48,6 +50,33 @@ async function enumerateCameras() { .map((d, i) => ({ id: d.deviceId, label: d.label || `Camera ${i + 1}` })); } +// Estimate focal length in pixels from image dimensions. Assumes a ~70° FOV +// lens, which covers most consumer webcams. The estimate is good enough for +// approximate pose when a calibration file isn't available. +function estimateFocalLength(w, h) { + return Math.max(w, h) * 0.85; +} + +// Returns approximate {x, y, z, headingDeg} in the same units as markerSizeMm. +// x/y are the floor position relative to the camera center; z is distance from +// the camera lens. headingDeg uses the corner-edge convention (corner 0→1). +function estimatePose(corners, w, h, markerSizeMm) { + if (!window.POS?.Posit) return null; + const cx = w / 2; + const cy = h / 2; + // POS.Posit expects corners relative to image center, Y pointing up. + const centered = corners.map(c => ({ x: c.x - cx, y: -(c.y - cy) })); + const focalLength = estimateFocalLength(w, h); + try { + const posit = new window.POS.Posit(markerSizeMm, focalLength); + const pose = posit.pose(centered); + const [x, y, z] = pose.bestTranslation; + return { x: Math.round(x), y: Math.round(y), z: Math.round(z) }; + } catch { + return null; + } +} + function drawOverlay(canvasEl, markers) { const ctx = canvasEl.getContext("2d"); const minDim = Math.min(canvasEl.width, canvasEl.height); @@ -79,18 +108,19 @@ function drawOverlay(canvasEl, markers) { } export async function initCvLocalize() { - const panel = document.getElementById("cv-localize-panel"); + const panel = document.getElementById("cv-localize-panel"); if (!panel) return; - const videoEl = document.getElementById("cv-video"); - const canvasEl = document.getElementById("cv-canvas"); - const selectEl = document.getElementById("cv-camera-select"); + const videoEl = document.getElementById("cv-video"); + const canvasEl = document.getElementById("cv-canvas"); + const selectEl = document.getElementById("cv-camera-select"); + const sizeEl = document.getElementById("cv-marker-size"); const refreshBtn = document.getElementById("cv-refresh-btn"); - const startBtn = document.getElementById("cv-start-btn"); - const stopBtn = document.getElementById("cv-stop-btn"); - const scanBtn = document.getElementById("cv-scan-btn"); - const statusEl = document.getElementById("cv-status"); - const resultsEl = document.getElementById("cv-results"); + const startBtn = document.getElementById("cv-start-btn"); + const stopBtn = document.getElementById("cv-stop-btn"); + const scanBtn = document.getElementById("cv-scan-btn"); + const statusEl = document.getElementById("cv-status"); + const resultsEl = document.getElementById("cv-results"); function setStatus(msg) { statusEl.textContent = msg; } @@ -162,6 +192,8 @@ export async function initCvLocalize() { const h = videoEl.videoHeight; if (!w || !h) throw new Error("no video frame — camera not ready"); + const markerSizeMm = Math.max(1, parseFloat(sizeEl.value) || 100); + canvasEl.width = w; canvasEl.height = h; const ctx = canvasEl.getContext("2d"); @@ -174,7 +206,8 @@ export async function initCvLocalize() { const cx = (c[0].x + c[1].x + c[2].x + c[3].x) / 4; const cy = (c[0].y + c[1].y + c[2].y + c[3].y) / 4; const headingRad = Math.atan2(c[1].y - c[0].y, c[1].x - c[0].x); - return { id: m.id, cx, cy, headingRad, corners: c }; + const pose = estimatePose(c, w, h, markerSizeMm); + return { id: m.id, cx, cy, headingRad, corners: c, pose }; }); drawOverlay(canvasEl, markers); @@ -185,9 +218,12 @@ export async function initCvLocalize() { } else { resultsEl.innerHTML = markers.map(m => { const deg = Math.round(m.headingRad * 180 / Math.PI); + const poseStr = m.pose + ? `${m.pose.x}, ${m.pose.y} mm` + : `(${Math.round(m.cx)}, ${Math.round(m.cy)}) px`; return `
id ${m.id} - (${Math.round(m.cx)}, ${Math.round(m.cy)}) px + ${poseStr} ${deg >= 0 ? "+" : ""}${deg}°
`; }).join(""); diff --git a/public/index.html b/public/index.html index d7cf3486..9448d2f0 100644 --- a/public/index.html +++ b/public/index.html @@ -106,6 +106,7 @@

+ diff --git a/public/styles.css b/public/styles.css index 396e47d7..c27d4942 100644 --- a/public/styles.css +++ b/public/styles.css @@ -2785,6 +2785,23 @@ body.phone textarea { font-size: 13px; max-width: 160px; } +.cv-size-label { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: var(--ink-muted); +} +.cv-size-input { + width: 52px; + padding: 4px 6px; + border: 1px solid var(--border-default); + border-radius: var(--radius); + background: var(--bg); + color: var(--ink); + font-size: 13px; + text-align: right; +} #cv-video { width: 100%; max-height: 200px; diff --git a/scripts/calibrate_camera.py b/scripts/calibrate_camera.py deleted file mode 100644 index 5c012ebc..00000000 --- a/scripts/calibrate_camera.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Camera calibration for the overhead CV localization system. - -Prints a chessboard pattern (see assets/chessboard.pdf or generate one at -https://calib.io/pages/camera-calibration-pattern-generator), holds it at -various angles in front of the camera, and collects frames. Once you have -enough frames the script computes camera_matrix and dist_coeffs and writes -them to scripts/camera_calibration.json — used by the CV panel for Phase 2 -metric pose estimation. - -Usage: - python scripts/calibrate_camera.py - -Controls: - SPACE — capture the current frame (only saved if corners are found) - Q — quit and run calibration with collected frames - ESC — abort without saving - -Options (edit below): - CAMERA_INDEX — which camera to use (0 = default) - BOARD_COLS — inner corner count along the long edge of the board - BOARD_ROWS — inner corner count along the short edge - SQUARE_SIZE_MM — physical size of one square in millimeters - MIN_FRAMES — minimum captures before calibration is allowed - OUT_FILE — where to write the calibration JSON -""" - -import cv2 -import numpy as np -import json -import os -import sys - -CAMERA_INDEX = 0 -BOARD_COLS = 9 # inner corners, long axis -BOARD_ROWS = 6 # inner corners, short axis -SQUARE_SIZE_MM = 25 # measure your printed square — accuracy matters here -MIN_FRAMES = 15 -OUT_FILE = os.path.join(os.path.dirname(__file__), "camera_calibration.json") - -BOARD_SIZE = (BOARD_COLS, BOARD_ROWS) - -# 3-D object points for one board: (0,0,0), (1,0,0), … in square units, -# then scaled by SQUARE_SIZE_MM so tvecs come out in mm. -objp = np.zeros((BOARD_COLS * BOARD_ROWS, 3), np.float32) -objp[:, :2] = np.mgrid[0:BOARD_COLS, 0:BOARD_ROWS].T.reshape(-1, 2) -objp *= SQUARE_SIZE_MM - -obj_points = [] # 3-D world coords per accepted frame -img_points = [] # 2-D image coords per accepted frame - -cap = cv2.VideoCapture(CAMERA_INDEX) -if not cap.isOpened(): - print(f"ERROR: could not open camera {CAMERA_INDEX}") - sys.exit(1) - -print(f"Camera {CAMERA_INDEX} opened.") -print(f"Board: {BOARD_COLS}×{BOARD_ROWS} inner corners, {SQUARE_SIZE_MM} mm squares.") -print("Hold the chessboard at different angles and distances.") -print(" SPACE — capture frame Q — finish & calibrate ESC — abort") -print() - -criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) - -while True: - ret, frame = cap.read() - if not ret: - print("ERROR: failed to read frame") - break - - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - found, corners = cv2.findChessboardCorners(gray, BOARD_SIZE, None) - - display = frame.copy() - if found: - cv2.drawChessboardCorners(display, BOARD_SIZE, corners, found) - - n = len(obj_points) - color = (0, 200, 0) if found else (0, 80, 200) - label = f"{'Board found' if found else 'No board'} | {n} frame{'s' if n != 1 else ''} captured" - if n < MIN_FRAMES: - label += f" (need {MIN_FRAMES - n} more)" - cv2.putText(display, label, (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2, cv2.LINE_AA) - if n >= MIN_FRAMES: - cv2.putText(display, "Press Q to calibrate", (10, 56), - cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 1, cv2.LINE_AA) - - cv2.imshow("Calibration", display) - key = cv2.waitKey(1) & 0xFF - - if key == ord("q") or key == ord("Q"): - if n < MIN_FRAMES: - print(f"Need at least {MIN_FRAMES} frames, only have {n}. Keep capturing.") - else: - break - - elif key == 27: # ESC - print("Aborted — no file written.") - cap.release() - cv2.destroyAllWindows() - sys.exit(0) - - elif key == ord(" "): - if not found: - print(" No corners found in this frame — skipped.") - continue - refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) - obj_points.append(objp) - img_points.append(refined) - print(f" Frame {len(obj_points)} captured.") - -cap.release() -cv2.destroyAllWindows() - -print(f"\nCalibrating with {len(obj_points)} frames…") -h, w = gray.shape -rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( - obj_points, img_points, (w, h), None, None -) -print(f"RMS reprojection error: {rms:.4f} px (< 1.0 is good, < 0.5 is excellent)") - -result = { - "camera_matrix": camera_matrix.tolist(), - "dist_coeffs": dist_coeffs.tolist(), - "image_size": [w, h], - "rms_error": round(rms, 6), - "board": { - "cols": BOARD_COLS, - "rows": BOARD_ROWS, - "square_size_mm": SQUARE_SIZE_MM, - }, - "frames_used": len(obj_points), -} - -with open(OUT_FILE, "w") as f: - json.dump(result, f, indent=2) - -print(f"Saved to {OUT_FILE}") -print() -print("camera_matrix:") -print(np.array(result["camera_matrix"])) -print() -print("dist_coeffs:") -print(np.array(result["dist_coeffs"])) From b66cd9c2edb2d1c48ae2bca34a5af7e257688f99 Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:12:03 -0400 Subject: [PATCH 4/7] CV: add printable marker PDFs and download links Copy DICT_4X4_50 marker sheets into public/assets/ and surface them as Sheet 1 / Sheet 2 links in the overhead localization panel. Also register application/pdf in the dev server MIME table so they open correctly. Co-Authored-By: Claude Sonnet 4.6 --- public/assets/aruco_markers_0.pdf | Bin 0 -> 132874 bytes public/assets/aruco_markers_1.pdf | Bin 0 -> 131302 bytes public/index.html | 1 + scripts/serve.js | 1 + 4 files changed, 2 insertions(+) create mode 100644 public/assets/aruco_markers_0.pdf create mode 100644 public/assets/aruco_markers_1.pdf diff --git a/public/assets/aruco_markers_0.pdf b/public/assets/aruco_markers_0.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9dd0765ad718a0611cd716a9eadee3e2474aede6 GIT binary patch literal 132874 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 Iz?$=Fc=5J$8(?teT3o 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 lz*IGj9oIOi;4l
+
Print markers: Sheet 1 · Sheet 2 — tape one flat on top of each robot.
diff --git a/scripts/serve.js b/scripts/serve.js index 9cce67b7..e464d422 100755 --- a/scripts/serve.js +++ b/scripts/serve.js @@ -32,6 +32,7 @@ const MIME = { '.woff2': 'font/woff2', '.wasm': 'application/wasm', '.map': 'application/json', + '.pdf': 'application/pdf', }; const server = http.createServer((req, res) => { From aa4fd6b6d6c0d68cbc39fec68c83398527b20b93 Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:21:06 -0400 Subject: [PATCH 5/7] CV: periodic position update + robot state write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an interval selector (manual / 5s / 10s / 30s / 60s) that auto-scans on a timer when the camera is running. Each scan maps marker ID to robot by index (marker 0 → first paired robot, etc.), writes the ground-truth pose to entry.cvPosition {x, y, headingDeg, updatedAt}, and shows the robot name in the results table. Robots not in frame are left at their last known position; out-of-range marker IDs are silently ignored. Co-Authored-By: Claude Sonnet 4.6 --- public/cv-localize.js | 178 ++++++++++++++++++++++++++---------------- public/index.html | 7 ++ public/styles.css | 2 + 3 files changed, 121 insertions(+), 66 deletions(-) diff --git a/public/cv-localize.js b/public/cv-localize.js index 902cb6ce..41484941 100644 --- a/public/cv-localize.js +++ b/public/cv-localize.js @@ -4,10 +4,15 @@ // which matches the DICT_4X4_50 PDFs from the object-tracking project. // Separate detector instance — no conflict with the per-robot ARUCO tracker. // -// Metric pose is computed via POS.Posit using the known printed marker size. -// Focal length is estimated from the image dimensions (assumes a typical -// 60-70° FOV webcam). No calibration file needed — accuracy is ~5-15%, -// which is sufficient for robot position correction. +// Marker ID is used as a direct index into the ordered robot list: +// marker 0 → first paired robot, marker 1 → second, etc. +// Robots not in frame are left at their last known cvPosition. Marker IDs +// with no corresponding robot are silently ignored. +// +// Metric pose via POS.Posit uses the known printed marker size and a focal +// length estimated from image dimensions — no calibration file needed. + +import { state } from "./state.js"; const CDN = "https://cdn.jsdelivr.net/gh/damianofalcioni/js-aruco2@master/src"; const SCRIPTS = ["cv.js", "aruco.js", "posit1.js"]; @@ -16,6 +21,7 @@ const DICTIONARY = "ARUCO_4X4_50"; let _detector = null; let _detectorPromise = null; let _stream = null; +let _intervalId = null; function loadScript(url) { return new Promise((resolve, reject) => { @@ -50,21 +56,18 @@ async function enumerateCameras() { .map((d, i) => ({ id: d.deviceId, label: d.label || `Camera ${i + 1}` })); } -// Estimate focal length in pixels from image dimensions. Assumes a ~70° FOV -// lens, which covers most consumer webcams. The estimate is good enough for -// approximate pose when a calibration file isn't available. +function getOrderedRobots() { + return [...state.devices.values()]; +} + function estimateFocalLength(w, h) { return Math.max(w, h) * 0.85; } -// Returns approximate {x, y, z, headingDeg} in the same units as markerSizeMm. -// x/y are the floor position relative to the camera center; z is distance from -// the camera lens. headingDeg uses the corner-edge convention (corner 0→1). function estimatePose(corners, w, h, markerSizeMm) { if (!window.POS?.Posit) return null; const cx = w / 2; const cy = h / 2; - // POS.Posit expects corners relative to image center, Y pointing up. const centered = corners.map(c => ({ x: c.x - cx, y: -(c.y - cy) })); const focalLength = estimateFocalLength(w, h); try { @@ -111,16 +114,17 @@ export async function initCvLocalize() { const panel = document.getElementById("cv-localize-panel"); if (!panel) return; - const videoEl = document.getElementById("cv-video"); - const canvasEl = document.getElementById("cv-canvas"); - const selectEl = document.getElementById("cv-camera-select"); - const sizeEl = document.getElementById("cv-marker-size"); - const refreshBtn = document.getElementById("cv-refresh-btn"); - const startBtn = document.getElementById("cv-start-btn"); - const stopBtn = document.getElementById("cv-stop-btn"); - const scanBtn = document.getElementById("cv-scan-btn"); - const statusEl = document.getElementById("cv-status"); - const resultsEl = document.getElementById("cv-results"); + const videoEl = document.getElementById("cv-video"); + const canvasEl = document.getElementById("cv-canvas"); + const selectEl = document.getElementById("cv-camera-select"); + const sizeEl = document.getElementById("cv-marker-size"); + const intervalEl = document.getElementById("cv-interval-select"); + const refreshBtn = document.getElementById("cv-refresh-btn"); + const startBtn = document.getElementById("cv-start-btn"); + const stopBtn = document.getElementById("cv-stop-btn"); + const scanBtn = document.getElementById("cv-scan-btn"); + const statusEl = document.getElementById("cv-status"); + const resultsEl = document.getElementById("cv-results"); function setStatus(msg) { statusEl.textContent = msg; } @@ -138,54 +142,35 @@ export async function initCvLocalize() { startBtn.disabled = false; } + function stopPeriodicScan() { + if (_intervalId) { clearInterval(_intervalId); _intervalId = null; } + } + + function startPeriodicScan(ms) { + stopPeriodicScan(); + if (ms <= 0) return; + _intervalId = setInterval(() => doScan({ silent: true }), ms); + } + function setRunning(on) { videoEl.hidden = !on; startBtn.hidden = on; stopBtn.hidden = !on; scanBtn.disabled = !on; + intervalEl.disabled = !on; selectEl.disabled = on; refreshBtn.disabled = on; if (!on) { + stopPeriodicScan(); canvasEl.hidden = true; resultsEl.innerHTML = ""; } } - refreshBtn.addEventListener("click", async () => { - refreshBtn.disabled = true; - await populateSelect(); - refreshBtn.disabled = false; - }); - - startBtn.addEventListener("click", async () => { - startBtn.disabled = true; - setStatus("Starting camera…"); - try { - const deviceId = selectEl.value; - const constraints = { video: deviceId ? { deviceId: { exact: deviceId } } : true }; - _stream = await navigator.mediaDevices.getUserMedia(constraints); - videoEl.srcObject = _stream; - await new Promise(res => { videoEl.onloadedmetadata = res; }); - videoEl.play(); - await populateSelect(); - setRunning(true); - setStatus("Camera ready · click Scan"); - } catch (err) { - startBtn.disabled = false; - setStatus(`Camera error: ${err.message}`); - } - }); - - stopBtn.addEventListener("click", () => { - if (_stream) { _stream.getTracks().forEach(t => t.stop()); _stream = null; } - videoEl.srcObject = null; - setRunning(false); - setStatus("Camera stopped · select a camera and click Start"); - }); - - scanBtn.addEventListener("click", async () => { - scanBtn.disabled = true; - setStatus("Scanning…"); + // Core scan: capture frame, detect markers, update state, render results. + // `silent` suppresses the "Scanning…" flash for periodic auto-scans. + async function doScan({ silent = false } = {}) { + if (!silent) setStatus("Scanning…"); try { const detector = await ensureDetector(); const w = videoEl.videoWidth; @@ -201,13 +186,29 @@ export async function initCvLocalize() { const imageData = ctx.getImageData(0, 0, w, h); const raw = detector.detect(imageData); + const robots = getOrderedRobots(); + const now = Date.now(); + const markers = raw.map(m => { const c = m.corners; const cx = (c[0].x + c[1].x + c[2].x + c[3].x) / 4; const cy = (c[0].y + c[1].y + c[2].y + c[3].y) / 4; const headingRad = Math.atan2(c[1].y - c[0].y, c[1].x - c[0].x); const pose = estimatePose(c, w, h, markerSizeMm); - return { id: m.id, cx, cy, headingRad, corners: c, pose }; + const robot = robots[m.id] || null; + + // Write ground-truth position back to the robot's state entry so the + // pose controller (and Pip) can read it without depending on odometry. + if (robot && pose) { + robot.cvPosition = { + x: pose.x, y: pose.y, + headingDeg: Math.round(headingRad * 180 / Math.PI), + markerSizeMm, + updatedAt: now, + }; + } + + return { id: m.id, cx, cy, headingRad, corners: c, pose, robot }; }); drawOverlay(canvasEl, markers); @@ -218,26 +219,71 @@ export async function initCvLocalize() { } else { resultsEl.innerHTML = markers.map(m => { const deg = Math.round(m.headingRad * 180 / Math.PI); - const poseStr = m.pose - ? `${m.pose.x}, ${m.pose.y} mm` - : `(${Math.round(m.cx)}, ${Math.round(m.cy)}) px`; + const robotName = m.robot ? m.robot.name : `no robot at index ${m.id}`; + const posStr = m.pose + ? `${m.pose.x}, ${m.pose.y} mm` + : `${Math.round(m.cx)}, ${Math.round(m.cy)} px`; return `
id ${m.id} - ${poseStr} + ${robotName} + ${posStr} ${deg >= 0 ? "+" : ""}${deg}°
`; }).join(""); } - const t = new Date().toLocaleTimeString(); - setStatus(`${markers.length} marker${markers.length === 1 ? "" : "s"} · ${t}`); + const t = new Date(now).toLocaleTimeString(); + const auto = _intervalId ? "Auto · " : ""; + const updated = markers.filter(m => m.robot && m.pose).length; + setStatus(`${auto}${markers.length} marker${markers.length === 1 ? "" : "s"} · ${updated} robot${updated === 1 ? "" : "s"} updated · ${t}`); + } catch (err) { + if (!silent) setStatus(`Scan failed: ${err.message}`); + } + } + + refreshBtn.addEventListener("click", async () => { + refreshBtn.disabled = true; + await populateSelect(); + refreshBtn.disabled = false; + }); + + startBtn.addEventListener("click", async () => { + startBtn.disabled = true; + setStatus("Starting camera…"); + try { + const deviceId = selectEl.value; + const constraints = { video: deviceId ? { deviceId: { exact: deviceId } } : true }; + _stream = await navigator.mediaDevices.getUserMedia(constraints); + videoEl.srcObject = _stream; + await new Promise(res => { videoEl.onloadedmetadata = res; }); + videoEl.play(); + await populateSelect(); + setRunning(true); + const ms = parseInt(intervalEl.value) || 0; + startPeriodicScan(ms); + setStatus(ms > 0 ? `Camera ready · auto-scanning every ${intervalEl.options[intervalEl.selectedIndex].text}` : "Camera ready · click Scan"); } catch (err) { - setStatus(`Scan failed: ${err.message}`); - } finally { - scanBtn.disabled = false; + startBtn.disabled = false; + setStatus(`Camera error: ${err.message}`); } }); + stopBtn.addEventListener("click", () => { + if (_stream) { _stream.getTracks().forEach(t => t.stop()); _stream = null; } + videoEl.srcObject = null; + setRunning(false); + setStatus("Camera stopped · select a camera and click Start"); + }); + + intervalEl.addEventListener("change", () => { + if (!_stream) return; + const ms = parseInt(intervalEl.value) || 0; + startPeriodicScan(ms); + setStatus(ms > 0 ? `Auto-scanning every ${intervalEl.options[intervalEl.selectedIndex].text}` : "Manual mode · click Scan"); + }); + + scanBtn.addEventListener("click", () => doScan({ silent: false })); + await populateSelect(); setStatus("Select a camera and click Start."); } diff --git a/public/index.html b/public/index.html index 2d188a30..01b309b9 100644 --- a/public/index.html +++ b/public/index.html @@ -107,6 +107,13 @@

+ diff --git a/public/styles.css b/public/styles.css index c27d4942..6c3b6ecf 100644 --- a/public/styles.css +++ b/public/styles.css @@ -2827,5 +2827,7 @@ body.phone textarea { } .cv-result-row:last-child { border-bottom: none; } .cv-marker-id { font-weight: 500; min-width: 36px; } +.cv-robot-name { flex: 1; font-size: 13px; } +.cv-no-robot { color: var(--ink-muted); font-style: italic; } .cv-no-markers { padding: 4px 0; } .cv-status { margin-top: 6px; } From 597aaf2af7e4fc8d78bbadc18cf72e4a43d8f286 Mon Sep 17 00:00:00 2001 From: JaredBaileyDuke Date: Mon, 11 May 2026 20:22:14 -0400 Subject: [PATCH 6/7] CV: update scan intervals to 1s / 2s / 5s / 10s --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 01b309b9..cde7b0a1 100644 --- a/public/index.html +++ b/public/index.html @@ -109,10 +109,10 @@

From 0d9f086424d57dc47580c3e6277d14b264d35c93 Mon Sep 17 00:00:00 2001 From: Jonas Neves <72263638+jonasneves@users.noreply.github.com> Date: Wed, 13 May 2026 08:42:16 -0400 Subject: [PATCH 7/7] CV: overhead ArUco as a helper, not a panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframe: ArUco detection is a consumer of camera frames, not a UI surface of its own. The original PR added a dedicated card with its own camera selector, video element, and decode loop — duplicating the helpers card whenever a paired phone was the source. This collapses the two into one: every camera (phone or local) is a helper, and a "Camera role" picker on each helper card designates one as the overhead localizer. - `aruco.js` is now headless: `setOverheadSource(videoEl, {onResult})` + `clearOverheadSource()`. No DOM ownership, no second decoder. - Helper cards carry a single role select (Operator / Overhead / Mount on robot for phones; Idle / Overhead for local cams). Mutually exclusive across the whole helpers list. - SVG overlay paints detected markers on the helper's existing preview tile. The "patchArucoOverlay" shape from the deleted phone-on-robot aruco.js was correct — only the geometry was wrong. Pattern revived against the right surface. - Local cameras enumerate via `getUserMedia` and migrate empty deviceIds to real ones after permission grant; auto-resume on reload when permission is already granted, silent fail otherwise. - Multi-robot ready: `entry.arucoMarkerId` persists per-robot binding (settable via `window.bindArucoMarker`); positional fallback only when no entry has claimed a marker id. The detection loop iterates over all visible markers per scan, so two robots in frame get two `arucoPosition` updates in the same tick — the substrate for the multi-robot orchestration direction in `.claude/CLAUDE.md`. - Staleness contract documented: producer writes `updatedAt`, consumer is responsible for gating. The motion-planner PR will need to honor this when it rebases onto `arucoPosition`. Deletions: - `public/aruco.js` (the phone-on-robot tracker, wired but unproven). - `public/cv-localize.js` (the dedicated CV panel). - `
` and its controls in `index.html`. - `sendArucoStatus` + the `aruco-status` WebRTC channel + the phone-side `aruco-lock` box (phone-on-robot lifecycle). Bug fixes folded in: - js-aruco2's `posit1.js` does `require('./svd')` in browser context; loading `svd.js` first satisfies the `this.SVD ||` short-circuit so the require branch never runs. - Dictionary name: `ARUCO_4X4_50` doesn't exist in js-aruco2; switched to `ARUCO_4X4_1000` and explicitly loaded its dictionary file. First 50 codes match the OpenCV `DICT_4X4_50` PDFs in `/assets/`. - `getUserMedia({video:{deviceId:{exact:""}}})` fails when permission hasn't been granted yet (`enumerateDevices` returns empty deviceIds pre-permission). Fall back to `{video:true}` and use the resolved track's `getSettings().deviceId` to migrate the entry. - Race: in-flight `getUserMedia` no longer leaks a stream if the user switches role mid-await. - Track-ended: OS-level revoke / USB unplug now flips the role back to Idle instead of leaving the detector chewing on dead frames. - Stale dropdown after reload: `currentRole` now requires `live`, matching the phone variant's honesty. UI polish: - Helper cards drop the phone-on-robot SVG/help/status divs. - Helpers section has a heading; cards have proper card chrome matching the robot cards above, without re-using the green `.robot.status-connected` left-stripe (different semantic, would have stolen robot identity). - Camera-role select gets a custom chevron via inline SVG so it doesn't read as a debug field. - Print-marker affordance ("Sheet 1 / Sheet 2") is permanent under the role row when overhead is designated. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/CLAUDE.md | Bin 11829 -> 11898 bytes .claude/notes.md | Bin 21545 -> 22843 bytes public/app.js | 104 +---------- public/aruco.js | 250 +++++++++++++++----------- public/cv-localize.js | 289 ------------------------------ public/helpers.js | 408 +++++++++++++++++++++++++++++++++++++++--- public/index.html | 30 +--- public/mobile.js | 16 -- public/phone.html | 6 - public/phones.js | 9 - public/settings.js | 7 +- public/state.js | 12 +- public/styles.css | 271 ++++++++++++++-------------- public/sw.js | 2 +- 14 files changed, 689 insertions(+), 715 deletions(-) delete mode 100644 public/cv-localize.js diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6da8687228703ce5a844454cbd8f92dd2ff9b454..30cbda94326c05f52b4cf114f10994c1c8e5e58b 100644 GIT binary patch literal 11898 zcmV-=E``wmM@dveQdv+`0A;jLaM5YD>6Qk0>X*c%R~vBLq%u^$(e3EuTC9-_AFnwa zNDzlz67It1dsQYlW#xw<5!2+5&j7qibJb9qv&{w0_om$}8ciesIK}+T|1oZ$zf#jV zBVbp{*U%mF3+<|bW@JBqS!wHeV!VZktr2b35cr&g} zsnx_HBcqZY$kyG9zN8xRu`uEGw6v<{L4vtg;-`i$z6mg5Y4v3d3`Lix!=o1;%wgm6 zfC_-&5MpTN8+iri;fZs$mQ|l`VYj0?n+wMGwZnWQbN(SPD*1MQ`|A}c{B&)!?+?EE z!Pk1J4HFeShyBLgEX}|a8_M1x9fAX7cD1cz7%hNS1o@-wFmYhe_4Kp#t>82=>Zu@! zcX7B%p9mIgQ1{a^PU0vdO&4q+$A@tE(V7-=8Vh!n0Yn3KrLcKpx&8}EFeO(VqbS7d z%jQX{KXN!*>gLVYrcua62?>TMF03`vhzomzT%*A1Hgy0SKc;X!%!UpfVnTeT=Ry|# zNt>WGo@BEzY+a3+y;$i*CbQa0lw`|b_{(CnX7tH#(-6Km5Z+M1_xlxi**clQDwTgZYhIO6fC^TBAf~g0RQy)FjogShc)77wQyDDZ^hq ze+rMpv4Wk2If&Hzy+!D8`J7xF;SDCBL1<_BXwkIj-AFa~^o=6zsaCtO^H8M9Wx&ZC z={QfJLhKBQqkKq7Jnjgs8h3PrA3VQ+SbwJy<>VlLLgDO;(M8Q}y?j7#-{*{?745t2 zS8zCw*wt#baI>NN^y6g!v-1!OlRE*4`LN7E&Yim?+N@oUKK7mM4y9u%q9y^X^o-0p z3imYY7xfs0b&#CC1dE6Zg7^2`c2#QiLKYbhz`PGHpEoFI2VTA6u>fQ<=s%3#Pf1#zzIXPGMz{VV_-nXp9XhoV$KKHvkP875ra z)LFV$W1!7I2Akl{uMD}nN8wOg@ciNo1Y8q$tATI2sCS&u_IBmSHS;;|>LFYaB+IDS zWi|{s1uv5Lyqz6m;0%fi`4+zHvLDC8(N$N}z*OlA;k^9@b4IHIh%i@=RV}t;Mv|xF z$dl!euD!R?Jum-c1+HApI6T4zi(5Gdw$+9iV<+7Uy6slq&q);>-AjX3zE6@?HBGfi z0X>jTT zWtKVwW1+wa^qp_kd~bd8xNR`Yocf+dA^XbE9isr!mhf9o&{|#TOdoVQ{#Ki?OPV)F z)*qs5H9@g0aD19sDu7vlT^b};DR6;0jt!F@Pb1Xs#@~ZMk+sg5#$UEsJ}kpR z&*o1F8u$UwJga%(v(z!nHlJLW2+$DgEr0#%ufY(5~%U5Pk)r z?3ladjGk%o=u@53r|A(-NN5Ru<0{x(@0*!P&Yn8)o~>S;ZV%me*P|SRBqUM(ez8Y4W!eUocq&l47RkCElnjIozGG` zCn(Nry~ogZc}x#Eb($Pb0WcTw#t|{uFZMkSLQkk}dD?XuM?9k$l7bO0#iIP3@p5i@ zR@9rdypR#k`tm1eG227h4oh1^u!%M=Fe~`ci9KdLHC4_Di{FrqyPL0b{89(n#l|pV z@TyBy0L?fI;A5*z;x+c1Z8Y6UsrH>Gp%$#OX~A3BsC=dFVp1-vi|ri;SxPr;-U@-M!+z!sd3*KFH<6Lqkq<}V*bvK-^mX=a0Tzw z$Imr1=q<IJ#oBx@QP3hA5JN*P1LoL>6 zLKVIB8RUn3Yc);>9JH#I@&**J{iIz5edvv%LZj+qvo%q`^%Am&#CGlx#d843LM8`hGBMS`H*C0f< zO_~yZNzHue)c4N(^#Od;Dj=KMby6|Rdn^*Oyg6UPs&hm}{D5Lj`kh;N$YUR95yb@qhu7rbK5uyz+|%$iK6r<$kJI~VuRQUZ0!K)cD#y@G0+9UO3{6Kp zUa(|{vr%|v#@T7^PzFv3JJShHoWT*^`>jzC%v3dE@*JhxM|U2CQ^Q!KS7irURfVWQ zWnL0erind8gOjPYc7o_)7T=BM-LL8`76l8uR^smA7{f(I84D52bt4H7Z{^fmBn$y6 z4MRotarfIUe3$FV+xw!@JL?VSp0XYR_`JOIk6=_JrrFaw(tCeWD;|!@UrNu1PD&tT5X$oxpAl?dDyMml};!1Eo1WUlp?<|CkF# z;IkIk!{xK2oU74te4fhoKa{6wjzxIBELVce z1w7k_?blLu1x<%OI{PVF?@ip0FtYG^>OFiXgSkX`O4CWdFBEcRIj@WPz-fuNiFh*i zLsunh+lU`YIrlMN=^fQ$TFDx3wS%S`E_57OJ^SmX#;Z}v?xdO&ka664uStOARdxh@ z(so_yj;{TC)(2<8O}j<(3^sb**sE`ty=b~CnX^F`UzQ! z!ST38NhN;i1nFxc;DCUoVCq#CbIzPn?b&_#KC-LNuQu87O@~us`w)hKcCqi{-jfVE zcQ6u}$h_0SCwwLl&>5%)i~Wdr4zTOLie1DkuOm60mkuj%u!^_lU zW5Zdc1ZuGbv|m0VOwjnMPjV(>m(VA!0Y{xHu!v&dTUyM%i z9k(D#$u4k08(wES6bROByt@;4RzG;oA+9Jn7IW5L$@*RB(Ger=#`A&&c7jjgC}x{X}{+BHElw8QD41w}v6v&cXmeM}(sLVEsO_zrO_aNx4fUySlSybRP++`LV=f<9&_sVPygFl4Sns&aEb_s88+=X~Rza2+VYWg!GbKdo74rE`P@4Em-`5VI# z5GtNS6O%`YA69fX?O$%^k`ug>0jdo<^>RkyP^5MI%6TCdxAK4_z5adM&<0kSHE$k1FbeL(bCCA1_%6h|tZq&+LZocRLfz zW&(9P+@g5DSyJr<*??O$uBrHx8$juhf7}}E4TgTVq86HNmn-NX*KH)&Ty;|9U+F`2 zq~AeJSo_kjyWYSDB}H%(eT*6dqu~n^z*5z{;v<3H&ZD}oy_hAprVE6SQs;AIk@Mr| zhdi9nfj=0TV*u;`^GQie%_+ZPCDOo{3XsZwDR%hr!E|f(`*}c=Z9mvf+%$Rp^c@MF zuuAsu&6NoH-d}(-sz|7laa=M*{EW4Gstu6%?A!$;qPB+Osi8p}Cc{e5IMGYB>~(j| zy+=?`nAEm}XEiykDWhv!{lLqwTaP#JUBnxmBLb22rnnjvPI^{D=Z&Z;+azP~r2zbc z2~z*A;PHh^DB_GH=aSHF_D$VjLcG90@Tz4Y*x=yexTj#R{0#Lsc?~xu3dYRl$a${6 zVHu2=ZodB^I)bT?v6q_^jOieIy)t^Llm((3v7bKWQBZyDzB&h5^af^+nMDictX~tW zY=o5Im8(#UXzk}|!ZtU=gnp7Tv^I(iRdlOGdqS^}J`-b}|95VVtJJNKAdP(}xl z7WV#&65Qd{ZkI^ptA&3Yc)txTV1~*ADX?E?X9SKA2Gk`m92rh;3{o1>*)G0IlwoKP z;{KgGiAa-rm;jmUD{fV2-$eo&4-MLqRtP1d1wqxO{|3t1x~`x1$A$xs==HMl`2$t? z-WFHWlp8q~N+M#PX_n?w1Y!o7eR{B}{%>_4_(M5;yB+Wlg!YKSdgdd*qt1%81SNb93y6v3`1B z#}^x_bosomaCDJKje3c?tOpFhskF3s?5oHK)!g%cg3MaOA9zy#jA%K3ZE_)e7c z-F5^EkbMOPZq+AkA}1m!>w2bqiZ2y%-t7JAqLL5W(a=&zT&m=6c=Vx_q%i#QFR>DB z%^B$WQEMJP;RJ5$5uln7&t9njsV##txvOnUCxwr#MA7u@9>+iV3R%PKFYZJMVt|E# z$;}F=V(s`C7y_5m@4uHsn~AKNI3h0635-X5id5SnnDD`r0t`uVnH=>y&KmzpLVc&% zG(2b9Fwk26P+8FSZHaRiJuENs^fX0(^;H(F+Uxr3<=4&%bS9Q_^!@({{wLa`-Ok^%&no zm&;6b_Tw3$+yq19(HxZJnFe&nct1y<0Db$6G0qi#;lQ|#)T`J4&;jGZ#?@B zKQAWfuItnRGpMDUuIl=YQau>hg^H`cDOT-e$ixLJNS*_mf6ma3(CIhxnzNvZFjNc= z_RMWckSf#j@zihdNds(1oWx4$CsVgN3D-NTqBp_G00h}qtxjl5EY&rbTjwrWr)(GO z8t;Y?jjAv=JM)fky98c6kXi$I^^0N91a`EW_aLK7TK_(Me@5alHM^phBN+7DED*+! z{a*YjRa}vH@tQmSzRH0L>nm?AB1zpj*){d$)fV=zYU?<_VNFzwm3-q!xi}N)wE9$xXW9T*OyHb8(*y84qyS*}p<`Wu#_;t)4oHZtd;||>ZnfqAP?y4V2 zet!DGphxz>8I99UbzA7p>($323)b+tTX$!0eJ8X>{HAdf|U;lQr_uHO>C1$o^Sf`e{svg-XIQ z4FI2c4odcz!+WQ6(gvdISztr9C95v$4m4aS&-ZVD3&P%sjh+X>`}M5voOD~@5n^w? z|A4*lG0j^N}=*$_IyLG!O99$GuLcFocSYsZYJz(CSGso8V5Mt z4-02i0)?V+stkG(BiPbxO=kXJ(>^|Z2SFAZ-V@b&vp_b#kYX!EX!2Wb+0S&ZS1L+K z`wHQq0^QI>F)-#7VEYW$V&vVEVs=Xn(*kFv{JlkE+%YJ#N{cF_=1_wbEP-HiG`XjE zy0(`h?5~!V@-&QstY0c%lrV{2QT}pQy9c4DL2snYhvn z@kJVcdP?llThH?8RIK<-c}rpmPPF<%8oAXn9u@U3Gms|YNj}rYIHNd)J2x*xHEfZYDxUy&OragGU#m-1R zA|tuZB1JwnuMzWpS+|ic3N&*UK0ivogzHnibGRM8&V4b>H`ztZeFgE~AgCu5vX^J4 zMkfSfoMc57-KZ!{NzDFok^q7S|DpQgDmBDUgK>$*rxUX$B>iS$JEGa~H?^^#aqPc> zSL3-6OnrRjC?&A8kW;uq-5CLa0ee#Tf?AAfgnNnPtc?*~CqBmm&VIK_DlN`NcTgOb zSSg13Brbth?de*m;IZZK?-9BO9Q>K$D;zlrJG;1 zR3zMgg#1;f=c4I{AT;A{-*htUXzQ<+n&ToXs-28t%w%#BpO7IB9o_!ve7ffrOz=#ZtSrgh+R!)pub;L|J$qzM6A(BcSrilH7T~7JurH8h-WFsTBdhe(0hCZ z&5QXHq(R|I7-hhXANr0Ek_}zgen)uM3?F3Nx?7dRUXV!E>ZJ%%nt;6Ev4K6Z*+qt_ zV1LdMGUl>Dx18@B9qdIR)o_t|-^wXjYD@0M;w7{8flx@9|eu&9sU z+qmC-J}iq@Ot|0dm4*V3h}0W4YPax(cys528>xx>Z6Jil{}Os49A_)SI*^^?To;iTH1%?WXyN++VvdU zx;c{eK|xhIx$Ix$csj#IMFC(F()aJLxPM&G0xYgU0xTXP(!(i|PtvLfnobo*fb|~o zh;Sy^))dl`Jud7%qM%=-Q?XA-K1 z^~QG1L+EkN2;C)eJ8N*z6HwE2q4h|j=T0^bXCd#{^4mb>yIfjz<%9SfdOB6yHe&h1 z3}85Ei@pEJhWg`mx3OhTtz~|TuUd>31Agz_KzEz2n4CWM`GdZ*bc+duo^3sI^uNgD zkz4VHFoo}YB1;_B#!fI4Xo898FE+6!qs$pLk#Xjq7&OSGX#V%?h_uNNVd-K*s7jE1 z(1245!5m~LS?@*^I%^#XXG6+>t3-s+dKCDSb$HsL#_R#UEvr4Z^~~L1a7?oTLd3mL zjlWa0f0&QgvT+`xxS-0x_Kr~^uL|*c2w;m0FpI@W{JEoIr7@j{8=|YxLz_}nE$ptYO zM<zlZ6%us6i_}{KOIf$jeRZNe_9qvijNf3=0`%qRqTv^jc0T2A}`fzNX z(h((t1M<4?DD@of8vof#1&-Hj)u0GgZ6FuSit~k><6aJ)x23}%B(PxRx)80HF3^_{ zAfhzY-f?Y_J=S>7j5ITBuww-1SyX<=|}0_!9N^A55YU+nPJw7;+8v4V;2Ame$tGS$11 z6WtebWq41{G?Hlj?LQx&G~!JZ-rY^awerOtWgGZGZP0XlK!rZ-jroObz|-Tp;2d}0js8#1j(p%2saAj z;CaV;5^_K_C#FZ%N}v8Ae1$v9tP%_X&7bs^D0J$bze=|DqPrlp#CYs#n$Zi|qa@qZ zLjUf+&ye=~ktkuPqihBWXzQOx878M?iEd8|9LXAZn+2}~)tAanjxL^5zMQnB3#GpG zhjRNp&@c^xw-`Re1QpJS!g8jUt_Q?rznk%wyna9Svv3Nw0!+zSfM&zuBCcyW%dSs+ zy_M|%$??7s07iuw{p%@Vkg1|4NS-=ZDFAn8F_zT`M$79V)~ZYEl~A z^w{QM2jTrbJE`ksP`$OTK}j>sC8AwSFPSj?AttLM$5gqU|Ef9AE`oD@Ugr-MOl441h>TO{ zkh%L}+34#22j)0ZgvohACy$tVT7YWE7U0EOAO#1U%*q zwe%hw5nI)GrM#tv#9i@02qG2hM&D8cNj}Q2M4ighi9C~h-;i2GiAZ2gsvo)y$jtuI z`@&}-(`Ty3l=E;_&qs3H66g;H(s41&i@N0~F4G4j>?s<+fHh?*mwujs#^KH?#|lhO7i>F~)>bvUQ_;37J7*2a``MoZ>H+#biC zheITsv%u>Kr~n({Byu*32NMe7@sxui62qn>*j4aMK`=2+pBp2G%dCV>%UM=t>ilU@ z@R7EJB{d0xF(|Tm{L-8onUy1r#36yKC@~x9tG&zW2#hoskAh2`m0xu#{CsVe4z%F7 zf|M=)3CivnpOAWM%EmGz=C*M;4nHK@nXRcGNp}M($f1@^RcN4_P9!#`vQs_WR4k+Vso_)JNBk)lZi?p>w)U^W9sR|+7gHgs z28pgcf=B#%GO>IBx-C)+CDQp1wOP=Yk;=FnqMjDVrO7%6BDOgz$IDXaX>^~N_xIg~ zt+dj_0)1b!#<^)E4aVwNZ6&9dEsu#(3`{}m@dLtlGi))&387=L+nH7I^0p_}IBRxH z6AB1nn-OwMPK2yaNYP{BydojTlD()`bolc7GVQVnZ=S-|cZ|ER+ z@!4$Ff0J>UbM+T<-~z9Nj=9*M{MUCCpQ6#Ont+B^RR4)ga5hQoIby`;|<LDEvgE?6{U;-0ZcDi{xoHus}u|FYWOHG3nfxFsUQlyBv_?>rAbw>CK7wA};D&5+GJl)EeiMgw z7BiAw96Q#Cj_ozfro+!;ZX~8;;|N0QVTo&cu6=C#Z1YvK`ini-B2!xC2e||dn|@2+ zrYSOIL!HzWk+e27EFIE|We#Nd19xDFA=;!-Uez02hVm*e$RuWO9ie&^5x5MjU_TVl zfZYXI>M)(S3nO*!)L%5TQk+XZI42&JCm@Pi^)4GdT9AtnOH6>!dE5jQAd!WnIrfh# zSc#+9x9hrYh=usPfq;%=FUt%GreGUoinlHMuj)G8QHX7<=$!VJQQF*pQ={>u%C0Vk z7|Le27*!|KmF^6i|8XM}d#s583L=H4jeib-fWU1mk*}&^8gtQP=#=w9!0Goi2Wm4{ z37AAR)(^V#`5QnD^Tp6~2$+CmjXT`c;R7i`Qg-(|Rm3Jy^JJ)-N*{&@Gw&|cB#@6A zDbHM1ZPO{F$R`no?@wkwSbmmE1nc}3A=v*Ddcl!v?zhVMV2gw6M@UauxW%F1c#@b2 z#4c}`-yorD?FRbgjfKmmIN8jv3IlqUrk0QNnJ%t?kE}DhK*_@+oVK@$r-(Lu5`-32 zB$yd42KpMrugEc6v)pt5}Yxm(uZIsqdpQSagROnqE3gTNY#|M{2Pi7wy zH=4F&caeB$PxK#P0ubiK~1AyEF9~K*ihAAR2|lCreolf5yoYZ~&P7Zs@%G z(-mqpqP^T-HOIOb7mmx0X55ma z<#p@PMY*&Mn*|HKf_e`WUcTYyghgvyHA(Ux&Sgu^%#aIswI(AVP$F2IRXQI)ND=5w zzK$~9bB#pwdiwLl5sS`yj}n4oF0U##!!T9t?WI0=H5B0s#!Hi!k^8m>;}npCN-Ux| zHB-uQfj~b^lJ@xf`k1FW(h{L6wNys$*&V`q7u+!=`YFmirw7LrBMmH}0bknPnnUvO zVFW(~TzFWrr2X5UXvLsmL)UW8QoHKNqZ7yu1MQ+R{@dSY7V zWLe)20K;6)aEvM$V@7l_@{=>;2M#c*!Duo)dqE6cnmkn`sTMDAvbund$pYvU0~HB> z?FEHuf{&6(fbIsKW&wlp-n^iIjBQd~V{QJ{R>fWpCClSaL^fwL?)UU68PJoM3Lq3S z4Rz2a*t<-IQRs*NLEsi*s0=UF#Ol#HJ7$(!gu2@uioQm`(|3cm3{C`aGNS{Mi)i9s zpDFy{P!Tx45wNflq~wDtjXDus+Ki!hU~+4cXm8;xi`yxn_u(i9^Kb^un;M*U)sF!a z6QG0DPyFC=nTwLA*6<~wi8OE!^yWT=5UPQ*Cx^$J&v~3i3d+XG&9HWZ(yHmpf!+fjNu3ju zacCn5ctgH~?UG z>5jtRC(HODY2-0ax9J_DUHJ*b(UPu#!F%?zpB(L$2LLXDm`!K`|A^NN&@w4b1U;+s zj~99<32L=2$qNN$nwdF$=vRJ4F+ihJ^f|#XYkd~PUPi<1G^^!Qa;A{ zF_-bQqnK-A?7jS+q}N5~gxYCj_@@DwWe7>E@$b0T65`%x5>L`UH8!H<38oRc-HhnY zYZ^*WjsP`eo?8EvWELBOK&{Oy?>;Xy#VAdXXl~;Zb|?kFY1oG4h5ZwA>OdLM0^R5W z*J<_8UC_(jJ?}>mlr!n`9`6;p;2RqBHNh16k9qRDE88$zC5a@;m5wuA1zZQu@m)NsE95u;3x{b6Tt8x zw>%*)@2KP(14z5znVY&3Q(E%QMQvNU_~e|`$K~2e=!+TrBTe7C_mv_;*dQ4c*JAJJ z_eac!VMMGVBI;tmB*-v-R5wC)foP!Z{NQx3Zy5!Y0^OvNk? z{^bnN4lfBQ36;Y8uc?bC(CpKeTPr1We2P**Os;(+Wz^Y9CiVK^sdhmVFM1N6bUi(G zE=A+}^qx#YPuEj@VU^Z7 z27tq3iIwECEB4q>vOojH{XcGLzP>%Y^*x$fr{2ib%lfFti*1ZlS`{Y@{{T36U!|~c z={;(X=y!Esg)Dr`q)M(h!FL6f(f6akGZ=HDo)K7N?{h~qYl2Q;jH*(ix)Jes$=y2n zYEv3;DI3&-4w2@91rLBlFzDrbu#&sI6&5&0Gi^5ipEiN0iJWH%G<=WrPO~a1!!6;$ z*BU-%ZV`QyaT!Bc5!ae)cdbbkW~4P3u;}A0L{KMvD5?jI`SoWkWzdEITbZ#a?V_N_ zA;_!gS_d(8>_C25XP(Ff$mYIUcK1rzzghBRWi5oEZrCZ>gs)c9u&RTH+HCZGM_rM2 zW3S4ebR18Bx=5+7uWpsF=E~S=2t<`;1SwWih*{qNsR0p0w$HdcOyHo2yu!sb4QX%!y112KsO zuD{z5ZNPw^VtG2gKo@`{@#9(Q58qi%Q_~Wq(#F=5D5ao^sqo#y7OHR3W!NewEeaZ? zqlc-HK5NZEXf(k4oYPzAbMMGU{|hzi2UN9q$b&Cig^a;j6Y*LQAjri)5h_R;@7U}; zDGDna09~`dQREx8|&hD*(8_q6f`4Z|h zElMvG?}SC4hiC_Urx#t@qnu~iEXll$6Z9ngw75s6_~y?kf?(v27|F(_R6{k$V~qA*LB@shh_QtCZVLrxB>|&{bD3e^`zeGm zybQMdR%KNzUr9(j!yzr79i}Qkg7hT88yl>583MF;xBYZIR?uPrebT_S`jvL&aCTy z4REPKp9#-A4s%UVliS?`=XwqbH~^w!-`>R5{C5l- zeJzU8UgcHMs*E9^E`jfm4YlAUAW2pMF@K%dNW;_T+07XXrW#_*?&X1+sU_{Lg}~qw zF6TELMzi-{g#e{~yv)KN!lA^p1fI%yyikmo718l^G?y8ASm5>+yuM}VYrQpvVzO_n zxZO`Un7X{vYpX<#LF~-oxomKs;>(rQ72}yDPs!{KP9te2^#TV-^dHMKJw*BifJ?n; zX(E)kduue+Q90Xof*vls(pUP^sLK5c(CZ&Nr+N2*-}Uj#H%e!%h~zb<;<4?{hBYpc z-f*d1C4nHPvOxM6_fE^?@2+V?;x`9RM)d3;kKCQ-nKp3krOt%xYgG+MXe9CC>YATm z2z$wGS?~?C`}wuR8R(mhho)^vl&Upbp!SjY*gv+|Qhlh0QPZ9ym=_e^2?Nz<73@Iw zG8SwPj>1jvaI#~fm9ACXTTUd085o~ljl=4N@qn>mJ)5uh50|r;)G!41wOqL=>0qwM zhB8!fusF$(bOW0nMPO>CJJ9(Vk9R%)B&3l zKHypmOC)so*B1vBLqWc=HAwK3_k68<#g&Ckb?(3Sjy4Hx38A1K;&sZ{Kq1y~O42#b z@fZdbdPPc#x8*le)i-pE_`+#MJ|3U$Zwk;&Bddz%%i<_i5|q2JhCw1!UY#^n+9%Mh zaVe(N^pnI0go^>$WyAqyjWZ&gWIPStsrc?~0seS);>{lZP+uan8TkPeS49DH%%M*oGYGv@C>X=7;obh z;3Q!xjx6s_upCLv=MQO@h}?9+HY5mw9;(spQ*$Dl3$_cB9C;UeCflTn3~y8SO3LU7 zX=gAHWJ4})_vYF-8DP52_8zH5C~luu7-BF-70If^`t$b6RkO9uh*;pHKRxwM;*mJo z{|FS&2KPm0jDB$sQOUU9m`ZbmS$}Dig{v0^tWO?Yx4%jD3Uv{_zzd*6qvyrr&~C-{ z>poPV`r*^YI%8P4JoI0v?vclOa>TqcC+RGIq(4;?=&>!ogizwqY-0FW_mHH5M0pNF zHhOA=7EBKyv<5Z@K`9kEw+o9$Yc8`Cw4m4FO%3-hJLWoZZ~{${$h(&PwaDX}Xhbh# zxzu{7IN8_kvxy8NL2?n>wT8!%tVS%YHyg^T!jz|`CmTA$(u9C^nC5X(fusN) z0@0Gx!6qG>?Dj>rX9;ipVRTHy)uMc79At%cnPdC^&2fgaH_3G|q;b$WFDYuu+u5$Q zn=$UW)!F{ou4gIR^SX4_mT2^91Ndk?JA5$1!X`}0vb?!=gLG=e53yU%tlMJQ5wEak-*c8f30^P6K;$Zfo>+WbKPTZD<$-sf7`_>Fmy?X{;d zH1Jppv3CZqPSuNnigA|S8+PEj(~a}Vj9l+(yem4$a&T-WSLr{1oWFFUVOvw7RZ_`< zOsi9ADL|gxWBZgXwngMy_F>S?<}&`aAD;o`SpszqqR*2QtF=||AX6^8idEbu47(`c zhbRr0v?E)_@QXI9vp5mFfV@B5Nm?NLp24XwGJN$76^fR@yF)Yg94B1ho#(+uA6|eO z*sZI(IQm$Tg91V9o{-hF{!>d?h$kRa((%v_YVR-l-y75#`>~4mnf>GuFK~x5u*Cp5 zQbx+H{wL<+FaiR(Sc`SJai>G*mMD*f75=AgtSAw zKXZ}pkJM%5)ntAG_FN{E@lu;Cs6l6$hy@7jzc}7qe4LauY+_Zmd1v0D8Duhbj;m_$Q zcVy*DDp!0n5zj=?Is&H>V|EWf8t}MWOK^B3++~5RzFCEyQRV|Cg=p1AC2G`J5oNoC z`tF_&@)*gnk6Ic_n+!|z!-lr=4C9=Wk)MJT90L9UUQnS;SAzqZgN1g5O@6+JrjSeu zA6y^KhswpiI>Bn@6CZ*y`z*%t{GzkvpX#f8x+C!FPwQ0&qDDYb;kdf6YhqpY7pJ`T z*Yx;XPx}8Rj43?yr_^r*`(TH!`|Gm_4Jcm#{`R^sa#2VCcy7wy(4gMZL>!@KR9BbH zwUU(Hy0!M)kb@@rNAfqep8^N-hE)w=Q5t08ZlNriWTzljNy*-Xa^-DX9+41P*i&Dt zMr#}_;Mv`2=13*DxK_wHj@ZfyIpEN6>EO6w^kJ)x-q@j5#t_5`UvSubY?io%$BArx<)QaAs%idK)gFFE`j%*XdE-#BxR z%VP}dT>ufBmOuW5{mR(q+u|Z}zi1nC`Vye5n$p~lKw?_g>TzPQ&-^xZZfppt^6!8! zsb0@qjJbR?4IYvk`T=GMGo@vH+C1HA!gX(SxX9I5PHR)A z3(gWA(EyIsd1pTbKzR-D1#BOYO$RQ)FxRcy8oecNLbH=HTeGa_k~=0pFQq`NK+F@U zZ)yed>p`*$pbd7ygX@76nEI@oMukX6)&tALhF)h|~LPd+$q)oC$6+{+*0h~CzTr!37;>az>OE4&W+kOW=-`d%ZQ4;3aJ=gHO zq5%-zHrM+$F*y=sTzSH=C}+xgM!pFP+?VBcmc&>m7;JIi<>CcCon#+Yk! z;^VSU!%$y^ufO-g6spi|JX#l<^SOHl)#@1uWU2WM8SBv=W}Nu^rd0qY5qZjp&k@ zhm83iQK#SH|ss>2{3 z{+rGR>A)#rz}fRsO!L4HbuWDH(X_ucsBdnpGla)4B8c^M6F1bAJo*O9x1FUEc2Bey zJ4szEjal(3>6^riHuSMYjBJEj?O+NU$R} zxc-mPmrxldEl>>>^ll{8>RNcMaDQnp~&dx%Os z8iSiz{Mixd9Ykaq(M4_cW=WQ95Sp#caMiWNexeN_=-?f4j$n5{7VmRblx6^sB#GR^ zJKJS#fviIkg*L5~72)6qd#5bkq~$03L9b^@DCZ6cgR9Z(1XW2&e@UzsGA!=Gr*c$f zD#o|Jzc)5S9D~b(zHk-&Z&8~Zo<{sRUK?ga&~;WViRXWpToWRgEEi;yWhRrrYc*AX zNXTM-9JocETX{voDxzDw>1X20sc(C~rQs0bUh3K1FpS-{orNm!ieIH4n^}Q-3$Ee?{ z7g%2IbKQFl-{VZ*U|D|3lMY1o0VLGrMs~@9xKb*U+;N`}R3ZY=FfZjI4B{&XW?IDz zI??=GTVB0nH}+idK?DJkkJU`eW~Dm5EB}`an}_1szK7>`tRl(iRsM*wN7`+XuY~mF z-ESkp6;|!02M2?9QmwgO#({_>of^qe5IO^%ZrdE~|NbDqX(QKi59gIlC&(CK0M^J~ zpMy_2nZNc#&%gL7g6Rd2-yuL1JzwX3N|Guc?^23F!I|h8;=`q9PU$tmiI;$VTm7};UIum`U5ch)#o*hV*jpeIu7DRZlK~(oN~WX zKC~{*N2R*Oun6rSk9Zy$bm?sMxHJ_@wo3&_Y5)+reT3rD_Pi~p;jquo18RhMaVArz zn7ON*GW?uQdX4b1;tS7MWAIY8q(@Z9z#%5p%ghNO_v(D}jD-N)yo^d5gfDr-qa0fh z_ZCH!w2sa;ejL)R#UasBr*>5sKp+Slw8(Qb$_xmM>jyC&@{DrVM5p-$y?}3EvMOpl zTiXJ0A~>0dNl2(`a8bZo5n2npKvUbw6R)X1bKEZjy+r#7(^?2zO7ss`gv2gyGR_FX34! zJ&d(aV?1ud;0|kJn}-s<^9XK#^7^*&!ZQ@pGS`OO^4U%7F#Sz1mVL*ox{p6jMQeXJ z_4Mzq5`m2$(mFx5dJOBrP22l^D@Ag$$KF0R>93}>xJ}1 z4}v0$qH!rtHm!!`IpZzH{ebq8(CAV#-|~scm8&5zvavFshXa0*;@ofDNm2hGI9+5 zK7vOyy?pVr_bbsqgo6o}VM1Vf9;@T$R2IAUK8-MbI-h~xhYU|eQXWTRP!K=fJRq0H zJ|Ebq%A$o_w5_H8%`G69Pv9fdaI}o_x{E;dCZo4=#?}__GxRu?!!({s>!-UpEt?pt zYfN^)h;-xH1i_<25~^n+@eocY&J2iU$6Q|N{R^x#E58@#INso1roo&!k)zrOmtIZw zZ?s>r1SJ4tljAVmSn6+9Y2W1mDi=Io(0x!tDL(?tljhM3i4W~0ngC4#AX3s{^Y32 zEzBlMsUCEN9le%uOt3t1eIklt=2$_bwcT|%@D=gqNIjtRi45+oDP3V%RI^PjqM zPByJ|OZ0vu!w}OV?FcX50R4unD_|R;S%v=De-F`KJEG~OIAY-AR|sM8$*?-gzSnxw zd#{dncdxQNP<4}SU0JdVKlKj|-t+YTPRmK%Xz3N&2cQE}wDYU2JGHd>jYNx3aQ!%L>0Z(<@kk(pf zpIhIucJiAHD~=L}x3MOyS!7;sKwAlCd1&^=w;D-IRmIkgL+v6$D0z8q0k!8Q?{X~% zoh-c~#r=q#Aset{QLEF%V~C}w1r@u}JS5*GY9~I^$t&vT>ShI!^BgHjAy**C-~+b55Mw* z!?6Y-U*9oBtXirp)@E7aAR%I-Yt}@b{FM~3I7cpWCzZjPyHJUS>D6N~e7DNZa!(Og z1@fcS;{Gly7*L~sLrKdXUyxhRq<$fA(L&S6TiLh6=^l&E>^uCndaqZ^$0Tc^4UdaUb?4jC~q%%aU)qKQpp3`TS z`ZUfzl_%g+IT1zr{vWBy&-g9*`PfbT0y8ON8+kvRfAYPN4GPx#%)C{A_93r1=aASu z?dWd^Q-8>yfFj;HDA41yarXB(`E+WAa(XS^95gcb^Zjd+;|zL+a1(B@ zhA&lR=oWtmto$($9nAD&S1I5OHqSBm0qFX<&?rni-?^?RxvCLxS2Y9OYizMpH9fwY zf&FnXWW81%>8ob2jo`-?|DrRHwv%>LYD>)WCU}&JdOfaYob{6dbm_Xl4Q!d{3}WGu zbfsJoi0(RBElUVnuDARlbT{jY^4I;gIEr_sLAdE> zaYE2eU|`kCvEDrk?}2QCh%Xx${y2cOrz{d~a_ywppUE~fjR|bE@Sh!U%i3;oFvEuW z^$hd}IJm{1y>a?BOISI}tn<%w+JP5~ftT7FkEtEU1-8jW@)*9PR8d!KJTrUN{aPMu zaIZBAsE^OPTgM{It`Bi64%$U-3)NQM4SGUgw?GwRc!6AHk1(Th>kXhmJMc7rFc_mk z$%?zG_^`@g*@f*XPFSRu?vT-@MFy--dsifIS`4W+u@ienAvRk+$vNsh+(m%)WOy;R{PtG)2J^TNb-`yH>O!>F0VN_t77Ui# zBz$eyA&*o2OeS1YT_W~Y;_~~1&VH}40Z?SJ$N`LAvA}LYHxtDZv-hhY;sqMUN%ftp z6j!?8cOjt%=Wp{(InV|qhP1uUEdiwIjhcQn??Hmr1bvb=tFkv z5;A}|$O>x!^-M_Z3f|(Zohn>A6If-t^4}_R?M2B2QT9*$+7sQ?p?7$&me-KgaMD?a zQD=uYhSQ-w?iy^s#{5(Y)7_)7Te+u2FX+&ge~DioU#3C zuj2sHW&bZ|O;nSqg|-c7Fp1nBL7d?t0{UQ7kZJB`=;%;=hi9$X@pZZg2Jl&Xjc@mC zAB0xi!)T*BC}g;Bnq1sni-OI)9kT*fO>3DGy<$0U2S%{HLAU#ss{^~E z)9S?B22F{F(pp1lXVq%I^7aCp;4Sk&2x3;{zsgZ%97Yzk2HF#3U?}^rAY@3KWZh8O ziVUg$K?3#-*TFO7*Tj*GnSBCcY8uO|(FQP3hjhq>R~4APu=ceXOBQe6>_en*MB(Wg zfvax!#&##XhohIbU?oto#5MDhBz9(u8^E|S3?a{6tI`KxW-?+@D`)IH?kNBdR465= z9q}SF!p}SxQ&)RQJ_MZ~hYZ|4Hr_@dC|8Z=0kVBVt=9AV;G6gdOV8S8Jdx%@;}tg* z=khzD{F=g&kB{0D5@-d!faS4G6L7UbEuENO8xDDAF+M$vxo8o>k{Et0@yj+~3+_uA zKb=%-i}Jj;&G<%Ea6FMU@%Zs-4@_;bbpgMN0xqZ`p+s`~1A4rprlALwMalwK(?bEi zOdfQHym4Y$+AfhlWxIZG?dQn~@`W)=JQxHxiorq+mUh)LW15up&)>*D9z`XYnPE7- zEB=>OD;ikO_TLY`;&ydhbo|wczYYgHehy=3X`_u1HMYh7;yP{PHYUxn8Fzsj_ ziD+A`OyT!NiO5o3$oUH@pc|_rEFA_}d&WM6Z(eF7OSXI0nz>)6yCbPh!AxWK1)*1*peW)mXck+vyOczb*&LhY;d zc{k!oM%Ui{+PwLoVVBC1RG8eGSyq^jAg??Dy`G7kIEOlrMO{I>VDd)$i~cF6pi0)q z&cAdYz2On(1U>WMKgK)n%ZfybYh2^ZV14)Q((ai4(ATMD9E<$J-cHSg(A*$knW2!L zW%O*y4Tt0lIk2bo6&Dr-SUUa*D+{c|V-e$&;2Ne{maJT1aNAp-43tuN=dh0z{0=~J zjpFQa{6LAmc|u>!&-uRtCNHmPU1?XepwK7@!v2UIs+T<96j+}LqA@_uNp9EsFG2up@tvH9r10>@IQb=8BDbQ`cl1s&a&^~g9@rIVF zfNL?~9I7j{(aEY@$P7`XyrKJ6+vbT^gT%#@JN=EqSYahqP2s#sN!k(%&MK6)PId?;q})lI+RXt;IOy5_4vcj*7_SYGuD8I?*7Q z)d~UE!deXfHB{_?7UQJB-HCaK)mPd<)Kv`hw{sebk-rhd93ZRJgh zY2{xRaH@zN1d*nOrc7AE3Ww15?t~MjEbn(O&DdR5btb%Qb>j~<#{NaMSqMrOIJwZ=@9lIT% zZ1rhe%$+=bg2HE4tum9ms(AxQ@Coi0?87!pOH{{M>C1}I-$y?wVyOAgUl zpRb6r??H}i#EkW6t05%E$F%fvJKV?#nLuYwatuT7aOvzjk}eW*fJ~K@T{muQ5eAz* zsZSteV(NnSSz`df4+>Z0T1Cb%re+*r^zYh!`-vHlxYn3pOpJck142-0uGJYR9Cm4; zj>gb(dw_W=d>0k@lxg2$xew=)_=a#^dD2-B6<7=xLB^9=Ij-OwMrg2>NssMZMGia| zZ9ty4h!9sRmqj48-`IK752LeDU}dPI#>XTP4uPv4oXQxUnc8QWuZ3z@V9f-psO2^o z+R*%cKlq+gu9MmjFwNQBB68ac>_~1&zRPTTG*L?2ym^6!{$#cetvjV&iIA>(rX+ac zo^<}OiKzZ{Kq4+c(c6cXm;GkE*+0N{id!Gsv2YO?(*JVX2vu2wvqkS&fb*LNLHgt=;eW!;%da92Ri)7lF9wRK&J`0vk;^8=(Gd~8bcQ?Cs!n2v8dka_;#RO|6AH-dn zB7Nc!6$Dl3Mb6Y(Svk+9uJi9BubYDk!_l|A)#QR<^DV!VElnkw0fgrK=X_TOS%$>% zx_{%I_R>esgF^Q{L}??2t;V88TMG(A}Ko+KRq*~-qKlWCk>w-woFryulBwk!8z1wRj2_yrY^b%?o`z*_+4?Nk)9+UUv%gu%_Y$nRO2inR-MAm&bLmk$Zt++_RF>VhmXA9?lvsv25#m7 z*n9C_O&|)_(JwQ-y>oKh!tYs*NjRF*Pv1|b)|KA_eyN*j0TVreUKmjx^6U3U8#c*~ zz`m57*CD8@Z-^&XH+SXpU6lXkj)>xS={rh_>3gT)NAOjnb8HR5PPyyaTyv1sjC$wjl=GZt_~faX+ne^U>U zy*tP3$z?N@x;xlNbvfw^TcRSzp8s62H*B|Wr+GN|XMxI^aXiDii$b{e)yaeGQ?yY7 zD>j3wYc!m2DG#l-?B!RH&wd{Xf)2kH<5rpV!JkgJ{|>i-BAh8VURn+hE~WKRnVPf> z@#4~$XdQB4P#C6byAHlB>T>|lU20@CN5bQ$nKVRpTvQRg&JhLV=jnS~g7ptpp0>&M zSJ2NogCLOSLWgOhS#>RQ3i!pn>~@F4?!A2o8$~>h9dl<+;aN&uBz;SU*f!dr)Vg4Vjrdux$9l2 znxOKc;*bI z#{?`Lhwfuw)G9XJh~AnOdsdRX$PQ%$CHYl9R^`t)xaR*q|l@EYso)uu1L~?G@9If}TSQ39ycCfSnoqYPK(ZET}$60k}ak`cw^hU?1-FSGeJ#tdrOfcTi=rvvsHh< zfp)_bWGme8L+tZaYUc5hU+Ma5@oq;U>rCoHk;%AJ8zjQ&v_DdjADbqc3%X2d} z>ybi|yiu29X#av9%(Z??gFq+6%=tvWAs6(S2}y;NiZ&`pqpx?47&z6M-x>F4^Ln_+ zFx?n=AVd|N;0^<^>HKdIsP&mKjdC_%NToIiQByQG&6jR1I$xWQ^)f9`jGXW0KyPM? z;OMz5bC&+*%x?m{k2rh7{Ok+Z?7()~2cC_zn$cglj!cC8$eH}%4L+Um-$rq|mrxL_(FB0h#RFlcHM1`p~zh>pMvQS_Z#BNM|@Bdmc<%nsNs9WN-T2DLeK4|YGH^S8_>{-R+0>2 z2^_rF6Z`pFoNY|H8Lu?ZFoKk>SA!a&zuoDA|rkR46 z;F~Yg;NJo(*D`I8Xlvl781(xI$J6IE-UK^{y$4h$CezmRHx}Sm)S%;83b}F}T}KbT zfPJhYd&aFs0+|>5TMeSRPR?;Qp@Fj<9=2elUF6X=eYs+JZ2XFZy+7K@Y2W5!8cQc% zUXVKfw+~9-il-sa-0j-U4#P5;|D#atBhM$0DHS)KRdRCo#h{UJJ%a}IthBthdEnY% zTqE5yg3E{H_UneRFR?+eBd1BY{2uN_RMZ}frU8q9?7Z=nJL6zVggeg-NxDCOMN?%s zl4VAP7lPQ?t>|3GY{l}llXkf;^(+~=_EuvM4l7+hpsL&qtb1!hNP6QU;x3#*HZFni zCHDs3IO!=`)o<~7rta{#EkP@kSXS^iFDu2!w`KZGv2|ems#-Z{=2d)WP|bN%I3KLw z_zudl2usv*(m>+Yrw09thV_M`?^(W_Wqcc}& zgfOKJ;<@^hq4!F8%vn<)o;`4Xx3T(B;@*$a28HqDxJ;w09@0__PZ ja40rIt7r^H7DE#{J%gdqCZ#>29x~RRd7V~t`mi$n+`jZ_ diff --git a/.claude/notes.md b/.claude/notes.md index e689f411b0bd306f1a351f42d758adf17e754432..1300ea213b4b8f70cf4b91118511643d35e4a81f 100644 GIT binary patch literal 22843 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 literal 21545 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�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 markExpectingReconnect(id)); // A phone helper's camera mounted on this robot (phone-as-eye). The video // element is discoverable by perception.js's findCameraElement enumerator // via [data-attached-camera-id]. srcObject is bound by renderEntry after -// innerHTML rebuild. The SVG sibling is the ArUco debug overlay — sized -// to match the video's natural dims via patchArucoOverlay so corner -// coords from aruco.js (image-pixel) don't need re-scaling per render. +// innerHTML rebuild. function attachedCameraHtml(entry) { if (!entry.attachedCameraStream) return ""; return ` @@ -54,89 +54,12 @@ function attachedCameraHtml(entry) {
-
-
- Print marker - — "Original ArUco" dictionary, id 0, tape flat on top of the robot. -
-
Loading detector…
`; } -// Surgical patcher for the ArUco debug overlay. Called from the tracker -// each tick — mutates the SVG in place so a 10 Hz detection rhythm -// doesn't trigger full-card re-renders that would destroy other -// in-flight UI (perception prompt, hover state, etc). -// -// `frameCount` in the status is load-bearing diagnostic — without it, -// "detector still loading", "loop running but nothing found", and -// "loop wedged" all read identically to the operator. -function patchArucoOverlay(entry, { markers, frameCount, error }) { - const node = entry.node; - if (!node) return; - const svg = node.querySelector(`svg[data-aruco-overlay-id="${entry.id}"]`); - if (!svg) return; - const status = node.querySelector(`[data-aruco-status-id="${entry.id}"]`); - if (error) { - if (svg) svg.innerHTML = ""; - if (status) { - status.classList.remove("aruco-locked"); - status.textContent = `Detector error: ${error}`; - } - return; - } - if (markers.length === 0) { - svg.innerHTML = ""; - if (status) { - status.classList.remove("aruco-locked"); - status.textContent = `Scanning · ${frameCount} frame${frameCount === 1 ? "" : "s"} · no marker yet`; - } - // Push to phone-as-eye holder so they see lock state without checking - // the dashboard. Only on lock-state transitions to keep the data - // channel quiet (10 Hz of no-marker pings would be churn for nothing). - if (entry.attachedFromPhoneId && entry.arucoLastLocked !== false) { - sendArucoStatus(entry.attachedFromPhoneId, { locked: false, detail: "Scanning for marker…" }); - entry.arucoLastLocked = false; - } - return; - } - const { frameW, frameH } = markers[0]; - svg.setAttribute("viewBox", `0 0 ${frameW} ${frameH}`); - // preserveAspectRatio default ("xMidYMid meet") matches how the video - // is letterboxed in its container — corners line up. - const pieces = []; - for (const m of markers) { - const pts = m.corners.map(c => `${c.x.toFixed(1)},${c.y.toFixed(1)}`).join(" "); - // Heading line from center along the marker's "top edge" direction. - const len = Math.min(frameW, frameH) * 0.08; - const hx = m.cx + Math.cos(m.headingRad) * len; - const hy = m.cy + Math.sin(m.headingRad) * len; - pieces.push(``); - pieces.push(``); - pieces.push(`id ${m.id}`); - } - svg.innerHTML = pieces.join(""); - if (status) { - status.classList.add("aruco-locked"); - const ids = markers.map(m => `id ${m.id}`).join(", "); - status.textContent = `Tracking ${ids} · frame ${frameCount}`; - } - // Phone-side lock indicator. Send on transition into locked AND on - // marker-id change while locked; suppress while still locked on the - // same id to avoid 10 Hz traffic. - if (entry.attachedFromPhoneId) { - const primaryId = markers[0].id; - if (entry.arucoLastLocked !== true || entry.arucoLastMarkerId !== primaryId) { - sendArucoStatus(entry.attachedFromPhoneId, { locked: true, markerId: primaryId }); - entry.arucoLastLocked = true; - entry.arucoLastMarkerId = primaryId; - } - } -} - // The header meta line ("WiFi … · up …h · reset: …"). Reused by renderEntry // (full render) and patchSecondaryRow (telemetry-driven updates that would // otherwise flash the whole card every 10 s). Composes pure formatters @@ -264,9 +187,9 @@ function gattConnectWithTimeout(device) { async function loadPaired() { // Restore remembered robots first — works even when getDevices() is missing. - for (const { id, name, fwType, autoReconnect, lastConnectedAt } of loadKnown()) { + for (const { id, name, fwType, autoReconnect, lastConnectedAt, arucoMarkerId } of loadKnown()) { if (!state.devices.has(id)) { - state.devices.set(id, makeEntry(id, name, fwType, { autoReconnect, lastConnectedAt })); + state.devices.set(id, makeEntry(id, name, fwType, { autoReconnect, lastConnectedAt, arucoMarkerId })); } } if (navigator.bluetooth.getDevices) { @@ -1057,16 +980,6 @@ function renderEntry(entry) { if (entry.attachedCameraStream) { const v = entry.node.querySelector(`video[data-attached-camera-id="${entry.id}"]`); if (v) v.srcObject = entry.attachedCameraStream; - // Lazy-start ArUco tracking. The tracker is idempotent (returns if - // already running) — sourceFn re-resolves the