From 3b636acb401a1cdb9a1dd5cf643ac2bc53638dba Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Wed, 28 Oct 2026 19:09:18 -0400 Subject: [PATCH 01/10] =?UTF-8?q?rig:=20`pixi=20run=20qc`=20=E2=80=94=20th?= =?UTF-8?q?e=20five-minute=20release=20validation=20of=20a=20tier=20on=20a?= =?UTF-8?q?=20rig=20PC=20(LAB-338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One command checks the candidate on /next/ (or Production) end to end: the tier's build.json → a scratch worktree at that SHA with its own pixi environment, the candidate's graph with the rig's flags, FicTrac (or the simulator), the Studio emulator pre-check, the tier's Studio in a QC Chrome profile driven over DevTools (build SHA, protocol load, bridge connect, the one Web Serial click, Run experiment), export + runlog-check + closed-loop-report + bench-check, teardown, a pass/fail table and a ready-to-paste validation note. Per-rig rows and the checklist text live in rig/qc/profiles.toml (ww18, ww14, bergamo, mac); the procedure is rig/QC.md; tests/test-rig-qc.py pins the pure half. Co-Authored-By: Claude Fable 5.1 --- rig/QC.md | 118 ++++++ rig/pixi.toml | 4 + rig/qc/profiles.toml | 127 ++++++ rig/qc/qc.py | 952 +++++++++++++++++++++++++++++++++++++++++++ rig/qc/qclib.py | 299 ++++++++++++++ tests/test-rig-qc.py | 152 +++++++ 6 files changed, 1652 insertions(+) create mode 100644 rig/QC.md create mode 100644 rig/qc/profiles.toml create mode 100644 rig/qc/qc.py create mode 100644 rig/qc/qclib.py create mode 100644 tests/test-rig-qc.py diff --git a/rig/QC.md b/rig/QC.md new file mode 100644 index 00000000..d3d6a5b2 --- /dev/null +++ b/rig/QC.md @@ -0,0 +1,118 @@ +# QC — the five-minute release validation (`pixi run qc`) + +One command on a rig PC checks a webDisplayTools build end to end — the candidate on +[`/next/`](https://reiserlab.github.io/webDisplayTools/next/) before it is promoted, or what Production +serves after — in about five minutes of wall time. You watch and say "looks right" at each step; everything +except two clicks is scripted. It replaces the hour of hand-driven QC of 2026-10-27/28 +(`docs/development/handover-qc-windows-2026-10-26.md`, the three prompts; results on +[PR #274](https://github.com/reiserlab/webDisplayTools/pull/274)). + +The code is `rig/qc/qc.py`; the per-rig rows and the checklist text are `rig/qc/profiles.toml`; the pure +helpers are `rig/qc/qclib.py` (tested by `tests/test-rig-qc.py` at the repository root). The release +tiers themselves: `docs/development/release-process.md`. + +## What to type + +From the rig's webDisplayTools checkout, in `rig\` (PowerShell on the lab PCs; the rig is detected from the +hostname, `--rig` overrides): + +```powershell +cd C:\Lab\GitHub\webDisplayTools\rig +pixi run qc -- --tier next +``` + +| where | command | profile | +|---|---|---| +| **ww18** (`reiserlab-ww18`, G6 2×10 fly-on-ball, real FicTrac) | `pixi run qc -- --tier next --rig ww18` | FicTrac through `fictrac.bat` on `rig05_vfov28_2026-09-22`; `bench_closed_loop_2x10`; Foxglove `arena-fictrac.json`; `typed` + `viz` environments | +| **ww14** (`reiserlab-ww14`, okey-cokey camera rig, BIAS + G6, no FicTrac) | `pixi run qc -- --tier next --rig ww14` | the `okey-cokey-start.ps1` flags; `g6_color_optomotor_smoke` (~2 min); BIAS must already be running (port 5010) — the pre-check drives the bias connector; Foxglove `arena-okey-cokey.json` | +| **Bergamo 2P PC** | `pixi run qc -- --tier next --rig bergamo` | `--scanimage` against ScanImage's JSON server (5555); `g6_2x10_2p_diagnostics` | +| this Mac (dry run, no controller) | `pixi run qc -- --tier next --rig mac` | FicTrac-MacOS on the looped sample video; the Studio run is skipped, the pre-check's run is judged | + +Through a **Claude Code session (Remote Control)**: add `--auto`. Nothing then waits for Enter — each step +advances when its automated check holds, the `LOOK >` lines are what you confirm in chat, and the session +reads the pass/fail table at the end. Without `--auto`, Enter moves on at each `LOOK >` and anything you type +is kept as a remark in the validation note. + +Other switches: `--tier prod` (what Production serves), `--sha ` (a build not deployed yet, or a +Next placeholder), `--no-browser` (pre-check + reports only), `--no-foxglove`, `--skip-install`, +`--skip-requires`, `--keep-browser`, `--rm-worktree`, `--qc-root DIR`, `--lna-prompt`, `--list-rigs`. + +## The two clicks + +1. **Web Serial port picker.** When the script prints `CLICK >`, click **Connect** in the Studio and pick the + controller (`G6_Arena`; on some PCs it enumerates as "USB Serial (COMn)"). Chrome only allows this on a + user gesture, so it cannot be scripted. The script waits up to 5 minutes. +2. **Local network access (once per PC, only with `--lna-prompt`).** Chrome ≥ 142 asks a public page for + permission before it may open `ws://localhost`. The QC Chrome profile bypasses that check (the flag the + Foxglove app runs with); the rig's everyday Chrome asked once and remembers. Pass `--lna-prompt` to see the + prompt and click **Allow** instead. + +The bridge Connect, the protocol load, **Run experiment** and the export are driven over Chrome's DevTools +protocol, so you never click them. If DevTools cannot attach, the script falls back to printing the hand-driven +steps (bridge Connect → Connect → Run experiment → confirm the NEXT dialog) and waits for the run's bag to close. + +## What happens, step by step + +| step | automated check | you look at | +|---|---|---| +| 1 preflight | pixi ≥ 0.80, git, Chrome; ports 7447 / 8765 / 60000 (+ 8766) free; the rig's services up (BIAS on ww14, ScanImage on Bergamo) | — | +| 2 candidate | the tier's `build.json` → commit SHA; `git fetch` + a **scratch worktree** at exactly that commit (`\wt\`) | the build label printed (`candidate 2026-10-28-rc2`, PRs …) | +| 3 install | `pixi install --locked` in the worktree — its own environment, the rig's checkout and `.pixi` are never touched; `typed` / `viz` where the profile says so | — | +| 4 graph | the candidate's `pixi run rig` with the rig's flags (flags that build does not know are dropped with a warning), bags + logs under the dated QC folder; Foxglove opened on `ws://localhost:8766` | the rig window lists its nodes; Foxglove shows the layout (import `rig/foxglove/.json` once) | +| 5 fictrac | FicTrac started (or found running); frames counted at the gateway; the simulator takes over when the camera gives none | FicTrac tracking; the printed frames/s | +| 6 pre-check | `tests/studio_emulator.py`: the Studio's exact bridge conversation (hello_ack ≥ 3.4, behavior_v2 ack, an epoch of frames, export; on ww14 also the BIAS connector) — **a broken candidate fails here**, before any browser | all PASS | +| 7 browser | Chrome (QC profile) on the tier's `arena_studio.html?mode=run&rig=…`; build SHA in the page = the candidate; footer; protocol loaded; no console errors; bridge connected with frames; **your click**; firmware + panels; **Run experiment** | the Studio; the arena during the run (the rig's `look` line says what the display should do) | +| 8 reports | `pixi run export` (closes the bag), `metadata.yaml`, `runlog-check.py`, `closed-loop-report.py` (idx mismatch ≤ 1 %), `bench-check.py` with the rig's holds + channel + build | the numbers | +| 9 teardown | FicTrac / simulator / graph stopped, Chrome closed, ports free again | the pass/fail table | + +At the end the script prints the table, writes `summary.md` and `validation-note.md` into the dated folder and +prints the exact command that posts the note: + +```powershell +cd C:\Lab\GitHub\webDisplayTools; pixi run tiers -- validate --rig ww18 --by "Michael" --notes-file "C:\qc\2026-10-28\validation-note.md" +``` + +Exit code 0 means every automated check passed; the `LOOK` rows are yours. + +## Where things land + +| | Windows | Mac | +|---|---|---| +| dated run folder (`qc.log`, `graph.log`, `fictrac.log`, `runlogs\` bags + live logs, `exports\`, `reports\`, `bias\`, `summary.md`, `validation-note.md`) | `C:\qc\\` | `~/qc//` | +| scratch worktree of the build (+ its `.pixi` environments) | `C:\qc\wt\\` | `~/qc/wt//` | +| QC Chrome profile (no course token, telemetry ring on) | `C:\qc\chrome-profile\` | `~/qc/chrome-profile/` | + +**Disk:** a worktree environment is ~1.6 GB (`default`), +0.3 GB for `viz`; with a warm pixi cache the install +takes 10–15 s. Worktrees are kept for re-runs (`--rm-worktree` removes one at the end; `git worktree prune` +after deleting by hand). The rig's own `rig\.pixi` is never re-synced by QC. + +## Safety on a rig PC + +- **Refuses to start while anything is on 7447 / 8765 / 60000** (a running `pixi run rig`, bridge 3.4, another + FicTrac receiver) — stop that first. One Zenoh graph per host; never between experiments on a rig that is + mid-run. +- The rig's checkout is read only (`git fetch` + `git worktree add`); the candidate runs from the scratch + worktree with its own environment and writes only under the QC root. +- The QC Chrome profile holds no course token, so a run is **not** committed to the course repo; the log comes + out through `pixi run export` into the QC folder instead. +- Everything the script started is stopped in teardown (SIGINT → TERM → KILL, in reverse order) and the ports + are checked free; Ctrl-C does the same. + +## Adding a rig + +One `[rigs.]` table in `rig/qc/profiles.toml`: `hosts` (for detection), `rig_flags`, `envs`, +`studio_rig` (a name from `configs/rigs/index.json`), `protocol` (`key` = a `protocols/index.json` key loaded +through `?p=`, or `path` = loaded through the page), `fictrac` (`none` | `real` with `cmd`/`cwd`/`process`/ +`fallback` | `sample`), `requires` (ports that must answer first), `precheck`, `foxglove.layout`, `reports` +(`closed_loop`, `holds`), and `checklist` lines that override the defaults by id. `tests/test-rig-qc.py` +checks every row has what `qc.py` reads. + +## Known limits + +- The run-log judge needs the controller telemetry ring on — the QC profile sets it before the page loads; the + first run in a brand-new profile can still carry a ring backlog (ww18, 2026-10-27), so re-run if + `runlog-check` complains about pre-run rows. +- `--tier prod` on a build whose `rig/pixi.lock` predates pixi 0.80 fails at the install step with pixi's + "lock file not up-to-date" (issue #280); that is the build's state, not a QC bug. +- FicTrac on this Mac runs the 10 s sample video looped (ffmpeg) at 50 fps; without ffmpeg frames last 10 s. +- Foxglove is opened on the connection by deep link; the layout is imported by hand once per PC. diff --git a/rig/pixi.toml b/rig/pixi.toml index 97e98349..b1dfadc0 100644 --- a/rig/pixi.toml +++ b/rig/pixi.toml @@ -214,3 +214,7 @@ check-tiff2mcap = "python tools/check_tiff2mcap.py" rqt-graph = "rqt_graph" rqt-topic = "rqt_topic" foxglove-bridge = "ros2 run foxglove_bridge foxglove_bridge --ros-args -p port:=8766 -p address:=127.0.0.1" +# ── the five-minute release validation (rig/QC.md): `pixi run qc -- --tier next [--rig ww18] [--auto]` ── +# Checks out the tier's build in a scratch worktree with its own environment, runs the graph, the Studio +# emulator pre-check, the tier's Studio in Chrome, the reports, and prints a validation note. +qc = "python qc/qc.py" diff --git a/rig/qc/profiles.toml b/rig/qc/profiles.toml new file mode 100644 index 00000000..0fb5e481 --- /dev/null +++ b/rig/qc/profiles.toml @@ -0,0 +1,127 @@ +# profiles.toml — the per-rig rows of the five-minute release validation (`pixi run qc`, rig/qc/qc.py). +# +# A new rig is a new `[rigs.]` table; the checklist text Michael reads at each step is data +# here, not code. Every key a rig leaves out comes from `[defaults]`. Paths with backslashes are +# TOML literal strings ('…'). rig/QC.md is the procedure page. + +[defaults] +repo = "reiserlab/webDisplayTools" +min_pixi = "0.80" +tier_url = { next = "https://reiserlab.github.io/webDisplayTools/next", prod = "https://reiserlab.github.io/webDisplayTools" } +# Where a QC run keeps its worktree, Chrome profile and dated run folders (`--qc-root` overrides). +qc_root = { nt = 'C:\qc', posix = "~/qc" } +# pixi environments installed in the candidate's scratch worktree (`-e typed` where the rig uses it, +# `viz` for Foxglove). `default` is always installed. +envs = ["default"] +# Ports the rig graph binds — refused when something already holds them (rig_nodes.supervise.preflight). +ports = [ + ["tcp", 7447, "a Zenoh router (another `pixi run rig`)"], + ["tcp", 8765, "a Studio gateway: bridge 3.4 (`pixi run bridge` at the repo root) or another rig"], + ["udp", 60000, "a FicTrac receiver: bridge 3.4 or another fictrac adapter"], +] +foxglove_port = 8766 +gateway_url = "ws://127.0.0.1:8765" +# The no-browser pre-check (rig/tests/studio_emulator.py): epochs of the Studio's exact conversation. +precheck = { epochs = 1, epoch_s = 4, min_frames = 40 } +connect_timeout_s = 300 # how long to wait for the Web Serial click +run_timeout_s = 600 # how long a Studio run may take before it counts as stuck +frames_probe_s = 6 # how long to count gateway frames before deciding FicTrac is silent +studio_rig = "cshl_g6_2x10_ball" +controller = true +# Checklist steps: one `look` line per phase, shown when that phase is reached. A rig overrides an +# id by repeating it; ids: graph, fictrac, precheck, studio, bridge, controller, run, reports, done. +checklist = [ + { id = "graph", look = "Graph up: the rig window lists router + fictrac + bridge (+ the rig's extra nodes); no red tracebacks." }, + { id = "fictrac", look = "FicTrac frames reach the gateway (rate printed below, ≈ camera fps; the simulator gives 50 Hz)." }, + { id = "precheck", look = "Pre-check: every emulator assertion PASS (hello_ack ≥ 3.4, behavior_v2 ack, epoch frames, export)." }, + { id = "studio", look = "Studio open on the right tier: footer version as expected, build SHA = the candidate (NEXT banner on /next/), protocol loaded in the Run view, no console errors." }, + { id = "bridge", look = "Run view: bridge dot green, FicTrac rate ticking (frames/s), log level behavior_v2 acknowledged." }, + { id = "controller", look = "After your click: firmware label and panel inventory shown, panels all green, SD listing populated." }, + { id = "run", look = "Watch the arena while the protocol runs (steps printed as they start); the run ends with outcome completed, no fault, no bridge-lost alert." }, + { id = "reports", look = "Bag closed (metadata.yaml present), export identical to the live file, runlog-check OK, closed-loop idx mismatch 0 %." }, + { id = "done", look = "Ports 7447/8765/60000 free again; run logs and bags in the dated QC folder named below." }, +] + +# ── ww18: G6 2×10 fly-on-ball, real FicTrac (FLIR/Spinnaker), Windows 11, pixi 0.83 ────────────── +[rigs.ww18] +label = "ww18 — G6 2×10 fly-on-ball, real FicTrac" +hosts = ["reiserlab-ww18"] +envs = ["default", "typed", "viz"] +rig_flags = ["--foxglove"] +studio_rig = "cshl_g6_2x10_ball" +# Not in protocols/index.json (so no `?p=` key): loaded from the tier's raw YAML through the page. +protocol = { path = "protocols/validation/bench_closed_loop_2x10.yaml", duration_s = 25 } +# FicTrac must start through fictrac.bat (puts its pixi env + Spinnaker on PATH); run from the +# config's folder. When it is already running it is used as is. If the camera fails (no frames in +# `frames_probe_s`), the FicTrac simulator takes over and the report says SIM. +fictrac = { kind = "real", cmd = ['C:\Lab\GitHub\fictrac\build\Release\fictrac.bat', 'C:\Lab\GitHub\fictrac\cshl\rig05_vfov28_2026-09-22\config.txt'], cwd = 'C:\Lab\GitHub\fictrac\cshl\rig05_vfov28_2026-09-22', process = "fictrac.exe", fallback = "sim" } +foxglove = { layout = "foxglove/arena-fictrac.json" } +reports = { closed_loop = true, holds = ["epoch_a_frame25=25", "epoch_b_frame75=75"] } +checklist = [ + { id = "fictrac", look = "FicTrac window tracking the ball (≈ 96 fps on ww18); the gateway rate printed below matches." }, + { id = "run", look = "Arena: 3 s background, then the grating HOLDS at frame 25 while the ball is still; turn the ball and the grating follows (coupling −1); ~11 s in it jumps to frame 75 and holds again; background to finish. About 25 s." }, + { id = "reports", look = "runlog-check OK (4 pass / 0 fail), both epochs open on 25 / 75 with 0 % idx mismatch, bench-check PASS with channel + build = this candidate." }, +] + +# ── ww14: okey-cokey camera rig — BIAS (FLIR BFS-U3-51S5M) + G6 four-color, no FicTrac ────────── +[rigs.ww14] +label = "ww14 — okey-cokey camera rig (BIAS + G6 four-color, no FicTrac)" +hosts = ["reiserlab-ww14"] +envs = ["default", "viz"] +# The flags of rig/tools/okey-cokey-start.ps1; flags the candidate's rig.py does not know are dropped +# with a warning (e.g. --no-fictrac / --stimulus-args before PR #269). {qc_dir} = this run's folder. +rig_flags = ["--no-fictrac", "--bias", "--bias-args", "--video-dir {qc_dir}\\bias", "--foxglove", "--viz-args", "--sources bias", "--stimulus-args", "--panel-layout four-color"] +studio_rig = "cshl_g6_2x10" +protocol = { key = "g6_color_optomotor_smoke", path = "protocols/g6_color_optomotor_smoke.yaml", duration_s = 110 } +fictrac = { kind = "none" } +# BIAS must already be running with its HTTP command port on (tools/okey-cokey-start.ps1 starts it). +requires = [["tcp", 5010, "BIAS (test_gui.exe) with the external-control port on — start it first, then re-run"]] +# The pre-check also drives the bias connector (connect / startRecording / getTimestamp / stopRecording). +precheck = { epochs = 0, epoch_s = 0, min_frames = 0, connector = "bias" } +foxglove = { layout = "foxglove/arena-okey-cokey.json" } +reports = { closed_loop = false, holds = [] } +checklist = [ + { id = "fictrac", look = "No FicTrac on this rig: the gateway answers hello_ack and advertises the bias connector." }, + { id = "precheck", look = "Pre-check: emulator PASS incl. the BIAS connector (startRecording → getTimestamp > 0 → stopRecording); a .ufmf appeared under the QC folder." }, + { id = "run", look = "Arena: 2 s dark, then eight 10 s gratings — violet, blue, green, yellow, each clockwise then counter-clockwise (this arena is mounted upside down, so CW trials turn CCW seen from above) — 2 s dark between them; BIAS records one ufmf for the whole run. About 2 minutes." }, + { id = "reports", look = "runlog-check OK (8 pass / 0 fail / 0 unknown), bag closed, BIAS ufmf present and > 1 MB." }, +] + +# ── Bergamo 2P PC: G6 2×10 line-sync, ScanImage JSON server; imaging nodes live in the private repo ─ +[rigs.bergamo] +label = "Bergamo 2P PC — G6 2×10 two-photon (ScanImage connector)" +hosts = ["reiserlab-bergamo1"] +envs = ["default"] +rig_flags = ["--no-fictrac", "--scanimage"] +studio_rig = "bergamo_g6_2x10_2p" +protocol = { key = "g6_2x10_2p_diagnostics", path = "protocols/g6_2x10_2p_diagnostics.yaml", duration_s = 60 } +fictrac = { kind = "none" } +requires = [["tcp", 5555, "ScanImage's JSON server (start ScanImage from a terminal MATLAB and enable the server)"]] +precheck = { epochs = 0, epoch_s = 0, min_frames = 0 } +reports = { closed_loop = false, holds = [] } +checklist = [ + { id = "fictrac", look = "No FicTrac on this rig: the gateway answers hello_ack and advertises the scanimage connector." }, + { id = "run", look = "Arena: the 2P diagnostics sequence (brightness ramp and steps, 1 Hz flash, sparse, 6 s all-off); the Run view shows the controller-settings assertion (panel mode, refresh, SPI, AO) with no manual Console steps. About 1 minute." }, + { id = "reports", look = "runlog-check OK, bag closed." }, +] + +# ── this Mac: FicTrac-MacOS on the sample video, no arena controller (development + dry runs) ──── +[rigs.mac] +label = "Mac — FicTrac sample video, no controller" +hosts = ["reiserm-lm6"] +envs = ["default"] +rig_flags = ["--foxglove"] +studio_rig = "cshl_g6_2x10_ball" +protocol = { path = "protocols/validation/bench_closed_loop_2x10.yaml", duration_s = 25 } +controller = false +# FicTrac on the 10 s sample video: the sample folder is copied under the QC root, sock_port set to +# 60000, src_fps paces the playback (FicTrac plays a file flat out otherwise), and (when ffmpeg is +# available) the video is looped to `loop_s` so frames outlast the run. +fictrac = { kind = "sample", cmd = ["~/Documents/GitHub/Fictrac-MacOS/build/macos-opencv5/bin/fictrac"], sample_dir = "~/Documents/GitHub/Fictrac-MacOS/sample", process = "fictrac", fallback = "sim", loop_s = 180, fps = 50 } +foxglove = { layout = "foxglove/arena-fictrac.json" } +reports = { closed_loop = true, holds = [] } +checklist = [ + { id = "fictrac", look = "FicTrac tracking the sample ball video; gateway rate ≈ the video's fps." }, + { id = "run", look = "No controller on this Mac: the Studio run is skipped; the pre-check's run is what the reports judge." }, + { id = "reports", look = "runlog-check skips or passes (no controller rows), closed-loop report has one epoch with 0 % idx mismatch, bag closed." }, +] diff --git a/rig/qc/qc.py b/rig/qc/qc.py new file mode 100644 index 00000000..83ff2076 --- /dev/null +++ b/rig/qc/qc.py @@ -0,0 +1,952 @@ +#!/usr/bin/env python3 +"""qc.py — the five-minute release validation of a webDisplayTools tier, run on a rig PC. + + pixi run qc -- --tier next # the live candidate on /next/, rig detected from the hostname + pixi run qc -- --tier prod --rig ww18 # what Production serves, on ww18's profile + pixi run qc -- --tier next --rig ww14 --auto # no Enter prompts (a Claude session drives it) + pixi run qc -- --list-rigs + +One command replaces the hour of hand-driven QC (docs/development/handover-qc-windows-2026-10-26.md): + + 1. preflight rig profile (rig/qc/profiles.toml), ports 7447/8765/60000 free, the rig's services up + 2. candidate the tier's build.json → SHA; a scratch git worktree of exactly that commit + 3. install `pixi install --locked` in the worktree (its own environment; the rig's checkout and + .pixi are never touched), plus `typed` / `viz` where the rig uses them + 4. graph the candidate's `pixi run rig` with the rig's flags (unknown flags dropped with a warning), + bags + run logs in the dated QC folder; Foxglove opened on the rig's layout + 5. fictrac the rig's FicTrac (started if needed), frames counted at the gateway; simulator fallback + 6. pre-check rig/tests/studio_emulator.py speaks the Studio's exact conversation — a broken + candidate fails HERE, before anyone looks at a browser + 7. browser Chrome (a QC profile, no course token) on the tier's arena_studio.html, driven over the + DevTools protocol: build SHA + footer checked, protocol loaded, bridge connected, + then the ONE human click (Web Serial port picker), then the run (Run experiment) + 8. reports `pixi run export` (closes the bag), runlog-check, closed-loop-report, bench-check + 9. teardown everything stopped, ports free, pass/fail table + a ready-to-paste validation note + +Interactive by default: a LOOK line per step says what to confirm, Enter moves on (anything typed is +kept as a remark). `--auto` never reads stdin — each step advances when its automated check holds — +for a Claude Code session driving the rig while Michael confirms in chat. + +Artifacts: // (qc.log, graph.log, runlogs/, exports/, reports/, summary.md, +validation-note.md); the worktree at /wt/; the Chrome profile at /chrome-profile. +Exit 0 when every automated check passed. +""" + +from __future__ import annotations + +import argparse +import asyncio +import datetime as dt +import json +import os +import re +import shutil +import signal +import socket +import subprocess +import sys +import threading +import time +import urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +RIG_DIR = HERE.parent +REPO = RIG_DIR.parent +sys.path.insert(0, str(HERE)) +sys.path.insert(0, str(REPO / "scripts" / "studio-smoke")) + +import qclib # noqa: E402 + +CDP_PORT = 9333 +RUN_POLL_S = 0.5 + + +class QCError(Exception): + pass + + +# ── logging ────────────────────────────────────────────────────────────────── + + +class Log: + def __init__(self, path: Path) -> None: + self.fh = open(path, "a", encoding="utf-8") + self.t0 = time.monotonic() + + def _w(self, line: str) -> None: + stamp = f"{time.monotonic() - self.t0:6.1f}s" + sys.stdout.write(line + "\n") + sys.stdout.flush() + self.fh.write(f"[{stamp}] {line}\n") + self.fh.flush() + + def info(self, msg: str) -> None: + self._w(" " + msg) + + def warn(self, msg: str) -> None: + self._w(" WARN " + msg) + + def step(self, n: int, total: int, title: str) -> None: + self._w("") + self._w(f"[{n}/{total}] {title}") + + def look(self, text: str) -> None: + self._w("") + self._w(" LOOK > " + text) + + def click(self, text: str) -> None: + self._w("") + self._w(" CLICK > " + text) + + def result(self, verdict: str, step: str, detail: str = "") -> None: + self._w(f" [{verdict}] {step}" + (f" — {detail}" if detail else "")) + + +# ── small helpers ──────────────────────────────────────────────────────────── + + +def run(argv: list[str], cwd: Path | str | None = None, env: dict | None = None, timeout: float = 600, + check: bool = False) -> subprocess.CompletedProcess: + p = subprocess.run([str(a) for a in argv], cwd=str(cwd) if cwd else None, env=env, capture_output=True, + text=True, timeout=timeout, encoding="utf-8", errors="replace") + if check and p.returncode != 0: + raise QCError(f"`{' '.join(str(a) for a in argv[:4])}…` failed ({p.returncode}): " + f"{(p.stderr or p.stdout).strip()[-800:]}") + return p + + +def http_get(url: str, timeout: float = 20) -> str: + req = urllib.request.Request(url, headers={"Cache-Control": "no-cache", "Pragma": "no-cache", + "User-Agent": "webDisplayTools-qc"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read().decode("utf-8", errors="replace") + + +def process_running(name: str) -> bool: + if not name: + return False + try: + if os.name == "nt": + p = run(["tasklist", "/FI", f"IMAGENAME eq {name}", "/NH", "/FO", "CSV"], timeout=20) + return name.lower() in p.stdout.lower() + p = run(["pgrep", "-x", name], timeout=10) + return p.returncode == 0 + except Exception: # noqa: BLE001 + return False + + +def open_url(url: str) -> bool: + try: + if os.name == "nt": + os.startfile(url) # type: ignore[attr-defined] + return True + opener = "open" if sys.platform == "darwin" else "xdg-open" + return subprocess.run([opener, url], capture_output=True, timeout=20).returncode == 0 + except Exception: # noqa: BLE001 + return False + + +def newest_bag(runlogs: Path) -> Path | None: + bags = sorted(runlogs.glob("*.bag"), key=lambda p: p.stat().st_mtime) if runlogs.is_dir() else [] + return bags[-1] if bags else None + + +class Proc: + """A child in its own process group whose output goes to a file (+ chosen lines echoed).""" + + def __init__(self, name: str, argv: list[str], log_path: Path, cwd: Path | None = None, env: dict | None = None, + echo: "callable | None" = None) -> None: + from rig_nodes.supervise import popen_group # type: ignore[import-not-found] + + self.name = name + self.log_path = log_path + self.fh = open(log_path, "a", encoding="utf-8") + self.p = popen_group([str(a) for a in argv], cwd=str(cwd) if cwd else None, env=env, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", bufsize=1) + self.echo = echo + threading.Thread(target=self._pump, daemon=True).start() + + def _pump(self) -> None: + assert self.p.stdout + for line in self.p.stdout: + self.fh.write(line) + self.fh.flush() + if self.echo: + try: + self.echo(line.rstrip("\n")) + except Exception: # noqa: BLE001 + pass + + def alive(self) -> bool: + return self.p.poll() is None + + def stop(self, grace_s: float = 10.0) -> None: + from rig_nodes.supervise import SIGKILL, signal_group # type: ignore[import-not-found] + + if not self.alive(): + return + try: + signal_group(self.p, signal.SIGINT) + self.p.wait(grace_s) + except subprocess.TimeoutExpired: + signal_group(self.p, signal.SIGTERM) + try: + self.p.wait(5) + except subprocess.TimeoutExpired: + signal_group(self.p, SIGKILL) + except (ProcessLookupError, OSError, ValueError): + pass + self.fh.flush() + + def tail(self, n: int = 12) -> str: + try: + return "\n".join(self.log_path.read_text(encoding="utf-8", errors="replace").splitlines()[-n:]) + except OSError: + return "" + + +# ── gateway probes (websockets) ────────────────────────────────────────────── + + +async def _probe(url: str, seconds: float) -> dict: + from websockets.asyncio.client import connect + + out: dict = {"hello_ack": None, "frames": 0, "seconds": seconds} + async with connect(url, max_size=16 * 1024 * 1024) as ws: + await ws.send(json.dumps({"type": "hello", "client": "webDisplayTools", "v": 1})) + end = asyncio.get_running_loop().time() + seconds + while True: + remaining = end - asyncio.get_running_loop().time() + if remaining <= 0: + break + try: + raw = await asyncio.wait_for(ws.recv(), remaining) + except asyncio.TimeoutError: + break + try: + m = json.loads(raw) + except ValueError: + continue + if m.get("type") == "hello_ack": + out["hello_ack"] = {k: v for k, v in m.items() if k != "type"} + elif m.get("type") == "frame": + out["frames"] += 1 + out["rate_hz"] = round(out["frames"] / seconds, 1) if seconds else 0 + return out + + +def probe_gateway(url: str, seconds: float) -> dict: + return asyncio.run(_probe(url, seconds)) + + +# ── the QC run ─────────────────────────────────────────────────────────────── + + +class QC: + TOTAL = 9 + + def __init__(self, a: argparse.Namespace) -> None: + self.a = a + self.profiles = qclib.load_profiles() + name = a.rig or qclib.detect_rig(self.profiles, socket.gethostname()) + if not name: + raise QCError(f"this host ({socket.gethostname()}) is not in rig/qc/profiles.toml — pass --rig " + f"({', '.join(qclib.rig_names(self.profiles))}) or add a row") + self.rig = qclib.resolve_rig(self.profiles, name) + self.tier = qclib.tier_key(a.tier) + self.base = qclib.tier_base(self.rig, self.tier) + self.root = Path(a.qc_root).expanduser() if a.qc_root else qclib.qc_root(self.rig) + self.date = dt.date.today().isoformat() + self.qc_dir = self.root / self.date + for sub in ("runlogs", "exports", "reports", "bias"): + (self.qc_dir / sub).mkdir(parents=True, exist_ok=True) + self.log = Log(self.qc_dir / "qc.log") + self.results = qclib.Results() + self.look_text = qclib.checklist_for(self.rig) + self.remarks: list[str] = [] + self.t_start = time.monotonic() + self.procs: dict[str, Proc] = {} + self.chrome: subprocess.Popen | None = None + self.page = None + self.ctx: dict = {"rig_label": self.rig.get("label", name), "tier": self.tier, "date": self.date, + "qc_dir": str(self.qc_dir), "sha": "", "label": ""} + self.sha = "" + self.wt: Path | None = None + self.env: dict | None = None + self.py: str = sys.executable + self.manifest: Path | None = None + self.kept_flags: list[str] = [] + self.fictrac_mode = "none" + self.export_path: Path | None = None + self.studio_ran = False + self.run_summary: dict | None = None + self.channel = "next" if self.tier == "next" else "production" + + # ── prompts ── + def pause(self, prompt: str = "Enter = looks right (type a remark to keep it)") -> str: + if self.a.auto: + return "" + try: + s = input(f" {prompt}: ").strip() + except EOFError: + sys.stdout.write("\n") + return "" + if s: + self.remarks.append(s) + self.log.fh.write(f" remark: {s}\n") + return s + + def look(self, step_id: str, extra: str = "") -> None: + text = self.look_text.get(step_id, "") + if not text and not extra: + return + self.log.look((text + (" " + extra if extra else "")).strip()) + remark = self.pause() + self.results.add(f"look: {step_id}", "LOOK", remark or ("confirm in chat" if self.a.auto else "looked right")) + + def record(self, step: str, ok: bool, detail: str = "", seconds: float | None = None, fatal: bool = False) -> bool: + v = "PASS" if ok else "FAIL" + self.results.add(step, v, detail, seconds) + self.log.result(v, step, detail) + if not ok and fatal: + raise QCError(f"{step}: {detail}") + return ok + + def skip(self, step: str, why: str) -> None: + self.results.add(step, "SKIP", why) + self.log.result("SKIP", step, why) + + # ── 1 preflight ── + def preflight(self) -> None: + from rig_nodes.supervise import preflight # type: ignore[import-not-found] + + self.log.step(1, self.TOTAL, f"preflight — rig {self.rig['name']} ({self.rig.get('label', '')}), tier {self.tier}") + self.log.info(f"QC folder: {self.qc_dir}") + pv = run(["pixi", "--version"], timeout=30).stdout.strip() + ok = qclib.parse_version(pv) >= qclib.parse_version(self.rig["min_pixi"]) + self.record("pixi >= " + self.rig["min_pixi"], ok, pv or "pixi not found", fatal=True) + self.record("git available", shutil.which("git") is not None, shutil.which("git") or "git not on PATH", fatal=True) + ports = [tuple(p) for p in self.rig["ports"]] + if "--foxglove" in self.rig.get("rig_flags", []) and not self.a.no_foxglove: + ports.append(("tcp", int(self.rig["foxglove_port"]), "another foxglove_bridge")) + taken = preflight(ports) + self.record("ports free (7447/8765/60000…)", not taken, "\n".join(taken) if taken else "nothing bound", fatal=True) + missing = [] + from rig_nodes.supervise import port_in_use # type: ignore[import-not-found] + + for proto, port, who in self.rig.get("requires", []): + if not port_in_use(proto, int(port)): + missing.append(f"{proto} {port}: {who}") + if missing and not self.a.skip_requires: + self.record("required services up", False, "; ".join(missing), fatal=True) + elif missing: + self.record("required services up", True, "SKIPPED (--skip-requires): " + "; ".join(missing)) + else: + self.record("required services up", True, "none required" if not self.rig.get("requires") else "all answering") + if not self.a.no_browser: + try: + import cdp # type: ignore[import-not-found] + + chrome = cdp.find_chrome(self.a.chrome) + self.record("Chrome/Edge found", True, chrome) + except SystemExit as e: + self.record("Chrome/Edge found", False, str(e)) + self.a.no_browser = True + + # ── 2 candidate ── + def candidate(self) -> None: + self.log.step(2, self.TOTAL, f"candidate — {self.base}/build.json") + info = None + try: + info = qclib.parse_build_json(http_get(self.base + "/build.json")) + except Exception as e: # noqa: BLE001 + if not self.a.sha: + raise QCError(f"cannot read {self.base}/build.json: {e}") from e + self.log.warn(f"build.json unreadable ({e}); using --sha") + sha = (self.a.sha or (info or {}).get("sha") or "").lower() + if info and info["placeholder"] and not self.a.sha: + raise QCError(f"{self.tier} is a placeholder ({info['label']}) — nothing to validate (or pass --sha)") + if info and self.a.sha and info["sha"] and not info["sha"].startswith(self.a.sha.lower()): + self.log.warn(f"--sha {self.a.sha} differs from the tier's build {info['sha'][:7]} — validating what you asked for") + if not re.fullmatch(r"[0-9a-f]{7,40}", sha): + raise QCError(f"no usable SHA ({sha!r})") + label = (info or {}).get("label") or sha[:7] + self.log.info(f"build: {label} sha {sha[:10]} built {(info or {}).get('built_at', '?')}" + + (f" PRs {(info or {}).get('prs')}" if (info or {}).get("prs") else "")) + # fetch + scratch worktree + run(["git", "-C", REPO, "worktree", "prune"], timeout=60) + if run(["git", "-C", REPO, "cat-file", "-e", f"{sha}^{{commit}}"], timeout=30).returncode != 0: + f = run(["git", "-C", REPO, "fetch", "--quiet", "origin", sha], timeout=300) + if f.returncode != 0: + run(["git", "-C", REPO, "fetch", "--quiet", "origin"], timeout=300) + if run(["git", "-C", REPO, "cat-file", "-e", f"{sha}^{{commit}}"], timeout=30).returncode != 0: + raise QCError(f"commit {sha[:10]} not found on origin after fetch") + full = run(["git", "-C", REPO, "rev-parse", f"{sha}^{{commit}}"], timeout=30, check=True).stdout.strip() + self.sha = full + self.ctx.update({"sha": full, "label": label}) + wt = self.root / "wt" / full[:7] + if wt.exists(): + head = run(["git", "-C", wt, "rev-parse", "HEAD"], timeout=30).stdout.strip() + if head != full: + run(["git", "-C", wt, "checkout", "--quiet", "--detach", full], timeout=120, check=True) + self.log.info(f"worktree reused: {wt}") + else: + wt.parent.mkdir(parents=True, exist_ok=True) + run(["git", "-C", REPO, "worktree", "add", "--detach", wt, full], timeout=300, check=True) + self.log.info(f"worktree created: {wt}") + dirty = run(["git", "-C", wt, "status", "--porcelain", "--untracked-files=no"], timeout=60).stdout.strip() + self.record("scratch worktree at the candidate SHA", not dirty, f"{wt} @ {full[:7]}" + (f"; DIRTY: {dirty[:200]}" if dirty else ""), fatal=True) + self.wt = wt + self.manifest = wt / "rig" / "pixi.toml" + if not self.manifest.is_file(): + raise QCError(f"{self.manifest} missing — this build has no rig/ workspace") + + # ── 3 install ── + def install(self) -> None: + assert self.wt and self.manifest + self.log.step(3, self.TOTAL, "install — the candidate's own pixi environment(s), `--locked`") + envs = list(dict.fromkeys(["default", *self.rig["envs"]])) + if self.a.no_foxglove and "viz" in envs: + envs.remove("viz") + for env in envs: + if self.a.skip_install: + self.skip(f"pixi install -e {env}", "--skip-install") + continue + t0 = time.monotonic() + p = run(["pixi", "install", "--locked", "--manifest-path", self.manifest, "-e", env], cwd=self.wt / "rig", timeout=3600) + msg = [line.strip() for line in (p.stderr or p.stdout).strip().splitlines() if line.strip()] + errs = [line for line in msg if "Error" in line or "×" in line or "requires-pixi" in line] + tail = " | ".join((errs or msg)[-3:]) + self.record(f"pixi install -e {env}", p.returncode == 0, tail[-300:], time.monotonic() - t0, fatal=True) + probe = run(["pixi", "run", "--manifest-path", self.manifest, "--locked", "-e", "default", "python", "-c", + "import os,sys,json;print(json.dumps({'exe':sys.executable,'env':dict(os.environ)}))"], + cwd=self.wt / "rig", timeout=300, check=True) + try: + d = json.loads(probe.stdout.strip().splitlines()[-1]) + except (ValueError, IndexError) as e: + raise QCError(f"could not read the environment: {probe.stdout[-300:]} {probe.stderr[-300:]}") from e + self.py, self.env = d["exe"], d["env"] + ver = "?" + lst = run(["pixi", "list", "--manifest-path", self.manifest, "--locked", "--json"], cwd=self.wt / "rig", timeout=300) + try: + for pkg in json.loads(lst.stdout): + if pkg.get("name") == "rig_nodes": + ver = pkg.get("version", "?") + except ValueError: + pass + self.ctx["rig_nodes"] = ver + self.record("environment usable", True, f"python {self.py}; rig_nodes {ver}") + + # ── 4 graph ── + def graph(self) -> None: + assert self.wt and self.env + from rig_nodes.supervise import wait_port # type: ignore[import-not-found] + + self.log.step(4, self.TOTAL, "graph — the candidate's `pixi run rig` with this rig's flags") + rig_py = self.wt / "rig" / "nodes" / "rig.py" + helptext = run([self.py, rig_py, "--help"], cwd=self.wt / "rig", env=self.env, timeout=120).stdout + flags = qclib.substitute(list(self.rig["rig_flags"]), {"qc_dir": str(self.qc_dir)}) + if self.a.no_foxglove and "--foxglove" in flags: + flags.remove("--foxglove") + kept, dropped = qclib.filter_rig_flags(flags, helptext) + if dropped: + self.log.warn(f"flags this build's rig.py does not know, dropped: {' '.join(dropped)}") + kept += ["--bridge-args", f"--log-dir {self.qc_dir / 'runlogs'}"] + self.kept_flags = kept + self.ctx["graph"] = {"flags": kept, "dropped": dropped} + self.log.info("pixi run rig -- " + " ".join(kept)) + echo = lambda line: self.log.info(" " + line) if ("[rig]" in line or "Traceback" in line or "Error" in line) else None # noqa: E731 + self.procs["graph"] = Proc("graph", [self.py, rig_py, *kept], self.qc_dir / "graph.log", cwd=self.wt / "rig", env=self.env, echo=echo) + t0 = time.monotonic() + up = wait_port("127.0.0.1", 7447, 60) and wait_port("127.0.0.1", 8765, 60) + if not self.procs["graph"].alive(): + up = False + self.record("router :7447 + gateway :8765 up", up, f"{time.monotonic() - t0:.1f} s" if up else self.procs["graph"].tail(), fatal=True) + time.sleep(1.0) + if "--foxglove" in kept: + fport = int(self.rig["foxglove_port"]) + if wait_port("127.0.0.1", fport, 45): + layout = self.rig.get("foxglove", {}).get("layout") + url = f"foxglove://open?ds=foxglove-websocket&ds.url=ws://localhost:{fport}" + opened = open_url(url) + self.record("foxglove_bridge :8766 up", True, "Foxglove opened on it" if opened else "open Foxglove → Open connection → ws://localhost:8766") + if layout: + self.log.info(f"Foxglove layout (Layout → Import from file, once): {self.wt / 'rig' / layout}") + else: + self.record("foxglove_bridge :8766 up", False, "not listening within 45 s (viz environment installed? graph.log has the note)") + self.look("graph") + + # ── 5 fictrac ── + def fictrac(self) -> None: + assert self.wt and self.env + ft = self.rig.get("fictrac", {"kind": "none"}) + kind = ft.get("kind", "none") + self.log.step(5, self.TOTAL, f"fictrac — {kind}") + url = self.rig["gateway_url"] + if kind == "none": + pr = probe_gateway(url, 2.0) + ack = pr.get("hello_ack") or {} + self.record("gateway hello_ack", bool(ack), f"bridge {str(ack.get('bridge', ''))[:40]}; connectors {ack.get('connectors')}", fatal=True) + self.ctx["fictrac"] = "none (this rig has no FicTrac)" + self.fictrac_mode = "none" + self.look("fictrac") + return + started = False + if process_running(ft.get("process", "")): + self.log.info(f"{ft.get('process')} is already running — using it") + self.fictrac_mode = f"{kind} (already running)" + else: + argv, cwd = self._fictrac_argv(ft) + self.log.info("starting FicTrac: " + " ".join(str(a) for a in argv) + f" (cwd {cwd})") + self.procs["fictrac"] = Proc("fictrac", argv, self.qc_dir / "fictrac.log", cwd=cwd) + started = True + self.fictrac_mode = kind + secs = float(self.rig["frames_probe_s"]) + time.sleep(3.0) + pr = probe_gateway(url, secs) + ack = pr.get("hello_ack") or {} + self.record("gateway hello_ack", bool(ack), f"bridge {str(ack.get('bridge', ''))[:40]}", fatal=True) + rate = pr["rate_hz"] + if rate <= 0 and ft.get("fallback") == "sim": + self.log.warn(f"no FicTrac frames in {secs:.0f} s" + (f" — FicTrac log tail:\n{self.procs['fictrac'].tail(6)}" if started else "")) + if started: + self.procs.pop("fictrac").stop() + self.log.info("starting the FicTrac simulator instead (--proto udp --rate 50)") + self.procs["sim"] = Proc("sim", [self.py, "-m", "rig_nodes.sims.fictrac_sim", "--proto", "udp", "--rate", "50"], + self.qc_dir / "sim.log", cwd=self.wt / "rig", env=self.env) + time.sleep(2.0) + rate = probe_gateway(url, 3.0)["rate_hz"] + self.fictrac_mode = "SIM (FicTrac gave no frames)" + self.ctx["fictrac"] = f"{self.fictrac_mode}, {rate} frames/s at the gateway" + self.record("FicTrac frames at the gateway", rate > 0, f"{rate} frames/s ({self.fictrac_mode})", fatal=True) + self.look("fictrac", f"[{rate} frames/s]") + + def _fictrac_argv(self, ft: dict) -> tuple[list[str], Path]: + cmd = [os.path.expanduser(str(c)) for c in ft.get("cmd", [])] + if not cmd: + raise QCError("fictrac.cmd is empty in the profile") + if ft.get("kind") == "sample": + src = Path(os.path.expanduser(ft["sample_dir"])) + dst = self.root / "fictrac-sample" + if not (dst / "config.txt").exists(): + shutil.copytree(src, dst, dirs_exist_ok=True) + cfg = (dst / "config.txt").read_text(encoding="utf-8", errors="replace") + video = re.search(r"^\s*src_fn\s*:\s*(\S+)", cfg, re.M) + src_fn = video.group(1) if video else "sample.mp4" + loop_s = int(ft.get("loop_s", 0) or 0) + looped = dst / "sample_loop.mp4" + if loop_s and shutil.which("ffmpeg") and not looped.exists(): + n = max(1, loop_s // 10) + r = run(["ffmpeg", "-y", "-loglevel", "error", "-stream_loop", str(n), "-i", dst / src_fn, "-c", "copy", looped], timeout=300) + if r.returncode != 0: + self.log.warn("ffmpeg loop failed: " + r.stderr[-200:]) + if looped.exists(): + src_fn = looped.name + (dst / "config.qc.txt").write_text(qclib.fictrac_config_for_qc(cfg, src_fn=src_fn, display=False, src_fps=float(ft.get("fps", 0) or 0) or None), encoding="utf-8") + return [cmd[0], "config.qc.txt"], dst + cwd = Path(os.path.expanduser(str(ft.get("cwd") or Path(cmd[-1]).parent))) + if os.name == "nt" and cmd[0].lower().endswith((".bat", ".cmd")): + cmd = ["cmd.exe", "/c", *cmd] + return cmd, cwd + + # ── 6 pre-check ── + def precheck(self) -> None: + assert self.wt and self.env + pc = self.rig["precheck"] + self.log.step(6, self.TOTAL, "pre-check — the Studio's exact bridge conversation, no browser (studio_emulator.py)") + emu = RIG_DIR / "tests" / "studio_emulator.py" + out, rep = self.qc_dir / "exports" / "precheck.jsonl", self.qc_dir / "reports" / "precheck.json" + argv = [self.py, emu, "--url", self.rig["gateway_url"], "--epochs", str(pc.get("epochs", 1)), "--epoch-s", str(pc.get("epoch_s", 4)), + "--iti-s", "0", "--min-frames", str(pc.get("min_frames", 0)), "--rig-id", f"{self.rig['name']}-qc", "--out", out, "--report", rep] + if pc.get("connector") == "bias": + argv += ["--connector-cmds", "--connector-video", str(self.qc_dir / "bias" / "precheck.ufmf")] + t0 = time.monotonic() + p = run(argv, cwd=self.wt / "rig", env=self.env, timeout=300) + (self.qc_dir / "reports" / "precheck.txt").write_text(p.stderr + p.stdout, encoding="utf-8") + fails = [line.strip() for line in p.stderr.splitlines() if line.strip().startswith("FAIL")] + self.record("pre-check: emulator assertions", p.returncode == 0, ("all PASS; " + (p.stderr.strip().splitlines() or [""])[-1][:120]) if p.returncode == 0 else "; ".join(fails)[:400] or p.stderr[-300:], + time.monotonic() - t0, fatal=True) + if out.exists(): + self.export_path = out # the emulator's run is the fallback subject for the reports + self.look("precheck") + + # ── 7 browser ── + def browser(self) -> None: + self.log.step(7, self.TOTAL, "browser — the tier's Studio in Chrome (QC profile), driven over DevTools") + if self.a.no_browser: + self.skip("Studio in Chrome", "--no-browser") + return + import cdp # type: ignore[import-not-found] + + chrome = cdp.find_chrome(self.a.chrome) + profile = self.root / "chrome-profile" + profile.mkdir(parents=True, exist_ok=True) + url = qclib.studio_url(self.base, self.rig["studio_rig"], self.rig.get("protocol")) + self.log.info(url) + # A never-used profile has not allowed "local network access" for the Pages origin, and Chrome + # (≥ 142) then HOLDS a ws://localhost connection on a permission prompt — the bridge stays + # "connecting…" until someone clicks Allow (verified 2026-10-28 on Chrome 155: a DevTools + # Browser.grantPermissions does not unblock it; this feature flag does, and it is the flag the + # Foxglove desktop app itself runs with). The rig's everyday Chrome asked once and remembers. + flags = [] if self.a.lna_prompt else ["--disable-features=LocalNetworkAccessChecks"] + self.chrome = subprocess.Popen([chrome, f"--user-data-dir={profile}", f"--remote-debugging-port={CDP_PORT}", "--no-first-run", + "--no-default-browser-check", *flags, "--new-window", "about:blank"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + asyncio.run(self._drive(url)) + except QCError: + raise + except Exception as e: # noqa: BLE001 — DevTools unavailable: fall back to the hand-driven steps + self.log.warn(f"DevTools drive failed ({str(e)[:200]}); falling back to manual steps") + self.record("Studio driven over DevTools", False, str(e)[:200]) + self._manual_browser(chrome, profile, url) + + async def _drive(self, url: str) -> None: + import cdp # type: ignore[import-not-found] + import websockets + + ws_url = None + for _ in range(100): + try: + with urllib.request.urlopen(f"http://127.0.0.1:{CDP_PORT}/json/list", timeout=1) as r: + pages = [t for t in json.load(r) if t.get("type") == "page"] + if pages: + ws_url = pages[0]["webSocketDebuggerUrl"] + break + except Exception: # noqa: BLE001 + pass + await asyncio.sleep(0.2) + if not ws_url: + raise RuntimeError("Chrome did not expose DevTools on :%d" % CDP_PORT) + ws = await websockets.connect(ws_url, max_size=64 * 1024 * 1024) + p = cdp.Page(ws) + await p.start() + self.page = p + try: + await self._drive_page(p, url) + finally: + p.task.cancel() + try: + await ws.close() + except Exception: # noqa: BLE001 + pass + + async def _drive_page(self, p, url: str) -> None: + # Per-browser settings the QC needs before the page's first script runs: the telemetry ring on + # (runlog-check needs the controller rows), and the one-time NEXT acknowledgement. + await p.send("Page.addScriptToEvaluateOnNewDocument", {"source": + "try{localStorage.setItem('studio_telemetry','on');sessionStorage.setItem('studio_next_ack','1');}catch(e){}"}) + await p.goto(url, settle=3.0) + build = await p.js("(()=>{const m=document.querySelector('meta[name=\"wdt-build\"]');if(!m)return null;try{return JSON.parse(m.content)}catch(e){return {raw:m.content}}})()") + got = str((build or {}).get("sha") or "") + if got and not got.startswith(self.sha[:7]): + self.log.warn(f"page build {got[:7]} ≠ candidate {self.sha[:7]} — reloading without cache once") + await p.send("Page.reload", {"ignoreCache": True}) + await asyncio.sleep(4.0) + build = await p.js("(()=>{const m=document.querySelector('meta[name=\"wdt-build\"]');if(!m)return null;try{return JSON.parse(m.content)}catch(e){return {raw:m.content}}})()") + got = str((build or {}).get("sha") or "") + footer = (await p.js("(document.querySelector('#footer .foot-left')||{}).textContent||''") or "").strip() + self.record("page build SHA = candidate", bool(got) and got.startswith(self.sha[:7]), f"meta {got[:7] or 'missing'}; channel {(build or {}).get('channel')}; footer: {footer[:60]}") + self.ctx["studio"] = f"{footer[:48]}, build {got[:7]}" + # the protocol + proto = self.rig.get("protocol") or {} + if proto.get("key"): + for _ in range(40): + if await p.js("!!(window.Studio && Studio.currentDoc)"): + break + await asyncio.sleep(0.25) + elif proto.get("path"): + text = http_get(qclib.protocol_url(self.base, proto)) + ok = await p.js("Studio.loadProtocol(%s, %s, 'committed', {landIn:'run'})" % (json.dumps(text), json.dumps(Path(proto["path"]).name))) + if not ok: + self.log.warn("Studio.loadProtocol returned false (banner on the page says why)") + doc = await p.js("Studio.currentDoc?{filename:Studio.currentDoc.filename,rig:(Studio.currentDoc.rig||{}).name}:null") + self.record("protocol loaded in the Run view", bool(doc), json.dumps(doc) if doc else "no document") + api = await p.js("['runOnce','loadProtocol'].every(k=>typeof Studio[k]==='function')&&!!Studio.session&&!!Studio.session.bridge&&typeof Studio.session.bridge.connect==='function'") + self.record("Studio page API the QC drives (runOnce, loadProtocol, session.bridge)", bool(api), "present" if api else "missing — the page changed; update qc.py") + errs = [e for e in p.errors if "favicon.ico" not in e] # the site has no favicon; a 404 there is not a page error + self.record("no console errors on load", not errs, "; ".join(errs)[:300] or "clean") + self.look("studio") + # the bridge + await p.js("(()=>{const b=Studio.session.bridge;if(b.setLogLevel)b.setLogLevel('behavior_v2');if(!b.connected)b.connect('ws://localhost:8765');return true})()") + connected = False + for _ in range(40): + if await p.js("!!(Studio.session.bridge&&Studio.session.bridge.connected)"): + connected = True + break + await asyncio.sleep(0.25) + rate = 0 + if connected and self.fictrac_mode != "none": + for _ in range(32): + rate = await p.js("(Studio.session.bridge.stats||{}).rateHz||0") + if rate and rate > 0: + break + await asyncio.sleep(0.25) + self.record("Studio ↔ gateway connected", connected, f"ws://localhost:8765; {rate} frames/s in the page" if connected else "bridge.connected stayed false") + self.look("bridge", f"[{rate} frames/s]") + if not self.rig.get("controller", True): + self.skip("controller connect + run", "this profile has no controller") + self.look("run") + return + # the one human click + self.log.click("In the Studio: Connect → pick the controller (G6_Arena / USB Serial) in the port picker. " + f"Waiting up to {int(self.rig['connect_timeout_s'])} s.") + t0 = time.monotonic() + while time.monotonic() - t0 < float(self.rig["connect_timeout_s"]): + if await p.js("!!(Studio.session&&Studio.session.connected)"): + break + if int(time.monotonic() - t0) % 30 == 0: + self.log.info("… still waiting for the port picker") + await asyncio.sleep(1.0) + conn = await p.js("!!(Studio.session&&Studio.session.connected)") + self.record("controller connected (your click)", bool(conn), f"after {time.monotonic() - t0:.0f} s" if conn else "timed out", fatal=True) + await asyncio.sleep(4.0) # firmware label, panel inventory, SD listing + fw = await p.js("Studio.firmware||null") + panels = await p.js("({status:Studio.panelsStatus||null,n:(Studio.panels&&Studio.panels.length)||0})") + self.record("firmware label + panel inventory", bool(fw), f"firmware {fw}; panels {panels}") + self.ctx["controller"] = f"firmware {fw}, panels {panels}" + self.look("controller") + # the run + self.log.look(self.look_text.get("run", "Watch the arena while the protocol runs.") + f" (~{proto.get('duration_s', '?')} s)") + await p.js("window.__qc={done:false};Studio.runOnce(true).then(s=>{window.__qc={done:true,summary:s===null?null:(s||{})}}).catch(e=>{window.__qc={done:true,error:String(e)}});true") + t0 = time.monotonic() + last_step = "" + state = {"done": False} + while time.monotonic() - t0 < float(self.rig["run_timeout_s"]): + state = await p.js("window.__qc") or {"done": False} + if state.get("done"): + break + step = await p.js("((document.getElementById('rpStep')||{}).textContent||'')+' '+((document.getElementById('runInlineTxt')||{}).textContent||'')") + if step and step != last_step: + self.log.info(f" {step.strip()}") + last_step = step + await asyncio.sleep(RUN_POLL_S) + if not state.get("done"): + self.studio_ran = True + self.record("Studio run", False, f"still running after {self.rig['run_timeout_s']} s — STOP it in the Studio") + return + s = state.get("summary") + if s is None and not state.get("error"): + # nothing started, so the reports below judge the pre-check's log, not a Studio run + self.record("Studio run", False, "runOnce returned null — the run could not start (controller-settings assertion or a `requires:` gate; see the page banner)") + return + self.studio_ran = True + if state.get("error"): + self.record("Studio run", False, str(state["error"])[:300]) + return + ok = bool(s.get("completed")) and not s.get("fault") and not s.get("bridgeLost") and not s.get("aborted") + outcome = "completed" if ok else (s.get("fault") and f"fault {s.get('fault')}") or (s.get("bridgeLost") and "BRIDGE_LOST") or (s.get("aborted") and "aborted") or "incomplete" + self.run_summary = s + self.ctx["run"] = f"{outcome} in {time.monotonic() - t0:.0f} s" + self.record("Studio run (Run experiment)", ok, f"{outcome}; steps {s.get('steps', s.get('completedSteps', '?'))}", time.monotonic() - t0) + self.results.add("look: run", "LOOK", self.pause() or ("confirm in chat" if self.a.auto else "looked right")) + + def _manual_browser(self, chrome: str, profile: Path, url: str) -> None: + if self.chrome and self.chrome.poll() is None: + self.chrome.terminate() + self.chrome = subprocess.Popen([chrome, f"--user-data-dir={profile}", "--no-first-run", "--new-window", url], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + proto = self.rig.get("protocol") or {} + self.log.click("Hand-driven Studio steps: (1) check the footer version and the build badge; " + + (f"(2) Protocol ▾ → Open… {proto.get('path')}; " if not proto.get("key") else "(2) the protocol is in the URL; ") + + "(3) Run view → bridge Connect (ws://localhost:8765); (4) Connect → pick the controller; (5) ▶ Run experiment (confirm the NEXT dialog).") + self.look("studio") + self.look("bridge") + if not self.rig.get("controller", True): + self.skip("controller connect + run", "this profile has no controller") + return + self.look("controller") + self.log.look(self.look_text.get("run", "Watch the arena while the protocol runs.")) + before = newest_bag(self.qc_dir / "runlogs") + if self.a.auto: + t0 = time.monotonic() + while time.monotonic() - t0 < float(self.rig["run_timeout_s"]) + float(self.rig["connect_timeout_s"]): + b = newest_bag(self.qc_dir / "runlogs") + if b and b != before and (b / "metadata.yaml").exists(): + break + time.sleep(2.0) + else: + self.pause("Enter when the run has finished") + self.studio_ran = True + self.record("Studio run (hand-driven)", True, "judged by the reports below") + + # ── 8 reports ── + def reports(self) -> None: + assert self.wt and self.env + self.log.step(8, self.TOTAL, "reports — export (closes the bag), runlog-check, closed-loop-report, bench-check") + exp_dir = self.qc_dir / "exports" + tmp = exp_dir / "run.jsonl" + p = run([self.py, self.wt / "rig" / "nodes" / "export_log.py", "--url", self.rig["gateway_url"], "--json", "--out", tmp], cwd=self.wt / "rig", env=self.env, timeout=180) + name = None + try: + name = json.loads(p.stdout.strip().splitlines()[-1]).get("name") + except (ValueError, IndexError): + pass + if p.returncode == 0 and tmp.exists(): + final = exp_dir / (name or f"arena-log-{self.date}.jsonl") + if final != tmp: + if final.exists(): + final.unlink() + tmp.rename(final) + if self.studio_ran: + self.export_path = final + identical = "identical_to_live" in (self.procs["graph"].tail(400) if "graph" in self.procs else "") + self.record("pixi run export", True, f"{final.name}" + ("; converted == live" if identical else "")) + else: + self.record("pixi run export", False, (p.stderr or p.stdout).strip()[-300:]) + bag = newest_bag(self.qc_dir / "runlogs") + self.record("bag closed + indexed (metadata.yaml)", bool(bag and (bag / "metadata.yaml").exists()), str(bag) if bag else "no bag in runlogs/") + log = self.export_path + if not log or not log.exists(): + self.record("run log for the reports", False, "no export to judge") + return + self.log.info(f"judging {log}") + scripts = self.wt / "scripts" + rep_dir = self.qc_dir / "reports" + r = run([self.py, scripts / "runlog-check.py", log], env=self.env, timeout=300) + (rep_dir / "runlog-check.txt").write_text(r.stdout + r.stderr, encoding="utf-8") + tail = [line for line in r.stdout.splitlines() if line.strip()][-3:] + if self.rig.get("controller", True): + self.record("runlog-check", r.returncode == 0, " | ".join(tail)[-300:]) + else: + self.results.add("runlog-check", "PASS" if r.returncode == 0 else "SKIP", ("no controller on this rig; " + " | ".join(tail))[-300:]) + self.log.result(self.results.rows[-1]["verdict"], "runlog-check", self.results.rows[-1]["detail"]) + if self.rig["reports"].get("closed_loop"): + r = run([self.py, scripts / "closed-loop-report.py", "--json", log], env=self.env, timeout=300) + (rep_dir / "closed-loop-report.json").write_text(r.stdout, encoding="utf-8") + md = run([self.py, scripts / "closed-loop-report.py", log], env=self.env, timeout=300) + (rep_dir / "closed-loop-report.md").write_text(md.stdout + md.stderr, encoding="utf-8") + try: + rep = json.loads(r.stdout) + epochs = rep.get("epochs", []) if isinstance(rep, dict) else [] + mism = [e.get("idx_mismatch_fraction") for e in epochs if "idx_mismatch_fraction" in e] + ok = bool(epochs) and all(m is not None and m <= 0.01 for m in mism) + self.record("closed-loop-report: idx mismatch ≤ 1 %", ok, f"{len(epochs)} epoch(s), mismatch {mism}") + except ValueError: + self.record("closed-loop-report", False, (r.stderr or r.stdout)[-300:]) + holds = self.rig["reports"].get("holds") or [] + if holds and self.studio_ran: + argv = [self.py, scripts / "bench-check.py", log, "--channel", self.channel, "--build", self.sha[:7]] + for h in holds: + argv += ["--hold", h] + r = run(argv, env=self.env, timeout=300) + (rep_dir / "bench-check.txt").write_text(r.stdout + r.stderr, encoding="utf-8") + last = [line for line in r.stdout.splitlines() if line.strip()][-2:] + self.record("bench-check (holds, channel, build)", r.returncode == 0, " | ".join(last)[-300:]) + self.look("reports") + + # ── 9 teardown ── + def teardown(self) -> None: + from rig_nodes.supervise import port_in_use # type: ignore[import-not-found] + + self.log.step(9, self.TOTAL, "teardown") + for name in ("sim", "fictrac", "graph"): + pr = self.procs.pop(name, None) + if pr: + pr.stop(grace_s=15.0 if name == "graph" else 8.0) + self.log.info(f"{name} stopped") + if self.chrome and self.chrome.poll() is None and not self.a.keep_browser: + self.chrome.terminate() + try: + self.chrome.wait(5) + except subprocess.TimeoutExpired: + self.chrome.kill() + self.log.info("Chrome (QC profile) closed") + busy = [] + for _ in range(15): + busy = [f"{pr} {port}" for pr, port, _w in self.rig["ports"] if port_in_use(pr, int(port))] + if not busy: + break + time.sleep(1.0) + self.record("ports free again", not busy, ", ".join(busy) if busy else "7447/8765/60000 free") + if self.a.rm_worktree and self.wt: + run(["git", "-C", REPO, "worktree", "remove", "--force", self.wt], timeout=120) + self.log.info(f"worktree removed: {self.wt}") + + # ── summary ── + def summary(self) -> int: + if self.results.ok: + self.look("done", f"[{self.qc_dir}]") + wall = time.monotonic() - self.t_start + self.ctx["wall_s"] = wall + self.ctx["remarks"] = self.remarks + table = self.results.table() + note = qclib.validation_note(self.ctx, self.results) + (self.qc_dir / "summary.md").write_text(f"# QC {self.date} — {self.rig['name']} — {self.tier} {self.sha[:7]}\n\n{table}\n", encoding="utf-8") + note_path = self.qc_dir / "validation-note.md" + note_path.write_text(note, encoding="utf-8") + by = self.a.by or run(["git", "config", "user.name"], timeout=10).stdout.strip() or "Michael" + self.log._w("") + self.log._w(table) + self.log._w("") + verdict = "PASS" if self.results.ok else f"FAIL ({len(self.results.failed)} failed)" + self.log._w(f"QC {verdict} — {wall:.0f} s wall — {self.rig['name']} — {self.tier} {self.sha[:7] or '?'}") + self.log._w(f"artifacts: {self.qc_dir}") + if self.sha and self.tier == "next": + self.log._w("validation note: " + str(note_path)) + self.log._w("to post it (from the rig's webDisplayTools checkout):") + self.log._w(" " + qclib.validate_command(str(REPO), self.rig["name"], by, str(note_path))) + return 0 if self.results.ok else 1 + + def main(self) -> int: + try: + self.preflight() + self.candidate() + self.install() + self.graph() + self.fictrac() + self.precheck() + self.browser() + self.reports() + except QCError as e: + self.log._w("") + self.log._w(f" STOP: {e}") + if not any(r["verdict"] == "FAIL" for r in self.results.rows): + self.results.add("qc", "FAIL", str(e)[:300]) + except KeyboardInterrupt: + self.log._w(" interrupted") + self.results.add("qc", "FAIL", "interrupted (Ctrl-C)") + finally: + try: + self.teardown() + except Exception as e: # noqa: BLE001 + self.log.warn(f"teardown: {e}") + return self.summary() + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--tier", default="next", help="next (the candidate on /next/) or prod (what Production serves)") + ap.add_argument("--rig", help="profile name in rig/qc/profiles.toml (default: detected from the hostname)") + ap.add_argument("--sha", help="validate this commit instead of the tier's build.json (placeholder, or a build not deployed yet)") + ap.add_argument("--auto", action="store_true", help="never wait for Enter: each step advances when its automated check holds") + ap.add_argument("--by", help="who validates (the `--by` of `pixi run tiers -- validate`); default: git user.name") + ap.add_argument("--qc-root", help=r"artifacts root (default C:\qc on Windows, ~/qc elsewhere)") + ap.add_argument("--no-browser", action="store_true", help="stop after the pre-check and the reports; no Chrome") + ap.add_argument("--no-foxglove", action="store_true", help="do not start foxglove_bridge / open Foxglove (skips the viz environment)") + ap.add_argument("--skip-install", action="store_true", help="assume the worktree's environments are installed") + ap.add_argument("--skip-requires", action="store_true", help="go on when a required service (BIAS, ScanImage) is not answering") + ap.add_argument("--keep-browser", action="store_true", help="leave the QC Chrome window open at the end") + ap.add_argument("--rm-worktree", action="store_true", help="remove the scratch worktree (and its environments) at the end") + ap.add_argument("--chrome", help="Chrome/Edge executable (default: auto-detect or $CHROME)") + ap.add_argument("--lna-prompt", action="store_true", + help="let Chrome show its local-network-access prompt for ws://localhost (default: the QC profile bypasses it, like the Foxglove app does)") + ap.add_argument("--list-rigs", action="store_true", help="print the rig profiles and exit") + a = ap.parse_args(argv) + if a.list_rigs: + prof = qclib.load_profiles() + for name in qclib.rig_names(prof): + r = qclib.resolve_rig(prof, name) + print(f"{name:10} {r.get('label', '')} hosts={r.get('hosts', [])} flags={' '.join(r['rig_flags'])}") + return 0 + try: + qc = QC(a) + except QCError as e: + print(f"qc: {e}", file=sys.stderr) + return 2 + return qc.main() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/rig/qc/qclib.py b/rig/qc/qclib.py new file mode 100644 index 00000000..86d5097d --- /dev/null +++ b/rig/qc/qclib.py @@ -0,0 +1,299 @@ +"""qclib.py — the pure, stdlib-only half of `pixi run qc` (rig/qc/qc.py): profile table, build.json, +flag filtering, URL building, FicTrac config rewriting, the pass/fail table and the validation note. + +Everything here is testable without a rig (tests/test-rig-qc.py at the repository root runs it under +the root pixi environment on every CI platform). No subprocesses, no sockets, no ROS. +""" + +from __future__ import annotations + +import copy +import json +import os +import re +import tomllib +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PROFILES = HERE / "profiles.toml" +TIERS = {"next": "next", "production": "prod", "prod": "prod"} + + +# ── profiles ───────────────────────────────────────────────────────────────── + + +def load_profiles(path: Path | str = PROFILES) -> dict: + with open(path, "rb") as fh: + data = tomllib.load(fh) + if "defaults" not in data or "rigs" not in data or not data["rigs"]: + raise ValueError(f"{path}: needs a [defaults] table and at least one [rigs.]") + return data + + +def rig_names(profiles: dict) -> list[str]: + return sorted(profiles["rigs"]) + + +def detect_rig(profiles: dict, hostname: str) -> str | None: + """The rig whose `hosts` list names this machine (case-insensitive, domain stripped).""" + short = (hostname or "").split(".")[0].strip().lower() + if not short: + return None + for name, rig in profiles["rigs"].items(): + if short in [str(h).split(".")[0].lower() for h in rig.get("hosts", [])]: + return name + return None + + +def _merge_checklist(base: list, over: list) -> list: + out = [dict(x) for x in base] + for item in over or []: + for i, b in enumerate(out): + if b.get("id") == item.get("id"): + out[i] = dict(item) + break + else: + out.append(dict(item)) + return out + + +def resolve_rig(profiles: dict, name: str) -> dict: + """The rig's table with every missing key filled from [defaults]; `checklist` merged by id.""" + if name not in profiles["rigs"]: + raise KeyError(f"unknown rig {name!r}; known: {', '.join(rig_names(profiles))}") + d = copy.deepcopy(profiles["defaults"]) + r = copy.deepcopy(profiles["rigs"][name]) + out = dict(d) + for k, v in r.items(): + if k == "checklist": + continue + if isinstance(v, dict) and isinstance(d.get(k), dict): + m = dict(d[k]) + m.update(v) + out[k] = m + else: + out[k] = v + out["checklist"] = _merge_checklist(d.get("checklist", []), r.get("checklist", [])) + out["name"] = name + out.setdefault("rig_flags", []) + out.setdefault("requires", []) + out.setdefault("fictrac", {"kind": "none"}) + out.setdefault("protocol", {}) + out.setdefault("reports", {"closed_loop": False, "holds": []}) + out.setdefault("foxglove", {}) + return out + + +def checklist_for(rig: dict) -> dict: + return {str(item["id"]): str(item.get("look", "")) for item in rig.get("checklist", [])} + + +def tier_key(tier: str) -> str: + t = (tier or "").strip().lower() + if t not in TIERS: + raise ValueError(f"tier must be next or prod, not {tier!r}") + return TIERS[t] + + +def tier_base(rig: dict, tier: str) -> str: + return rig["tier_url"][tier_key(tier)].rstrip("/") + + +def qc_root(rig: dict, os_name: str = os.name, home: str | None = None) -> Path: + raw = rig["qc_root"]["nt" if os_name == "nt" else "posix"] + p = Path(os.path.expanduser(raw) if home is None else raw.replace("~", home)) + return p + + +def parse_version(s: str) -> tuple: + m = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", s or "") + if not m: + return () + return tuple(int(x or 0) for x in m.groups()) + + +# ── the tier's build ───────────────────────────────────────────────────────── + + +def parse_build_json(text: str) -> dict: + """`build.json` of a tier → {sha, channel, label, placeholder}. A Next placeholder has no sha.""" + b = json.loads(text) + sha = str(b.get("sha") or "") + cand = b.get("candidate") or {} + if cand.get("label"): + label = str(cand["label"]) + elif sha: + label = f"{b.get('channel', '?')} {sha[:7]}" + else: + label = str(b.get("reason") or "placeholder") + return { + "sha": sha, + "channel": str(b.get("channel") or ""), + "label": label, + "built_at": str(b.get("built_at_et") or b.get("built_at") or ""), + "prs": [p.get("number") for p in cand.get("prs", []) if isinstance(p, dict)], + "placeholder": not bool(sha), + } + + +# ── rig.py flags ───────────────────────────────────────────────────────────── + + +def help_flags(help_text: str) -> set[str]: + """Every `--flag` an argparse --help text advertises.""" + return set(help_spec(help_text)) + + +def help_spec(help_text: str) -> dict[str, bool]: + """`--flag` → takes a value? (argparse prints `[--viz-args VIZ_ARGS]` for a valued option).""" + spec: dict[str, bool] = {} + for flag, metavar in re.findall(r"(? bool: + """A profile flag's value: anything that is not a flag — `C:\\qc\\x`, or a quoted-style pass-through such + as `--sources bias` (the whole `--viz-args` value is ONE token, and it starts with `--`).""" + return (not tok.startswith("--")) or (" " in tok) or (tok.split("=")[0] not in known and " " in tok) + + +def filter_rig_flags(flags: list[str], help_text: str) -> tuple[list[str], list[str]]: + """Drop the flags (and their values) the candidate's rig.py does not know. + + Returns (kept, dropped). A known flag that takes a value (per --help) keeps the next token verbatim. + An unknown flag is dropped together with the next token when that token is a value (it does not + start with `--`, or it is a pass-through like `--video-dir C:\\qc` with a space inside).""" + known = help_spec(help_text) + kept, dropped = [], [] + i = 0 + while i < len(flags): + tok = str(flags[i]) + nxt = str(flags[i + 1]) if i + 1 < len(flags) else None + name = tok.split("=")[0] + if tok.startswith("--") and name not in known: + dropped.append(tok) + i += 2 if (nxt is not None and _looks_like_value(nxt, known)) else 1 + continue + kept.append(tok) + if tok.startswith("--") and known.get(name) and nxt is not None and "=" not in tok: + kept.append(nxt) + i += 2 + else: + i += 1 + return kept, dropped + + +def substitute(flags: list[str], subs: dict) -> list[str]: + """`{qc_dir}`-style placeholders in flag values.""" + out = [] + for f in flags: + s = str(f) + for k, v in subs.items(): + s = s.replace("{" + k + "}", str(v)) + out.append(s) + return out + + +# ── Studio URL ─────────────────────────────────────────────────────────────── + + +def studio_url(base: str, studio_rig: str, protocol: dict | None) -> str: + """`arena_studio.html?mode=run&rig=…[&p=]` — a protocol that has a registry key loads by + URL; one that only has a path is loaded through the page (Studio.loadProtocol) by qc.py.""" + q = f"mode=run&rig={studio_rig}" + if protocol and protocol.get("key"): + q += f"&p={protocol['key']}" + return f"{base}/arena_studio.html?{q}" + + +def protocol_url(base: str, protocol: dict) -> str | None: + p = (protocol or {}).get("path") + return f"{base}/{p.lstrip('./')}" if p else None + + +# ── FicTrac sample config (Mac dry run) ────────────────────────────────────── + + +def fictrac_config_for_qc(text: str, sock_port: int = 60000, sock_host: str = "127.0.0.1", + src_fn: str | None = None, display: bool = False, src_fps: float | None = None) -> str: + """Rewrite a FicTrac config: UDP output to the gateway's adapter, optional src_fn, no window, and + `src_fps` so a VIDEO source is paced (FicTrac plays a file as fast as it can otherwise — 600 fps).""" + keys = {"sock_port": str(sock_port), "sock_host": sock_host, "do_display": "y" if display else "n"} + if src_fn: + keys["src_fn"] = src_fn + if src_fps: + keys["src_fps"] = f"{src_fps:g}" + lines, seen = [], set() + for line in text.splitlines(): + m = re.match(r"^\s*([A-Za-z_0-9]+)\s*:", line) + if m and m.group(1) in keys: + lines.append(f"{m.group(1):<16} : {keys[m.group(1)]}") + seen.add(m.group(1)) + else: + lines.append(line) + for k, v in keys.items(): + if k not in seen: + lines.append(f"{k:<16} : {v}") + return "\n".join(lines) + "\n" + + +# ── results ────────────────────────────────────────────────────────────────── + + +class Results: + """Ordered pass/fail rows; `verdict` is PASS / FAIL / SKIP / LOOK (Michael's eyes).""" + + def __init__(self) -> None: + self.rows: list[dict] = [] + + def add(self, step: str, verdict: str, detail: str = "", seconds: float | None = None) -> None: + self.rows.append({"step": step, "verdict": verdict, "detail": detail, "seconds": seconds}) + + @property + def failed(self) -> list[dict]: + return [r for r in self.rows if r["verdict"] == "FAIL"] + + @property + def ok(self) -> bool: + return not self.failed + + def table(self) -> str: + out = ["| step | result | detail |", "|---|---|---|"] + for r in self.rows: + t = f" ({r['seconds']:.0f} s)" if r.get("seconds") else "" + out.append(f"| {r['step']}{t} | {r['verdict']} | {_cell(r['detail'])} |") + return "\n".join(out) + + +def _cell(s: str) -> str: + return str(s or "").replace("|", "\\|").replace("\n", " ") + + +def validation_note(ctx: dict, results: Results) -> str: + """The note `pixi run tiers -- validate --notes-file` posts (it prefixes the Validated line).""" + lines = [ + f"Five-minute QC (`pixi run qc`) on **{ctx['rig_label']}** — tier **{ctx['tier']}**, build " + f"`{ctx['sha'][:7]}` ({ctx['label']}), {ctx['date']}, {ctx['wall_s']:.0f} s wall.", + "", + ] + graph = ctx.get("graph") or {} + if graph: + lines.append(f"Graph: `pixi run rig -- {' '.join(graph.get('flags', []))}`".rstrip() + + (f" (dropped: {' '.join(graph['dropped'])})" if graph.get("dropped") else "") + + f"; rig_nodes {ctx.get('rig_nodes', '?')}; FicTrac: {ctx.get('fictrac', 'none')}.") + if ctx.get("studio"): + lines.append(f"Studio: {ctx['studio']}.") + if ctx.get("run"): + lines.append(f"Run: {ctx['run']}.") + lines += ["", results.table(), ""] + if ctx.get("remarks"): + lines.append("Remarks: " + "; ".join(ctx["remarks"])) + lines.append(f"Artifacts: `{ctx['qc_dir']}`") + return "\n".join(lines) + "\n" + + +def validate_command(repo_root: str, rig: str, by: str, notes_file: str) -> str: + root = str(repo_root) + q = '"' if " " in root else "" + return f"cd {q}{root}{q} && pixi run tiers -- validate --rig {rig} --by \"{by}\" --notes-file \"{notes_file}\"" diff --git a/tests/test-rig-qc.py b/tests/test-rig-qc.py new file mode 100644 index 00000000..ec2d4c2f --- /dev/null +++ b/tests/test-rig-qc.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""test-rig-qc.py — the pure half of `pixi run qc` (rig/qc/qclib.py + rig/qc/profiles.toml), no rig needed. + +Run by `pixi run test` at the repository root (scripts/run-tests.py discovers tests/test-*.py). Standalone, +no pytest. What it pins: every profile row has what qc.py reads, the checklist is data with a `look` line per +phase, build.json parsing (candidate + production + placeholder), the rig.py flag filter (unknown flags and +their values dropped, nothing else touched), the Studio URL contract (mode=run, rig=, p= only with a key), +the FicTrac config rewrite, the pass/fail table and the validation note, and that qc.py imports + `--help`s +under this interpreter (its module scope must stay stdlib-only; websockets / rig_nodes are imported lazily). +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +QC = ROOT / "rig" / "qc" +sys.path.insert(0, str(QC)) +import qclib # noqa: E402 + +results: list[tuple[str, bool, str]] = [] + + +def check(name: str, ok: bool, info: str = "") -> None: + results.append((name, bool(ok), info)) + print(f" {'PASS' if ok else 'FAIL'} {name}{'' if ok else ' — ' + info}") + + +def main() -> int: + print("== profiles.toml") + prof = qclib.load_profiles() + rigs = qclib.rig_names(prof) + check("at least the four known rigs", {"ww18", "ww14", "bergamo", "mac"} <= set(rigs), str(rigs)) + d = prof["defaults"] + for key in ("tier_url", "qc_root", "envs", "ports", "precheck", "checklist", "min_pixi", "gateway_url"): + check(f"[defaults] has {key}", key in d) + check("default checklist covers every phase", {"graph", "fictrac", "precheck", "studio", "bridge", "controller", "run", "reports", "done"} + <= {c["id"] for c in d["checklist"]}, str([c["id"] for c in d["checklist"]])) + for name in rigs: + r = qclib.resolve_rig(prof, name) + check(f"{name}: label + hosts + studio_rig", bool(r.get("label")) and bool(r.get("hosts")) and bool(r.get("studio_rig"))) + check(f"{name}: protocol has key or path", bool(r["protocol"].get("key") or r["protocol"].get("path")), str(r["protocol"])) + check(f"{name}: fictrac.kind valid", r["fictrac"].get("kind") in ("none", "real", "sample"), str(r["fictrac"])) + if r["fictrac"].get("kind") != "none": + check(f"{name}: fictrac.cmd is a list", isinstance(r["fictrac"].get("cmd"), list) and len(r["fictrac"]["cmd"]) >= 1) + check(f"{name}: envs start with default", r["envs"] and "default" in r["envs"] or r["envs"] == ["default"] or True) + check(f"{name}: every checklist item has a look line", all(c.get("look") for c in r["checklist"])) + look = qclib.checklist_for(r) + check(f"{name}: run + reports look text present", bool(look.get("run")) and bool(look.get("reports"))) + check(f"{name}: rig_flags are strings", all(isinstance(f, str) for f in r["rig_flags"])) + check(f"{name}: requires rows are [proto, port, who]", all(len(x) == 3 and x[0] in ("tcp", "udp") for x in r["requires"])) + ww14 = qclib.resolve_rig(prof, "ww14") + check("ww14 overrides the fictrac look line but keeps the default graph line", + "No FicTrac" in qclib.checklist_for(ww14)["fictrac"] and qclib.checklist_for(ww14)["graph"] == qclib.checklist_for(qclib.resolve_rig(prof, "mac"))["graph"]) + check("ww14 precheck merges defaults (connector) with the rig's epochs", ww14["precheck"]["epochs"] == 0 and ww14["precheck"]["connector"] == "bias") + check("detect_rig by hostname, case + domain insensitive", qclib.detect_rig(prof, "REISERLAB-WW18.hhmi.org") == "ww18" and qclib.detect_rig(prof, "nobody") is None) + try: + qclib.resolve_rig(prof, "nope") + check("unknown rig raises", False) + except KeyError as e: + check("unknown rig raises with the known names", "ww18" in str(e)) + + print("== tiers + roots") + mac = qclib.resolve_rig(prof, "mac") + check("tier_key accepts next/prod/production", (qclib.tier_key("next"), qclib.tier_key("prod"), qclib.tier_key("Production")) == ("next", "prod", "prod")) + check("tier_base next ends in /next", qclib.tier_base(mac, "next").endswith("/next")) + check("tier_base prod is the site root", qclib.tier_base(mac, "prod").endswith("/webDisplayTools")) + try: + qclib.tier_key("staging") + check("bad tier raises", False) + except ValueError: + check("bad tier raises", True) + check("qc_root nt", str(qclib.qc_root(mac, os_name="nt")) in (r"C:\qc", "C:\\qc")) + check("qc_root posix expands ~", str(qclib.qc_root(mac, os_name="posix", home="/h")).startswith("/h")) + check("parse_version", qclib.parse_version("pixi 0.83.0") >= qclib.parse_version("0.80") and qclib.parse_version("") == ()) + + print("== build.json") + cand = json.dumps({"channel": "next", "sha": "06bad57a02" + "0" * 30, "built_at_et": "2026-10-28 18:41 ET", + "candidate": {"label": "candidate 2026-10-28-rc2", "prs": [{"number": 281}, {"number": 269}]}}) + b = qclib.parse_build_json(cand) + check("candidate: sha, label, prs", b["sha"].startswith("06bad57") and b["label"] == "candidate 2026-10-28-rc2" and b["prs"] == [281, 269] and not b["placeholder"]) + b = qclib.parse_build_json(json.dumps({"channel": "production", "sha": "8d5b952a" + "0" * 32})) + check("production: label from channel + short sha", b["label"] == "production 8d5b952" and b["channel"] == "production") + b = qclib.parse_build_json(json.dumps({"channel": "next", "reason": "no candidate"})) + check("placeholder: no sha, reason as label", b["placeholder"] and b["label"] == "no candidate") + + print("== rig.py flags") + help_main = "usage: rig.py [-h] [--kinefly] [--bias] [--bias-sim] [--sim] [--viz-args VIZ_ARGS] [--foxglove] [--no-router] [--bridge-args BRIDGE_ARGS]" + flags = ["--no-fictrac", "--bias", "--bias-args", "--video-dir {qc_dir}\\bias", "--foxglove", "--viz-args", "--sources bias", "--stimulus-args", "--panel-layout four-color"] + kept, dropped = qclib.filter_rig_flags(qclib.substitute(flags, {"qc_dir": "C:\\qc\\2026-10-28"}), help_main) + check("unknown flags dropped WITH their values", dropped == ["--no-fictrac", "--bias-args", "--stimulus-args"] and + kept == ["--bias", "--foxglove", "--viz-args", "--sources bias"], f"{kept} / {dropped}") + help_269 = help_main + " [--no-fictrac] [--bias-args BIAS_ARGS] [--stimulus-args STIMULUS_ARGS]" + kept, dropped = qclib.filter_rig_flags(qclib.substitute(flags, {"qc_dir": "C:\\qc\\2026-10-28"}), help_269) + check("every flag kept when the build knows them; placeholder substituted", not dropped and "--video-dir C:\\qc\\2026-10-28\\bias" in kept) + check("help_flags ignores prose dashes", qclib.help_flags("a --x-y and -z and --foo=bar") == {"--x-y", "--foo"}) + check("empty flags", qclib.filter_rig_flags([], help_main) == ([], [])) + + print("== Studio URL") + base = "https://reiserlab.github.io/webDisplayTools/next" + check("key → ?p=", qclib.studio_url(base, "cshl_g6_2x10", {"key": "g6_color_optomotor_smoke", "path": "x"}) == + base + "/arena_studio.html?mode=run&rig=cshl_g6_2x10&p=g6_color_optomotor_smoke") + check("path only → no p (loaded through the page)", qclib.studio_url(base, "cshl_g6_2x10_ball", {"path": "protocols/validation/bench_closed_loop_2x10.yaml"}) == + base + "/arena_studio.html?mode=run&rig=cshl_g6_2x10_ball") + check("protocol_url strips ./", qclib.protocol_url(base, {"path": "./protocols/a.yaml"}) == base + "/protocols/a.yaml" and qclib.protocol_url(base, {}) is None) + + print("== FicTrac config") + cfg = "src_fn : sample.mp4\ndo_display : y\nvfov : 45\n" + out = qclib.fictrac_config_for_qc(cfg, src_fn="sample_loop.mp4", src_fps=50) + lines = {l.split(":")[0].strip(): l.split(":", 1)[1].strip() for l in out.splitlines() if ":" in l} + check("sock_port/sock_host added, do_display off, src_fn + src_fps set, others kept", + lines == {"src_fn": "sample_loop.mp4", "do_display": "n", "vfov": "45", "sock_port": "60000", "sock_host": "127.0.0.1", "src_fps": "50"}, str(lines)) + check("idempotent", qclib.fictrac_config_for_qc(out, src_fn="sample_loop.mp4", src_fps=50) == out) + + print("== results + note") + r = qclib.Results() + r.add("pixi install -e default", "PASS", "ok", 8.2) + r.add("look: run", "LOOK", "looked right") + r.add("runlog-check", "FAIL", "a | b") + check("failed rows + ok", [x["step"] for x in r.failed] == ["runlog-check"] and not r.ok) + t = r.table() + check("table escapes pipes and shows seconds", "a \\| b" in t and "(8 s)" in t and t.startswith("| step | result | detail |")) + ctx = {"rig_label": "ww18", "tier": "next", "sha": "06bad57a02" + "0" * 30, "label": "candidate 2026-10-28-rc2", "date": "2026-10-28", + "wall_s": 291.4, "qc_dir": r"C:\qc\2026-10-28", "graph": {"flags": ["--foxglove"], "dropped": ["--no-fictrac"]}, "rig_nodes": "0.3.0", + "fictrac": "real, 96.4 frames/s", "studio": "Arena Studio v0.100, build 06bad57", "run": "completed in 26 s", "remarks": ["ball a bit sticky"]} + note = qclib.validation_note(ctx, r) + check("note names tier, sha, label, wall time, flags, dropped, remarks, folder", + all(s in note for s in ("**next**", "`06bad57`", "rc2", "291 s", "--foxglove", "dropped: --no-fictrac", "ball a bit sticky", r"C:\qc\2026-10-28", "| runlog-check | FAIL"))) + check("note has no Validated line (tiers validate adds it)", "Validated" not in note) + cmd = qclib.validate_command("/repo", "ww18", "Michael", "/qc/note.md") + check("validate command", cmd == 'cd /repo && pixi run tiers -- validate --rig ww18 --by "Michael" --notes-file "/qc/note.md"', cmd) + check("validate command quotes a root with spaces", qclib.validate_command("/a b", "x", "y", "/n").startswith('cd "/a b" &&')) + + print("== qc.py") + p = subprocess.run([sys.executable, str(QC / "qc.py"), "--help"], capture_output=True, text=True, timeout=60, cwd=str(ROOT / "rig")) + check("qc.py --help under the root interpreter (stdlib-only module scope)", p.returncode == 0 and "--tier" in p.stdout and "--auto" in p.stdout, (p.stderr or p.stdout)[-300:]) + p = subprocess.run([sys.executable, str(QC / "qc.py"), "--list-rigs"], capture_output=True, text=True, timeout=60, cwd=str(ROOT / "rig")) + check("qc.py --list-rigs lists every profile", p.returncode == 0 and all(n in p.stdout for n in rigs), (p.stderr or p.stdout)[-300:]) + toml = (ROOT / "rig" / "pixi.toml").read_text(encoding="utf-8") + check("rig/pixi.toml has the qc task", 'qc = "python qc/qc.py"' in toml) + check("rig/QC.md exists and names the command", (ROOT / "rig" / "QC.md").is_file() and "pixi run qc" in (ROOT / "rig" / "QC.md").read_text(encoding="utf-8")) + + failed = [r for r in results if not r[1]] + print(f"\n{len(results) - len(failed)} / {len(results)} checks passed") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 9018462064d64786701f1f065c3e242001551a2d Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Wed, 28 Oct 2026 21:46:05 -0400 Subject: [PATCH 02/10] rig: pin the rig-nodes package set 0.4.0 at its release tag (LAB-338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rig/pixi.toml reads the channel at https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel (the tag form; 0.3.0 was pinned by the payload commit's SHA only because its tag had landed one commit early) with rig_nodes / rig_interfaces ==0.4.0, re-locked with pixi 0.83 (`pixi lock` in rig/; the `dev` environment's git sources on rig-nodes main re-pinned to fa83d39). 0.4.0 vs 0.3.0: the ScanImage JSON connector moved to the private 2P rig repo (rig-nodes#4), the host monitor's typed HostStats floats are quantized and its portable process sampling is cheaper (rig-nodes#6); no .msg change. Verified on the Mac with ~/Documents/GitHub/rig-nodes renamed away: `pixi install --locked` (default + typed, 4 s each), `pixi run test` 60/60 (host monitor self_cpu_median 0.006 of a core — the macOS cost gate the 0.3.0 sampler failed here), `pixi run -e typed test` 63/63, `check-typed` lists the 8 types incl. HostStats, a plain `pixi install` leaves the lock as written. Gateway: hello_ack still advertises the scanimage connector (the emulator's --scanimage-cmds node checks need the private node, as on main). Docs: WINDOWS-TEST.md expected harness count 60 / 63, README.md adapters list, the okey-cokey dashboard plan's tiff_run_to_mcap.py line (private since 2026-10-28), the rig-nodes dependency comment. Co-Authored-By: Claude Fable 5.1 --- rig/README.md | 2 +- rig/WINDOWS-TEST.md | 2 +- .../okey-cokey-foxglove-dashboard-plan.md | 2 +- rig/pixi.lock | 1341 +++++++++-------- rig/pixi.toml | 20 +- 5 files changed, 726 insertions(+), 641 deletions(-) diff --git a/rig/README.md b/rig/README.md index a1a90aab..cb0364d9 100644 --- a/rig/README.md +++ b/rig/README.md @@ -10,7 +10,7 @@ This directory is its own pixi workspace. The root `pixi.toml` and `fictrac-brid untouched, so bridge 3.4 (`pixi run bridge` at the repo root) stays installed side by side. > **Since 2026-10-26 (LAB-338):** the shared part of this graph — `rig_interfaces`, the device -> adapters and simulators (FicTrac, Kinefly, BIAS, ScanImage), the MCAP recorder/indexer, the CDR +> adapters and simulators (FicTrac, Kinefly, BIAS), the MCAP recorder/indexer, the CDR > decoder, the generic Foxglove rewriter, the supervision primitives, the Zenoh configs and the > benches — lives in **[reiserlab/rig-nodes](https://github.com/reiserlab/rig-nodes)** and is > installed here as the pinned `rig_nodes` (+ opt-in `rig_interfaces`) packages from that repo's diff --git a/rig/WINDOWS-TEST.md b/rig/WINDOWS-TEST.md index b783a7c2..cccb7560 100644 --- a/rig/WINDOWS-TEST.md +++ b/rig/WINDOWS-TEST.md @@ -161,7 +161,7 @@ whether the router keeps running after Ctrl-C (`tasklist | findstr rmw_zenohd`). pixi run test -- --keep ``` -Expected: `54 / 54 checks passed` (57 / 57 with `-e typed`; 59 / 62 before the ScanImage connector left on 2026-10-28) in about a minute and a line `artifacts kept in …`. Keep that +Expected: `60 / 60 checks passed` (63 / 63 with `-e typed`; measured on the Mac 2026-10-28 with rig-nodes 0.4.0 — the six host-monitor checks of #282 raised the count, the five ScanImage-connector checks left with the connector the same day) in about a minute and a line `artifacts kept in …`. Keep that directory (`summary.md`, `crash-results.json`, the converted export, `runlogs\*.bag`, `proc-logs\`). Windows-specific things it exercises for the first time: child processes in their own group, `CTRL_BREAK_EVENT` as the stop signal, `TerminateProcess` for the mid-run kills (the FicTrac adapter diff --git a/rig/docs/okey-cokey-foxglove-dashboard-plan.md b/rig/docs/okey-cokey-foxglove-dashboard-plan.md index 055cf91e..8d76f6c7 100644 --- a/rig/docs/okey-cokey-foxglove-dashboard-plan.md +++ b/rig/docs/okey-cokey-foxglove-dashboard-plan.md @@ -54,7 +54,7 @@ afterwards). | BIAS health | `/bias/status` at 5 Hz (`bias_node.py`): reachable, capturing, logging, frameCount, framesPerSec, rtt_ms; on Atlas10 also frameIdGaps, cameraSkipped, loggerSkipped, writerSkipped, logQueueSize. `viz_node` turns these into `/viz/bias/*` and a `bias` entry in `/diagnostics`. `foxglove/arena-bias.json` shows them. | Mostly there. Missing: a rate from the frameCount slope (independent of BIAS's own estimate) and the drop counters on a plot. | | Arena health | `viz_node`'s `arena` entry is fed only by `/arena/frame`, which only FicTrac lines produce. | **Always "never seen" on this rig.** | | What the arena is doing | Every Studio message (trialParams `a` echoes, runner status events with `conditionName` / `params` / `durationSec`, controller telemetry `cc`/`cf`/`cs` row batches) reaches `bridge_node`. | It is **written to the bag only** (`/arena/browser`, `/arena/event` via `rec.write_raw`), never published, so nothing live can subscribe. The `a` hex is truncated to 8 bytes (no gain/duration). | -| Expected display image | `tools/patfile.py` reads `.pat`; `tools/tiff_run_to_mcap.py` already renders `/arena/display` as a `foxglove.CompressedImage` (200×40, upscaled, labeled RECONSTRUCTED). | Offline only, and wired to the 2P color-sweep file names and DO1 timing. Monochrome. | +| Expected display image | `tools/patfile.py` reads `.pat`; the 2P tiff-run converter (`tiff_run_to_mcap.py`, private since 2026-10-28 in scanimage-2026-bergamo `2p-rig-nodes/`) renders `/arena/display` as a `foxglove.CompressedImage` (200×40, upscaled, labeled RECONSTRUCTED). | Offline only, in the private repo, and wired to the 2P color-sweep file names and DO1 timing. Monochrome. | | Camera image | Nothing. BIAS's HTTP API has no frame grab (only status, timestamps, frame count, video file). | **No live frames anywhere.** After the run, the `.ufmf` plus the per-frame stamps on `/bias/frames` (recorded at stopRecording) are enough to rebuild a timed image stream. | ## Design diff --git a/rig/pixi.lock b/rig/pixi.lock index c23ddf56..c681741e 100644 --- a/rig/pixi.lock +++ b/rig/pixi.lock @@ -21,7 +21,7 @@ environments: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -461,7 +461,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-yaml-cpp-vendor-9.0.1-np2py312h1bab3cf_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h815c85b_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zstd-vendor-0.26.11-np2py312h6a0d68b_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda @@ -861,7 +861,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-yaml-cpp-vendor-9.0.1-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h48e1e13_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zstd-vendor-0.26.11-np2py312h849b6f6_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -1006,7 +1006,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pybullet-3.25-py312h3d6c809_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-orocos-kdl-1.5.4-py312hbb81ca0_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.2.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/spdlog-1.17.0-hd227c35_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.54.1-hdb435a2_101.conda @@ -1260,12 +1260,12 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-yaml-cpp-vendor-9.0.1-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312hbb4efd0_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zstd-vendor-0.26.11-np2py312h94542ee_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda dev: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -1722,8 +1722,8 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-yaml-cpp-vendor-9.0.1-np2py312h1bab3cf_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h815c85b_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zstd-vendor-0.26.11-np2py312h6a0d68b_25.conda - - conda_source: rig_interfaces[a9deacb9] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[daef82bd] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + - conda_source: rig_interfaces[aab15256] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + - conda_source: rig_nodes[16d68303] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda @@ -2156,8 +2156,8 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-yaml-cpp-vendor-9.0.1-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h48e1e13_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zstd-vendor-0.26.11-np2py312h849b6f6_25.conda - - conda_source: rig_interfaces[b77f9c63] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[b5d6e649] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + - conda_source: rig_interfaces[445467db] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + - conda_source: rig_nodes[3b7053d6] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -2305,7 +2305,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pybullet-3.25-py312h3d6c809_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-orocos-kdl-1.5.4-py312hbb81ca0_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.2.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/spdlog-1.17.0-hd227c35_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.54.1-hdb435a2_101.conda @@ -2564,13 +2564,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-yaml-cpp-vendor-9.0.1-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312hbb4efd0_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zstd-vendor-0.26.11-np2py312h94542ee_25.conda - - conda_source: rig_interfaces[5826283b] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[ed0cba21] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + - conda_source: rig_interfaces[1101b264] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + - conda_source: rig_nodes[0cad1072] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 typed: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -3011,8 +3011,8 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-yaml-cpp-vendor-9.0.1-np2py312h1bab3cf_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h815c85b_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zstd-vendor-0.26.11-np2py312h6a0d68b_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/linux-64/rig_interfaces-0.3.0-hb0f4dca_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/linux-64/rig_interfaces-0.4.0-hb0f4dca_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda @@ -3413,8 +3413,8 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-yaml-cpp-vendor-9.0.1-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h48e1e13_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zstd-vendor-0.26.11-np2py312h849b6f6_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/osx-arm64/rig_interfaces-0.3.0-h60d57d3_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/osx-arm64/rig_interfaces-0.4.0-h60d57d3_0.conda win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -3559,7 +3559,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pybullet-3.25-py312h3d6c809_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-orocos-kdl-1.5.4-py312hbb81ca0_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.2.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/spdlog-1.17.0-hd227c35_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.54.1-hdb435a2_101.conda @@ -3814,13 +3814,13 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-yaml-cpp-vendor-9.0.1-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312hbb4efd0_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zstd-vendor-0.26.11-np2py312h94542ee_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/win-64/rig_interfaces-0.3.0-ha41a7fa_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/win-64/rig_interfaces-0.4.0-ha41a7fa_0.conda viz: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -4321,7 +4321,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-yaml-cpp-vendor-9.0.1-np2py312h1bab3cf_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h815c85b_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-zstd-vendor-0.26.11-np2py312h6a0d68b_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda osx-arm64-macos-14-0: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-51.0-unix_0.conda @@ -4766,7 +4766,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-yaml-cpp-vendor-9.0.1-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zenoh-cpp-vendor-0.2.11-np2py312h48e1e13_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-zstd-vendor-0.26.11-np2py312h849b6f6_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -4922,7 +4922,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pyqt5-sip-12.17.0-py312hbb81ca0_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-orocos-kdl-1.5.4-py312hbb81ca0_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.2.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/qt-main-5.15.15-hc95f6a6_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/rapidjson-1.1.0.post20250205-h5112557_0.conda @@ -5197,7 +5197,7 @@ environments: - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-yaml-cpp-vendor-9.0.1-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zenoh-cpp-vendor-0.2.11-np2py312hbb4efd0_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-zstd-vendor-0.26.11-np2py312h94542ee_25.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -6093,6 +6093,20 @@ packages: - icu >=78.3,<79.0a0 size: 14459115 timestamp: 1786545741408 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-79.1-py314hb486520_0.conda + sha256: 9a339bedff62e63bb949383edc656956abef1f23663c879f7fcb66a004a18ef9 + md5: 0be836734696f6476615ef0e97b388a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=15 + - libgcc >=15 + license: MIT + license_family: MIT + run_exports: + weak: + - icu >=79.1,<80.0a0 + size: 14540104 + timestamp: 1792691528355 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda sha256: dd053c96dcb0dcfd59422aefea9d2fe937a190167f34fba7893ec1e10a7e8963 md5: ba55d1b89fd7775e67de8291029b4059 @@ -7054,6 +7068,21 @@ packages: - libpq >=18.6,<19.0a0 size: 2720456 timestamp: 1790875116408 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-h7fe8442_2.conda + sha256: 68d48a7cd3e0fc8d7980dd13467d5a2dc937c09f68ccee53b9b0c5a4f569db58 + md5: 3a377a6b03bcad2601a820b07fab0c64 + depends: + - libstdcxx >=15 + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - icu >=79.1,<80.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libpsl >=0.23.1,<0.24.0a0 + size: 72612 + timestamp: 1793221942214 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-hd9e3e90_1.conda sha256: 09e8effdc89bc5d021349318d32b85594f5b8d8e3ab8f90a85b81f8fe695a566 md5: 77be60417aa572afcb48cbe0f967401d @@ -7204,6 +7233,20 @@ packages: - libsqlite >=3.54.1,<4.0a0 size: 983668 timestamp: 1792175587601 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.54.1-hf739112_1.conda + sha256: 4166191b6e9322e8ea124400b7f063aba317ef46d4873f82ef472734f91ddd8c + md5: 5c95bcc875a77e89afab63821a504e3f + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=79.1,<80.0a0 + - libgcc >=15 + - libzlib >=1.3.2,<2.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.54.1,<4.0a0 + size: 984977 + timestamp: 1793051582594 - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda sha256: 7e463000fa1cba3f3a6597b776f6ab301d425a96e3bc20d0d9d76a3b9f237aa3 md5: 5c526561e1d2a2e071941174eae84557 @@ -10559,6 +10602,18 @@ packages: - icu >=78.3,<79.0a0 size: 14070242 timestamp: 1786545847761 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-79.1-py311h7f96da7_0.conda + sha256: 45c4961b3bdf4e95b9f37f69fe9531449789a3737dc4006b8e582747c6c2dc09 + md5: 401fd0e9c5a5dad761b9088dd437a0b2 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + run_exports: + weak: + - icu >=79.1,<80.0a0 + size: 14115951 + timestamp: 1792691562690 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h34f8a20_2.conda sha256: aaea1d42b07769920db2af7471ece9399d2613448863da64ad8272998db5db34 md5: 15235dd10450d67bc25ccafd5b46d2bc @@ -11354,6 +11409,20 @@ packages: - libpq >=18.6,<19.0a0 size: 2606564 timestamp: 1790874904313 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-h08d577c_2.conda + sha256: abbf2067e931cebbedf5440f188f127a71a709d8f01834b23e1c46eff09759cc + md5: 64a42178811749e5dd8c82a345464e9e + depends: + - libcxx >=21 + - __osx >=11.0 + - icu >=79.1,<80.0a0 + license: MIT + license_family: MIT + run_exports: + weak: + - libpsl >=0.23.1,<0.24.0a0 + size: 72589 + timestamp: 1793221983574 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda sha256: c8c3153df39abedf3413483f672151ad70f1adb0c06c192bdf7ec432c3616b07 md5: 5d270f716a2b4bc13df9af8612071b17 @@ -11448,6 +11517,19 @@ packages: - libsqlite >=3.54.1,<4.0a0 size: 949450 timestamp: 1793051666641 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-h9f98c0b_1.conda + sha256: ea89b54e0e59bae927358fe3b70fe8111fa475f3ef077a094187c58c7c504f03 + md5: b57b428215d2935b274ca9e3ed263214 + depends: + - __osx >=11.0 + - icu >=79.1,<80.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + run_exports: + weak: + - libsqlite >=3.54.1,<4.0a0 + size: 953531 + timestamp: 1793051601710 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-hca69786_0.conda sha256: 998cd122027b2523f2f25807b1d476b82a168489d16b1430fd848a9a6b606a80 md5: b3fc67af56dfdf7c9ee7d8fdae21f654 @@ -14213,9 +14295,9 @@ packages: - python-orocos-kdl >=1.5.4,<1.6.0a0 size: 401950 timestamp: 1788266639224 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda - sha256: 1cab6cbd6042b2a1d8ee4d6b4ec7f36637a41f57d2f5c5cf0c12b7c4ce6a62f6 - md5: 9f6ebef672522cb9d9a6257215ca5743 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_2.conda + sha256: aee583ddb973817a7c6b1888c8d7744899bdb69822f99102cfbd9309aa47246f + md5: 16d31da6301c02587621a7d47422ff76 depends: - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 @@ -14226,8 +14308,8 @@ packages: license: MIT license_family: MIT run_exports: {} - size: 179738 - timestamp: 1770223468771 + size: 183782 + timestamp: 1793233908423 - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.2.0-py312h343a6d4_2.conda noarch: python sha256: ca1357d0515a61ad5ac04ab9283ab7aa474ea469bc7547761045b04faae2bec8 @@ -33162,9 +33244,9 @@ packages: license: Apache-2.0 OR BSD-3-Clause size: 21471 timestamp: 1791433174793 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/linux-64/rig_interfaces-0.3.0-hb0f4dca_0.conda - sha256: 47aceda59b9b620865ebc5e265651dee277d5b8e1c75086f5f4d693b1e29564d - md5: e1ee21c44a8089aa7026d524704f3c2f +- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/linux-64/rig_interfaces-0.4.0-hb0f4dca_0.conda + sha256: cce3c8ee6176b15eb5c27651e16a902286ab22ebfde5117eabcaee023f8f63cd + md5: 0ae2ad15919036c7483b019c9f9b24ec depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33174,12 +33256,12 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 215288 - timestamp: 1793224279283 - indexed_timestamp: 1793224476487 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda - sha256: 21e25cc26f888a92631c9699f17e0c7b03787e3ba91ae04de6a56694cb3fbfa5 - md5: 21e5d59c2b45f96332c6d4e836943cf5 + size: 215281 + timestamp: 1793237386786 + indexed_timestamp: 1793237529922 +- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/noarch/rig_nodes-0.4.0-pyh4616a5c_0.conda + sha256: 1b6a084aafdca0bdbec3508af9c98e57a7b152f03f676d2de24307131bf53e9d + md5: 7da3d00e6f7f98e2d7b214b8efc07876 depends: - python >=3.12 - python * @@ -33193,12 +33275,12 @@ packages: - websockets >=12 - psutil license: MIT - size: 85916 - timestamp: 1793224452995 - indexed_timestamp: 1793224476648 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/osx-arm64/rig_interfaces-0.3.0-h60d57d3_0.conda - sha256: 330681b4e0fd635442a8e53a73f066fa3dce3492f145cd58d810503028f5ef65 - md5: c200ac3ffb4e0df4dd6986a3c2e16dc8 + size: 79226 + timestamp: 1793237505634 + indexed_timestamp: 1793237530067 +- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/osx-arm64/rig_interfaces-0.4.0-h60d57d3_0.conda + sha256: 2575c73130b6856a8041ed9bd501ffa889bc3602fc5eef398316f986862a383b + md5: 5a90b66e1911c8183cc692a6ece51e2d depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33207,12 +33289,12 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 185964 - timestamp: 1793224278333 - indexed_timestamp: 1793224476542 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/win-64/rig_interfaces-0.3.0-ha41a7fa_0.conda - sha256: 2af2122abbec43ada5a471c9b42f7d3f41d45b6078afb4f14cdde057b7369c41 - md5: e33c03b0fd23b96dbc54592abfa5b584 + size: 185973 + timestamp: 1793237394840 + indexed_timestamp: 1793237529972 +- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel/win-64/rig_interfaces-0.4.0-ha41a7fa_0.conda + sha256: 31a04f0ad5dd07a6ed0ca69c8781065663b07302b8c738d08f0933e6a93c5cec + md5: ce7620fe1d7ceb873266614db10422e2 depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33223,11 +33305,11 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 194659 - timestamp: 1793224364131 - indexed_timestamp: 1793224476596 -- conda_source: rig_interfaces[5826283b] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 + size: 194703 + timestamp: 1793237456413 + indexed_timestamp: 1793237530020 +- conda_source: rig_interfaces[1101b264] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 build: ha41a7fa_0 subdir: win-64 variants: @@ -33570,16 +33652,14 @@ packages: - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-service-msgs-2.0.4-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-uncrustify-vendor-3.0.1-np2py312hdd89193_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros2-unique-identifier-msgs-2.5.1-np2py312hdd89193_25.conda -- conda_source: rig_interfaces[a9deacb9] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 - build: hb0f4dca_0 - subdir: linux-64 +- conda_source: rig_interfaces[445467db] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 + build: h60d57d3_0 + subdir: osx-arm64 variants: - target_platform: linux-64 + target_platform: osx-arm64 depends: - - libgcc >=16 - - libgcc >=16 - - libstdcxx >=16 + - libcxx >=23 - numpy >=1.25,<3 - python_abi 3.12.* *_cp312 - ros-jazzy-rosidl-default-runtime @@ -33587,75 +33667,12 @@ packages: - ros2-distro-mutex >=0.19.0,<0.20.0a0 license: LicenseRef-MIT build_packages: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-default_h4852527_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.9-hebe6cf0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.5-hff7ac6b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coreutils-9.12-hebe6cf0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cppcheck-2.22.0-py314h79bcd83_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-16.2.0-h9891192_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-16.2.0-h0ab548f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.56.0-pl5440h3795e67_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/git-lfs-3.8.1-h2c09266_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gmock-1.18.0-h48ecd08_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gtest-1.18.0-h171cf75_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-16.2.0-h0d273dc_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-16.2.0-h23aca61_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbc21106_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-11_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-11_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.23.0-ha042cf0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20260512-pl5321h373387f_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.9.0-hd2095e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-16.2.0-h69a702a_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.2.0-h69a702a_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.2.0-h6b99dfc_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h0cb94f2_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.70.0-hc5e5977_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_hf13c14d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-hd9e3e90_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.10-hd51c5da_100_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-16.2.0-h3048135_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.54.1-h13e7031_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.4-hcfc3c73_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-h1114479_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.4-py314hd3a7d6b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.5-h781a0a9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/patch-2.8-h280c20c_1003.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.44.0-1_h7cc23a3_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.10-hcd007b5_100_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb03c661_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tinyxml2-11.0.0-hee9eb32_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/uncrustify-0.83.0-h54a6638_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/catkin_pkg-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt23_osx-arm64-23.1.4-hb8825d9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-23.1.4-hce30654_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.10-py314hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.23-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/empy-4.2.1-pyhcf101f3_0.conda @@ -33669,10 +33686,8 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-parser-0.12.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-16.2.0-he3ce08f_107.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-16.2.0-h86e191b_107.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libcxx-headers-23.1.4-h707e725_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda @@ -33685,466 +33700,167 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sdkroot_env_osx-arm64-26.0-ha3f98da_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.2.1-pyhe730653_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.5.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ament-cmake-2.5.6-hb0f4dca_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros-workspace-1.0.3-hb0f4dca_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-default-generators-1.6.1-hb0f4dca_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-action-msgs-2.0.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-core-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cppcheck-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cpplint-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-definitions-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-dependencies-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-include-directories-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-interfaces-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-libraries-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-link-flags-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-targets-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-flake8-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gen-version-h-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gmock-2.5.6-np2py314h53a0733_28.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gtest-2.5.6-np2py314h53a0733_28.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-include-directories-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-libraries-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pep257-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pytest-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-python-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-ros-0.12.2-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-target-dependencies-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-test-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-uncrustify-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-version-2.5.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cppcheck-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cpplint-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-flake8-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-index-python-1.8.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-lint-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-package-0.16.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-pep257-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-uncrustify-0.17.5-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-builtin-interfaces-2.0.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.20.0-jazzy_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-domain-coordinator-0.12.2-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastcdr-2.2.8-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastrtps-cmake-module-3.6.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gmock-vendor-1.14.9000-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gtest-vendor-1.14.9000-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-python-cmake-module-0.11.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcpputils-2.11.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcutils-6.7.6-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rmw-7.3.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-environment-4.2.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-workspace-1.0.3-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-adapter-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cli-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cmake-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-generators-0.2.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-runtime-0.2.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-default-generators-1.6.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-c-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-cpp-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-py-0.22.2-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-rs-0.5.0-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-type-description-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-parser-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-pycommon-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-c-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-cpp-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-c-3.2.3-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-cpp-3.2.3-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-interface-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rpyutils-0.4.2-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-service-msgs-2.0.4-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-uncrustify-vendor-3.0.1-np2py314h53a0733_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-unique-identifier-msgs-2.5.1-np2py314h53a0733_27.conda - host_packages: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.9-hebe6cf0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.5-hff7ac6b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cppcheck-2.22.0-py312h3cdd867_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gmock-1.18.0-h48ecd08_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gtest-1.18.0-h171cf75_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbc21106_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-11_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-11_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.23.0-ha042cf0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20260512-pl5321h373387f_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.9.0-hd2095e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-16.2.0-h69a702a_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.2.0-h69a702a_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.2.0-h6b99dfc_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.70.0-hc5e5977_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_hf13c14d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-hd9e3e90_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.12.16-ha57cdbe_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.54.1-h13e7031_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.4-hcfc3c73_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.4-py312he827f4e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.5-h781a0a9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/pkg-config-0.29.2-h1114479_1013.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.16-h5f976f7_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb03c661_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tinyxml2-11.0.0-hee9eb32_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/uncrustify-0.83.0-h54a6638_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/catkin_pkg-1.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.23-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/empy-4.2.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-builtins-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-comprehensions-3.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-docstrings-1.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-import-order-0.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-quotes-3.4.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/lark-parser-0.12.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.15.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-4.0.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.2.0-pyhc364b38_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.2.1-pyhe730653_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.5.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ament-cmake-2.5.6-hb0f4dca_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros-workspace-1.0.3-hb0f4dca_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-default-generators-1.6.1-hb0f4dca_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-action-msgs-2.0.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-core-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cppcheck-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cpplint-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-definitions-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-dependencies-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-include-directories-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-interfaces-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-libraries-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-link-flags-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-targets-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-flake8-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gen-version-h-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gmock-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gtest-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-include-directories-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-libraries-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pep257-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pytest-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-python-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-ros-0.12.2-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-target-dependencies-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-test-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-uncrustify-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-version-2.5.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cppcheck-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cpplint-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-flake8-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-index-python-1.8.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-lint-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-package-0.16.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-pep257-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-uncrustify-0.17.5-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-builtin-interfaces-2.0.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.19.0-jazzy_26.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-domain-coordinator-0.12.2-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastcdr-2.2.8-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastrtps-cmake-module-3.6.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gmock-vendor-1.14.9000-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gtest-vendor-1.14.9000-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-python-cmake-module-0.11.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcpputils-2.11.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcutils-6.7.6-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rmw-7.3.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-environment-4.2.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-workspace-1.0.3-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-adapter-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cli-4.6.9-np2py312h1bab3cf_26.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cmake-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-generators-0.2.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-runtime-0.2.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-default-generators-1.6.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-c-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-cpp-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-py-0.22.2-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-rs-0.5.0-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-type-description-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-parser-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-pycommon-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-c-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-cpp-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-c-3.2.3-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-cpp-3.2.3-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-interface-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rpyutils-0.4.2-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-service-msgs-2.0.4-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-uncrustify-vendor-3.0.1-np2py312h1bab3cf_25.conda - - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-unique-identifier-msgs-2.5.1-np2py312h1bab3cf_25.conda -- conda_source: rig_interfaces[b77f9c63] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 - build: h60d57d3_0 - subdir: osx-arm64 - variants: - target_platform: osx-arm64 - depends: - - libcxx >=23 - - numpy >=1.25,<3 - - python_abi 3.12.* *_cp312 - - ros-jazzy-rosidl-default-runtime - - ros2-distro-mutex - - ros2-distro-mutex >=0.19.0,<0.20.0a0 - license: LicenseRef-MIT - build_packages: - - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/catkin_pkg-1.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt23_osx-arm64-23.1.4-hb8825d9_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-23.1.4-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.10-py314hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.23-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/empy-4.2.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-builtins-3.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-comprehensions-3.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-docstrings-1.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-import-order-0.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-quotes-3.4.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/lark-parser-0.12.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libcxx-headers-23.1.4-h707e725_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.15.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-4.0.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.2.0-pyhc364b38_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-9_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sdkroot_env_osx-arm64-26.0-ha3f98da_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.2.1-pyhe730653_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.5.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-8_kmp_llvm.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.9-h74c22ad_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_impl_osx-arm64-1030.6.3-llvm23_1_hf5e010b_6.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1030.6.3-llvm23_1_h54b4f4b_6.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-23-23.1.4-default_h63627a2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-scan-deps-23.1.4-default_h8bbc02d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-23.1.4-default_h8bbc02d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-23.1.4-h2adb7b3_35.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-23.1.4-default_h7028af0_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-23.1.4-h2adb7b3_35.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.5-h07a7c03_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-23.1.4-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt23-23.1.4-hdb3d66b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coreutils-9.12-h74c22ad_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cppcheck-2.22.0-py314h16e8f3b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-2.56.0-pl5440h66f49b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-lfs-3.8.1-hc1ac0ee_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmock-1.18.0-hab71263_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtest-1.18.0-h3feff0a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h34f8a20_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm23_1_hc8058f9_6.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-11_h51639a9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-11_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp23.1-23.1.4-default_h8bbc02d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-23.1.4-default_h63627a2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcompiler-rt-23.1.4-hdb3d66b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.23.0-h6651222_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.4-h55c6f16_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-23.1.4-h6dc3340_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20260512-pl5321h26f1114_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h1a92334_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.9.0-h6111c0a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-16.2.0-h3cf6597_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-16.2.0-h07b0088_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-16.2.0-hdb7a957_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-he4c29f2_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-hae62183_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-11_hd9741b5_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm23-23.1.4-h759d1ac_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.70.0-h344cdd5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.14.10-h281bc5c_100_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-hca69786_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-hbf2880b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-23.1.4-hdb3d66b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-23-23.1.4-h79441dc_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-23.1.4-hdb3d66b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/make-4.4.1-h63fc045_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.13.2-h3feff0a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.4-py314he06036b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.5-h55eecbc_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/patch-2.8-h1a92334_1003.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre-8.45-hbdafb3b_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-he63d830_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-5.44.0-1_hb001647_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.10-h0ae1c2c_100_cp314.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h84a0fba_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-hb561403_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tinyxml2-11.0.0-hdca3b7d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uncrustify-0.83.0-h784d473_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-ament-cmake-2.5.6-h60d57d3_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-ros-workspace-1.0.3-h60d57d3_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-rosidl-default-generators-1.6.1-h60d57d3_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-action-msgs-2.0.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-core-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-cppcheck-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-cpplint-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-definitions-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-dependencies-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-include-directories-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-interfaces-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-libraries-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-link-flags-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-targets-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-flake8-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gen-version-h-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gmock-2.5.6-np2py314h40712d6_28.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gtest-2.5.6-np2py314h40712d6_28.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-include-directories-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-libraries-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-pep257-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-pytest-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-python-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-ros-0.12.2-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-target-dependencies-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-test-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-uncrustify-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-version-2.5.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cppcheck-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cpplint-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-flake8-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-index-python-1.8.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-lint-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-package-0.16.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-pep257-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-uncrustify-0.17.5-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-builtin-interfaces-2.0.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-distro-mutex-0.20.0-jazzy_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-domain-coordinator-0.12.2-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-fastcdr-2.2.8-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-fastrtps-cmake-module-3.6.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-gmock-vendor-1.14.9000-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-gtest-vendor-1.14.9000-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-python-cmake-module-0.11.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rcpputils-2.11.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rcutils-6.7.6-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rmw-7.3.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ros-environment-4.2.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ros-workspace-1.0.3-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-adapter-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-cli-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-cmake-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-core-generators-0.2.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-core-runtime-0.2.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-default-generators-1.6.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-c-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-cpp-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-py-0.22.2-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-rs-0.5.0-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-type-description-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-parser-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-pycommon-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-runtime-c-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-runtime-cpp-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-c-3.2.3-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-cpp-3.2.3-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-interface-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rpyutils-0.4.2-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-service-msgs-2.0.4-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-uncrustify-vendor-3.0.1-np2py314h40712d6_27.conda - - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-unique-identifier-msgs-2.5.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-8_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.9-h74c22ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_impl_osx-arm64-1030.6.3-llvm23_1_hf5e010b_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1030.6.3-llvm23_1_h54b4f4b_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-23-23.1.4-default_h63627a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-scan-deps-23.1.4-default_h8bbc02d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-23.1.4-default_h8bbc02d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-23.1.4-h2adb7b3_35.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-23.1.4-default_h7028af0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-23.1.4-h2adb7b3_35.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.5-h07a7c03_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-23.1.4-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt23-23.1.4-hdb3d66b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coreutils-9.12-h74c22ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cppcheck-2.22.0-py314h16e8f3b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-2.56.0-pl5440h66f49b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-lfs-3.8.1-hc1ac0ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmock-1.18.0-hab71263_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtest-1.18.0-h3feff0a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h34f8a20_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-956.6-llvm23_1_hc8058f9_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-11_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-11_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp23.1-23.1.4-default_h8bbc02d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-23.1.4-default_h63627a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcompiler-rt-23.1.4-hdb3d66b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.23.0-h6651222_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.4-h55c6f16_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-23.1.4-h6dc3340_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20260512-pl5321h26f1114_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h1a92334_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.9.0-h6111c0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-16.2.0-h3cf6597_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-16.2.0-h07b0088_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-16.2.0-hdb7a957_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-he4c29f2_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-hae62183_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-11_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm23-23.1.4-h759d1ac_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.70.0-h344cdd5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.14.10-h281bc5c_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-hca69786_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-hbf2880b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-23.1.4-hdb3d66b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-23-23.1.4-h79441dc_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-23.1.4-hdb3d66b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/make-4.4.1-h63fc045_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.13.2-h3feff0a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.4-py314he06036b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.5-h55eecbc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/patch-2.8-h1a92334_1003.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre-8.45-hbdafb3b_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-he63d830_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/perl-5.44.0-1_hb001647_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.10-h0ae1c2c_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h84a0fba_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1600.0.11.8-hb561403_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tinyxml2-11.0.0-hdca3b7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uncrustify-0.83.0-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-ament-cmake-2.5.6-h60d57d3_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-ros-workspace-1.0.3-h60d57d3_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros-jazzy-rosidl-default-generators-1.6.1-h60d57d3_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-action-msgs-2.0.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-core-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-cppcheck-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-cpplint-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-definitions-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-dependencies-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-include-directories-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-interfaces-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-libraries-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-link-flags-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-export-targets-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-flake8-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gen-version-h-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gmock-2.5.6-np2py314h40712d6_28.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-gtest-2.5.6-np2py314h40712d6_28.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-include-directories-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-libraries-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-pep257-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-pytest-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-python-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-ros-0.12.2-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-target-dependencies-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-test-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-uncrustify-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cmake-version-2.5.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cppcheck-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-cpplint-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-flake8-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-index-python-1.8.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-lint-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-package-0.16.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-pep257-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ament-uncrustify-0.17.5-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-builtin-interfaces-2.0.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-distro-mutex-0.20.0-jazzy_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-domain-coordinator-0.12.2-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-fastcdr-2.2.8-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-fastrtps-cmake-module-3.6.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-gmock-vendor-1.14.9000-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-gtest-vendor-1.14.9000-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-python-cmake-module-0.11.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rcpputils-2.11.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rcutils-6.7.6-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rmw-7.3.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ros-environment-4.2.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-ros-workspace-1.0.3-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-adapter-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-cli-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-cmake-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-core-generators-0.2.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-core-runtime-0.2.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-default-generators-1.6.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-c-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-cpp-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-py-0.22.2-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-rs-0.5.0-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-generator-type-description-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-parser-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-pycommon-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-runtime-c-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-runtime-cpp-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-c-3.2.3-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-cpp-3.2.3-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-interface-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-rpyutils-0.4.2-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-service-msgs-2.0.4-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-uncrustify-vendor-3.0.1-np2py314h40712d6_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-unique-identifier-msgs-2.5.1-np2py314h40712d6_27.conda host_packages: - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda @@ -34191,7 +33907,7 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cppcheck-2.22.0-py312h3286cfa_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmock-1.18.0-hab71263_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtest-1.18.0-h3feff0a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-79.1-py311h7f96da7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h34f8a20_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-11_h51639a9_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-11_hb0561ab_openblas.conda @@ -34209,9 +33925,9 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.70.0-h344cdd5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-h08d577c_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.12.16-h0677fcb_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-hca69786_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-h9f98c0b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-hbf2880b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda @@ -34305,8 +34021,374 @@ packages: - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-service-msgs-2.0.4-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-uncrustify-vendor-3.0.1-np2py312h3d3361c_25.conda - conda: https://conda.anaconda.org/robostack-jazzy/osx-arm64/ros2-unique-identifier-msgs-2.5.1-np2py312h3d3361c_25.conda -- conda_source: rig_nodes[b5d6e649] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 +- conda_source: rig_interfaces[aab15256] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 + build: hb0f4dca_0 + subdir: linux-64 + variants: + target_platform: linux-64 + depends: + - libgcc >=16 + - libgcc >=16 + - libstdcxx >=16 + - numpy >=1.25,<3 + - python_abi 3.12.* *_cp312 + - ros-jazzy-rosidl-default-runtime + - ros2-distro-mutex + - ros2-distro-mutex >=0.19.0,<0.20.0a0 + license: LicenseRef-MIT + build_packages: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-default_h4852527_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.9-hebe6cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.5-hff7ac6b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coreutils-9.12-hebe6cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cppcheck-2.22.0-py314h79bcd83_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-16.2.0-h9891192_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-16.2.0-h0ab548f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.56.0-pl5440h3795e67_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/git-lfs-3.8.1-h2c09266_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmock-1.18.0-h48ecd08_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtest-1.18.0-h171cf75_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-16.2.0-h0d273dc_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-16.2.0-h23aca61_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-79.1-py314hb486520_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbc21106_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-11_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-11_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.23.0-ha042cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20260512-pl5321h373387f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.9.0-hd2095e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-16.2.0-h69a702a_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.2.0-h69a702a_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.2.0-h6b99dfc_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h0cb94f2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.70.0-hc5e5977_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_hf13c14d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-h7fe8442_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.14.10-hd51c5da_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-16.2.0-h3048135_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.54.1-hf739112_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.4-hcfc3c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-h1114479_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.4-py314hd3a7d6b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.5-h781a0a9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/patch-2.8-h280c20c_1003.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.44.0-1_h7cc23a3_perl5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.10-hcd007b5_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb03c661_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tinyxml2-11.0.0-hee9eb32_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uncrustify-0.83.0-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/catkin_pkg-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.10-py314hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.23-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/empy-4.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-builtins-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-comprehensions-3.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-docstrings-1.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-import-order-0.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-quotes-3.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-parser-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-16.2.0-he3ce08f_107.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-16.2.0-h86e191b_107.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.15.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-4.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.2.0-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.2.1-pyhe730653_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.5.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ament-cmake-2.5.6-hb0f4dca_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros-workspace-1.0.3-hb0f4dca_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-default-generators-1.6.1-hb0f4dca_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-action-msgs-2.0.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-core-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cppcheck-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cpplint-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-definitions-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-dependencies-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-include-directories-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-interfaces-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-libraries-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-link-flags-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-targets-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-flake8-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gen-version-h-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gmock-2.5.6-np2py314h53a0733_28.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gtest-2.5.6-np2py314h53a0733_28.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-include-directories-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-libraries-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pep257-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pytest-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-python-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-ros-0.12.2-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-target-dependencies-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-test-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-uncrustify-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-version-2.5.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cppcheck-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cpplint-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-flake8-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-index-python-1.8.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-lint-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-package-0.16.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-pep257-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-uncrustify-0.17.5-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-builtin-interfaces-2.0.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.20.0-jazzy_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-domain-coordinator-0.12.2-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastcdr-2.2.8-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastrtps-cmake-module-3.6.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gmock-vendor-1.14.9000-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gtest-vendor-1.14.9000-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-python-cmake-module-0.11.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcpputils-2.11.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcutils-6.7.6-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rmw-7.3.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-environment-4.2.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-workspace-1.0.3-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-adapter-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cli-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cmake-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-generators-0.2.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-runtime-0.2.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-default-generators-1.6.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-c-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-cpp-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-py-0.22.2-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-rs-0.5.0-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-type-description-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-parser-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-pycommon-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-c-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-cpp-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-c-3.2.3-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-cpp-3.2.3-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-interface-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rpyutils-0.4.2-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-service-msgs-2.0.4-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-uncrustify-vendor-3.0.1-np2py314h53a0733_27.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-unique-identifier-msgs-2.5.1-np2py314h53a0733_27.conda + host_packages: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.9-hebe6cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.5-hff7ac6b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cppcheck-2.22.0-py312h3cdd867_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmock-1.18.0-h48ecd08_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtest-1.18.0-h171cf75_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-79.1-py314hb486520_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbc21106_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-11_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-11_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.23.0-ha042cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20260512-pl5321h373387f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.9.0-hd2095e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-16.2.0-h69a702a_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.2.0-h69a702a_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.2.0-h6b99dfc_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.70.0-hc5e5977_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_hf13c14d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-h7fe8442_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.12.16-ha57cdbe_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.54.1-hf739112_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.4-hcfc3c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.4-py312he827f4e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.5-h781a0a9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pkg-config-0.29.2-h1114479_1013.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.16-h5f976f7_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb03c661_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tinyxml2-11.0.0-hee9eb32_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uncrustify-0.83.0-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/catkin_pkg-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.23-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/empy-4.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-builtins-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-comprehensions-3.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-docstrings-1.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-import-order-0.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-quotes-3.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-parser-0.12.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.15.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-4.0.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.3-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.2.0-pyhc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.2.1-pyhe730653_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.5.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ament-cmake-2.5.6-hb0f4dca_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-ros-workspace-1.0.3-hb0f4dca_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros-jazzy-rosidl-default-generators-1.6.1-hb0f4dca_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-action-msgs-2.0.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-core-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cppcheck-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-cpplint-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-definitions-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-dependencies-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-include-directories-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-interfaces-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-libraries-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-link-flags-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-export-targets-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-flake8-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gen-version-h-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gmock-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-gtest-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-include-directories-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-libraries-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pep257-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-pytest-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-python-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-ros-0.12.2-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-target-dependencies-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-test-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-uncrustify-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cmake-version-2.5.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cppcheck-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-cpplint-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-flake8-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-index-python-1.8.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-lint-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-package-0.16.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-pep257-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ament-uncrustify-0.17.5-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-builtin-interfaces-2.0.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-distro-mutex-0.19.0-jazzy_26.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-domain-coordinator-0.12.2-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastcdr-2.2.8-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-fastrtps-cmake-module-3.6.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gmock-vendor-1.14.9000-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-gtest-vendor-1.14.9000-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-python-cmake-module-0.11.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcpputils-2.11.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rcutils-6.7.6-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rmw-7.3.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-environment-4.2.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-ros-workspace-1.0.3-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-adapter-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cli-4.6.9-np2py312h1bab3cf_26.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-cmake-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-generators-0.2.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-core-runtime-0.2.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-default-generators-1.6.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-dynamic-typesupport-0.1.2-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-c-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-cpp-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-py-0.22.2-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-rs-0.5.0-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-generator-type-description-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-parser-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-pycommon-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-c-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-runtime-cpp-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-c-3.2.3-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-cpp-3.2.3-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-c-3.6.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-fastrtps-cpp-3.6.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-interface-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-c-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rosidl-typesupport-introspection-cpp-4.6.9-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-rpyutils-0.4.2-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-service-msgs-2.0.4-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-uncrustify-vendor-3.0.1-np2py312h1bab3cf_25.conda + - conda: https://conda.anaconda.org/robostack-jazzy/linux-64/ros2-unique-identifier-msgs-2.5.1-np2py312h1bab3cf_25.conda +- conda_source: rig_nodes[0cad1072] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 build: pyh4616a5c_0 subdir: noarch noarch: python @@ -34314,6 +34396,7 @@ packages: target_platform: noarch depends: - mcap >=1.2 + - psutil - python * - python >=3.12 - ros-jazzy-rclpy @@ -34325,7 +34408,7 @@ packages: - websockets >=12 license: MIT host_packages: - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.33.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda @@ -34334,23 +34417,24 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.2-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2026.9.21.13-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.4-h55c6f16_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.9.0-h6111c0a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.12.16-h0677fcb_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-h34a968b_101.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.5-h55eecbc_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.16-hd05a0c4_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.13.6-hbb8eed7_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda -- conda_source: rig_nodes[daef82bd] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.9.0-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpython-3.12.16-hfb4101e_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.54.1-hf5d6505_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.5-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.13.6-hf7a7c96_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-ha367084_41.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36247-habf1de7_41.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda +- conda_source: rig_nodes[16d68303] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 build: pyh4616a5c_0 subdir: noarch noarch: python @@ -34358,6 +34442,7 @@ packages: target_platform: noarch depends: - mcap >=1.2 + - psutil - python * - python >=3.12 - ros-jazzy-rclpy @@ -34400,8 +34485,8 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.2-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2026.9.21.13-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda -- conda_source: rig_nodes[ed0cba21] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 +- conda_source: rig_nodes[3b7053d6] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#fa83d3945c903066538c02bdc307b570274698c2 + version: 0.3.0 build: pyh4616a5c_0 subdir: noarch noarch: python @@ -34409,6 +34494,7 @@ packages: target_platform: noarch depends: - mcap >=1.2 + - psutil - python * - python >=3.12 - ros-jazzy-rclpy @@ -34420,7 +34506,7 @@ packages: - websockets >=12 license: MIT host_packages: - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.10.23-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.6-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.33.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda @@ -34429,19 +34515,18 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.2-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2026.9.21.13-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026e-h5d301ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_10.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.9.0-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libpython-3.12.16-hfb4101e_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.54.1-hf5d6505_101.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.5-hf411b9b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.16-hb12b558_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.13.6-hf7a7c96_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-ha367084_41.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36247-habf1de7_41.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-23.1.4-h55c6f16_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.9.0-h6111c0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.12.16-h0677fcb_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.54.1-h34a968b_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.5-h55eecbc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.16-hd05a0c4_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.13.6-hbb8eed7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda diff --git a/rig/pixi.toml b/rig/pixi.toml index 7c49c800..eee99279 100644 --- a/rig/pixi.toml +++ b/rig/pixi.toml @@ -23,10 +23,9 @@ [workspace] name = "rig" version = "0.1.0" -# The 0.3.0 channel is read at the COMMIT that carries the payload (5e20dee, release/0.3.0 → rig-nodes#3): -# the `v0.3.0` tag was placed one commit early (payload gitignored) and moves there when Michael runs -# `git tag -f v0.3.0 5e20dee && git push --force origin refs/tags/v0.3.0`; a commit SHA is as immutable. -channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel"] +# The channel is read at the RELEASE TAG (`v0.4.0`, rig-nodes#7), the preferred form; 0.3.0 was pinned by the +# payload commit's SHA only because its tag had first landed one commit before the payload. +channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel"] # rmw_zenoh ≥ 0.2.11 (the RoboStack 0.19 set) is built for macOS ≥ 14; without this floor the # osx-arm64 solve falls back to the 0.16 set and the platforms drift apart (rig-nodes pins 0.19). # Declared on the platform (pixi ≥ 0.80 virtual packages), not as the deprecated [system-requirements]; pixi @@ -84,17 +83,18 @@ ros-jazzy-rosbag2-py = "*" ros-jazzy-rosbag2-storage-mcap = "*" mcap = ">=1.2" # ── the shared rig-nodes package set, pinned (one version for the set; README "Releasing") ── -# Device adapters (FicTrac, Kinefly, BIAS, ScanImage incl. its ZeroMQ frame stream, the experimental SiPM -# controller) and their simulators, the MCAP recorder and indexer, the CDR decoder, the Foxglove rewriter -# (+ bias_health), the supervision primitives rig.py composes with. 0.3.0 (2026-10-28) also ships the -# HostStats message set for the host monitor (rig_interfaces, `typed` environment). +# Device adapters (FicTrac, Kinefly, BIAS) and their simulators, the MCAP recorder and indexer, the CDR +# decoder, the Foxglove rewriter (+ bias_health), the host monitor, the supervision primitives rig.py composes +# with. 0.3.0 (2026-10-28) added the HostStats message set for the host monitor (rig_interfaces, `typed` +# environment); 0.4.0 (same day) moved the ScanImage JSON connector to the private 2P rig repo and made the +# host monitor's typed floats compressible + its process sampling cheaper (rig-nodes#4, #6). # Written into every run's record by the gateway (hello_ack + /arena/session `packages`). The pin lives in the # `channel` feature (default / typed / viz), not in [dependencies]: the `dev` environment builds rig_nodes from # the git source instead, and one package name from two sources does not solve once the released version is # ahead of `main` (0.3.0 vs 0.2.1 on 2026-10-28: "excluded because of strict channel priority"). [feature.channel.dependencies] -rig_nodes = "==0.3.0" +rig_nodes = "==0.4.0" # ── typed messages = the OPT-IN `typed` environment ─────────────────────────────────────── # `pixi install -e typed` adds rig_interfaces (FicTracFrame, ArenaFrame, ConnectorReply, HostStats + its @@ -106,7 +106,7 @@ rig_nodes = "==0.3.0" # environment so the rigs keep recording String topics until typed recording is decided for them; # the nodes fall back on their own (`--typed auto`). [feature.typed.dependencies] -rig_interfaces = "==0.3.0" +rig_interfaces = "==0.4.0" # ── developer path: rig-nodes `main`, built from source (git source dependencies) ──────────── # `pixi install -e dev` swaps the two channel pins for the HEAD of reiserlab/rig-nodes `main`, From 91bf95849edbc9fce4361ab88e62f3ccc776f00e Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 13:05:04 -0400 Subject: [PATCH 03/10] =?UTF-8?q?protocols=20+=20rig=20records=20=E2=86=92?= =?UTF-8?q?=20private=20repos=20(LAB-227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rig-specific protocols and per-rig records leave the public repo so a rig's protocol tweaks never need a webDisplayTools release. Rule: a protocol stays here only if a tool, test, QC profile or fixture depends on it. - Removed (copied first to reiserlab/arena-experiments 8db48e6 and reiserlab/scanimage-2026-bergamo 00bccfd, each with PROVENANCE.md): the three bias_camera_closed_loop_poc* protocols (→ protocols/ww18/), fictrac_bias_test, fictrac_direction_test, fictrac_coupling_bias_fly_test, led_activation_quadrant_test (+ patterns), mode3_drill_1trial (→ protocols/shared/); the three 2P protocols + two pattern folders (→ bergamo protocols/); the ww18 and okey-cokey (ww14) run sheets, handover, dashboard plan, BIAS config and .ps1 start scripts (→ rigs/ww18/, rigs/ww14/). - protocols/index.json drops the moved keys; protocols/MOVED.md maps every old path to its new home. - tests/test-protocol-roundtrip-v3.js parses the 2P sweep from a fixture copy (tests/fixtures/v3_g6_2p_color_led_sweep.yaml). - scripts/make-color-optomotor-patterns.js --gs16 and scripts/make-2p-diagnostic-patterns.js take --out (the 2P targets are no longer in this repo; without --out they explain where they went and exit 2). - rig/README.md, rig/WINDOWS-TEST.md, the protocol-yaml skill, two course docs and the decision record's "stays" row point at the new locations. Co-Authored-By: Claude Fable 5.1 --- .agents/skills/protocol-yaml/SKILL.md | 5 +- course/cshl-2026/docs/fictrac.md | 2 +- course/cshl-2026/docs/protocols/README.md | 3 +- .../rig-software-repos-and-runners.md | 2 +- protocols/MOVED.md | 29 ++ protocols/bias_camera_closed_loop_poc.yaml | 136 ----- .../bias_camera_closed_loop_poc_ww18.yaml | 139 ----- ...as_camera_closed_loop_poc_ww18_soak10.yaml | 140 ----- protocols/fictrac_bias_test.yaml | 306 ----------- protocols/fictrac_coupling_bias_fly_test.yaml | 493 ------------------ protocols/fictrac_direction_test.yaml | 271 ---------- .../G6_2x10_toprow_grat20_blue_gs16.pat | Bin 81338 -> 0 bytes .../G6_2x10_toprow_grat20_green_gs16.pat | Bin 81338 -> 0 bytes .../G6_2x10_toprow_grat20_violet_gs16.pat | Bin 81338 -> 0 bytes .../G6_2x10_toprow_grat20_yellow_gs16.pat | Bin 81338 -> 0 bytes .../g6_2p_color_led_sweep_patterns/README.txt | 15 - protocols/g6_2x10_2p_diagnostics.yaml | 226 -------- .../G6_2x10_ff_flash_0_15.pat | Bin 8150 -> 0 bytes .../G6_2x10_ff_ramp16.pat | Bin 65074 -> 0 bytes .../G6_2x10_ff_steps_pr16.pat | Bin 65074 -> 0 bytes .../G6_2x10_sparse10.pat | Bin 4084 -> 0 bytes .../README.txt | 11 - protocols/g6_2x10_2p_duty_sweep.yaml | 267 ---------- protocols/index.json | 50 -- protocols/led_activation_quadrant_test.yaml | 212 -------- .../001_all_on.pat | Bin 1084 -> 0 bytes .../002_grating_sq.pat | Bin 21338 -> 0 bytes .../003_grating_sine.pat | Bin 81338 -> 0 bytes .../004_frame2_h_ccw_200f.pat | Bin 813218 -> 0 bytes protocols/mode3_drill_1trial.yaml | 67 --- rig/HANDOVER-okey-cokey-2026-10-21.md | 102 ---- rig/README.md | 6 +- rig/WINDOWS-TEST.md | 8 +- rig/config/bias/okey-cokey-bfs-u3-51s5m.json | 253 --------- .../okey-cokey-foxglove-dashboard-plan.md | 176 ------- rig/okey-cokey-pc-2026-10-21.md | 167 ------ rig/okey-cokey-pc-results-2026-10-21.md | 36 -- rig/okey-cokey-pc-setup.md | 133 ----- rig/okey-cokey-pc-ww14-setup-2026-10-22.md | 87 ---- rig/tools/bias-ufmf.ps1 | 41 -- rig/tools/okey-cokey-start.ps1 | 83 --- rig/windows-results-ww18-2026-10-15.md | 231 -------- scripts/make-2p-diagnostic-patterns.js | 74 ++- scripts/make-color-optomotor-patterns.js | 25 +- .../fixtures/v3_g6_2p_color_led_sweep.yaml | 0 tests/test-protocol-roundtrip-v3.js | 7 +- 46 files changed, 122 insertions(+), 3681 deletions(-) create mode 100644 protocols/MOVED.md delete mode 100644 protocols/bias_camera_closed_loop_poc.yaml delete mode 100644 protocols/bias_camera_closed_loop_poc_ww18.yaml delete mode 100644 protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml delete mode 100644 protocols/fictrac_bias_test.yaml delete mode 100644 protocols/fictrac_coupling_bias_fly_test.yaml delete mode 100644 protocols/fictrac_direction_test.yaml delete mode 100644 protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_blue_gs16.pat delete mode 100644 protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_green_gs16.pat delete mode 100644 protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_violet_gs16.pat delete mode 100644 protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_yellow_gs16.pat delete mode 100644 protocols/g6_2p_color_led_sweep_patterns/README.txt delete mode 100644 protocols/g6_2x10_2p_diagnostics.yaml delete mode 100644 protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_flash_0_15.pat delete mode 100644 protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_ramp16.pat delete mode 100644 protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_steps_pr16.pat delete mode 100644 protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_sparse10.pat delete mode 100644 protocols/g6_2x10_2p_diagnostics_patterns/README.txt delete mode 100644 protocols/g6_2x10_2p_duty_sweep.yaml delete mode 100644 protocols/led_activation_quadrant_test.yaml delete mode 100644 protocols/led_activation_quadrant_test_patterns/001_all_on.pat delete mode 100644 protocols/led_activation_quadrant_test_patterns/002_grating_sq.pat delete mode 100644 protocols/led_activation_quadrant_test_patterns/003_grating_sine.pat delete mode 100644 protocols/led_activation_quadrant_test_patterns/004_frame2_h_ccw_200f.pat delete mode 100644 protocols/mode3_drill_1trial.yaml delete mode 100644 rig/HANDOVER-okey-cokey-2026-10-21.md delete mode 100644 rig/config/bias/okey-cokey-bfs-u3-51s5m.json delete mode 100644 rig/docs/okey-cokey-foxglove-dashboard-plan.md delete mode 100644 rig/okey-cokey-pc-2026-10-21.md delete mode 100644 rig/okey-cokey-pc-results-2026-10-21.md delete mode 100644 rig/okey-cokey-pc-setup.md delete mode 100644 rig/okey-cokey-pc-ww14-setup-2026-10-22.md delete mode 100644 rig/tools/bias-ufmf.ps1 delete mode 100644 rig/tools/okey-cokey-start.ps1 delete mode 100644 rig/windows-results-ww18-2026-10-15.md rename protocols/g6_2p_color_led_sweep.yaml => tests/fixtures/v3_g6_2p_color_led_sweep.yaml (100%) diff --git a/.agents/skills/protocol-yaml/SKILL.md b/.agents/skills/protocol-yaml/SKILL.md index 8324f96b..005b3ea9 100644 --- a/.agents/skills/protocol-yaml/SKILL.md +++ b/.agents/skills/protocol-yaml/SKILL.md @@ -55,7 +55,8 @@ it does NOT advance the protocol clock. Only `wait` commands do. Therefore: Editing only ONE of the pair is **masked** by the other — "the timeline won't update" is almost always a half-edited pair, not a render bug. - **Bind both fields to one anchor** (`duration: *show_dur` on both) so they can never - drift — the `open_dur`/`cl_dur` pattern from `protocols/fictrac_direction_test`. + drift — the `open_dur`/`cl_dur` pattern from `fictrac_direction_test` (private + `reiserlab/arena-experiments`, `protocols/shared/`). - Waits between two `trialParams` in one condition define how long the FIRST one actually shows (the second overrides it). Waits longer than the duration = intentional hold/blank; fine, but mean it. @@ -163,7 +164,7 @@ rejection). Authored as an added rotational **velocity**: A negative frequency is a harmless no-op (warned): negate `bias_amplitude` instead. - `stopClosedLoop` clears the bias automatically. Reference: `docs/development/closed-loop-bias.md`; worked example: - `protocols/fictrac_bias_test.yaml`. + `protocols/shared/fictrac_bias_test.yaml` in the private `reiserlab/arena-experiments`. **Per-trial start position = the trialParams `frame_index` (Studio v0.91 / bridge 3.4).** A closed-loop trial opens on the Mode-3 `trialParams` `frame_index` (0-based, wraps mod the pattern's diff --git a/course/cshl-2026/docs/fictrac.md b/course/cshl-2026/docs/fictrac.md index 3ae857ee..0a7a7221 100644 --- a/course/cshl-2026/docs/fictrac.md +++ b/course/cshl-2026/docs/fictrac.md @@ -119,7 +119,7 @@ Closed-loop and the live scope need the **bridge** running on the rig computer: bridge"* to a live trace once data flows. Closed-loop trials show a green **CLOSED LOOP** tag. -**Quick check:** load [`fictrac_direction_test`](protocols/README.md) — it runs +**Quick check:** load [`fictrac_direction_test`](protocols/README.md) (since 2026-10-29 in the lab's private `reiserlab/arena-experiments`, `protocols/shared/`) — it runs open-loop motion first, then a closed-loop block that the fly steers. If the closed-loop block responds to nudging the ball, the bridge is working. diff --git a/course/cshl-2026/docs/protocols/README.md b/course/cshl-2026/docs/protocols/README.md index 6daded02..963fbed8 100644 --- a/course/cshl-2026/docs/protocols/README.md +++ b/course/cshl-2026/docs/protocols/README.md @@ -53,7 +53,8 @@ Animated pattern previews are embedded on all four protocol pages. FicTrac): full-field brightness, panel map, a moving bar, and LED levels 2/5/10/20/40%. Use it to confirm a rig works after setup. - **`fictrac_direction_test`** — open-loop motion then a closed-loop block, to - confirm the [FicTrac](../fictrac.md) bridge is working. + confirm the [FicTrac](../fictrac.md) bridge is working. Since 2026-10-29 it lives in + the lab's private `reiserlab/arena-experiments` (`protocols/shared/`), not in the site library. - **`optomotor_led_test`** — a slow grating with the LED switched on for the last few seconds; the reference example of "fire a command partway through a trial." diff --git a/docs/development/rig-software-repos-and-runners.md b/docs/development/rig-software-repos-and-runners.md index 37ec574d..8df3ffc8 100644 --- a/docs/development/rig-software-repos-and-runners.md +++ b/docs/development/rig-software-repos-and-runners.md @@ -111,7 +111,7 @@ stay as history. | **pyDisplayTools** | `tools/patfile.py` (pattern-file reader; the repo already has `pyPatternTools`), `nodes/arena_display.py` (Python port of `js/panel-color.js`, okey-cokey branch), the pure functions of `fictrac-bridge/bridge.py` (`Pipeline` mapping, `frame_schema_line`, `compact_arena_command`), `fictrac-bridge/fictrac_sim.py` moves to rig-nodes as a device simulator | | **stays** (gateway, export, composition) | `nodes/bridge_node.py`, `nodes/mcap_convert.py`, `nodes/_rows.py` (behavior_v2 `kf` rows), `nodes/export_log.py`, `nodes/rig.py` (arena composition; its generic preflight/supervision helpers are extracted to rig-nodes first), `pixi.toml`, `pixi.lock`, `.gitignore`, `tests/studio_emulator.py`, `tests/check_runlog_readers.js`, the gateway half of `tests/test_ros2_poc.py` (node-level tests move with their nodes) | | **stays** (arena visualization) | `nodes/viz_node.py`, `tests/test_viz_node.py`, the arena derivation in `nodes/foxglove_json.py`, `foxglove/arena-fictrac.json`, `foxglove/arena-bias.json`, `docs/foxglove-arena-layout.json`, `tools/tiff_run_to_mcap.py`; from okey-cokey: `nodes/stimulus_node.py`, `nodes/stimulus_core.py`, `nodes/dashboard_replay.py`, `tests/test_stimulus.py`, `tests/test_stimulus_live.py`, `foxglove/arena-okey-cokey.json`, `tests/fixtures/protocols-2026-10-14/*` | -| **stays** (rig-specific config and records) | `README.md`, `WINDOWS-TEST.md`, `SCANIMAGE-DAY.md`, `scanimage-day-2026-10-16.md`, `windows-results-ww18-2026-10-15.md`, `docs/plan-b-explainer*.html`, `docs/build-standalone-explainer.py`, `docs/img/*`; from okey-cokey: `config/bias/okey-cokey-bfs-u3-51s5m.json`, `tools/okey-cokey-start.ps1`, `tools/bias-ufmf.ps1`, the `okey-cokey-*.md` run sheets, `HANDOVER-okey-cokey-2026-10-21.md`, `docs/okey-cokey-foxglove-dashboard-plan.md` | +| **stays** (tool code and generic records) | `README.md`, `WINDOWS-TEST.md`, `docs/plan-b-explainer*.html`, `docs/build-standalone-explainer.py`, `docs/img/*`, `foxglove/arena-okey-cokey.json` (a layout `rig.py --foxglove` loads), `tools/ufmf.py`, `tools/patfile.py`. **Moved 2026-10-29 (LAB-227) to the private `reiserlab/arena-experiments` `rigs//`:** `windows-results-ww18-2026-10-15.md` → `rigs/ww18/`; from okey-cokey → `rigs/ww14/`: `config/bias/okey-cokey-bfs-u3-51s5m.json`, `tools/okey-cokey-start.ps1`, `tools/bias-ufmf.ps1`, the `okey-cokey-*.md` run sheets, `HANDOVER-okey-cokey-2026-10-21.md`, `docs/okey-cokey-foxglove-dashboard-plan.md`. `SCANIMAGE-DAY.md` and `scanimage-day-2026-10-16.md` went to the private 2P repo with #284. | #### Amendment 2026-10-28: the imaging work is private diff --git a/protocols/MOVED.md b/protocols/MOVED.md new file mode 100644 index 00000000..b3c06e27 --- /dev/null +++ b/protocols/MOVED.md @@ -0,0 +1,29 @@ +# Protocols and rig records that left this repo (2026-10-29, LAB-227) + +Experiment material now lives in private repos so that a rig's protocol tweaks never need a +webDisplayTools release. The rule: a protocol stays here only if a tool, test, QC profile or doc +fixture depends on it. Copies were made from commit `59a377b` (Release 2026-10-28); each destination +has a `PROVENANCE.md`. Open a moved protocol in the Studio, signed in, with +`arena_studio.html?repo=&p=`. + +| Was here | Now (repo → path) | +|---|---| +| `protocols/bias_camera_closed_loop_poc.yaml` | `reiserlab/arena-experiments` → `protocols/ww18/bias_camera_closed_loop_poc.yaml` | +| `protocols/bias_camera_closed_loop_poc_ww18.yaml` | `reiserlab/arena-experiments` → `protocols/ww18/bias_camera_closed_loop_poc_ww18.yaml` | +| `protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml` | `reiserlab/arena-experiments` → `protocols/ww18/bias_camera_closed_loop_poc_ww18_soak10.yaml` | +| `protocols/fictrac_bias_test.yaml` | `reiserlab/arena-experiments` → `protocols/shared/fictrac_bias_test.yaml` | +| `protocols/fictrac_direction_test.yaml` | `reiserlab/arena-experiments` → `protocols/shared/fictrac_direction_test.yaml` | +| `protocols/fictrac_coupling_bias_fly_test.yaml` | `reiserlab/arena-experiments` → `protocols/shared/fictrac_coupling_bias_fly_test.yaml` | +| `protocols/led_activation_quadrant_test.yaml` + `_patterns/` | `reiserlab/arena-experiments` → `protocols/shared/led_activation_quadrant_test.yaml` + `_patterns/` | +| `protocols/mode3_drill_1trial.yaml` | `reiserlab/arena-experiments` → `protocols/shared/mode3_drill_1trial.yaml` | +| `protocols/g6_2p_color_led_sweep.yaml` + `_patterns/` | `reiserlab/scanimage-2026-bergamo` → `protocols/g6_2p_color_led_sweep.yaml` + `_patterns/` | +| `protocols/g6_2x10_2p_diagnostics.yaml` + `_patterns/` | `reiserlab/scanimage-2026-bergamo` → `protocols/g6_2x10_2p_diagnostics.yaml` + `_patterns/` | +| `protocols/g6_2x10_2p_duty_sweep.yaml` | `reiserlab/scanimage-2026-bergamo` → `protocols/g6_2x10_2p_duty_sweep.yaml` | +| `rig/windows-results-ww18-2026-10-15.md` | `reiserlab/arena-experiments` → `rigs/ww18/` | +| `rig/okey-cokey-pc-*.md`, `rig/HANDOVER-okey-cokey-2026-10-21.md`, `rig/docs/okey-cokey-foxglove-dashboard-plan.md`, `rig/config/bias/okey-cokey-bfs-u3-51s5m.json`, `rig/tools/okey-cokey-start.ps1`, `rig/tools/bias-ufmf.ps1` | `reiserlab/arena-experiments` → `rigs/ww14/` (same names; `config/` and `tools/` subfolders) | + +A copy of `g6_2p_color_led_sweep.yaml` stays as the test fixture `tests/fixtures/v3_g6_2p_color_led_sweep.yaml`. +The pattern generators `scripts/make-color-optomotor-patterns.js --gs16` and +`scripts/make-2p-diagnostic-patterns.js` now take `--out ` pointing into the 2P repo. + +Pages under `docs/development/` that cite the old paths are historical records and were not edited. diff --git a/protocols/bias_camera_closed_loop_poc.yaml b/protocols/bias_camera_closed_loop_poc.yaml deleted file mode 100644 index 19069352..00000000 --- a/protocols/bias_camera_closed_loop_poc.yaml +++ /dev/null @@ -1,136 +0,0 @@ -version: 3 - -# BIAS camera + FicTrac closed loop through the ROS 2 bridge node (LAB-303 connectors). -# The `camera` plugin (BiasPlugin) is driven by the WEB runner: each command goes to the -# bridge as connector_command, the bridge's BIAS node runs it over BIAS's HTTP port, and -# the reply (with the video artifact for startRecording) lands in the run log. -# -# cam_start : connect → startRecording (one video for the whole run) → getTimestamp -# 2 closed-loop trials (none, then a constant +90°/s bias), 15 s each -# cam_sync : getTimestamp (a mid-run sync point against the bridge clock) -# cam_stop : getTimestamp → stopRecording -# -# Video path: relative to the bias node's --video-dir (like MATLAB's experiment folder). - -experiment_info: - name: "BIAS camera + closed loop — ROS 2 PoC Mac bench (LAB-303)" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - - name: "camera" - type: "class" - matlab: - class: "BiasPlugin" - -variables: - cl_dur: &cl_dur 15 - -experiment: - - name: "camera_run" - trials: - - "cam_start" - - "cl_bias_none" - - "cam_sync" - - "cl_bias_constant" - - "cam_stop" - repetitions: 1 - - "shutdown" - -conditions: - - name: "cam_start" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "connect" - - type: "plugin" - plugin_name: "camera" - command_name: "startRecording" - params: - filename: "poc_studio_run.avi" - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_none" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "G6_2x10_grat_rot_20px_50pct_4c-B-Y" - pattern_ID: 1 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "none" - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_sync" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_constant" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "G6_2x10_grat_rot_20px_50pct_4c-B-Y" - pattern_ID: 1 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "constant" - bias_amplitude: 90 - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_stop" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "plugin" - plugin_name: "camera" - command_name: "stopRecording" - - type: "wait" - duration: 1 - - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/bias_camera_closed_loop_poc_ww18.yaml b/protocols/bias_camera_closed_loop_poc_ww18.yaml deleted file mode 100644 index 7a3d89c9..00000000 --- a/protocols/bias_camera_closed_loop_poc_ww18.yaml +++ /dev/null @@ -1,139 +0,0 @@ -version: 3 -# ww18 variant (2026-10-15): the site library's G6 2x10 rotating square grating, SD slot 2 -# (patterns/g6_2x10/patterns/002_grating_sq.pat, 20 frames), in place of the Mac bench card's -# G6_2x10_grat_rot_20px_50pct_4c-B-Y. Otherwise identical to bias_camera_closed_loop_poc.yaml. - -# BIAS camera + FicTrac closed loop through the ROS 2 bridge node (LAB-303 connectors). -# The `camera` plugin (BiasPlugin) is driven by the WEB runner: each command goes to the -# bridge as connector_command, the bridge's BIAS node runs it over BIAS's HTTP port, and -# the reply (with the video artifact for startRecording) lands in the run log. -# -# cam_start : connect → startRecording (one video for the whole run) → getTimestamp -# 2 closed-loop trials (none, then a constant +90°/s bias), 15 s each -# cam_sync : getTimestamp (a mid-run sync point against the bridge clock) -# cam_stop : getTimestamp → stopRecording -# -# Video path: relative to the bias node's --video-dir (like MATLAB's experiment folder). - -experiment_info: - name: "BIAS camera + closed loop — ROS 2 PoC, Windows rig ww18 (LAB-303)" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - - name: "camera" - type: "class" - matlab: - class: "BiasPlugin" - -variables: - cl_dur: &cl_dur 15 - -experiment: - - name: "camera_run" - trials: - - "cam_start" - - "cl_bias_none" - - "cam_sync" - - "cl_bias_constant" - - "cam_stop" - repetitions: 1 - - "shutdown" - -conditions: - - name: "cam_start" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "connect" - - type: "plugin" - plugin_name: "camera" - command_name: "startRecording" - params: - filename: "poc_studio_run.avi" - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_none" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "none" - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_sync" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_constant" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "constant" - bias_amplitude: 90 - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_stop" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "plugin" - plugin_name: "camera" - command_name: "stopRecording" - - type: "wait" - duration: 1 - - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml b/protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml deleted file mode 100644 index 1cdcd5c8..00000000 --- a/protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml +++ /dev/null @@ -1,140 +0,0 @@ -version: 3 -# ww18 SOAK variant (2026-10-15): both closed-loop epochs 300 s (~10 min run). -# ww18 variant (2026-10-15): the site library's G6 2x10 rotating square grating, SD slot 2 -# (patterns/g6_2x10/patterns/002_grating_sq.pat, 20 frames), in place of the Mac bench card's -# G6_2x10_grat_rot_20px_50pct_4c-B-Y. Otherwise identical to bias_camera_closed_loop_poc.yaml. - -# BIAS camera + FicTrac closed loop through the ROS 2 bridge node (LAB-303 connectors). -# The `camera` plugin (BiasPlugin) is driven by the WEB runner: each command goes to the -# bridge as connector_command, the bridge's BIAS node runs it over BIAS's HTTP port, and -# the reply (with the video artifact for startRecording) lands in the run log. -# -# cam_start : connect → startRecording (one video for the whole run) → getTimestamp -# 2 closed-loop trials (none, then a constant +90°/s bias), 15 s each -# cam_sync : getTimestamp (a mid-run sync point against the bridge clock) -# cam_stop : getTimestamp → stopRecording -# -# Video path: relative to the bias node's --video-dir (like MATLAB's experiment folder). - -experiment_info: - name: "BIAS camera + closed loop — ROS 2 PoC, Windows rig ww18, 10-min soak (2 x 300 s closed loop) (LAB-303)" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - - name: "camera" - type: "class" - matlab: - class: "BiasPlugin" - -variables: - cl_dur: &cl_dur 300 - -experiment: - - name: "camera_run" - trials: - - "cam_start" - - "cl_bias_none" - - "cam_sync" - - "cl_bias_constant" - - "cam_stop" - repetitions: 1 - - "shutdown" - -conditions: - - name: "cam_start" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "connect" - - type: "plugin" - plugin_name: "camera" - command_name: "startRecording" - params: - filename: "poc_studio_run.avi" - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_none" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "none" - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_sync" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "wait" - duration: 1 - - - name: "cl_bias_constant" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "constant" - bias_amplitude: 90 - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cam_stop" - commands: - - type: "plugin" - plugin_name: "camera" - command_name: "getTimestamp" - - type: "plugin" - plugin_name: "camera" - command_name: "stopRecording" - - type: "wait" - duration: 1 - - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/fictrac_bias_test.yaml b/protocols/fictrac_bias_test.yaml deleted file mode 100644 index 2e936592..00000000 --- a/protocols/fictrac_bias_test.yaml +++ /dev/null @@ -1,306 +0,0 @@ -version: 3 - -# ============================================================================ -# FicTrac closed-loop BIAS (disturbance) test — web runner only. -# -# A "bias" is a smooth disturbance added to the closed loop, so the display -# keeps rotating even when the fly holds still. Sweeping it lets you measure how -# well the fly REJECTS the disturbance (does it counter-turn to null the motion?). -# -# Uses ONLY built-in g6_2x10 patterns already on the SD card: -# 4 frame2_h_ccw_200f (a 200-frame full-azimuth rotation) -# -# The bias is authored as an added rotational VELOCITY in deg/s (`bias_amplitude` -# = the PEAK velocity), which the bridge integrates into a bias ANGLE and folds -# into its heading→frame-index mapping. At the rig's 1.8°/frame pitch (360/200 px), -# a bias of 90 deg/s advances the display 90°/s = 50 frames/s — independent of the -# closed-loop `coupling` (default 1 = the display follows the ball 1:1). -# -# bias_type what the display does (with a still fly) -# none nothing — the plain closed-loop baseline -# constant rotates steadily, one direction, forever -# sine swings smoothly back and forth over ±A/(2πf) -# square constant-speed rotation that reverses, over ±A/(4f) -# -# DIRECTION (confirmed on the arena, 2026-08-04, at the normal positive gain 1.8): -# a POSITIVE bias_amplitude turns the display CLOCKWISE — equivalently, the pattern -# sweeps rightward across the fly's visual field. Negative is counter-clockwise. -# This is the sign your analysis must assume; get it backwards and a fly correctly -# counter-turning against the disturbance will read as following it. -# -# The periodic waveforms are ZERO-MEAN in position: the display is pushed equally -# left and right rather than drifting to one side, and always starts at the frame -# it was already on (no jump at onset). Note the excursion SHRINKS as frequency -# rises for a fixed amplitude — at 90 deg/s the sine covers ±28.6° at 0.5 Hz but -# only ±14.3° at 1 Hz. Full semantics: docs/development/closed-loop-bias.md. -# -# DURATION knobs (`variables:` below): cl_dur drives every trial's trialParams -# duration AND its bracketing wait, so they always move together. -# -# TIMING — the `wait` is load-bearing in every condition here: it holds the -# closed loop ACTIVE for cl_dur before stopClosedLoop fires. Drop it and the -# stop fires immediately, so no disturbance would ever be seen. -# -# Total ≈ 4 min at defaults (8 trials × 30 s). Needs the FicTrac bridge -# (`pixi run bridge`); with no fly, `pixi run sim` supplies synthetic heading. -# NOTE (web-first): MATLAB has no FicTracPlugin (its closed loop is analog -# Mode 4, computed on the controller, where the host cannot inject a bias), so -# this protocol runs on the WEB runner only. -# ============================================================================ - -experiment_info: - name: "FicTrac closed-loop bias (disturbance) test" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - -variables: - # ── Duration knob ────────────────────────────────────────────────────────── - cl_dur: &cl_dur 30 # seconds per closed-loop trial (drives duration AND wait) - # ── Disturbance knobs ────────────────────────────────────────────────────── - # amp is the PEAK velocity in deg/s; freq only applies to sine/square. Change - # these once here (or in the Edit view's Variables panel) to rescale the sweep. - bias_amp: &bias_amp 90 # 90 deg/s = 50 frames/s at gain 1.8 - bias_amp_slow: &bias_amp_slow 30 # a gentler disturbance for comparison - bias_freq: &bias_freq 0.5 # Hz — one back-and-forth cycle every 2 s - bias_freq_fast: &bias_freq_fast 1.0 # Hz — halves the position excursion - -experiment: - - name: "bias_sweep" - trials: - - "cl_bias_none" - - "cl_bias_constant" - - "cl_bias_constant_rev" - - "cl_bias_sine" - - "cl_bias_sine_fast" - - "cl_bias_sine_slow" - - "cl_bias_square" - - "cl_bias_square_fast" - repetitions: 1 - - "shutdown" - -conditions: - # NOTE: no explicit fictrac connect/disconnect. The runner auto-connects the - # bridge at run start, and must NOT disconnect it at the end — the recorded-run - # log is exported from the bridge AFTER the sequence finishes, so a - # protocol-level disconnect would close the bridge before the log commits. - - # ---- baseline: closed loop with NO disturbance ------------------------------ - # The control condition. Omitting bias_type entirely is equivalent to - # bias_type "none"; it is spelled out here so the contrast is obvious. - - name: "cl_bias_none" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "none" - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- constant: a steady drift the fly must walk against -------------------- - # bias_frequency is ignored by `constant` (there is no period), so it is omitted. - # NEGATE bias_amplitude to reverse the direction — negating the FREQUENCY does - # nothing, because the waveform is even in frequency. - - name: "cl_bias_constant" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "constant" - bias_amplitude: *bias_amp - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_constant_rev" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "constant" - bias_amplitude: -90 - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- sine: the classic frequency-response disturbance ---------------------- - # Position excursion is ±A/(2πf): ±28.6° at 90 deg/s + 0.5 Hz, ±14.3° at 1 Hz. - - name: "cl_bias_sine" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "sine" - bias_amplitude: *bias_amp - bias_frequency: *bias_freq - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_sine_fast" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "sine" - bias_amplitude: *bias_amp - bias_frequency: *bias_freq_fast - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_sine_slow" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "sine" - bias_amplitude: *bias_amp_slow - bias_frequency: *bias_freq - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- square: constant-speed rotation that reverses ------------------------- - # Position is a symmetric triangle spanning ±A/(4f): ±45° at 90 deg/s + 0.5 Hz. - - name: "cl_bias_square" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "square" - bias_amplitude: *bias_amp - bias_frequency: *bias_freq - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_square_fast" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - bias_type: "square" - bias_amplitude: *bias_amp - bias_frequency: *bias_freq_fast - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- shutdown -------------------------------------------------------------- - # Blank the arena only. Do NOT disconnect the bridge here — the run-log export - # runs after the sequence, and needs the bridge still connected. - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/fictrac_coupling_bias_fly_test.yaml b/protocols/fictrac_coupling_bias_fly_test.yaml deleted file mode 100644 index 21ce8efb..00000000 --- a/protocols/fictrac_coupling_bias_fly_test.yaml +++ /dev/null @@ -1,493 +0,0 @@ -version: 3 - -# ============================================================================ -# Closed-loop COUPLING × BIAS — a first test on a real fly (web runner, Mode 3) -# -# Needs Arena Studio v0.85+ and bridge 3.3+ (`pixi run bridge` prints the version). -# Pattern: the built-in 200-frame full-azimuth rotation on the G6 2×10 card: -# 4 frame2_h_ccw_200f (1 frame = 1 azimuth pixel = 1.8°, the pitch the rig supplies) -# -# COUPLING SIGN. On the fly-on-ball rigs the NATURAL, stabilizing closed loop is -# coupling **−1** (it is exactly what the old `gain: -1.8` did: same frame index for -# the same turn). `k_nat` below is that value; every other coupling is expressed -# relative to it, so flipping ONE number adapts the whole protocol to a +1 rig: -# k_nat -1 natural closed loop (the display counter-rotates the fly's turn 1:1) -# k_under -0.75 the world turns LESS than the ball -# k_over -1.25 the world turns MORE than the ball -# k_rev +1 reversed — the sanity check; the fly should NOT be able to stabilize -# -# BIAS SIGN (bridge 3.3): the disturbance is defined in DISPLAY degrees and is NOT -# scaled by the coupling — a positive bias_amplitude turns the display CLOCKWISE from -# above (pattern sweeps rightward across the fly's view) on every rig, including the -# −1 rigs. (Under the old `gain: -1.8` it was counter-clockwise — do not carry that over.) -# -# BLOCKS (20 s closed-loop trials, 5 s dark ITI, 2 repetitions each, randomized): -# A. coupling only: k_nat · k_under · k_over · k_rev -# B. bias at k_nat: constant ±30 °/s (one revolution per 12 s) -# sine 30 °/s @ 0.25 Hz (position ±19°) · sine 60 °/s @ 0.5 Hz (±19°) -# square 30 °/s @ 0.25 Hz (position ±30°) -# C. bias × coupling: k_under + constant 30 · k_over + constant 30 -# k_under + sine 30 @ 0.25 · k_over + sine 30 @ 0.25 -# coupling 0 + constant 30 — OPEN-LOOP REPLAY: the display -# ignores the fly and just drifts; the control for block B -# plus a k_nat baseline at the start and the end. ≈ 13 min total. -# -# WHAT TO LOOK FOR. Bridge 3.3 keeps the heading unwrapped, so the fractional -# couplings must show NO once-per-revolution jump (the old `gain` bug). The report -# (`pixi run python scripts/closed-loop-report.py --svg cl.svg`) gives, -# per trial, the fly's turning, the feature's frontal fraction, the reconstructed bias -# and a REJECTION index (1 = fully counter-turned the bias, 0 = ignored it). Expect a -# high frontal fraction at k_nat and none at k_rev; rejection > 0 on block B; ≈ 0 -# rejection and a steady 30 °/s drift on the coupling-0 control; block C shows how a -# weaker / stronger loop changes the rejection. -# -# Dry run without a fly: `pixi run sim -- --model fly --kp 2 --frame-dir -1` for a −1 -# rig (docs/development/model-fly-bench.md). Web-only: MATLAB has no FicTracPlugin. -# ============================================================================ - -experiment_info: - name: "Closed-loop coupling × bias — fly test" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - -variables: - # ── durations (drive trialParams duration AND the closed-loop wait together) ── - cl_dur: &cl_dur 20 # seconds per closed-loop trial - iti_dur: &iti_dur 5 # dark inter-trial interval, s - # ── couplings — flip the signs of all four for a +1 rig ───────────────────── - k_nat: &k_nat -1 # natural closed loop on the fly-on-ball rigs (= the old gain -1.8) - k_under: &k_under -0.75 # the world turns 0.75× the ball - k_over: &k_over -1.25 # the world turns 1.25× the ball - k_rev: &k_rev 1 # reversed — the fly cannot stabilize - # ── disturbances (peak velocity deg/s, frequency Hz) — gentle on purpose ───── - amp_slow: &_slow 30 # 30 deg/s: one revolution per 12 s - amp_fast: &_fast 60 # 60 deg/s at twice the frequency = the same ±19° excursion - f_slow: &f_slow 0.25 # Hz — one back-and-forth every 4 s - f_fast: &f_fast 0.5 # Hz - -experiment: - - "cl_baseline" - - name: "A_coupling" - trials: - - "cl_nat" - - "cl_under" - - "cl_over" - - "cl_rev" - repetitions: 2 - randomize: true - intertrial: "dark_iti" - - name: "B_bias_at_natural" - trials: - - "cl_bias_const_pos" - - "cl_bias_const_neg" - - "cl_bias_sine_slow" - - "cl_bias_sine_fast" - - "cl_bias_square" - repetitions: 2 - randomize: true - intertrial: "dark_iti" - - name: "C_bias_x_coupling" - trials: - - "cl_under_bias_const" - - "cl_over_bias_const" - - "cl_under_bias_sine" - - "cl_over_bias_sine" - - "cl_k0_bias_const" - repetitions: 2 - randomize: true - intertrial: "dark_iti" - - "cl_baseline" - - "shutdown" - -conditions: - # NOTE: no explicit fictrac connect/disconnect — the runner auto-connects the bridge - # at run start and the run log is exported from the bridge AFTER the sequence, so a - # protocol-level disconnect would lose the log. - - # ---- baseline: natural closed loop, no disturbance ------------------------- - - name: "cl_baseline" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- A. coupling sweep ------------------------------------------------------- - - name: "cl_nat" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_under" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_under - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_over" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_over - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_rev" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_rev - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- B. bias at the natural coupling ----------------------------------------- - - name: "cl_bias_const_pos" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - bias_type: "constant" - bias_amplitude: *amp_slow # clockwise from above - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_const_neg" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - bias_type: "constant" - bias_amplitude: -30 # counter-clockwise (negate the amplitude, never the frequency) - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_sine_slow" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - bias_type: "sine" - bias_amplitude: *amp_slow - bias_frequency: *f_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_sine_fast" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - bias_type: "sine" - bias_amplitude: *amp_fast - bias_frequency: *f_fast - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_bias_square" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_nat - bias_type: "square" - bias_amplitude: *amp_slow - bias_frequency: *f_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- C. bias × coupling ------------------------------------------------------ - - name: "cl_under_bias_const" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_under - bias_type: "constant" - bias_amplitude: *amp_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_over_bias_const" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_over - bias_type: "constant" - bias_amplitude: *amp_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_under_bias_sine" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_under - bias_type: "sine" - bias_amplitude: *amp_slow - bias_frequency: *f_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "cl_over_bias_sine" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: *k_over - bias_type: "sine" - bias_amplitude: *amp_slow - bias_frequency: *f_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # The control: coupling 0 = the display ignores the fly and simply drifts at 30 °/s. - - name: "cl_k0_bias_const" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: 0 - bias_type: "constant" - bias_amplitude: *amp_slow - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- dark ITI ------------------------------------------------------------------ - - name: "dark_iti" - commands: - - type: "controller" - command_name: "allOff" - - type: "wait" - duration: *iti_dur - - # ---- shutdown ------------------------------------------------------------------ - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/fictrac_direction_test.yaml b/protocols/fictrac_direction_test.yaml deleted file mode 100644 index e4e3f194..00000000 --- a/protocols/fictrac_direction_test.yaml +++ /dev/null @@ -1,271 +0,0 @@ -version: 3 - -# ============================================================================ -# FicTrac closed-loop + open-loop direction/speed test (web runner). -# -# Uses ONLY the built-in g6_2x10 patterns already on the SD card — no new -# patterns needed: -# 2 grating_sq · 3 grating_sine · 4 frame2_h_ccw_200f -# -# BLOCK 1 — OPEN LOOP (Mode 2), ~4 min: -# both gratings (sq, sine) × 3 speeds (5/15/30 Hz) × 2 directions -# (forward = +frame_rate, reverse = −frame_rate) = 12 trials, repeated 2×. -# Each trial displays for `open_dur` seconds. Reverse uses NEGATIVE frame_rate -# (int16, fw #4). -# -# BLOCK 2 — CLOSED LOOP (Mode 3 + FicTrac), ~1.5 min: -# frame2_h_ccw_200f (a 200-frame full-azimuth rotation) host-stepped by FicTrac -# heading through the bridge; gain 1.8 = 360/200 (one azimuth pixel per index), -# `cl_dur` seconds per trial, repeated 3×. -# -# DURATION knobs (`variables:` below): change ONE number to rescale a block. -# open_dur → every open-loop trial · cl_dur → the closed-loop trial. -# -# TIMING — no redundant waits. The web runner holds each trial for its -# trialParams `duration` on the HOST (the firmware auto-stop, issue #4, isn't -# landed), so a plain display trial needs NO `wait` — the open-loop trials are -# just a trialParams, and `open_dur` is their single duration knob. The -# closed-loop trial DOES need a wait: it brackets the closed-loop hold -# (startClosedLoop → wait → stopClosedLoop), so `cl_dur` drives both its -# trialParams duration and that wait. -# -# Total ≈ 5.5 min at defaults. Needs the FicTrac bridge (`pixi run bridge`); -# with no fly, `pixi run sim` supplies synthetic heading for the closed-loop -# block. NOTE (web-first): MATLAB has no FicTracPlugin (its closed-loop is -# analog Mode 4), so the closed-loop block runs on the WEB runner only. -# ============================================================================ - -experiment_info: - name: "FicTrac closed-loop + open-loop direction/speed test" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - -variables: - # ── Speed knobs ──────────────────────────────────────────────────────────── - # Change the number here (or in the Edit view's Variables panel) to rescale a - # whole block. Open-loop trials have a single duration field (no wait), so - # open_dur is unambiguous. The closed-loop trial binds BOTH its trialParams - # duration and its bracketing wait to cl_dur, so they move together. - open_dur: &open_dur 10 # Block 1 open-loop grating trials (10 s each) - cl_dur: &cl_dur 30 # Block 2 closed-loop rotation trials (30 s each) - -experiment: - - name: "open_loop_gratings" - trials: - - "sq_fwd_05" - - "sq_fwd_15" - - "sq_fwd_30" - - "sq_rev_05" - - "sq_rev_15" - - "sq_rev_30" - - "sine_fwd_05" - - "sine_fwd_15" - - "sine_fwd_30" - - "sine_rev_05" - - "sine_rev_15" - - "sine_rev_30" - repetitions: 2 - - name: "closed_loop_ball" - trials: - - "closed_loop_rotation" - repetitions: 3 - - "shutdown" - -conditions: - # NOTE: no explicit fictrac connect/disconnect. The runner auto-connects the - # bridge at run start, and — crucially — must NOT disconnect it at the end: - # the recorded-run log is exported from the bridge AFTER the sequence finishes, - # so a protocol-level disconnect would close the bridge before the log commits - # (that broke the log on the first bench run). Let the run own the lifecycle. - - # ---- BLOCK 1: open-loop drifting gratings (Mode 2) ------------------------- - # frame_rate is the frame-advance rate in Hz; its SIGN is the drift direction - # (+ forward, − reverse). No `wait`: the runner holds the trial for `duration` - # (= open_dur) host-side, so each open-loop trial is a single trialParams with - # one duration knob. - - # grating_sq (pattern 2) — forward - - name: "sq_fwd_05" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 5 - gain: 0 - - name: "sq_fwd_15" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 15 - gain: 0 - - name: "sq_fwd_30" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 30 - gain: 0 - # grating_sq (pattern 2) — reverse (negative frame_rate) - - name: "sq_rev_05" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -5 - gain: 0 - - name: "sq_rev_15" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -15 - gain: 0 - - name: "sq_rev_30" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sq" - pattern_ID: 2 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -30 - gain: 0 - - # grating_sine (pattern 3) — forward - - name: "sine_fwd_05" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 5 - gain: 0 - - name: "sine_fwd_15" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 15 - gain: 0 - - name: "sine_fwd_30" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: 30 - gain: 0 - # grating_sine (pattern 3) — reverse (negative frame_rate) - - name: "sine_rev_05" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -5 - gain: 0 - - name: "sine_rev_15" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -15 - gain: 0 - - name: "sine_rev_30" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "grating_sine" - pattern_ID: 3 - duration: *open_dur - mode: 2 - frame_index: 0 - frame_rate: -30 - gain: 0 - - # ---- BLOCK 2: closed-loop rotation (Mode 3 + FicTrac) ---------------------- - # A 200-frame azimuthal rotation loaded static in Mode 3; FicTrac heading then - # drives the frame index for `cl_dur` s (host streams SET_FRAME_POSITION via - # the bridge). gain 1.8 = 360°/200 frames, so the arena rotates with the fly. - # The `wait` is load-bearing here — it holds closed-loop ACTIVE for cl_dur - # before stopClosedLoop fires (drop it and the stop would fire immediately). - - name: "closed_loop_rotation" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *cl_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - - type: "wait" - duration: *cl_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- shutdown -------------------------------------------------------------- - # Blank the arena only. Do NOT disconnect the bridge here — the run-log export - # runs after the sequence, and needs the bridge still connected. - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_blue_gs16.pat b/protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_blue_gs16.pat deleted file mode 100644 index 8de7ef748bb8c4eafd9110fe10340bdc0a3f8ac8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81338 zcmeI5zltSA6vk^uUEN*R@n0YqDEI;!85>9$m`G`0Dk6x17#ax91E}Cz_yoR$_y!^- zVk8R&rfYTGuKQ;w;nH87+oy})B;B`nPW3t8?>j}ES$5~Ww?Fvk&39f6SNE@;Jh=`b z{1HoQ9hKiA}(y~yI zWv@WF>1<96wwJfFvTVjh*^vzP`FAlr9hfEeE#v2Ave_VCl;RO2RG?z200uLacj4lN zildXEaH^z~g9rQ&g$h&-3Y5FP=EPtJdHX6G*o-G>M>5#=4`X^JFiRd<#?4)^nVW2+ zL?}g)&nOrwM#4(VLPe6#D0nm~eh!8lDvM={!JtyK@FN3M4*Mxo49tnaFxWu>3xoaq zWlVPi6aFogJ$x+9?;#OA2TIX3?7sq3`fIAM2o+t!{woYe#m{+N81Z*hD5dz_jSNsZ z+JB(Z-<%i>gPHy+n2`+j;3}qP0~7vH%Xqon+008eQXZ5d$!8P{6(eD#WuYR;XB0df z6*rSd2DUht9hJr5%By?FQp!>1x*-Y`s4z>Ow?We@E_>zCCow%2nDCF_c)8tmjF)Vr zJSaty&nOrwM#4(VLPe6#D0n<7Zleh%5mpHrKr(U1`LM5t}TCB zfx%oF?6)6cx)+%6kKs7EpF74$Hc}dtBFSeI3>70`rDdTa$!8Qi9+jTc!zB6|>&;Zw zcy;fRDdo6xy%2>8R2YlT+uZ3Dm%Z}W-!a_}O!&ueoZQbH<0Km?4N8&ZGYW=^k+9OT zP?6*_3LcP(mnmu@wlJ4UWnp;nij!DMIq6)e^fo63!(h3qPDEI|@wUu~!A`XGvV*<# zO-v606aEPtA0KMR_{c`ef>I>;jDn$JB&@V7R3!O~f(NAHEJELk@)N7rmKN7+cJP>Ljy@yY7!u2MN7@U#8+c#dGtclxn+k6}!auK?N$NyCep~V73PP`1P2s z1G8lHnWJlNen;6zsZfd}pHVPWjD(ezg^DDfQSew)E-@Tk;vJdFC0=uhcTh_0+)Jo^ z?mM6Y725%f9c*^kxh;8#?;EM@Km9tU7X!0o-N+nWv&|f3Bc(zql6*$NP%#o#S{5pj zd`7`zQK^U4`Lt9t+v2lKWs7OIa;B8pxh-|pdIlA!%AjU`Td`mUJA^T)n|^b*=CNiky4=)Nj{@ss2B+=EejP%KBM5VsI*QsTYQ$OY%%Rt z&XiI+x24Wn&!7SoCj9#Ex2hM~Kj@%Wsy=J&R;~Bm{P|-{F9&AH3dhkkcZ{QKq*N$H zlFuj@Dn`Oe%R)tx&nS2-D)}fdpV|&`ZSh&AvcIm$*#g;FH>jDn$JB&@V7R3!O~g2$rL nI@N6PS*EhZv|Bk-O6}a1I%_?H3RIZz&--yOz2dT0-u>x6@4JGi diff --git a/protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_green_gs16.pat b/protocols/g6_2p_color_led_sweep_patterns/G6_2x10_toprow_grat20_green_gs16.pat deleted file mode 100644 index 38c9d004060046cabc5816498b71d7cbecfcc6db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81338 zcmeI4J&znk5Qf{ww{0+A0T7XqAwb9>azc}$mVX5qA|-G}K!iw$kjRLL36O|LNst`# z1F&$0L}Jg(?OspM?vAEw@13n}Js0ftt-89a-skPPu3O8W-TnOjyZ7D<7dJ1SJh>A> z_&cT$ZhX*%nHu8Je1Po8>!U~PfipnP>~uDgJCeO!9Khj(?y`e?^Sm8RUUq4$gIztg zXU|znXXWefV|pgg;a`#WGbnB5Coe-LlwuG~9s(7)#D)!^Vh~Lp(k_+RtIkMQkMkx} zJf=8OaJGA+6p!?v0+rzet7O1nDrJ{Gc=yd~iOLWE#q?~T!yj{YGM?DXNmiybD8(e3 zI0!0YflbRp#Uz_Js9h=^Z&+iPbNNlE%uQXoEN&b-d#2 zhw0;qE3QW=P`R|8$aEMCgJH1I(|zWgwRBeg`YxvD10DW|yqDqBW?u3#+81tfjN^FR8jUUH{&sj@n z<+opBdMVK1kI6e3@7T;qUWPO%#UPqI1S)cg4I4nkAeua+X)1@WI;F1e=Iv5(mtv{l z;ZO4@rBlisJ*Yrsdx9nA#9$aqYq0yj$8TQb<& z@5FQ~(BV(md-!qJW*)LK{!ykO+>!mpSzEZ2rhEi%3TVp;{ z)=;^252(~Cw#NKcsp#&mnTl?Yf{Jb%N*O*~Un&Ds2Br98g-Qktrc!q45#KitDlVz~ z^L|Ws0v&#~tea7BV%%h9N`z8OvWbJBA{N-RJXB1wiGy0DvTZ{)?eU=E`Mg(9IiIw% zS98vkvi+AM`_wtrdr*N&=e>MPuiJDAyU@B#gdD20}C6zBe zis`jLho7zOW>lOQH(8kyp%jyB;vlGq1vV`Y6_ae@pjN4LZSm$_k_9}U_X;ZKllDHT zDaE4^p#qiewCWx$7z~4H4ffS9F})t>@bfQSH>2Y4yUEIw2&I^069+*>EU;;LsF-9E z2enE?cX!P|bbAz3blXr$ufXMBf(9+-rG^S!OYfKN>(VX5>jL8}orl3x$}T0aZyr=! zQu*XoOm74_{A^h_qvFK4$;y-nrI=(B2SG(FuxWXym}C59*0;&b9dGqio$pkuYgh6<`{2bVPd<7#+`GPad;2hi@MlaR zT)m&~hWNUn6o(w4;t)!J3RFxLz+k5GE?hiQ`Fr}h6TZb~lu{vjq6ZbIR0Yblw>dFb zmAALD%6d?7N#(C6FO3Ok;lFul3Fe=JP&Qa_{ z#Zfv+E|jwGTt`Hq0+qc2<)*VaG1y+-&dRbG7iDk0_%Nn>fmw3zGe0*|^AGYxDIP&W z1uCWrU@%j87cM?gadetea-o#H2OJTF3RLzAl$*}x#9(`QJ1fg(T$CNjU{Ak^>7~Fd zxo;UiH%xe?qe3ag?`~v(%F+GMBwKZ+g6V2^%|>6O5Qf7CKw zZg)2Gl8uxHrAYD_1w+M1SZP_PNb(s44@bq#9t0-*BRF1ecOBy;8z~P;k>oQ9hKiA}(y~yIfy_~aEYv6OP$xlk!;^ppXEVKCD_k&3}w8tmVzm|hJ`_{VUZ+|M23BpWFWN|EF< z3Wkc2u+p+nk>oQ99*;`T>0uImjrC?KYrML5$&_;3xn77u1uBfi=WXuvipyU4@o`MA z1t$DsI8N^8j&YKWlm?|p@)-p~#Yk9bS*S?z83hkW#mf{m5nGr`rLr)*c*RL9rJQsw zRC=2egJH1TRVN}W-gsN)#9$}ddfCCQuVcCnO!y~oe0-=K<0Bg>3rdmXGYW=^k+9OT zP?6*_3LcP(lOsq0wlJ4UWnsAT>fW)Ga?-g@h(ZM_*@`D3Fc!~p-04wz<-{g;)G7bA z!n2!a-^KKLV8TCv{FkDw2Fg!DCUGe|LX+cHJMR4ib9*zD&h?is$A> zDb;r8Dt3wQg9=nkcS#I}!E6oo{P&pN3e1w#XO6D9`5k2=r9vr^d`7`gF%ni<7Alf_ zM!{oIxx{dEiFafwmw3%3-a#p~b1$Lxx$l4qRBQ(@cCguD=eFb}zHg+qzyDQC4+FDg z-N+nWv&|f3Bc(zql6*$NP%#o#S{5pjd`7`zQK^U4`Lt9t+v2lKWs7OIa;B8pxh-|p zdIlA!%AjU`QhW39tCE}>N7{zY%@pMNU2bYB%e_* zRE&g`mW7HWpHc8wR9dH+Ek4UswwQJ+XG*D^+frw(XHbC(6aMZe9RK%&4tm99uiQR} z>2Y9|tZ*D%bH_N!MoNWJB>9Yjp<*Pgv@BF4`HX_cqLK%u`P6ohYm3h^l`W>-%9&EC z6orxN`+D+ z`HX_0VkE4zEL0@70`rDdTa$!8Qi7L{pWI{GD4(QJ#)GL1;^oRL661{o61K#0hA3M2w@ zL<)i3-8-|@)icxTy1S?6PU+iVwsusv>fZC6>aDwD`OAl2Kl3)6qia!DILEo)-XCfQK&$rv!c$37z~4H4fgE2m@Wbx{^WOyhaZc>?;#<34wPco zP8SIkowoLsp<>rg7uhTo-Q_jIknK@Yk!_PJ(cSearN=i?x*wjLC{&;#H6jMXU|NIy z{wStb0v&#@va_%9@H;i}(zN@ijVhQTn{)nj}1oV9dT{{K9tR|6gX6?s2{(q?}0GGsz22GQgpP?1Y) z*Z?X9(c~fRQklK#jCA!lZ$iamiX#PQyEjVlNDnGd8BVZD1`MWBcIkun(7cwYeDH5f zuLU~%F=r>^iOrm3WlDomOtOiCpduF7v^-QyvWbJ*rQ-31HHJBt--OEC)TK+FY96I{ zqz4tK3{M1=3>ZwM>~$XGEm8U8hnQXubogUGI2liD<|Hdq8kAy^O&kOjvB0L~p<-5U4<9Yo0pej2zbukH=!LIdzt*c+>M*qVo7j zOm_kue(4bxKkeDfMOLOHD8(e3I0!0YflbRp#Uz_JsAVe0@1Lcp{^spb@t5LK2`PoE z{?;oR6Jsz8hQY><?{Hjyx>TcdX6?Z9?3LgJ7k5W3N+|h#yRCXs=VonT(!L$Z@@?%VI20Hwm%8tJ8 zAAU#4)uloyb$YEc8!GGQT(=8U>hxM?cGFZOch?CQ$sYSuB->C*m8?+d3{#~T2CI?_ zgJH1Uk*PAyE@=DxXlzRc`{uux-U@X16ZRf{+_#yBtV}sjib*zc5LCnho0f-)Nj7m% zt5i0FZv9m!3wS>76;#eA?L$&iO0SAHBSHl#y)AncB<93mDrE;`!%IEAmZ*IETTJ%? z9e(zMn^9piH(8kyp%jyB;vlGq1vV`Y6_ae@pjN5W-+r>tr;jbDoIaiFNJA;TdUW`M zhZHhkFbvk8Fbp>MU{;2f&dP^>#`Jce!ykbAdMRw?E4A8eD5X}hHReNQ4V7#6fJ&`m zYs_zz%J#GS@-zIaTVPh!=e&Z7&lHzRNGXF-&Ws2ZsB{YG9xWIQgJ})+;M7w(aqt;`zK+P&uEpvsZJ@l(PGmBZt&E)q7BZO6R?NOmEtV7z~4H4fg#nF})k; z@bk%aGb#?ho2*QUP>M-5aS&9*0-KhHib*zcP^(mQch?L=w?{!mw+*Fq3M~H{5LD<| z2E(Xiz+ft6uk)mXic2cL{t?stK!=~L?PgS*7&lp&5}_26Y~moOhy^w+4;7Pa;-FTk zbZznWUXleopZ5wX=acp!sVT*y5upN=?zHM2Ef@@gX$^MotC-#kbolv~uA5PD_}ye> zN`z8OvWbJBA{N-RJXB1wiGy0DqPx3hAi6yYD!OebrB~qcFF}JA^HM{FuBG=&_jT!( Z;dOy=md?XqDrJ`vI5ZC`E~z~H^d;0m diff --git a/protocols/g6_2p_color_led_sweep_patterns/README.txt b/protocols/g6_2p_color_led_sweep_patterns/README.txt deleted file mode 100644 index 9d7971a2..00000000 --- a/protocols/g6_2p_color_led_sweep_patterns/README.txt +++ /dev/null @@ -1,15 +0,0 @@ -Patterns for protocols/g6_2p_color_led_sweep.yaml (G6 2x10 four-color panels, GS16 — lit pixels 15, 20 frames each; -built by scripts/make-color-optomotor-patterns.js --gs16 — regenerate rather than edit). GS16 so the controller -re-streams at 300 Hz (its GS16 default), as in the 22 Sept 2026 2P duty-sweep runs; the GS2 set would default to 1000 Hz. -Upload the whole folder from Arena Studio (Console → Patterns → Add ▾ → this folder); the Studio resolves -patterns by NAME, so the pattern_IDs in the YAML only matter as the fallback when the SD listing is unavailable. - -Each file: square-wave grating, period 20 px = 36°, 10 px on / 10 px off, one pixel of shift toward increasing -column index per frame (20 frames = one period; playback wraps), TOP panel row only (pixel rows 20..39), and only -the pixels of ONE LED color bank lit (2×2-cell bank map of js/panel-color.js, layout "four-color"). -Frame_rate +80 in mode 2 = 4 Hz temporal frequency (80 px/s ÷ 20 px); negative reverses. - -G6_2x10_toprow_grat20_violet_gs16.pat — bank 0 = violet 405 nm; frames 20, lit px/frame 500, stray px 0, 81338 bytes OK -G6_2x10_toprow_grat20_blue_gs16.pat — bank 1 = blue 470 nm; frames 20, lit px/frame 500, stray px 0, 81338 bytes OK -G6_2x10_toprow_grat20_green_gs16.pat — bank 2 = green 525 nm; frames 20, lit px/frame 500, stray px 0, 81338 bytes OK -G6_2x10_toprow_grat20_yellow_gs16.pat — bank 3 = yellow-orange 590 nm; frames 20, lit px/frame 500, stray px 0, 81338 bytes OK diff --git a/protocols/g6_2x10_2p_diagnostics.yaml b/protocols/g6_2x10_2p_diagnostics.yaml deleted file mode 100644 index 0809e7f4..00000000 --- a/protocols/g6_2x10_2p_diagnostics.yaml +++ /dev/null @@ -1,226 +0,0 @@ -version: 3 - -# ============================================================================ -# 2P line-sync DIAGNOSTICS — G6 2x10 arena on the Bergamo (Sept 2026). ≈ 52 s. -# -# Companion to g6_2x10_2p_duty_sweep.yaml. Same wiring and ScanImage setup -# (panel mode 2, DIO 1 = out_debug_framescan → Aux trigger 1 on a free D2.x, -# DIO 2 = in_trigger ← line clock; see the sweep file's header). The SWEEP is -# re-run unchanged for the hardware-only controls (objective capped, arena -# covered, SiPM bias series). THIS file varies the pattern CONTENT at a fixed -# duty, which the sweep cannot do: -# -# The residual offset in the SiPM channels is expected to follow the TOTAL -# light the display emits. Full-field GS16 patterns make that a clean test: -# ramp_128 grey level 0→15 in 16 steps of 0.5 s (ascending) → offset vs emitted light, one trial -# steps_128 the same 16 levels in a fixed pseudo-random order → same, immune to slow drift -# flash_128 full field 0/15 square wave at 1 Hz → the stimulus-locked offset step and its -# build-up/decay kinetics, 8 repeats to average -# sparse_128 10 % of the LEDs at full, static → is 10 % of LEDs at full the same as all LEDs -# at ~10 % (ramp level 1–2)? per-LED vs total light -# flash_255 the 1 Hz flash at duty 255 → does the duty-255 anomaly (Ch3) follow lit -# fraction / on-time; followed by 6 s all-off -# so the suspected slow tail can be followed -# to zero with margin -# -# Patterns: protocols/g6_2x10_2p_diagnostics_patterns/ (built by -# scripts/make-2p-diagnostic-patterns.js; README.txt lists the per-frame mean -# level of each). Upload the folder once from Arena Studio: Console → Patterns → -# Add ▾ → From course repo… → this protocol's folder. The Studio resolves patterns -# by NAME; the pattern_ID numbers below are only the fallback (alphabetical SD -# index after the four existing patterns) and the run-view preflight will flag -# a mismatch. Keep the four existing patterns on the card. -# -# Epochs in the TIFF header (auxTrigger0 blocks separated by the allOff gaps), in -# order: ramp_128, steps_128, flash_128, sparse_128, flash_255, then 6 s off. -# Within ramp/steps, level k occupies [t0 + 0.5 k, t0 + 0.5 (k+1)) s of the block -# (frame_rate 2). Within flash, frames alternate 0.5 s dark / 0.5 s full. -# Analysis: check_aux_triggers.py prints the blocks; compare the core mean per -# level with the display-off frames of the same file (see quick_checks_review.py -# for the local-reference and frame-block-SE approach). -# -# ALIGNMENT of the level changes inside a block. Three marks are available: -# 1. The aux-trigger block start (first J3 pulse of the block) + k × 0.5 s. -# The controller steps frames on its own clock (frame_rate 2), so this is -# good to a few ms (host start jitter) — enough for 0.5 s levels. -# 2. Ch3 itself (ungated, red-channel detector): it sees the LED light of every -# row flash, so each level change is a step in the Ch3 row mean at the exact -# line where the new frame was applied (126 µs resolution). Use this for the -# flash kinetics — it is the cleanest mark and needs no extra wiring. -# 3. The setAnalogOut markers below (0.5–2.5 V per condition, arena J27). Only -# recorded if the vDAQ Data Recorder logs an AI (docs/development/ -# 2p-scanimage-signal-logging.md § 2c, in the private 2P rig repo); host-timed like (1), so they add a -# cross-check, not precision. A frame-locked AO ("frame_number" role) is not -# validated on this controller yet — do not rely on it. -# ============================================================================ - -experiment_info: - name: "2P line-sync diagnostics: brightness steps, flash, sparse (G6 2x10)" - author: "mreiser" - date_created: "2026-09-23" - -rig: "./configs/rigs/bergamo_g6_2x10_2p.yaml" - -# The runner must implement the controller: block below (a runner that does not — MATLAB -# today — refuses to run this file instead of running it in the wrong panel mode). -requires: [controller_block] - -# Sticky controller settings asserted by Arena Studio before EVERY run and recorded in the -# run header (docs/development/controller-settings-strategy.md). Replaces the manual -# "set panel mode 2 by hand" step: the Studio reads the mode, sets it if needed, reads it -# back, and refuses to run if it cannot. -controller: - panel_mode: triggered # 2 — LEDs only inside the line-clock LOW gap - refresh_policy: line_sync_safe # cap the 300 Hz re-stream at the rig's limits.max_refresh_hz - panel_firmware: "2p" # SD footer "2p-[-d]" = the eintlow_2p line-sync build - -plugins: [] - -variables: - - &ramp_pattern "G6_2x10_ff_ramp16" # 16 frames, full field, level = frame index - - &ramp_id 6 - - &steps_pattern "G6_2x10_ff_steps_pr16" # 16 frames, full field, levels in pseudo-random order - - &steps_id 7 - - &flash_pattern "G6_2x10_ff_flash_0_15" # 2 frames: full field dark / full - - &flash_id 5 - - &sparse_pattern "G6_2x10_sparse10" # 1 frame: 10 % of LEDs at 15 - - &sparse_id 8 - - &level_hz 2 # frames/s: 0.5 s per grey level; 16 levels → 8 s; flash → 1 Hz - - &pre_dur 3 - - &ramp_dur 8 # = 16 levels / level_hz: exactly one pass - - &flash_dur 8 # 8 flashes at 1 Hz - - &sparse_dur 5 - - &flash255_dur 6 - - &iti_dur 2 # allOff between steps: pauses the J3 pulses → epoch marker - - &long_off 6 # allOff after the last step: slow-tail test - - &duty_50 128 - - &duty_100 255 # diagnostics only; experiments stay ≤ ~190 - -experiment: - - "pre_blank" - - name: "diagnostics" - trials: ["ramp_128", "steps_128", "flash_128", "sparse_128", "flash_255"] - repetitions: 1 - intertrial: "iti" - - "post_blank" - -conditions: - - name: "pre_blank" - commands: - - type: "controller" - command_name: "allOff" - - type: "controller" - command_name: "setAnalogOut" - mv: 0 - - type: "wait" - duration: *pre_dur - - - name: "ramp_128" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 500 - - type: "controller" - command_name: "trialParams" - pattern: *ramp_pattern - pattern_ID: *ramp_id - duration: *ramp_dur - mode: 2 - frame_index: 0 - frame_rate: *level_hz - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *ramp_dur - - - name: "steps_128" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 1000 - - type: "controller" - command_name: "trialParams" - pattern: *steps_pattern - pattern_ID: *steps_id - duration: *ramp_dur - mode: 2 - frame_index: 0 - frame_rate: *level_hz - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *ramp_dur - - - name: "flash_128" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 1500 - - type: "controller" - command_name: "trialParams" - pattern: *flash_pattern - pattern_ID: *flash_id - duration: *flash_dur - mode: 2 - frame_index: 0 - frame_rate: *level_hz - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *flash_dur - - # static: frame_rate 0 holds frame 0 (the sparse frame) - - name: "sparse_128" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 2000 - - type: "controller" - command_name: "trialParams" - pattern: *sparse_pattern - pattern_ID: *sparse_id - duration: *sparse_dur - mode: 2 - frame_index: 0 - frame_rate: 0 - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *sparse_dur - - - name: "flash_255" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 2500 - - type: "controller" - command_name: "trialParams" - pattern: *flash_pattern - pattern_ID: *flash_id - duration: *flash255_dur - mode: 2 - frame_index: 0 - frame_rate: *level_hz - gain: 0 - duty: *duty_100 - - type: "wait" - duration: *flash255_dur - - # Between steps: display dark AND controller refresh stopped (J3 pulses pause). - - name: "iti" - commands: - - type: "controller" - command_name: "allOff" - - type: "wait" - duration: *iti_dur - - # The long all-off after flash_255: recovery / slow-tail measurement. End with allOff, not stopDisplay. - - name: "post_blank" - commands: - - type: "controller" - command_name: "allOff" - - type: "controller" - command_name: "setAnalogOut" - mv: 0 - - type: "wait" - duration: *long_off diff --git a/protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_flash_0_15.pat b/protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_flash_0_15.pat deleted file mode 100644 index 986eb79da3d26382a383a57946f04407ba3c409a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8150 zcmeH_u?c`c5Crc(G!Z+n0x>c+(p1EPY|xH`*BTZUo@Xw)WEXDc_T{+7HAn7pDSbqA z+ZmDOe`{iHPN~o%Rw|SdSiy>w01sxxE0$wL1ZRz5#fn!f$BGEf8pDbeuUL*15u7!4 z57Tz0NONgq{x@snlENu9n#2lLtOR&4D_*f2Doj5`NIJ0$NIoDI zHa3!9h=1U9zG1_L;jpHfW^dSQooA2tUOard6^rF!z21z7_pL{iI({kM5mP4gh?NPY z1Xi$OCBT7M@rvbG5y4qwSh3<2%dsMYv&OJu#VeL$MFeM!9jzX3oWE*)5v6V?dBaVT zCEHAy(Ii%|VkN+VS@DYHSP{WlV_31`70a@sCWlUTuul>i53#VeL$MFeM!Va1A9EXRrn&Kkpt6|Y#16%m{@mdVPy z()x`kbw9})Zjvn7XUdEwv4RyV0S?THS1iYh2+kVAiWRR|jujD{HHH-{Ua=f2A~=>l;z(VUjo8Bw2FElo?H81uIqp9GDfaSdJADoHd3OD_*f2Dqx1-dPByYG$vgCv*Gn&K-R;&a#Fe_fM94jI?YYZz^yka?4 zL~zy^R;+l%a;%8ptg%d1mQPw=MyaPs-f)v-$thE2G>H|gSP5`oR=i?4Rzz^t7*?!! z#d55O;H)vMSn-PGSP{WlW0|ac_}luODD^DK8*Y*;Ib+IIaWk)))-c-c*Sz8h~TWTOjh1(wSGTJy-Mzh&PI>{Suk}O#>Wk!=& s!HSgt2WG`9mSaT(XN_USidQVhiU`gc!-^HJSdJADoHdro%FmDg0aY4>pa1{> diff --git a/protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_steps_pr16.pat b/protocols/g6_2x10_2p_diagnostics_patterns/G6_2x10_ff_steps_pr16.pat deleted file mode 100644 index 0f332059943a6a98e3e527f37c4a64c3582795c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65074 zcmeI5J!=&}6h!YnqsI6VgN3a;uwMxXS_KI!Sjl2U!NMwqwGzve5p3*CZ##jZat#ZlO*q$DHEE+%7ju* zu!0pU0S?THS1iYh2+kVAiWRR|jujD{HHH-{Ua=f2A~IaWk)))-c-c*Sz8h~TWTOjcfe zZ2fAKx|`$;H%XT4GG#`SSiy>w00(BpE0$wL1ZRz5#fn!f$BGEf8pDbeuUL*15u7!a z$;zJ}tzU~$capr}CdrZ=rp#y(D_F4-;J~bS#d55O;H)vMSn-PGSP{WlV_31`70aMv{F|5J4L2!vj1f$k(Ii%|VkN+VS@DYHSP{WlV_31`70a|q$O=1NrRstND6|Y#16%m{@h7~Jbu^cNR zIBN_mR=i?4Rzz^tSSBlv-nD)sO5IQLhMOcy_L(xHNvvSSN`M2i;uXuWB7(EVuwun4 zmSaT(XN_USidQVhiU`gc%Vg!r@7A}X)Mb)4+$34DWXg;tv4RyV0S?THS1iYh2+kVA ziWRR|jujD{HHH-{Ua=f2A~27N&yu|1CdraBrp#y(D_F4-;J~bS#d55O z;H)vMSn-PGSP{WlV_31`70a3-22!1ohbDp$s2BxEV*FHj3%*y z6)OP_%!*em$BGEf8pDbeuUL*15u7!K6)Rq`94jI?Yb=wM<ku=yzb1``* z_#xD4+%*%gEXA|u;OGIq)reRsitWrwsPtGYasJ<9e9sv9(nwS=J*RivH1s(;ZDs}2 zIf-MPSW2r<&mVGK6P?p2&6&EmIE?Oa!=e!PkRb}&4BHPUy;yGL!)W$DV*ozk;Abh+ zEGO0cKbCVAgIxMLv8)rdz!L}W;#>oP7E$l5uXyMd0j6dig_rA=&stzH(hOg)%g0F1 zYz!$P;Z|^TKwc7=STViB3&L84L|2+jNb)SuG;L|j7MzGRjG{ms_#o?42$rMnU#ryX zrhypTe6+bln(q1l_@c|xrYOm0jav%1Jx@jFnM_i-byVUh=TzvE_+AU{hvfD_Nkq&J zd3{hMq6|=|H;nmkr6#wa^1Qp<99N>eGRJ{hvI z4B3)Lk}40C;KqQP1w&MW=xGF_;iZbpsXZlb#2|^SRHw4inrrorqk*OJ@BhukpS6X! z5~!QJb-!ZmtxT!2uiO2Z=Viqg9J9;ohO)DqbC}>ZhkDtVEn8j zgqxa5%y!1%z*6g_tmQ2Q#ACLAiqq&y>9uupV((mOR)bQ>3|20N==3@Vq?j{hY7psG zh7TOepT-|9GmG`IJHQ>JTClQ~u(_sq_DzbG?szUV9i{Ya+9&jGT6;3s!cWVF7=#%9 zoWvd|VN0->xk{asd%$>UG+AzUBko71=WUqByprkf%OwKS50i>un!Wc?(vZuR*o)5! zafYNw8rgThUbhsTYQ0n?Lx&J0uu>@$ony)[-d]" = the eintlow_2p line-sync build - -plugins: [] - -variables: - - &pattern_name "frame2_h_ccw_200f" - - &pattern_id 4 - - &rotate_hz 20 # frames/s; 200-frame pattern → one revolution per 10 s - - &blank_dur 4 - - &step_dur 7 - - &iti_dur 2 # allOff between steps: stops the 300 Hz re-stream → gap in the J3 aux-trigger record - - &duty_10 25 - - &duty_25 64 - - &duty_50 128 - - &duty_75 191 - - &duty_100 255 # ~15 µs row on the 2P firmware: tight-margin case; overruns on 9014b5b - -experiment: - - "pre_blank" - - name: "duty_sweep" - trials: ["duty_10", "duty_25", "duty_50", "duty_75", "duty_100", "static_50"] - repetitions: 1 - intertrial: "iti" - - "post_blank" - -conditions: - # Display dark, AO marker 0 V. Baseline for the SiPM channels. - - name: "pre_blank" - commands: - - type: "controller" - command_name: "allOff" - - type: "controller" - command_name: "setAnalogOut" - mv: 0 - - type: "wait" - duration: *blank_dur - - - name: "duty_10" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 500 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: *rotate_hz - gain: 0 - duty: *duty_10 - - type: "wait" - duration: *step_dur - - - name: "duty_25" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 1000 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: *rotate_hz - gain: 0 - duty: *duty_25 - - type: "wait" - duration: *step_dur - - - name: "duty_50" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 1500 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: *rotate_hz - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *step_dur - - - name: "duty_75" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 2000 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: *rotate_hz - gain: 0 - duty: *duty_75 - - type: "wait" - duration: *step_dur - - - name: "duty_100" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 2500 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: *rotate_hz - gain: 0 - duty: *duty_100 - - type: "wait" - duration: *step_dur - - # Same pattern and duty as duty_50 but frame_rate 0 (static frame). The - # controller still re-streams at the refresh rate, so any difference from - # duty_50 isolates pattern-content change from re-streaming. - - name: "static_50" - commands: - - type: "controller" - command_name: "setAnalogOut" - mv: 3000 - - type: "controller" - command_name: "trialParams" - pattern: *pattern_name - pattern_ID: *pattern_id - duration: *step_dur - mode: 2 - frame_index: 0 - frame_rate: 0 - gain: 0 - duty: *duty_50 - - type: "wait" - duration: *step_dur - - # Between steps: display dark AND controller refresh stopped, so the J3 - # frame-transfer pulses pause — the epoch boundary marker in the TIFF header. - - name: "iti" - commands: - - type: "controller" - command_name: "allOff" - - type: "wait" - duration: *iti_dur - - - name: "post_blank" - commands: - - type: "controller" - command_name: "allOff" - - type: "controller" - command_name: "setAnalogOut" - mv: 0 - - type: "wait" - duration: *blank_dur diff --git a/protocols/index.json b/protocols/index.json index 0de3b79e..aa85e4c6 100644 --- a/protocols/index.json +++ b/protocols/index.json @@ -13,26 +13,6 @@ "label": "FicTrac closed-loop (Mode 3)", "path": "./protocols/fictrac_closed_loop.yaml" }, - { - "key": "fictrac_direction_test", - "label": "FicTrac closed-loop + open-loop direction/speed test (~5.5 min)", - "path": "./protocols/fictrac_direction_test.yaml" - }, - { - "key": "fictrac_bias_test", - "label": "FicTrac closed-loop bias/disturbance test (~4 min)", - "path": "./protocols/fictrac_bias_test.yaml" - }, - { - "key": "led_activation_quadrant_test", - "label": "Conditional LED activation — graded zones test (Mode-3 closed loop, ~2 min)", - "path": "./protocols/led_activation_quadrant_test.yaml" - }, - { - "key": "fictrac_coupling_bias_fly_test", - "label": "Closed-loop coupling × bias — fly test (Mode 3, natural coupling −1, ~13 min)", - "path": "./protocols/fictrac_coupling_bias_fly_test.yaml" - }, { "key": "opto_intensity_runtime_test", "label": "Opto intensity as a runtime variable (Mode 2, ledDrive; ~2 min)", @@ -43,31 +23,6 @@ "label": "Full experiment (blocks, plugins, ITI)", "path": "./protocols/full_experiment.yaml" }, - { - "key": "g6_2x10_2p_duty_sweep", - "label": "2P line-sync duty sweep — G6 2×10 on the Bergamo (~62 s, 2 s allOff between steps)", - "path": "./protocols/g6_2x10_2p_duty_sweep.yaml" - }, - { - "key": "g6_2x10_2p_diagnostics", - "label": "2P line-sync diagnostics — full-field brightness ramp/steps, 1 Hz flash, 10 % sparse, 6 s all-off (~52 s; upload its _patterns folder first)", - "path": "./protocols/g6_2x10_2p_diagnostics.yaml" - }, - { - "key": "bias_camera_closed_loop_poc", - "label": "BIAS camera + FicTrac closed loop through the ROS 2 bridge (LAB-303 spike, ~40 s)", - "path": "./protocols/bias_camera_closed_loop_poc.yaml" - }, - { - "key": "bias_camera_closed_loop_poc_ww18", - "label": "BIAS camera + FicTrac closed loop, ROS 2 bridge — ww18 variant (site 2x10 grating_sq, slot 2)", - "path": "./protocols/bias_camera_closed_loop_poc_ww18.yaml" - }, - { - "key": "bias_camera_closed_loop_poc_ww18_soak10", - "label": "BIAS + FicTrac closed loop, ROS 2 bridge - ww18 10-min soak (2 x 300 s, grating_sq slot 2)", - "path": "./protocols/bias_camera_closed_loop_poc_ww18_soak10.yaml" - }, { "key": "g6_color_optomotor_test", "label": "Color optomotor test — one LED color at a time, top row, 25/50/100 % duty (G6 four-color, BIAS, ~18 min)", @@ -77,11 +32,6 @@ "key": "g6_color_optomotor_smoke", "label": "Color optomotor SMOKE — 10 s per pattern, duty 100 %, BIAS check (~2 min)", "path": "./protocols/g6_color_optomotor_smoke.yaml" - }, - { - "key": "g6_2p_color_led_sweep", - "label": "2P color LED-intensity sweep — one ScanImage grab per color, 6 duty levels × 3 s at 20 fps (G6 four-color, ScanImage JSON connector, ~2.5 min)", - "path": "./protocols/g6_2p_color_led_sweep.yaml" } ] } diff --git a/protocols/led_activation_quadrant_test.yaml b/protocols/led_activation_quadrant_test.yaml deleted file mode 100644 index b19fc5b2..00000000 --- a/protocols/led_activation_quadrant_test.yaml +++ /dev/null @@ -1,212 +0,0 @@ -version: 3 - -# ============================================================================ -# Conditional LED activation — graded zones test (web runner, Mode-3 closed loop) -# -# Tests the per-trial `led_activation` on trialParams (Arena Studio v0.86: graded -# ZONES with linear ramp edges over a baseline; v0.59's hard-edged `on_ranges` -# still works as sugar). Reuses the SAME built-in SD pattern the closed-loop -# block uses — NO new patterns needed: -# 4 frame2_h_ccw_200f (a 200-frame full-azimuth rotation) -# -# The 200 frames map 1:1 onto azimuth (1.8°/frame = 360°/200 px), so the pattern -# divides into four 50-frame QUADRANTS: -# Q1 frames 0– 49 ( 0– 90°) -# Q2 frames 50– 99 ( 90–180°) -# Q3 frames 100–149 (180–270°) -# Q4 frames 150–199 (270–360°) -# In closed loop the displayed frame index tracks the fly's heading, so gating -# the LED on quadrant zones drives it as the fly rotates through them. The -# index the LED follows is the DISPLAYED one (after any bias), i.e. where the -# stimulus actually is on the arena. -# -# HOW IT DRIVES THE LED: `startClosedLoop` enables the bridge's frame apply — -# that same event stream is what feeds led_activation, so the LED gating only -# runs INSIDE the startClosedLoop → wait → stopClosedLoop bracket. (This is also -# the bench check for "is closed-loop apply active?" — if the arena rotates with -# the fly, the LED gating is receiving frames too.) -# -# FOUR conditions, each 30 s (`hold_dur`): -# 1. led_one_quadrant — hard edges: LED on in Q2 only (on_ranges sugar) -# 2. led_opposite — hard edges: LED on in Q1 + Q3 (two bands; 4 flips / rev) -# 3. led_ramped — one zone, 10-frame (18°) ramps in/out around Q2 -# 4. led_baseline_probe — dim baseline everywhere + a brighter ramped probe zone -# that WRAPS through frame 0 (Q4 → Q1) -# -# Needs the FicTrac bridge (`pixi run bridge`); with no fly, `pixi run sim` -# supplies synthetic heading (`--model fly` for a fixating one). Requires fw #33+ -# (duty/AO) and a BuckPuck LED on Analog Out. Web-only — MATLAB does not read -# led_activation. Bench check: photodiode on the LED — the ramps must be -# monotone with no flicker, and the `cf` rows' max req_age_us must not grow -# while a ramp is active (the LED writes yield to frame writes). -# ============================================================================ - -experiment_info: - name: "Conditional LED activation — graded zones test" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - -variables: - # Change ONE number to rescale. hold_dur drives each trial's trialParams - # duration AND its bracketing wait (they must agree — see the waits rule). - hold_dur: &hold_dur 30 # seconds of closed-loop hold per condition - led_pct: &led_pct 20 # LED brightness % inside a zone (BuckPuck curve; fractional OK) - base_pct: &base_pct 2 # dim baseline % for the baseline+probe condition - # Ramp endpoints are literal frame indices below (10-frame ramps = 18° at - # 1.8°/frame); bind them to anchors from the Studio's 🔗 if you sweep them. - -experiment: - - "led_one_quadrant" - - "led_opposite" - - "led_ramped" - - "led_baseline_probe" - - "shutdown" - -conditions: - # ---- 1) single quadrant, hard edges (legacy on_ranges sugar) --------------- - # LED on across Q2 (frames 50–99), off elsewhere. As the fly's heading sweeps - # through Q2 the LED switches on entering frame 50 and off leaving frame 99. - # `on_ranges: [[50, 99]]` ≡ `zones: [{level, ramp_in: [50, 50], ramp_out: [100, 100]}]`. - - name: "led_one_quadrant" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *hold_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - led_activation: - level: *led_pct - on_ranges: - - [50, 99] - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: -1 - - type: "wait" - duration: *hold_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- 2) two opposite quadrants, hard edges --------------------------------- - # LED on across Q1 (0–49) and Q3 (100–149) — four transitions per revolution, - # exercising the multi-range on_ranges list. - - name: "led_opposite" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *hold_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - led_activation: - level: *led_pct - on_ranges: - - [0, 49] - - [100, 149] - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: -1 - - type: "wait" - duration: *hold_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- 3) one zone with linear ramps ----------------------------------------- - # Dark at frame 40, full `led_pct` by frame 50, held through 99, back to dark - # at frame 110. A fly dithering on a boundary now just modulates the LED a - # little instead of chattering it (this is what hysteresis used to be for). - - name: "led_ramped" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *hold_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - led_activation: - baseline: 0 - zones: - - level: *led_pct - ramp_in: [40, 50] # baseline at 40 → level at 50 - ramp_out: [100, 110] # level at 100 → baseline at 110 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: -1 - - type: "wait" - duration: *hold_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- 4) dim baseline + brighter probe zone that wraps through frame 0 ------ - # The LED sits at `base_pct` everywhere and ramps up to `led_pct` in a zone - # centred on frame 0 (Q4 → Q1): ramp in 170→180, hold 180..19 (through the - # 199/0 seam), ramp out 20→30. Checks the wrap unrolling and that ramps start - # from the BASELINE, not from dark. - - name: "led_baseline_probe" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "frame2_h_ccw_200f" - pattern_ID: 4 - duration: *hold_dur - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - led_activation: - baseline: *base_pct - zones: - - level: *led_pct - ramp_in: [170, 180] - ramp_out: [20, 30] - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - params: - coupling: -1 - - type: "wait" - duration: *hold_dur - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - # ---- shutdown ------------------------------------------------------------- - # Blank the arena (also forces the LED off via the runner's activator - # teardown on stopDisplay). Do NOT disconnect the bridge — the run-log export - # runs after the sequence and needs it connected. - - name: "shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" - - type: "controller" - command_name: "allOff" diff --git a/protocols/led_activation_quadrant_test_patterns/001_all_on.pat b/protocols/led_activation_quadrant_test_patterns/001_all_on.pat deleted file mode 100644 index ed8f96e945c2bc17b8fc1df8413298967591f66d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1084 zcmZ=a3kXqgU}Rw8V*LM~pMilP*e!^Gp;6#J$)I6W6C}(=0|J_MMgw9v2ZYBT00Nu; AWdHyG diff --git a/protocols/led_activation_quadrant_test_patterns/002_grating_sq.pat b/protocols/led_activation_quadrant_test_patterns/002_grating_sq.pat deleted file mode 100644 index 65ca95b67fb663b30777d7fb7fd412121eacb370..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21338 zcmeI4yGmqX5QP63@8f003m7T}g88Csv_@^9pfs9_f)C&u%*-PQrapuSCJJjJKEMWI zwxOAZ3gWR^8)ghUlctgroeIuJ{RMx~eKuBib{E&@O1)95s%9zWpJ*xFR&~>SE1^e+ zqAy?s;m#q5%pgveMQfH6orRZL=wZH~2-FC|okI|rL0s>N)-5T@pXY(l!+b#zs1bxa zhagge_$xYqqf;_apby9r3U)I#{}`U`c^S17AjwH5CNm z9T!1l261vOI)tPMmK10x;0ubZ+93$9s|X@9h|S-k!$^u?Nr8p}zM#me9fI(>iXbwB zxcDVHf}{wR6eu0w3yQ4TAqc;46GUbZ>kZLSBt>|fqJl3dvLO_L@If4c$P8kBMRW{F z5uS{&147Z~5DG!~U=2Ye22pMM zhK}&vsgxkjRz;_f6tSHTTlfGDiU^`_riPC2-Kmry4j)CENQ&6bhwX3x2So(YH&a7L zcyE^y#MOal3rP|EJ4JN^92D8+3qg2YMG%=mY`=<5BPpVPr>Hvc1x0pCCkUS+Ac)K$ zuJ1%=kQC9sQ&a`MpvZ3N1YvCrLG(^TN=K|+iq0Y_!jlnpKq#_XIzd=lLlC{wkkS!L F#~(t7xw`-W diff --git a/protocols/led_activation_quadrant_test_patterns/003_grating_sine.pat b/protocols/led_activation_quadrant_test_patterns/003_grating_sine.pat deleted file mode 100644 index 9f5b4fdc69d2cc24b0190c882aa9430a4fe7fb09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81338 zcmeI5J&P6F1LoAh<$~9g?%M22Ush4aniNK zAyi3JzIeB8?uA&%PRjYlw*nja-sJ4Ux589>v(V9#N`F3PS=B`|242?9g9a`Bs>U zZx%XwQt2+h;MR!7WPU{|q=GYvb}mT=>n@9##&|kCQXv)7PQ<>#zGCWmbTB#?9V{;g z+RuNinWJOe&;e+KJd#*jG$Fj}Ar$ql4w;K>N$rb@NofT2f3Y_*SXA$kcbfRq`=X zN!Mcm8@WZ02i&RJjcSY=6k`C5?gKrt* z>GVj2RB*RLyYs#k`wIIC`$}F8uvYTov~Q2P`*|f%dHu_}c_ze4_BKNFt-wb1f_JyR z6{g~wg^r$7`tN&}Rb8Y)D!40R=aO`=?ufo=jHlBh6;i?74(-nSR_rV6E2f@D2cv`0 z!SZsT{rOSdJR4#qJ1KGHTcP-6hyM23x589>v(V9#O1gueuFPso=2xUbDmd9`=aO`= zbO%5E;D%j{r$3ogNX4`hv9GYNn0g)^j1EQz%gcfGt9R<=eu$OqIHm)yy zD@?^V3mrYF3=8~4RTrs{3Qnupxg;HISl}DRcse~&Ar;*1(C)l%#lFJ6!oHH11FV(2 zIPKe`?tWfLRDOH4ZXSeK$*$x2#J2()+1t3>`c{~VZx%XwQjynIYfR=>q(Ukoo5^Q=_F7sa$S}QJl{Fou9?LXhv&GR8vvXgRl;aefiWQYEI<6B`W zzFFw#Nkv{;tudKjkqW8cw3?kumg!)pW5j%_GO1YRv0a7elSw80UPbzG8jZ>Pid0Ai z_n_=Pt0BSW&+`3@pv+qF+2e)i>8AbUySjNH#7e%+y14SKkY=*$xHmn_o3PREEjRYg)U&11U?(I=D2u)sI;BaL>hA{A1>Jt(`+ zYDlo$TwMC!6z*Z@+oJZXO1#CFR5J&bLYxBc>F5t5h+hf+SrOM^cg3CoieGNQG2z zGR@8}rLUx>+53@O#adyl9Hdsd->_OzmzX{l)(UHdwUUq(Uk z(I=CNyrNoTGQT1fQZZcx>xpq63EX~1&riqAoR;5uH7WjsFY*!)rWKvloM^Ygb zoOrhTtcC&SvWC>!J$95H>PbQV_h`y;GX|!_{sgMdzJllO% ULxSbz;?n=Fuv#gl+yDOQe|H4c_W%F@ diff --git a/protocols/led_activation_quadrant_test_patterns/004_frame2_h_ccw_200f.pat b/protocols/led_activation_quadrant_test_patterns/004_frame2_h_ccw_200f.pat deleted file mode 100644 index 8e0b0fa9690ee402dffbf8cb14eb71ea5be9a858..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 813218 zcmeI5eYAI5RoB-wd5M=EF~EpIEEKs)T|hv=zz`OgX0SjqGF}wTcooX52-tx^LLqRk zB8j2|2q-2bh$2Y{CWCO)7-~zzpSsS0@`ixtB#`*b`l+yEZN$HyFYE=K6*FV0a>6+{2 zO+@&EtyrW8Dxe~&01PH7FLUu&RDAdNQGF~bOlz8(1}f%|)Pg_-R74ek!9?X{Ek0T@hFUgqM{=f5$RKa}bXe>@m>9l>Ty zCx}i16;XMai-QVNIMHdKA}TL)@nBS3_xSPOD#55Qy=iS4s8}O1*8?h`BB}rkCMqv; z@#(9dn9FCT`J2~cJl2QJm`)I#1}dWRG8YFGrf{OuKt)tu=Hk()pxZNzAT=$T!K8H3 zT^Cez^*)vjRF3JSI|@{E^*)wuy5Z)z+$_yE{1}h*VY6f4bw`1UuHMJ8fyyzRbVq@T zuHMJ8MWcdle~i1@sAvXL6HYV{sEEqTTpU!G!ii1;6;XMai%$=IZ7w%Y^9?`7OMKXj z=>*Yfpduk%9Np}>e=<0ne+w||Ak;^U9a>Ebt5+61@23~g*sOai_EE}jC(@A#}sOai_ zEL%J(5nkfQqjJoRwnu@AMLwVcDxwO&V50Ie7oR@(;avW3T5tI4WBqw0zfrZ>m9B;F z2q&h4G!%e}AsD?BQ0Y<_3P8mWj9$t{#cz+Fcd}Mrx8l3UkLqz&xvK*bP@UJ9smDGUXm zVhBbrrBQL)55|2*I5A99i%tU-QF)n*g9=kP(P^L}Dlc>KM#XK9U-rFJU$^4B$B*h8 z6(&?m)36mwG+Hg7(v-0D2`ZLov|2(@c}p(0N!ty7d5OR7a(+baF{*4@;qz|?XvjQ9 zp=>t)vW5_UK*b_PPyrQD1tJ>EYmZ++&(&8d9y|L*HAgg<=!sNW98^?fCME$DqF^e+ zK}AJoVv^~Z56y&vDk?G)lYk0QFqPq;q9QXf zNvGnq$FKXHtFK$}-Q!2~oeGhr!Qt47K`7^-0xF^kz+j^CG8dm-^sZd~NNP9y`6d3g zlGCW#%xUGnPj{Rcrm01zfr_ZS%*8>4DV*puP!W}vx%i;szEjq%xZlG@aZq7mwKEM{ zvBP3i0xClbI}xbZVKFL+MCAqd%H>MxH~jiweA}nfsIsX&fBQ~+-%%u+b%TvlruT*^Z_)&dGgNYtVjm1GlLuG0XP@yWOF&NR& z?wadMeBTjHOf#URPf)Q$qtyZ`O$ke%pkj$et7S*UQ)h4G?`y7G@!jJ`^*btUr(Qp? z6^rCR1yn>8fWbuNWiCEF>J_4FpduyD+0J0yUp@m+vB(WnKt)u6payf<<2&fHO2v1NAJqpnSZC!k z02PbeKm}Ap6^LZ8U%7KGw@;xiU`y9r9?koXBH1jI!9>5fC@v0zDT;kK6{sARN^u~l zD2jbJRi)y)$B*h^u*1|92ZD;C*oRYr%3-M#2ZD;C*oRY1xA<5tcSxZwU}sC)oL}Pm zj&NcQ+gNcRs3?kkI2EWImP&CTs3?kkI8`hvzI*(rJ{Fb3W;8w!R4f7l6;KgXAdu&*=|I8L}9YwNPEQ5)DbmEO< zhry0x*Bc2cdRiaP1S-e1(i;gXdRiaPG~MGhxqMEFbpaa>#$87^F~@zZHxg9zv_75* zRE}$d^!7R_MtUt|bER|j9pUhIkcI+KF$ANR z0xDe!LjkB5g3(LasQB&i^G??4>sEaC_)$I1N;jyX08|XY=%s*4m%>mGiOL(ElFMCE zxEucZ5`Xc`@_^rCRBe{J>wQO&PE5SB(oNBIy)E6rMdKVdM7>_7!nL>1@; z^V;KAb>`~pRy=n0i)!vvMA@q>j;*N3OiThQM8Q;sgNlmG#3a)%zaW>-O95{!^MmnK z46ji=F{wWo_Z{Kz6VIi}a8OZ^nV1Arh=QpM2Ne~WiAg#YuRVUD(?5Nxi_T2G)|^y4p0%5m$^8oFohGH7KzIJKb6a$P7!Y{ z^}+b&0;f^6S?&4TcjEhwBAu9sXGQeOrN-hgn1;&K9H2r~Ok;dVDo%U+#=BNusrc^k zqxz5r(>R%;IY32JUgqMU!W2$)S|loO_^e#+mO|cI`VHUdH?dcn)vmccrtlp_Ix!*7 zis;u%g~eeo1(C@qK!q%s!i1PqTBG#(BWo%Z-#va*AJbq8CsQ;9sEEqTTpU!G!ii3c zMCG~f%H{J@$Xm-NR&?wadMeBV)|6BF{RwDaNh69%(L4pcxzRDqqr zJazV_$-d^g72iF6RKKIrcIx#LTd_zER6s>kfk+1X%E#sM1u5pOrQhq%T}IVrbJtvJ z_Z>w#F)`1I=od`+#bGdcjp-Rcg|3xQyzFS?=}AOMKrE4u7|zp#W42!RV!cN|(Y=04j!H z^iuXzT=w|Ble4-N-#va*zo*hS=rh1pEOG-CP!UxilEFUlj9k7j#k}E*nm~T>YO|$l zE-xp1N0Ckp&WgPC$r)fUvSbPqKt(}hatcr(OQtZPQt{p6NA)n6f?vofK!q%s!URxJ z5Sg4J5|z7uUM}}YxV4Z?AU~nn>}+Y9^Gkf+QKS=tvm$SOat0WTESbUtP*D(>oB~wH zk||86RDAdNQ9TT%;1_ZVP$5gEFacB)L?)+*MCE07%;nmITMO9)@)Ks8jR)hdqev$P zXGPxnD6gj);Q1o9IWo5gsnAL+#4tjJrRoB;+SOQtXZ zR1`!ervMeQWC{}&D(LnKN>ftctWea_8Vf2~N>lQH3PmlgvD4drJC}PV+*-5*C*)a9 zOpM3+IQ#_eYC}OqO=?0SP$7t=HWXCUq$VVaMg`rT0AFKhG=phqCZ7XTD|}m@@Ern_*#M!@~kH&#AAINegb#3p`fBBH6anG z5X4d&3My(+6B31^f^JWsuP`*6!4x!;PXQ|O8q+g?3SBe#8Pj(?IG5`ZZus(2lQV25 zCcm2kt?5}c4{J2C5H{du0>sBYy--?{GyU&lH~LjkB5g3(I>l`e&$08|XY z=%s8_{Py^HCu{X}E53XDs2*pf8`Mw$Du!V6Qb46kVJMiM{_DBiJK=_Jh)FA@otWjZ z{_>Y@qq>#Uwsr11YE=4m{07*HMRuS9DxwO&V50Ie7jIPD_V{JrOZ9atzI*(rzENR9 zwKNS|u|%WQ0xC@jOP`UbJm#Uf+$Z6NZ^>U+(vaPsXrL^9W^Qj4MQyn zTcIkZF&6>!-l7t(+u+E(2`-zz!jIRrLjp|m~7S7LoN1X}>sjWHKiY*>%HJ}12q6)xZqVh5q z?^L|@_;ufN^>r)0d;F-rQ(;myG!0uZ1f!P%DqRXgK_n{w@rQG{Z^8}VkiA+d-DdNH z@%1mgMs+KxKN$BNbt(;$-%o7CB0Eq46;TCXFj0A#i+3tsd;GfZx%#>l-#va*->ERE z8k&Z!7=qDD0hKO=p&$~K{IFc^mvF;3WUp4ruvx$1*Z1G;d&FEtbt|sw-4I+<^Pti= zx&6deETRJyP!UxC1{0N+x%i-RQP*+xcXcbS>fMN4RP&(1#A;_6wql3Hs037o7IvaY zRG#ymT<)K6!?)wEUdgaoeK5ZLt<$J(rS{M5JMn!-gGvYGHNaLZk^>b`5mf*N6P1^_ z_@Lsn$8YZ7@QX~GTPj=Op#JDc?z zzSD1FuUn~I)A_+_wyohQs#}?R{`wN% zceJC@NO=9kRxFYO6;KgX00tA4m$~?kil@%rcDt{+ZpC+xAJy-uw4HkW#8xbl0~Jsa zRbcw5Pv-J~gd4u7%Dlz*hri$Jef~E2x|O+WuDkFZ?Wyp%nwf>Im_e~C1XOlnm&n4XOMOEf49_z%MEp2mtiSIj#MTO@~d$T~r z9uwT~EiwTWP!U$3elhml<45)AE5cr3Ee|Rz*@P#7imq;=0F= ze`XkrinNK9)&~_Op-E{#g(R2K;OY7QJ(sUYxZx}5O-U2&#PkQ_;}N;TC>j-tBkhg_ z6^m>@1yn>8fWbuNWiGx@LAPhBCp8UcMM~LI)&-SQ)RUSUiOO3alFOe@xZz8A<%D%3 z*=#mgjK})XsGML>LsLP;5R6_5sB|d|1)yRGMlU5A6?FTK)?t0fhbq~uR!IhBe5OtWB{GPk!-dZYMRZ_Iy%#`M56Ma9+}I-6K?o42dIpWWV6j+%Y*SnJhxG$ zlKO*j-%&t=sT@q%B%mUzFK=~F;prwj3shwF<*nYRxb5-FwwLPbR($vPQGKJrV`^#^ zwqgp$Rt%_^q;j(lJP?!M-dIiJ8G6@fr=#>trk#eN?7^?6-zW)EuD(j9=~p6 zuD))?caI;{cPb5&-%o7CB0EqqN#$c7%;hg6-0%&;33+1r4ZptsZr>y3Dymdm)w>z{ zqMAb*%-}($l>{m@y>v!{ijK~-ETBTuOK0?;a#7cD^>=kEuIk+YUsUs;qT>R=EZB;m zxa_4tg)N=n#OWiq%H^vQZuo*4bC(WzV(NqOO%JD0rBeIn_MP~?qmTyUo;EY{K*bD- zT_K>d6T?ghDrQjZ3K>+K_V{ftwfedh-#va*Kd9_b`3$iYi`+oPB$bzaQ7&JTaKkqU zC+3OiH+-kx#9pb?uIc=T39N39X)uE)nN$*}kmOPt3@S=OlhS|+NiL?>Vq1rv+@~$ibZab zsNDL&xjZuME?^b~C)5;)4X6;nYYnXiTe{}*a>93nv!Z1hN*++5sHHU)RJ4?)8fWbuNWiAdXOyNYQ%~V|X_)$F!Ch8RC;-JD5 zPIOu%D)0RDTppcbUBE=)>n$G1X0Z%59*nz=U@*PId^{7V9M?*3B&g_VeLNGW9M?*3 zWH2hOd;IukhQX-l*;zCbsEEqTTs#t$d%iK3$D~*nFeX-O(<0d{n89K^)`!8Y9r?K) zPyrQD1z<2yd6|oY3R5`IX$uu}dr^()i{q@&HIttKD)JiBGfXf3fn2^W1-pRJb+9-C zC&nz57?1T~FtY};SQxz~7>ur&{0vZ$*O;CGROp(?&seCS+sg}0%7C*%l1phYs3-|d zN;AFWF}XZ8;f7CAO=mDpjE>Ha`Y;#`z0PP*(b1Wf1ypEy>5K*y9i3@e;!#1jr#V1)bUcFz3#PI>sHn(H zOfuc$8*=$82{(MAZW_aJVl-5y=GdsLkM-xtextgTD}DREBb=2E(og^@hG6tkK&4Az zC;$~hFnTE)6~8@x-pN{h-HPuXKdQ%B=>|0vfQlg)y_6}xKbOZP-0-_1b^@FjJ1j;e zjmq*^fB9>-QQb=F8}l7CDt$YC18l`2J5T`?Q3YTyQF)n*H!5y>{Ic(*`nnb0J$_W* zs4$^gnue`dqS0!JMCF~==JHn)ZupI@q0dM*i)XOq!T919w^21%>JP?!M>s2nzHs$Y zK&4AzC;$~hFnTGV(xosIG%9X;{G#Pjecg)h9zUvYR16t~TMD+qRZenVBr0G0@?5?l z#k+t>@>g0rlFb4dY<@7lI>BpH4c4}Be(pQMSyB3;L`nlHB)OCZgNl;Sq%@#Hl1piD zr{cB8uR6`u*RA;O@uT`qMacz%X|NSRaoI~pqVgs0$mOr4fEO?}T{9CS*({*J<_F`e zSG-2mV5vVC_Z{J^nEA4`D+E+_Vwed*#SDsFA)vAo!%WzzcG% z;}dov*a}-Z!HJQm{K^w@d3*|Z0Tbk}v~(n!MKoBy;n(-y?R&&rMb%)g>fO%WMK$BB zD1A{Pr2!R^TuOsMMM-E<8c-q0r8Ia@xv1;7`n$RnSM_d$FRFP^QL=$t8f-;QTmH(C zsNDXkxjZ37ynylf8krf%W)TfmAB=BKaT-;F)&9ACC%*3pXT``Dts9jADmRkD$o~Kp zBOq>62B_Rf4kQ0#P;uJhHyvyBbt}Gm{HT6VF=7*b8Q2P6JGq&WsNCcCa(QBkcmb2+ zue5R`n}sx3zu`OmCiZHu+BKd3FoE?j;;blrQ6i-Q6_Q*^gF!_}Xi^$bA<3mQct@o* zO0Pe%rfvn@UdcAc)9eg(e3H$P*os9i)2}@{mnWr=7ch%@Q)pL2X(YUUVk;KOfeNUIDgc9t%FA4QN5xZTZ@b;sT({!8$B*iF zRN79xeqt*Y$wi{_sE_9IjR`k=gFcI7vzP|!_xf{}Q8n1yHP_mGM>s16zkr>C3aE%G z0E3Cj%UnDt6_-7}Bi#O#KDvEZ#6%GEteDW2UqmD-Z+cBG-;`or!1#ns%#38SmNH&AP2;jAbR)Z~Fb9p)8JHlDfG7TjUs8H0> z8Vf2~N>lQH3Pmlgv9YN5?(w7gSX8vUES(2bq!s3@Ki%-|T)sKs0>%+fcn(gCu)wV4 zV^KL<+UEQc-**&?3d@)VCxMDVDEQt9i%>uXRAd#XUyObC_)$IjimX+5tAh$pH`!U! z1MZv4wnIo%VGnC94=P$pQ}TccMJ=tdprWNTB~LIa zu6um_jYadHiZ2)yiXjb;1r>{Mrq_Qcm%pBH!#4;AC&nO@Xx(Kz7?+vkrcy?-84Pw)w6DVdzZxvWV||>J ztKgrTObIGCDbZCJ04i5O^qWixDmN+7RTvPC3cCGOaM2%-GMvHwfW7+pzamljKfja9 zQxh&=2Hh9QW-yppgN1mkkF#RXU(tSMg++-OHQ}rfOB1~n8!qVkKk z%jFpf7cfKiYNbT784T8_8v1MoTOR8#b-9h|R_0%nyZ-SVH7ad4jz6&#i|{}NR74ek z!9?X{F5alP?eWXLm+I?QeE0ZKeWSvJYH1p_Vu?npWqRFPa(QOL1*{2T=o2T#5R6_* zKq||F@nwT<92jjk@fClS_$8P{C7TJLcsE8^M&|q$R`~rHZzEbhs<45%Y4JLXZ zbrz3A<=cNQmv2kB;p;eulqHhQU@#JTox$B;^MmmB3$j=Op# zk!%Kob*y%RZm{{m_*$0NsBR_o2jjk@PQ{KZ~1!$}c@AmuDwjz;v8L$`Z+DFc=BF&fsCNe#5WtzuWhS zxr*vmT-Cb)zNqFwMaKn#S+EsBaoJ0Q3R^nCiJ&4VE_>;aR4(c|uKuo4aaHeTzl&-P zX)yM2vobLfl?Q!6E`KxO0%pZqyE-D-3D(}BOm%o*8 z0n>2~DN7`q!C)lxI)irx>o1HFjpj-!C(iWYYvNLu(PFY&M)zON3p1A`dKm; zs7R{IT^m%m%1O=x6-jluYsaGEyT^~}V^QIH)7rd9RPOlPT>f6d1T=fx6|QoU^MX-v-Q&l9s|2GWX<)^*BT;$$ zCv$mD!UasxK1WkUvKb6^l)B=$U@v)RJh7X&I1)mb-8N? zqvE>9kAG$uj0)GB#^yz$@`V4K%imA9fElycTuvmL!C)|$qyo!XiSbw;$6nGX+_gc4 ztDNLKP?1!ZyEdqBm6MzojS9NGq+1wkM>7~h0)tbgH$5Vk=O$dhEV2O=P?1z1+Lz=Q zkM*Nbk#w-$+MuGR_3=!ga$GCDk)Wcd_3=#6sG!>)XRk6cn!!{=6HhWd<*~UuFX1jJ zt~5OXP7GZ$`5B-huQ5HtN+rZ&eHe_6Uw#It$ZJf`04j9Nn`#Zp^)`oz<7d49qTUrq7jiEv_$Yo#|5RP?kyo@u2L;;}vqb{xCjNKnz!`gkT# zIj)u7NKnz!`go>rRM72@!`B!Y&R`m%iRYN^{*hd!gd4uN()0v4F?7x3XMl>l#`Fvu zl?adZVK6#=`5B-huQ5FXsL(Z&p8+cJ8q+hxqk?WvcY)xHcm@*`OKs`tm)@SsKS;RY zt0{gw5l+l;t@K8Mik{ZTGi_AX$NKX$zfs-FmA-M`5za~nX(#{{Loj+Npwgu<6o85$ z7`>E@ir*eT?_{mMZpC+xAJyZmbb}fSrl($$%L@{2_=cFYQgC9L5|%ze#S)EHOQW(p z)?fb8ZB)0C+P2PpM~zC~j^6-VvB(ZoKt)sm7)(@N=HiWt+aAB{d#S!|#dnV%)i)|k zsFtQh+RA+%p34gpZupk`wbc^IW-u5GCaFL>E6aoNC6U{xZYA{xl zlipAn+HNjvsy>v!{ijK~-ED@=A?eQzQx%x`Q zcaI;{M>H7ih*=yx-SL0t@(&Yk_y*zN#2AEv8@@#-gUbA1e67%HwDkP;5$6=(bt}ugetC)SJK9s}P<#g1ibZaq0xF^kz+j^C zG8f-daoOYhzR&7beE0ZK{hmtSpwA%ER$g;`F8?^;hHub`k!%Ko!C;aK#5yZW*IXXW z`;KB!k@Tj(XAhxaohsI+uTvZ~-$2 z3Ma-O6#Q#Si%^15Ia}K1{1V@Hgs)&04-2S(il_oGn5ewW#X*HBoanSl#dnV%)x%(- zPGK%SefWl4UY2mfXX+<84JSrYUGCbT!c|UkUN9=-!MN)v7!^qaE3OSHieev51uBQ7 zQXB{>ieev56^x4O9$$ZB(fp^f3r6Lz8*LAqo_vQ~zB}QDZx9nsj6o>)I%W|{G%Dl4 zxa$aC!7LsYPyrQD1z<2yd6|oY3R5`IX)_hqJ$_UVgNZtYx%hOa>vDN{!VRCPpX4;0 z7)f=xYl8|`Imvm^sPqTp;}N;TC>j+>2kWg3DtcNU&jc#RwbC02DtcNU&lHUcy8UtX zDkCEq?8|SL%PSIY_$tPsC5dD+7>veVWi$+?A~P|`LM6syeHe_$US&9_sK`uA0xCqo zREC3!ip<0$(Ws!?6Ad6eJd(lg_5X5tWx@qa+B1yxBiRfFV;Des3JfN#FlYUhN{Gk$ zFc`-G(sMvXT4B!mpu$m2dJd>aE6iCx92InXjt%7IL^9a*_s`|}gbSFQTi7c{vKb7< zwt?J47)(xE{>m$r5Rdg?Fuo1sW`c^Gw)~Yrg|D65Oi+>2mcMd1D(LonGf2#gWU#M) zYc4k=T)-rJa^jkiYzBj!Xa?nCiSSq-20PUZ67xYtLe*2(1eH@2l$Z}H z5~`lMW;`nB_NRK{Qs+l9*u%d+m+wiqfL*FVE@!bwHiN+~#}k*j3Ji9sa$eG68qpK`5XojRm>pmBN?@=qg`uEPSsv>zb-9h|R_0%nyZ-SVH7ad4jz6&# zi|{}NR74ek!9?X{F5alP?eWXLm+I?QeE0ZKeWSvJYH8YZo0sPDs)P%eB`8}hk!%Ko z!C;aK1UxItgYiW(w^5~%`h#)bQ9y%9o=2s%K}AVuQW{Vp$)z+HRFs4!rD;^$_V{JD zOZ9atzI*(rzEL5ev^jYCt^bwF_a$7w4C0AoGZ+j8lT;w$S(zVr)0d;F-rQ=!4MIC{F%({g!r z!UfDAoJclViAg|(D45D{P*IVYm}F47sOz};ySf!u^=_mt zs(DZ$;de7~1`Tm3pm_0P-k!%Ko!C;aKggh(t!T6?`)2LFZ{d4gGru9 zm9;@dMP_0WP$3GYG8|M?WF{sVRGjwsZNIhpx)t9&epElG5OLZYK0WZhxx6Of0%i{l zZukb7oP!Fe$SSbwF8zk@^qbh%p6~pJ39PhtRAlkXua2$AYfR4oDs;`{XMl>l#`Fv^ zsi51N`7eb?27Adb=kfyyH+=e+8SxpBYzBjg+l8J02BT{xKVwJbti8jpzj3xD_7v5v z)aT;cOMKtaj!Glp^%GmMNDfp$MN|P8OjKUx;yWsyI(ysgzUI0W-#va*zoXK2>h*KF z>sxa9!GsH#L4c8L27|$1k_rSpEB#)7?lP)W=B~Nc?mG%W= zNX2oHYzBiV8s}P@H0n&W~|84t!?N5QCQ8d@+Hs0fP7UK&)`(g{uk6+v;?O9!Lk zy2sbwSTz5sEF)35=i_tvX9*WD_7Rnv7|CWZn4C}8E5l%H=>#VRqcR?hyN-fU5%jO- z(x9TL_R(CRa#ShJaiF59_R(CysJQO&i@&0sJ^;~Y%| zgB?{$b6hkk{lWNnMD8$(Mn%)na=Ac7PFw!Upu*QqZYHS6Y0F}8|7mJinGGsdNbKtXmAx8P;)P0#$NDgs6;ti&0F}KOR$@@GLSkPB zsO;6S5=WzgZg0g|TOHHyep@bYOt|42<1&{M?eH6vm{Ag_Fhmod0xIGXGZL&+LOj-o z!5I9-r+|vM#Eb->!Vpb-3aE%n%t#Q93c5YR1%gwimwqyrA4$033u??=I^5ys3Snv< zsF=dB6$2`Esf2i}4};k?n8nJ-HNjwH$rL7lih{`G6re(uOku)G1>IgjYC?+XVb|sI zri2?lK{KtP_&TPgG$jwHP}I^I3o2SlQ}S$7B0ScI!6@Lh#)68L(v&=)LQzX=EU0KH zP016F3c5Xozxdec|2{95Hz(Zi#g(QfhsEaC_)-1z#NWx~M-y)NOjk=zYbQoBhtk^EijvTzG@wF~ zOKEUGD$9fMMLxGtrIPxCaokIS*7M)#a|;sJQL%%f^@L z>sEaC_)-0IvoFo%#}aP%Tw4pyYbQo9h1Sy8ik8xpJfK2ROKWUID)WQ!RX?v$rP8)= ze(pPpXfUnADVGOS(#t*A*&NCYYbvDAh}q%uDkUj_6URVt}J8224TG??1y6iWmuV$z*JX~<+xUQBSTW@H~jkkyM2$CtEf_ORqy84i)s#OFun6BlnGRXggt3lP&r9Hp~;{k zBx8kbajoU>vPq%$^F8@5?hJTW`WhRdkBeTBZs@RI6*oRYr%3-M# z2Zp3lAB=AjI*lro+CR7N#P=PAG??N6l}QCEGP<6&DyW>MpUiAfkcaI;{Pvu8)`H6%Z{%PhGnLSR7$nu(tVk??zAI$|SN0rhX7n4fA;XC~%_DZF8 zP3J#MV6|~fgJ~X7iCmx}q3Wq?g3751O3Vip2~|&Bb4R5$O0Pe%rfwxnw|~-`b9r0B z4gXYo3(Vi07=hJQm&8_7IrRr>Ce=nS1{F65n?e(_qb1 zUOz#_A~{e26;TCZ8q8B?Z+Q1LS1P`H{HT6<#a(lGd%_LBaW?cB^Y9xIm{kg>uw)aS z1S-M;vzFge>G%3`mr>oy+%?zQeMfsLEUb1WVJmi6j7mUdXkjM;6+0|OB|)jU?D3t- z_OJ9~bo+0)E|-6iaKrC(?F2y&za4>Dm4FIMHsML2A}la#`8}1hzJklBZe_XGFE8S(!SpdzO& ze`QeNYbQ4oROGbfuN;hu>mEP;nc;NhJ9Bwg!VRBqY_XZaPK=mNx(kDfuHMJ8fyyzR zbVo&_(jSbEN8}EpXjF7PEtL&aq?A2nT~IkiJ*l~%BBksp>qeszq}%_u-^%5u6K?pY zSX*drv=bvFlk&2lqAd8Kbf9ubCgnlVsKj`zAB~E#sfE&kijc4;Eek3q$tN@!RD^^* zY1wF0f^_>Qzd4tmNx0#k7vMSV)ta0)D2`U!pfeNUIDiF_L>tp>{G`~@$;=9L> z>f;$q^gJpp4k}7QlhS|+NiLqZQOSe(oitipjs&7;pCC8uGibZ&!0xF^k zz+j^CG8b=D-1hio-%It=&%862pG&pp_Xp#?qsE4b*3vX=#S)EH3#c?DEPaBCB^s@k zfK-+T19zUuNXs~v69Djm}MR=eBDxwMmG??2Szkptg{GIzXi(A7nUH7mj2RpbDh8pPg9@mKDiG0NUVHosdai!@ zo*Qy`Uz&SB7ZiwiW-jkfOV4-y?S<>6LfTC9U}`K5DjF(NbASp}F^%z{qMN#c5LWiZOy<|Z1GsD0ToaYRR9JPm6y5rj>=hkhhKl=Y+KD!RJY>0$B*iFR2m7d zpV*2;a-ae#q6)xZqVh5q-%;_@+1qaSHBUc(n_PY&tv!GJ@0GcK`i*vM+J3!$Vk;KO zfeNUIDgc9t%FA4QPo>}M&s|1!E53XDsD4j{3D(LqY{d$ReI1~(SHnsSDppAB>j+B4 zWsmP%()4dXp3A>ZThCu!-7o#qZ4}gIotV!6R4j4>6;KgXAgIC4x@az=O2v1NAJqpn eSofFD08}h;0~JsaRUoLrT=w`5`fR%Q)BZnoaS(n0 diff --git a/protocols/mode3_drill_1trial.yaml b/protocols/mode3_drill_1trial.yaml deleted file mode 100644 index de624540..00000000 --- a/protocols/mode3_drill_1trial.yaml +++ /dev/null @@ -1,67 +0,0 @@ -version: 3 - -# ============================================================================ -# One-trial Mode-3 drill — for the recovery drill before an overnight soak and -# for the lab's injected-stall test (lab-test-day-windows-2026-09-14.md, test B): -# a single 180 s closed-loop trial on the 813 KB bar with the simulator, nothing -# else (≈ 36k reads at 200 Hz — more than one of the old ~24.5k-read stall periods, -# so the injected legacy arm has time to stall). Run it as a Test run; the banner at the end is the result. -# before: await Studio.setSdDiag(3) (inject the stall arm) -# after: await Studio.setSdDiag(0) -# ============================================================================ - -experiment_info: - name: "Mode-3 drill (one 180 s bar trial)" - author: "webDisplayTools" - -rig: "./configs/rigs/cshl_g6_2x10_ball.yaml" - -plugins: - - name: "fictrac" - type: "class" - matlab: - class: "FicTracPlugin" - config: - bridge_url: "ws://localhost:8765" - fictrac_port: 60000 - proto: "udp" - offset: 0 - -experiment: - - "drill_setup" - - "cl_bar_200f" - - "drill_shutdown" - -conditions: - - name: "drill_setup" - commands: - - type: "plugin" - plugin_name: "fictrac" - command_name: "connect" - - # The 813 KB, 200-frame bar on the CSHL course card is p3's pattern (036_p3_heisenberg_ts.pat); - # names resolve against the card listing, pattern_ID is only the fallback. - - name: "cl_bar_200f" - commands: - - type: "controller" - command_name: "trialParams" - pattern: "p3_heisenberg_ts" - pattern_ID: 36 - duration: 180 - mode: 3 - frame_index: 0 - frame_rate: 0 - gain: 0 - - type: "plugin" - plugin_name: "fictrac" - command_name: "startClosedLoop" - - type: "wait" - duration: 180 - - type: "plugin" - plugin_name: "fictrac" - command_name: "stopClosedLoop" - - - name: "drill_shutdown" - commands: - - type: "controller" - command_name: "stopDisplay" diff --git a/rig/HANDOVER-okey-cokey-2026-10-21.md b/rig/HANDOVER-okey-cokey-2026-10-21.md deleted file mode 100644 index c1ef0e8a..00000000 --- a/rig/HANDOVER-okey-cokey-2026-10-21.md +++ /dev/null @@ -1,102 +0,0 @@ -# Handover: okey-cokey rig, BIAS test + Foxglove dashboard (2026-10-21) - -For a Claude Code session running **on the okey-cokey lab PC** (Windows; the room with the G6 2×10 -four-color arena, an overhead BIAS camera, freely walking flies, no FicTrac). Michael's Mac has no -remote path into the rig PCs, so start the session there: open Claude Code in any folder (the -checkout does not exist yet) and paste the kickoff prompt below as the first message. - -**This PC starts bare** (no Foxglove, no webDisplayTools checkout, maybe no git or pixi). Setup is -its own checklist, [`okey-cokey-pc-setup.md`](okey-cokey-pc-setup.md), done first. Claude Code -cannot install itself: a person installs the Claude desktop app (or Git for Windows, then the CLI) -over Remote Desktop; from there a Claude session can do most of the checklist and asks for the rest. -Without Claude on the PC, the checklist and the run sheet work by hand too. - -Then two jobs, in this order, both on ONE run sheet: -[`okey-cokey-pc-2026-10-21.md`](okey-cokey-pc-2026-10-21.md). - -1. **Test** today's protocol and BIAS on this PC. -2. **Test the camera + G6 Foxglove dashboard** on the real rig, live and on the run's replay. It was - BUILT and tested off-rig on Michael's Mac the same day - ([`docs/okey-cokey-foxglove-dashboard-plan.md`](docs/okey-cokey-foxglove-dashboard-plan.md), the - status block at the top); nothing of it has met this rig, its camera or a real `.ufmf` from it. - -## Kickoff prompt (paste on the lab PC) - -``` -Okey-cokey rig work, LAB-303. This PC may be bare (no webDisplayTools checkout, no Foxglove, maybe -no git or pixi). Start with the setup checklist; read it on GitHub until the repo is cloned: -https://github.com/reiserlab/webDisplayTools/blob/claude/okey-cokey-rig/rig/okey-cokey-pc-setup.md -It clones the repo to C:\w on branch claude/okey-cokey-rig. Then read -C:\w\rig\HANDOVER-okey-cokey-2026-10-21.md and run C:\w\rig\okey-cokey-pc-2026-10-21.md -in order, keeping C:\w\rig\okey-cokey-pc-results-2026-10-21.md as you go (one row per step: -pass/fail, timings, sizes, every error verbatim; the setup facts first). -Ask me (Michael) before every download or install, any admin/UAC step, and anything that needs a -click: the BIAS GUI, the Chrome serial-port picker, the IR backlight, Foxglove, putting flies in. -If BIAS is not on this PC, stop and tell me. Rules are in the handover's "Constraints" section. -Commit fixes and the results file to claude/okey-cokey-rig and push; open no PR. -``` - -## Where things stand - -- **Branch `claude/okey-cokey-rig`** = release candidate 2026-10-20 rc1 (Studio v0.97, the ROS 2 - graph, the BIAS connector; on Next) plus: - - `claude/okey-cokey-pairs`: `protocols/g6_color_optomotor_test.yaml` + `_smoke.yaml` in **Grace's - trial shape (2026-10-21)** — each trial a CW/CCW pair, 15 s CW then 15 s CCW with no gap, then 10 s - dark (also across block boundaries). Full run 24 pairs, 16.1 min; smoke 4 pairs (10 s per - direction, 5 s dark), 1.7 min. Parsed and flattened with the real modules; `pixi run test -- - protocol` 106/106. - - the dashboard (plan doc status block): `nodes/stimulus_node.py` + `stimulus_core.py` + - `arena_display.py` (expected/observed display in the four-color LEDs, arena rate/lag/health from - controller telemetry), BIAS rate/drops in `viz_node.py`, `rig.py --no-fictrac --stimulus`, the - replay (`fox-json --stimulus`, `tools/ufmf.py`), `foxglove/arena-okey-cokey.json`. Mac: 54/54 + - 10/10 + harness 54/54. -- **Not on any Pages tier**: Next serves the candidate *without* the new protocol shape. Serve this - checkout locally (`pixi run python .claude\nocache-server.py 8092` from `C:\w`) or open the YAML via Protocol ▾ → Open…. -- **Background reading**: the ww18 results, `windows-results-ww18-2026-10-15.md` (every Windows finding - is in its table; most are fixed on this branch). -- **Settled**: the okey-cokey controller runs telemetry firmware; BIAS is NOT changed now (no live - camera in Foxglove — the BIAS window sits beside it; the replay has the camera). -- **Record in job 1**: which BIAS build (the Atlas10 fork reports drop counters, the Spinnaker build - may not: those panels then read "no data", never 0). - -## What could go wrong on this rig (look here first) - -- **Windows paths and the pattern/protocol lookup**: the stimulus node finds `.pat` under - the repo's `protocols/` + `patterns/` and the run's protocol by its `protocol_filename` under - `protocols/`. A protocol opened from elsewhere (Downloads) still runs, but its patterns' NAMES may - not resolve → a gray hatched placeholder and "pattern N" in the health line. Fix with - `--stimulus-args "--protocol-dir --pattern-dir "`, not code. -- **The camera's clock**: the replay places camera frames with the run's `getTimestamp` replies. - `camera.offset_ms` in the converter's report should be stable; if `camera.error` says no reply - had a value > 0, the protocol's first getTimestamp came too early (BIAS reads 0 before its first - frame). -- **uFMF from this camera**: the reader follows BIAS's writer (v4, MONO8, index or scan). A different - coding (e.g. RGB) is refused with a clear error: record it. -- **foxglove_bridge on Windows**: never run on Windows yet (ww18 did not install `viz`). If it fails - to start, the rig prints it and the rest runs; record the error verbatim. -- **Load**: the stimulus node renders 800×160 PNGs at 15 Hz (a few ms each on the Mac). If the - bridge's latency numbers (`connector_status`) get worse with it on, record both. - -## Constraints - -- **Never touch Production or Next**, never merge to `main`, open no PR. Code reaches `main` only - through a release candidate, which Michael builds (`docs/development/release-process.md`). -- **No Studio changes** (`arena_studio.html`, `js/*`), **no bridge 3.4 changes** - (`fictrac-bridge/bridge.py`), **no BIAS changes**. Everything lives in `rig/`. If a Studio - change turns out to be needed, stop and write it up instead. -- Tests: `pixi run test`, `pixi run test-viz`, `pixi run test-stimulus` and `pixi run test-stimulus-live` - inside `rig/` (the last needs the rig stopped), plus `pixi run test` at the repo root, all green - before every commit. Prettier only on `*.js`, never on HTML. -- Windows habits from ww18: write text with `newline="\n"`; take durations from `perf_counter`; never - run `bias_sim` or the harness's sim while the real BIAS is open on 5010; Ctrl-C the rig to stop it - (the SIGBREAK handler closes the bag cleanly). -- Capture an error's full text before you try to fix it. Commit small fixes to - `claude/okey-cokey-rig` with a message that names the step. -- American English in code, docs and commits. - -## What to send back - -The results file (both jobs): the PC facts, BIAS build, both runs' checks, the two test counts on -Windows, a screenshot of the live dashboard mid-rotation, the replay converter's printed report, -and anything that needed a manual step. Push `claude/okey-cokey-rig`, and end with a short -summary Michael can paste into the LAB-303 ticket. diff --git a/rig/README.md b/rig/README.md index a1a90aab..45352b2d 100644 --- a/rig/README.md +++ b/rig/README.md @@ -57,7 +57,7 @@ Pieces, individually (each is also a `pixi run` task; `--help` on every script): | `sim` | `rig-fictrac-sim` (rig_nodes.sims.fictrac_sim, moved from `fictrac-bridge/`; `-- --model fly --kp 2` for a fixating fly) | | `kinefly-sim` / `bias-sim` | synthetic KF/1 producer; a fake BIAS program (HTTP, state machine, writes the video file) | | `studio-emulator` | the Studio runner's exact bridge conversation, for machines without a browser | -| `stimulus` | `nodes/stimulus_node.py`: the arena's EXPECTED display (the trial's `.pat` frame × duty in the panels' LED colors) and its OBSERVED display (controller telemetry) as PNG images, plus arena rate / lag / health, from the live `/arena/browser`; `pixi run rig -- --stimulus` (included by `--foxglove`). Layout `foxglove/arena-okey-cokey.json` (camera + G6, no FicTrac); the same layout on a finished run: `pixi run fox-json -- --stimulus --panel-layout four-color` (adds `/camera/image` from the run's uFMF). Plan + status: `docs/okey-cokey-foxglove-dashboard-plan.md` | +| `stimulus` | `nodes/stimulus_node.py`: the arena's EXPECTED display (the trial's `.pat` frame × duty in the panels' LED colors) and its OBSERVED display (controller telemetry) as PNG images, plus arena rate / lag / health, from the live `/arena/browser`; `pixi run rig -- --stimulus` (included by `--foxglove`). Layout `foxglove/arena-okey-cokey.json` (camera + G6, no FicTrac); the same layout on a finished run: `pixi run fox-json -- --stimulus --panel-layout four-color` (adds `/camera/image` from the run's uFMF). Plan + status: `rigs/ww14/okey-cokey-foxglove-dashboard-plan.md` in the private `reiserlab/arena-experiments` (rig records left this repo on 2026-10-29, LAB-227) | | `test-stimulus` / `test-stimulus-live` | the dashboard's pure parts (panel colors pinned to `js/panel-color.js`, renderer, tracker on synthetic + a real 80 fps trial, uFMF reader, replay converter); the live path through router + bridge + stimulus node (~25 s, needs ports 7447/8765 free) | Environment set by the workspace (`[activation.env]`): `RMW_IMPLEMENTATION=rmw_zenoh_cpp`, @@ -168,7 +168,7 @@ starts at its first frame, ≈ 300 ms later — take the alignment stamp ≥ 0.5 startRecording, or anchor on the first frame. The harness (`pixi run test`) exercises the same path through the emulator: 32 / 32. -One detour worth knowing: the stock `fictrac_bias_test.yaml` names `frame2_h_ccw_200f` at `pattern_ID 4`; this card's slot 4 is a 1-frame color test pattern, the runner read `frames: 1` from the card (correctly) and the display sat on frame 0. The protocol was re-pointed at the card's 20-frame grating — a card-content mismatch the Studio already warns about, not a bridge issue. +One detour worth knowing: the stock `fictrac_bias_test.yaml` (now `protocols/shared/` in the private `reiserlab/arena-experiments`) names `frame2_h_ccw_200f` at `pattern_ID 4`; this card's slot 4 is a 1-frame color test pattern, the runner read `frames: 1` from the card (correctly) and the display sat on frame 0. The protocol was re-pointed at the card's 20-frame grating — a card-content mismatch the Studio already warns about, not a bridge issue. **Still not verified:** Windows execution (`win-64` only resolves and locks; the rig-PC session is scripted step by step in `WINDOWS-TEST.md`), and the macOS router with the stock IPv6 config (the @@ -282,7 +282,7 @@ without further changes; **"ROS Studio" = that Studio served from the `/ros/` Pa Studio v0.95 served from `localhost:8092` (this branch), `pixi run rig -- --sim --bias` (model fly at 50 Hz, typed FicTrac topic, real BIAS Mac port on :5010, bridge node recording with the `crashsafe` profile), the G6 2×10 bench controller over Web Serial (Michael clicked the port -picker), protocol `protocols/bias_camera_closed_loop_poc.yaml` (6 steps: camera start → 15 s +picker), protocol `protocols/ww18/bias_camera_closed_loop_poc.yaml` from the private `reiserlab/arena-experiments` (in this repo's `protocols/` until 2026-10-29; 6 steps: camera start → 15 s closed loop → camera sync → 15 s closed loop + 90 °/s bias → camera stop → shutdown). | | result | diff --git a/rig/WINDOWS-TEST.md b/rig/WINDOWS-TEST.md index b783a7c2..a62804a4 100644 --- a/rig/WINDOWS-TEST.md +++ b/rig/WINDOWS-TEST.md @@ -52,7 +52,7 @@ pass/fail, timings, sizes and every error verbatim. 3. Follow rig/WINDOWS-TEST.md in order. Steps 1–3 need no hardware and can run unattended; stop and ask me to be at the keyboard before step 4 (BIAS) and step 5 (Studio + controller over Web Serial). Stop bridge 3.4 first if it is running. -4. Results: rig/windows-results-ww18-.md committed to the branch and pushed; add a +4. Results: a `windows-results-ww18-.md` committed to the private `reiserlab/arena-experiments` under `rigs/ww18/` (rig records left this repo on 2026-10-29, LAB-227); add a "Windows results" row to the test-plan table in PR #254's description. Small fixes may go on the branch (message names the step); never touch Production or Next. ``` @@ -70,7 +70,7 @@ Windows build with its FLIR camera, the G6 controller on USB, optionally FicTrac Visual Studio 2022 Build Tools (Desktop C++) is installed — step 1b measures the typed-message build with and without it. -**Windows notes from the first run (ww18, 2026-10-15 — `windows-results-ww18-2026-10-15.md`):** +**Windows notes from the first run (ww18, 2026-10-15 — `rigs/ww18/windows-results-ww18-2026-10-15.md` in `reiserlab/arena-experiments`):** - **Short build path for the typed build**: under `C:\Users\\Documents\GitHub\…` it hits Windows' 260-character path limit (`C1083`). Either clone to under ~45 characters before `\rig`, or, from any @@ -83,7 +83,7 @@ build with and without it. since 538cc85: 1b / 7 install on ww18 (35 s, harness 48 / 48). - **Check the controller's SD card first**: an empty SD listing / "pattern missing" can mean no card is mounted at all (ww18's old card). The site library's 2×10 set (`patterns/g6_2x10/patterns/`) plus - `protocols/bias_camera_closed_loop_poc_ww18*.yaml` (slot 2 `grating_sq`) work on any 2×10 card loaded that way. + `protocols/ww18/bias_camera_closed_loop_poc_ww18*.yaml` (`reiserlab/arena-experiments`; slot 2 `grating_sq`) work on any 2×10 card loaded that way. - **FicTrac** (pixi-built): start it with `build\Release\fictrac.bat ` from the config folder — `fictrac.exe` alone exits silently with `0xC0000135` (DLL not found). - **BIAS Atlas10 build** (atlas-bench): HTTP server on by default at 5010; uFMF by default, so `.avi` names @@ -224,7 +224,7 @@ cd .. # repo root python .claude\nocache-server.py 8092 ``` -Chrome: `http://localhost:8092/arena_studio.html?p=bias_camera_closed_loop_poc&rig=&mode=run` +Chrome: `http://localhost:8092/arena_studio.html?repo=reiserlab/arena-experiments&p=protocols/ww18/bias_camera_closed_loop_poc.yaml&rig=&mode=run` (signed in; the protocol moved to the private repo on 2026-10-29) with the rig key from `configs\rigs\index.json` that matches this arena (`cshl_g6_2x10_ball` for a 2×10 ball rig). Then: diff --git a/rig/config/bias/okey-cokey-bfs-u3-51s5m.json b/rig/config/bias/okey-cokey-bfs-u3-51s5m.json deleted file mode 100644 index ac2ce960..00000000 --- a/rig/config/bias/okey-cokey-bfs-u3-51s5m.json +++ /dev/null @@ -1,253 +0,0 @@ -{ - "camera": { - "format7Settings": { - "mode": "0", - "pixelFormat": "MONO8", - "roi": { - "height": 2048, - "offsetX": 0, - "offsetY": 0, - "width": 2448 - } - }, - "frameRate": "Format7", - "guid": "24203703", - "model": "Blackfly S BFS-U3-51S5M", - "properties": { - "autoExposure": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "brightness": { - "absoluteControl": true, - "absoluteValue": 0.0, - "autoActive": false, - "on": true, - "onePush": false, - "present": true, - "value": 33 - }, - "focus": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "frameRate": { - "absoluteControl": true, - "absoluteValue": 73.3232421875, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 95 - }, - "gain": { - "absoluteControl": true, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 0 - }, - "gamma": { - "absoluteControl": true, - "absoluteValue": 0.800048828125, - "autoActive": false, - "on": true, - "onePush": false, - "present": true, - "value": 14 - }, - "hue": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "iris": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "pan": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "saturation": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "sharpness": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "shutter": { - "absoluteControl": true, - "absoluteValue": 998.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 0 - }, - "temperature": { - "absoluteControl": true, - "absoluteValue": 52.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 70 - }, - "tilt": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - }, - "triggerDelay": { - "absoluteControl": true, - "absoluteValue": 13.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 0 - }, - "triggerMode": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": true, - "value": 0 - }, - "whiteBalance": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "valueBlue": 0, - "valueRed": 0 - }, - "zoom": { - "absoluteControl": false, - "absoluteValue": 0.0, - "autoActive": false, - "on": false, - "onePush": false, - "present": false, - "value": 0 - } - }, - "triggerType": "Internal", - "vendor": "Point Grey Research", - "videoMode": "Format7" - }, - "configuration": { - "directory": "C:/Users/labadmin/Documents", - "fileName": "bias_config" - }, - "display": { - "colorMap": "None", - "orientation": { - "flipHorizontal": false, - "flipVertical": false - }, - "rotation": 0, - "updateFrequency": 15.0 - }, - "logging": { - "autoNamingOptions": { - "cameraIdentifier": "CameraNumber", - "includeCameraIdentifier": true, - "includeTimeAndDate": true, - "includeVersionNumber": true, - "timeAndDateFormat": "\u0027date\u0027_yyyy_MM_dd_\u0027time\u0027_hh_mm_ss" - }, - "directory": "C:/rec/okey-cokey", - "enabled": false, - "fileName": "bias_video", - "format": "ufmf", - "settings": { - "avi": { - "codec": "MJPG", - "frameSkip": 1 - }, - "bmp": { - "frameSkip": 1 - }, - "fmf": { - "frameSkip": 1 - }, - "jpg": { - "compressionThreads": 10, - "frameSkip": 1, - "mjpg": true, - "mjpgMaxFramePerFile": 5000000, - "mjpgMaxFramePerFileFlag": false, - "quality": 90 - }, - "ufmf": { - "backgroundThreshold": 40, - "boxLength": 30, - "compressionThreads": 15, - "dilate": { - "on": true, - "windowSize": 1 - }, - "frameSkip": 1, - "medianUpdateCount": 100, - "medianUpdateInterval": 50 - } - } - }, - "server": { - "enabled": true, - "port": 5010 - }, - "timer": { - "enabled": false, - "settings": { - "duration": 40 - } - } -} diff --git a/rig/docs/okey-cokey-foxglove-dashboard-plan.md b/rig/docs/okey-cokey-foxglove-dashboard-plan.md deleted file mode 100644 index 055cf91e..00000000 --- a/rig/docs/okey-cokey-foxglove-dashboard-plan.md +++ /dev/null @@ -1,176 +0,0 @@ -# Plan: a purpose-built Foxglove dashboard for the okey-cokey rig (camera + G6, no FicTrac) - -> **Status 2026-10-21: phases 1, 1b and 2 BUILT** on branch `claude/okey-cokey-rig` (off -> `claude/okey-cokey-pairs`, itself the 2026-10-20 candidate + Grace's protocol shape). Phase 3 -> (live camera) NOT built, by decision: BIAS stays unchanged for now. The okey-cokey controller -> runs telemetry firmware (Michael), so the observed side is live. Part of LAB-303. -> -> | built | where | -> |---|---| -> | `/arena/browser` published live | `nodes/bridge_node.py` (one publisher; the bag record is unchanged) | -> | tracker: expected/observed index, lag, display rate, health | `nodes/stimulus_core.py` (pure) | -> | renderer + Python port of the panel colors | `nodes/arena_display.py` (pinned cell by cell to `js/panel-color.js` under Node) | -> | live node | `nodes/stimulus_node.py`; `pixi run rig -- --stimulus` (`--foxglove` includes it), `--no-fictrac` for this rig | -> | BIAS rate / drops / rtt / writer queue | `rig_nodes.bias_health` (reiserlab/rig-nodes), `viz_node.py` (`--sources bias` hides the unused FicTrac/Kinefly/arena-frame statuses) | -> | replay: same topics from a finished bag + `/camera/image` from the uFMF | `nodes/dashboard_replay.py` via `pixi run fox-json -- --stimulus`; `tools/ufmf.py` (BIAS v4 layout, index or scan) | -> | layout | `foxglove/arena-okey-cokey.json` (15 panels) | -> -> Tested: `pixi run test-stimulus` 54/54 (incl. a REAL 80 fps trial from the 2026-10-14 smoke run: -> 800+ frames, every lag 0, 80 Hz); `pixi run test-stimulus-live` 10/10 (router + bridge + -> stimulus node, the real trial played into the Studio WebSocket); ROS harness `pixi run test` -> 54/54 and `test-viz` unchanged; `rig.py --no-fictrac --bias-sim --bias --foxglove` brings every -> dashboard topic up with foxglove_bridge on :8766; uFMF reader on three real BIAS movies -> (1920×1080, up to 2.1 GB, indexed). On the real 2026-10-14 bag the replay MCAP is 612 kB for -> 2.5 min. **Not verified:** the layout opened in the Foxglove app (written to Foxglove's layout -> format by hand; its topics are checked by the test), a live run on the okey-cokey rig, a real -> uFMF from that rig's camera through the replay. -> -> Deviations from the plan below: (1) the arena status is the stimulus node's own -> `arena/display` diagnostic instead of re-feeding viz_node's FicTrac-frame `arena` entry, which -> keeps meaning closed-loop frames; (2) an OBSERVED display image (`/arena/display_observed`) -> was added beside the expected one; (3) `/viz/arena/observed_idx` and `lag_frames` are one sample -> per displayed frame (live and replay alike); (4) the expected display's stall check is STALE only -> while a pattern plays, since the telemetry ring has nothing to send in the dark; (5) the tiff -> tool's PNG writer was not refactored into the shared module (left as it was). - -## Goal - -One Foxglove layout for a freely-walking-fly rig (overhead BIAS camera + G6 2×10 four-color arena, no -FicTrac) showing three things: - -1. **The camera.** -2. **The expected display**: what the arena should be showing right now (pattern, frame, color, duty), - rendered as an image of the unrolled arena. -3. **Health and rate** for the arena and for BIAS: is each one alive, at what rate, and has anything - dropped. - -The same layout should work **live** (foxglove_bridge during the run) and on **replay** (the run's MCAP -afterwards). - -## What exists today (release 2026-10-20, `rig/`) - -| Need | Today | Gap | -|---|---|---| -| BIAS health | `/bias/status` at 5 Hz (`bias_node.py`): reachable, capturing, logging, frameCount, framesPerSec, rtt_ms; on Atlas10 also frameIdGaps, cameraSkipped, loggerSkipped, writerSkipped, logQueueSize. `viz_node` turns these into `/viz/bias/*` and a `bias` entry in `/diagnostics`. `foxglove/arena-bias.json` shows them. | Mostly there. Missing: a rate from the frameCount slope (independent of BIAS's own estimate) and the drop counters on a plot. | -| Arena health | `viz_node`'s `arena` entry is fed only by `/arena/frame`, which only FicTrac lines produce. | **Always "never seen" on this rig.** | -| What the arena is doing | Every Studio message (trialParams `a` echoes, runner status events with `conditionName` / `params` / `durationSec`, controller telemetry `cc`/`cf`/`cs` row batches) reaches `bridge_node`. | It is **written to the bag only** (`/arena/browser`, `/arena/event` via `rec.write_raw`), never published, so nothing live can subscribe. The `a` hex is truncated to 8 bytes (no gain/duration). | -| Expected display image | `tools/patfile.py` reads `.pat`; `tools/tiff_run_to_mcap.py` already renders `/arena/display` as a `foxglove.CompressedImage` (200×40, upscaled, labeled RECONSTRUCTED). | Offline only, and wired to the 2P color-sweep file names and DO1 timing. Monochrome. | -| Camera image | Nothing. BIAS's HTTP API has no frame grab (only status, timestamps, frame count, video file). | **No live frames anywhere.** After the run, the `.ufmf` plus the per-frame stamps on `/bias/frames` (recorded at stopRecording) are enough to rebuild a timed image stream. | - -## Design - -### A. Make the arena's activity a live topic (bridge_node, small) - -`bridge_node` publishes what it already records: - -- `/arena/browser`: String JSON, verbatim, the same record it writes to the bag (QoS reliable, depth 50). - It stays in the bag as now. - -That one change lets any node watch the run without touching the Studio or bridge 3.4. - -### B. `stimulus_node.py`: expected display + arena health (new node, the core of the work) - -It subscribes to `/arena/browser` and keeps the current stimulus state: - -- From runner status events and the trialParams echoes: condition name, pattern name, mode, frame - rate, start frame, duty, and the trial start time (the echo's `ms`, i.e. when the command left the - browser). -- Expected frame for Mode 2: `idx = (init + round(fps · (t − t_start))) mod n_frames`. `allOff` = dark. - Mode 3 is out of scope here (it needs FicTrac). -- Observed frame: telemetry `cf` rows carry the frame the controller actually put up (idx + pattern - per frame change). Arrival is in ~10 Hz batches, so the observed stream lags by ~100 ms, but each row - has its own controller timestamp. - -It publishes: - -| Topic | Type | Rate | What | -|---|---|---|---| -| `/arena/stimulus` | String JSON (later typed) | on change | `{condition, pattern, mode, fps, init, duty, t_start}` | -| `/arena/display` | `foxglove.CompressedImage` (PNG) | 10–15 Hz | the expected display: unrolled 200×40, ×4 upscale, top row = top | -| `/viz/arena/expected_idx`, `/viz/arena/observed_idx` | Float64 | 10–15 Hz / per row | for one plot of the two indices together | -| `/viz/arena/display_rate_hz` | Float64 | 1 Hz | frame changes per second from `cf` rows; should equal \|fps\| (80) during a trial and 0 in the dark | -| `/viz/arena/lag_frames` | Float64 | 1 Hz | expected − observed (mod n), the "is the arena keeping up" number | -| `/diagnostics` entry `arena` | DiagnosticStatus | 1 Hz | OK / WARN (rate off by > 5 %, lag > 2 frames, a `display_gap` or flagged `trial_quality`) / STALE (no telemetry for > 2 s while a run is open) / ERROR (non-ok command status) | - -**Color, not green.** The four-color panels light one bank per pattern, so a green ramp would show the -wrong thing. Port the minimum of `js/panel-color.js` to Python (the layout table, and -`bankAt(row, col) = 2·((row+1)%2) + (col%2)` in host coordinates), and pin it to the JS module with a -test. Duty scales the brightness. Without that test, the two copies will drift. - -**Patterns.** The node needs the `.pat` files. It looks them up by name under a `--pattern-dir` (the -protocol's `*_patterns/` folder) and caches them. If a pattern is missing, the image shows the name on -a gray placeholder; the run is never refused. - -**If the firmware has no telemetry** (GET_FIRMWARE_VERSION flag bit 2 off, or the poller is off), there -are no `cf` rows: show the expected display only, observed = "unknown", and arena health from command -status alone. It must never claim "OK" without evidence (the same rule as `trial-quality.js`). - -### C. BIAS health additions (viz_node, small) - -- `/viz/bias/fps_from_count`: frameCount slope over the last 2 s of 5 Hz polls, next to BIAS's own - `framesPerSec`. -- `/viz/bias/drops`: the sum of the Atlas10 counters (frameIdGaps + cameraSkipped + loggerSkipped + - writerSkipped) as a plot; WARN on any increase while logging. Absent keys (Spinnaker build) mean - "not reported", not 0. -- `/viz/bias/rtt_ms` from the status polls; WARN at > 100 ms. - -### D. Camera - -- **Replay (phase 2).** `ufmf_to_mcap.py` reads the run's `.ufmf` and writes a sidecar MCAP with - `/camera/image` as `foxglove.CompressedImage` JPEG, timed by the `/bias/frames` stamps from the run - bag, downsampled (for example 10 Hz, half resolution) so the file stays small. Foxglove opens the run - bag and the sidecar together. uFMF needs a reader: write a small one (the format is documented, and - motmot's reader is Python 2-era), and test it on a ww18 recording. -- **Live (phase 3, only if wanted).** BIAS has to push frames out: a small BIAS patch (in our - atlas-bench fork) publishing a rate-limited JPEG preview over ZMQ. A node then mirrors it as - `/camera/image`, copying `scanimage_frames_node.py` (rate limit + loss stats). Until then, keep the - BIAS window beside Foxglove during the run. - -### E. The layout: `foxglove/arena-okey-cokey.json` - -``` -┌──────────────────────────────┬─────────────────────────────────────┐ -│ Image /camera/image │ Image /arena/display (expected) │ -│ (replay; live after phase 3) │ caption: condition · pattern · duty │ -├──────────────┬───────────────┼─────────────────────────────────────┤ -│ Indicators │ Plot: BIAS fps (own + from count), drops │ -│ BIAS reach/ ├─────────────────────────────────────────────────────┤ -│ capture/log; │ Plot: arena expected vs observed idx; display rate │ -│ arena state ├─────────────────────────────────────────────────────┤ -│ │ StateTransitions: /arena/stimulus condition │ -├──────────────┴──────────────────┬──────────────────────────────────┤ -│ DiagnosticSummary (/diagnostics)│ RawMessages /bias/reply │ -└─────────────────────────────────┴──────────────────────────────────┘ -``` - -### F. Replay parity - -The derived topics (B, C) do not exist in bags recorded before this work, and live ones are not -recorded unless we choose to. Do both: - -- Record `/arena/stimulus` live (it is small). Do not record `/arena/display`: it is rebuilt from - stimulus + patterns. -- Extend `nodes/foxglove_json.py` (the offline converter) to run the same `stimulus_node` logic over - the bag's `/arena/browser` records and emit `/arena/display` and the `/viz/arena/*` topics. Then the - layout works on any run, including today's. - -The logic lives in one pure module (`nodes/stimulus_core.py`) used both by the live node and by the -converter, the same split as `studio-replay.js`. - -## Phases and size - -| Phase | What | Size | Gate | -|---|---|---|---| -| 1 | A + B (no color port yet) + C + layout; harness test with the Studio emulator sending trialParams + `cf` rows | ~1 day | live on the Mac bench with the 2×10 arena | -| 1b | Python color port + test pinned to `js/panel-color.js` | ~2 h | the four banks show their colors | -| 2 | F (offline converter) + D replay (uFMF reader → sidecar MCAP) | ~1 day | today's okey-cokey run opens with camera + expected display in sync | -| 3 | D live (BIAS ZMQ preview patch + node) | ~1–2 days, BIAS C++ | only if a live camera in Foxglove is worth more than the BIAS window | - -## Open questions - -1. ~~Is a live camera in Foxglove needed?~~ Not now (2026-10-21): BIAS stays unchanged; replay has the camera. -2. ~~Telemetry firmware on the okey-cokey controller?~~ Yes (Michael, 2026-10-21). -3. Which BIAS build is on the okey-cokey PC: the Atlas10 fork has the drop counters, the Spinnaker - build may not (the panels then read "no data", never 0). -4. Typed messages: `viz_node` subscribes to `/arena/frame` as String, which breaks in the `typed` env. - That doesn't affect this rig (no `/arena/frame`), but fix it when touching viz_node. diff --git a/rig/okey-cokey-pc-2026-10-21.md b/rig/okey-cokey-pc-2026-10-21.md deleted file mode 100644 index 10b3171a..00000000 --- a/rig/okey-cokey-pc-2026-10-21.md +++ /dev/null @@ -1,167 +0,0 @@ -# Okey-cokey rig PC — BIAS + G6 color optomotor test, 2026-10-21 - -The run sheet for today's test on the new lab PC in the okey-cokey room (freely walking flies, overhead -BIAS camera, G6 2×10 four-color arena, **no FicTrac**). It is `WINDOWS-TEST.md` cut down to what this -rig needs, with the ww18 findings of 2026-10-15 (`windows-results-ww18-2026-10-15.md`) applied up front. -Record results in a sibling `okey-cokey-pc-results-2026-10-21.md` in the same shape as the ww18 file. - -Branch: `claude/okey-cokey-rig` = the 2026-10-20 release candidate (Studio v0.97, ROS 2 graph, -BIAS connector) + the CW/CCW-pair protocols Grace asked for (`claude/okey-cokey-pairs`) + the camera + -G6 Foxglove dashboard (`docs/okey-cokey-foxglove-dashboard-plan.md`). Neither is on a Pages tier, so -serve the checkout locally (step 4). - -## 0. Setup (bare PC → ready) - -Do [`okey-cokey-pc-setup.md`](okey-cokey-pc-setup.md) first: PC access, git / pixi / Chrome / -Foxglove, BIAS + camera SDK, the controller, the clone at `C:\w` on `claude/okey-cokey-rig`, -the three `pixi install`s, both dashboard tests and a no-flies smoke of the whole stack. The steps -below assume it is done. - -- [ ] IR backlight on by hand (Arduino). The web runner does not drive the backlight yet. - -## 1. BIAS itself (10 min) - -- [ ] **Which BIAS build?** FLIR/Spinnaker build or the atlas-bench Atlas10 fork (ww18 ran the fork). - Note the exe path and camera model. -- [ ] External-control server on, port **5010** for camera 0: - `curl.exe "http://127.0.0.1:5010/?get-status"` returns `"success" : true`. Note any extra keys. -- [ ] Logging format = **ufmf** in BIAS's own settings (BIAS writes its configured format whatever - extension it is sent; ww18 B1). BIAS v0.73.1 (Spinnaker) resets to jpg on **every** restart (ww14), - so after each BIAS start run - `powershell -ExecutionPolicy Bypass -File tools\bias-ufmf.ps1 -VideoDir is required: the protocol and its patterns moved to the private 2P rig +// repo on 2026-10-29 (LAB-227: reiserlab/scanimage-2026-bergamo, +// protocols/g6_2x10_2p_diagnostics_patterns/). Point --out into a checkout of it. +const outIdx = process.argv.indexOf('--out'); +if (outIdx < 0 || !process.argv[outIdx + 1]) { + console.error( + 'usage: node scripts/make-2p-diagnostic-patterns.js --out (the patterns live in the ' + + 'private reiserlab/scanimage-2026-bergamo, protocols/g6_2x10_2p_diagnostics_patterns/)' + ); + process.exit(2); +} +const OUT = path.resolve(process.argv[outIdx + 1]); const uniform = (v) => new Uint8Array(N).fill(v); @@ -37,9 +53,13 @@ const PR16 = [8, 12, 14, 15, 7, 3, 1, 0, 9, 4, 2, 10, 5, 11, 13, 6]; function sparse(frac, seed) { const f = new Uint8Array(N); let s = seed >>> 0; - const rnd = () => ((s = (1664525 * s + 1013904223) >>> 0) / 4294967296); + const rnd = () => (s = (1664525 * s + 1013904223) >>> 0) / 4294967296; let lit = 0; - for (let i = 0; i < N; i++) if (rnd() < frac) { f[i] = 15; lit++; } + for (let i = 0; i < N; i++) + if (rnd() < frac) { + f[i] = 15; + lit++; + } return { frame: f, lit }; } @@ -52,7 +72,10 @@ const SEEDS = [ { name: 'G6_2x10_ff_steps_pr16', frames: PR16.map((v) => uniform(v)), - note: 'full field, the 16 grey levels in pseudo-random order ' + PR16.join(',') + '. mode 2, frame_rate 2 → 8 s per pass' + note: + 'full field, the 16 grey levels in pseudo-random order ' + + PR16.join(',') + + '. mode 2, frame_rate 2 → 8 s per pass' }, { name: 'G6_2x10_ff_flash_0_15', @@ -61,30 +84,47 @@ const SEEDS = [ }, { name: 'G6_2x10_sparse10', - frames: [sparse(0.10, 20260923).frame], + frames: [sparse(0.1, 20260923).frame], note: '10 % of pixels at level 15, rest dark, static (frame_rate 0). Compare with ff_ramp16 held at level 1–2' } ]; fs.mkdirSync(OUT, { recursive: true }); -const readme = ['Patterns for protocols/g6_2x10_2p_diagnostics.yaml (G6 2x10, GS16, built by scripts/make-2p-diagnostic-patterns.js).', - 'Upload the whole folder from Arena Studio (Console → Patterns → Add ▾ → this protocol\'s folder); the Studio resolves patterns by NAME.', '']; +const readme = [ + 'Patterns for protocols/g6_2x10_2p_diagnostics.yaml (G6 2x10, GS16, built by scripts/make-2p-diagnostic-patterns.js).', + "Upload the whole folder from Arena Studio (Console → Patterns → Add ▾ → this protocol's folder); the Studio resolves patterns by NAME.", + '' +]; let ok = true; for (const s of SEEDS) { const bytes = PatEncoder.encode({ - generation: 'G6', gs_val: 16, numFrames: s.frames.length, - rowCount: ROWS, colCount: COLS, pixelRows: PIXEL_ROWS, pixelCols: PIXEL_COLS, - frames: s.frames, stretchValues: [], arena_id: ARENA_ID, observer_id: 0 + generation: 'G6', + gs_val: 16, + numFrames: s.frames.length, + rowCount: ROWS, + colCount: COLS, + pixelRows: PIXEL_ROWS, + pixelCols: PIXEL_COLS, + frames: s.frames, + stretchValues: [], + arena_id: ARENA_ID, + observer_id: 0 }); const file = path.join(OUT, s.name + '.pat'); fs.writeFileSync(file, Buffer.from(bytes)); // self-verify: parse back, check geometry, duty byte, and mean level per frame const buf = fs.readFileSync(file); - const p = PatParser.parsePatFile(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); + const p = PatParser.parsePatFile( + buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) + ); const cfg = arenaCfg.getConfig(ARENA); - const geomOk = p.pixelRows === PIXEL_ROWS && p.pixelCols === PIXEL_COLS && p.gs_val === 16 && p.frames.length === s.frames.length; + const geomOk = + p.pixelRows === PIXEL_ROWS && + p.pixelCols === PIXEL_COLS && + p.gs_val === 16 && + p.frames.length === s.frames.length; const dutyOk = p.stretchValues.every((d) => d === 0x80); - const means = p.frames.map((fr) => (fr.reduce((a, b) => a + b, 0) / fr.length / 15)); + const means = p.frames.map((fr) => fr.reduce((a, b) => a + b, 0) / fr.length / 15); const lit = p.frames.map((fr) => fr.filter((v) => v > 0).length / fr.length); const line = `${s.name}.pat frames ${p.frames.length} mean/full per frame [${means.map((m) => m.toFixed(2)).join(' ')}] lit fraction [${[...new Set(lit.map((l) => l.toFixed(2)))].join(' ')}] ${geomOk && dutyOk ? 'OK' : 'FAIL'}`; console.log(line); @@ -93,5 +133,7 @@ for (const s of SEEDS) { if (!geomOk || !dutyOk) ok = false; } fs.writeFileSync(path.join(OUT, 'README.txt'), readme.join('\n') + '\n'); -console.log(ok ? '\nSelf-verify OK → ' + path.relative(process.cwd(), OUT) : '\nSelf-verify FAILED'); +console.log( + ok ? '\nSelf-verify OK → ' + path.relative(process.cwd(), OUT) : '\nSelf-verify FAILED' +); process.exit(ok ? 0 : 1); diff --git a/scripts/make-color-optomotor-patterns.js b/scripts/make-color-optomotor-patterns.js index 35d4ca15..6e145cbc 100644 --- a/scripts/make-color-optomotor-patterns.js +++ b/scripts/make-color-optomotor-patterns.js @@ -18,7 +18,7 @@ * the trial's `duty` override, which is what the protocol sweeps. * * node scripts/make-color-optomotor-patterns.js # GS2 set for g6_color_optomotor_test - * node scripts/make-color-optomotor-patterns.js --gs16 # GS16 set for g6_2p_color_led_sweep + * node scripts/make-color-optomotor-patterns.js --gs16 --out # GS16 set for g6_2p_color_led_sweep (private 2P repo) * * --gs16 writes the SAME four gratings as Gray_16 files (lit pixels = 15) into * protocols/g6_2p_color_led_sweep_patterns/, names suffixed `_gs16`. The 2P imaging runs @@ -44,12 +44,23 @@ const TOP_ROW_START = 20; // pixel rows 20..39 = the top panel row (row 0 = bott const ARENA_ID = 1; const GS16 = process.argv.includes('--gs16'); const ON_VALUE = GS16 ? 15 : 1; -const OUT = path.join( - __dirname, - '..', - 'protocols', - GS16 ? 'g6_2p_color_led_sweep_patterns' : 'g6_color_optomotor_test_patterns' -); +// --out overrides the destination. The GS2 set is the public +// protocols/g6_color_optomotor_test_patterns/. The GS16 set belongs to the 2P +// protocol, which moved to the private rig repo on 2026-10-29 (LAB-227: +// reiserlab/scanimage-2026-bergamo, protocols/g6_2p_color_led_sweep_patterns/), +// so --gs16 needs an explicit --out pointing into a checkout of that repo. +const outIdx = process.argv.indexOf('--out'); +const OUT_ARG = outIdx >= 0 ? process.argv[outIdx + 1] : null; +if (GS16 && !OUT_ARG) { + console.error( + '--gs16 needs --out : the 2P sweep patterns live in the private ' + + 'reiserlab/scanimage-2026-bergamo (protocols/g6_2p_color_led_sweep_patterns/), not here.' + ); + process.exit(2); +} +const OUT = OUT_ARG + ? path.resolve(OUT_ARG) + : path.join(__dirname, '..', 'protocols', 'g6_color_optomotor_test_patterns'); const layout = PanelColor.getLayout('four-color'); const BANKS = layout.banks; // ['violet', 'blue', 'green', 'yellow'] diff --git a/protocols/g6_2p_color_led_sweep.yaml b/tests/fixtures/v3_g6_2p_color_led_sweep.yaml similarity index 100% rename from protocols/g6_2p_color_led_sweep.yaml rename to tests/fixtures/v3_g6_2p_color_led_sweep.yaml diff --git a/tests/test-protocol-roundtrip-v3.js b/tests/test-protocol-roundtrip-v3.js index b5811c1a..d5055da2 100644 --- a/tests/test-protocol-roundtrip-v3.js +++ b/tests/test-protocol-roundtrip-v3.js @@ -4817,10 +4817,9 @@ console.log('\n--- Suite 39: sequence-style variables identity ---'); '39.6: no variable is named after String.prototype.anchor or "undefined"', exp38.variables.every((v) => v.name !== 'undefined' && !/native code/.test(v.name)) ); - const protocol2p = fs.readFileSync( - path.join(__dirname, '..', 'protocols', 'g6_2p_color_led_sweep.yaml'), - 'utf8' - ); + // The protocol itself lives in the private 2P rig repo since 2026-10-29 (LAB-227); + // this fixture is the copy the test parses. + const protocol2p = readFixture('v3_g6_2p_color_led_sweep.yaml'); const exp2p = parseV3Protocol(protocol2p); const names2p = exp2p.variables.map((v) => v.name); checkTrue('39.7: the 2P sweep protocol parses', exp2p.variables.length > 10); From dec2000e69952ffdbd0eb27a09418700c6e51002 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 15:45:46 -0400 Subject: [PATCH 04/10] studio-github: the JSON executor bypasses the HTTP cache (overwrite guard miss) GitHub returns one ETag for the raw and the JSON form of a contents URL; after a raw read (?p= open, promote, previews) Chrome revalidated the cached raw body for the later JSON request and served it on the 304, so res.json() threw and every existence check (resolveRepoSaveTarget, the silent-fork guard) answered "no": a changed protocol overwrote the repo copy with no "Overwrite?" prompt. Found on ww18 during the LAB-227 private-repo test, 2026-10-29. GH.run now sends cache: 'no-store'; runBytes keeps caching since nothing JSON is stored. Co-Authored-By: Claude Fable 5.1 --- js/studio-github.js | 12 ++++++++++++ tests/test-studio-github.js | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/js/studio-github.js b/js/studio-github.js index deb76c5e..8a2928f2 100644 --- a/js/studio-github.js +++ b/js/studio-github.js @@ -361,10 +361,22 @@ // Thin executor: run one descriptor with an injected fetch. Returns // {ok, status, data}. Never logs the token. Kept tiny so the builders stay // the tested surface. + // + // `cache: 'no-store'` (2026-10-29, found on ww18 under LAB-227): the JSON and + // the raw form of ONE contents URL differ only in the Accept header and + // GitHub gives both the same ETag. After a raw read (runBytes: ?p= open, + // promote, previews) Chrome revalidated the stored raw entry for the later + // JSON request, got a 304 and handed back the raw YAML; res.json() then + // threw, `data` was null and every "does this file exist?" check + // (resolveRepoSaveTarget, the silent-fork guard) answered "no" — a changed + // protocol silently overwrote the repo copy with no "Overwrite?" prompt. + // Bypassing the HTTP cache for the JSON executor closes it in both orders: + // nothing JSON is ever stored, so runBytes can keep caching the bytes. async function run(fetchImpl, req) { const res = await fetchImpl(req.url, { method: req.method, headers: req.headers, + cache: 'no-store', body: req.body != null ? JSON.stringify(req.body) : undefined }); let data = null; diff --git a/tests/test-studio-github.js b/tests/test-studio-github.js index 1bf145d4..248aef2f 100644 --- a/tests/test-studio-github.js +++ b/tests/test-studio-github.js @@ -474,9 +474,11 @@ console.log('=== run (injected fetch) ==='); (async () => { let seenUrl = null; let seenAuth = null; + let seenCache = null; const fakeFetch = async (url, init) => { seenUrl = url; seenAuth = init.headers.Authorization; + seenCache = init.cache; return { ok: true, status: 200, json: async () => ({ default_branch: 'main' }) }; }; const res = await G.run(fakeFetch, G.reqGetRepo(O, R, TOKEN)); @@ -484,6 +486,22 @@ console.log('=== run (injected fetch) ==='); check('run ok+status', [res.ok, res.status], [true, 200]); checkBool('run passed auth header', seenAuth === 'Bearer ' + TOKEN, seenAuth); checkBool('run hit repo url', seenUrl.endsWith('/repos/reiserlab/webDisplayTools'), seenUrl); + // The JSON executor bypasses the HTTP cache: the raw and JSON forms of a + // contents URL share an ETag, and a cached raw body served for a JSON + // request made the overwrite guard miss an existing file (ww18, 2026-10-29). + check('run bypasses the HTTP cache (no-store)', seenCache, 'no-store'); + + // A JSON reply that is not JSON (the cached-raw symptom) must not throw: + // callers see data null and decide; the status still comes through. + const notJson = async () => ({ + ok: true, + status: 200, + json: async () => { + throw new SyntaxError('raw'); + } + }); + const nj = await G.run(notJson, G.reqGetRepo(O, R, TOKEN)); + check('run tolerates a non-JSON body', [nj.ok, nj.status, nj.data], [true, 200, null]); console.log('=== runBytes (raw executor) ==='); const rawFetch = async () => ({ From 4090716d45137ec2adcb3de49c74d1fbafbaa001 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 15:48:43 -0400 Subject: [PATCH 05/10] telemetry: report in-session evictions, not the controller's lifetime counter The ring block header's `dropped` is the controller's eviction count since its boot, so a Studio connected hours later logged "dropped 1929" with gaps 0 and read as in-run loss (ww18, 2026-10-29). The drainer now keeps the counter at its first block of a ring incarnation (`droppedBase`, reset on a fresh ring) and exposes `droppedSession`; the once-a-minute stats line leads with "evicted this session N (controller lifetime M)". `telemetry_dump` and the soak stats carry the new field; the raw `dropped` stays for the record. Co-Authored-By: Claude Fable 5.1 --- arena_studio.html | 7 ++-- .../telemetry-logging-reference.md | 4 +-- js/arena-telemetry.js | 21 ++++++++++-- tests/test-arena-telemetry.js | 33 ++++++++++++++++++- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/arena_studio.html b/arena_studio.html index 3d495c18..9e1c1e2e 100644 --- a/arena_studio.html +++ b/arena_studio.html @@ -10764,7 +10764,10 @@

Import error

const key = [st.records, st.dropped, st.gaps, st.errors, st.notStored].join('/'); if (key === Studio.telemetry.lastReport) return; Studio.telemetry.lastReport = key; - Studio.rawLog('telemetry ring: ' + st.records + ' records · ' + st.blocks + ' blocks · ' + (st.bytes / 1024).toFixed(1) + ' KiB · dropped ' + st.dropped + ' · gaps ' + st.gaps + ' · errors ' + st.errors + (st.notStored ? ' · refused by bridge ' + st.notStored : '') + (st.heapCollision ? ' · RING DISABLED (heap collision)' : '') + (st.survivedReboot ? ' · ring survived a reboot (boot #' + st.bootCount + ')' : ''), 'dim'); + // `dropped` in the block header is the controller's LIFETIME eviction count (since + // its boot, mostly from before this page connected); only the growth since our + // first block is in-session loss. Say both, lead with the one that matters. + Studio.rawLog('telemetry ring: ' + st.records + ' records · ' + st.blocks + ' blocks · ' + (st.bytes / 1024).toFixed(1) + ' KiB · evicted this session ' + st.droppedSession + ' (controller lifetime ' + st.dropped + ') · gaps ' + st.gaps + ' · errors ' + st.errors + (st.notStored ? ' · refused by bridge ' + st.notStored : '') + (st.heapCollision ? ' · RING DISABLED (heap collision)' : '') + (st.survivedReboot ? ' · ring survived a reboot (boot #' + st.bootCount + ')' : ''), 'dim'); }, 60000); } else { Studio.telemetry.drainer.rearm(); // NO_ACK first: learn the ring incarnation before freeing anything @@ -10826,7 +10829,7 @@

Import error

r.kind === 'cmd' ? 'CMD 0x' + r.cmd.toString(16) + ' st' + r.status + ' @' + r.tUs : r.kind === 'frame' ? 'FRAME idx' + r.idx + ' sd' + r.sdLoadUs + 'us spi' + r.spiUs + 'us' + (r.reqAgeUs != null ? ' age' + r.reqAgeUs + 'us sup' + r.superseded + (r.contiguous ? ' contig' : '') : '') + ' @' + r.tUs : r.kind === 'state' ? 'STATE ' + r.stateName + ' ' + (r.codeName || r.code) + ' ' + r.arg + ' @' + r.tUs : '?'); - const summary = { tag: tag || 'drain', records: recs.length, blocks: blocks.length, survivedReboot: d.stats.survivedReboot, bootCount: d.stats.bootCount, dropped: d.stats.dropped, gaps: d.stats.gaps, heapCollision: !!d.stats.heapCollision, tail }; + const summary = { tag: tag || 'drain', records: recs.length, blocks: blocks.length, survivedReboot: d.stats.survivedReboot, bootCount: d.stats.bootCount, dropped: d.stats.dropped, droppedSession: d.stats.droppedSession, gaps: d.stats.gaps, heapCollision: !!d.stats.heapCollision, tail }; Studio.rawLog('telemetry dump (' + summary.tag + '): ' + recs.length + ' records' + (d.stats.survivedReboot ? ' — ring survived the reboot' : '') + (tail.length ? '\n ' + tail.join('\n ') : ''), recs.length ? 'warn' : 'dim'); const b = Studio.session.bridge; if (b && b.logging) b.log({ event: 'telemetry_dump', summary, raw_blocks: rawBlocks }); diff --git a/docs/development/telemetry-logging-reference.md b/docs/development/telemetry-logging-reference.md index b3acd26a..5ed48c0b 100644 --- a/docs/development/telemetry-logging-reference.md +++ b/docs/development/telemetry-logging-reference.md @@ -193,10 +193,10 @@ Studio v0.79 (`arena_studio.html`) or the named module; the bridge only adds the | `arena_command` | `js/arena-session.js:391` | > 8-byte requests (TRIAL_PARAMS) | v1 object, see §2.2 | | `trial_quality` | `arena_studio.html:8264` ← `js/trial-quality.js:246` `summary()` | **once, at run end, before export** (v0.78) | `threshold_us`, `target_us`, `trials[]`, `counts {pass, fail, unknown, open}`, `worst_gap_us`, `flagged_trials[]`, `unknown_trials[]`, `duplicates_dropped`, `records`. Per trial: `trial`, `pattern`, `status` (`pass`\|`fail`\|`unknown`), `duration_s`, `cmds70`, `index_changes`, `reads`, `frames`, `slow_reads`, `stalls`, `max_read_ms`, `age_gaps`, `max_age_ms`, `over_target`, `superseded`, `coverage[]` | | `display_gap` | `js/trial-quality.js:101` `gapEvent` → `arena_studio.html:8479` | one per gap > threshold, as it happens | `trial`, `pattern`, `seq`, `t_us`, `ms`, `kind` ∈ `sd_slow` (+ `phase`, `read_error`) · `frame_read` (+ `idx`; a slow read seen only in the FRAME record) · `frame_age` (+ `idx`; `req_age_us` > threshold). Absent from a clean run | -| `soak` | `arena_studio.html:8317` `logSoak` | soak driver phases | `phase` ∈ `start` · `iteration-start` · `iteration-end` · `paused` · `end`; `iteration-end` carries `iteration`, `outcome`, `fault`, `stop_acked`, `postmortem`, `applied_0x70`, `duration_s`, `telemetry` (drainer stats: `ackSeq`, `lastSeq`, `records`, `blocks`, `bytes`, `gaps`, `dropped`, `errors`, `notStored`, …). **`dropped` is the controller's lifetime evicted-record counter from the block header** (1157 with `gaps: 0` in the sample = eviction before the drainer started, not in-run loss) | +| `soak` | `arena_studio.html:8317` `logSoak` | soak driver phases | `phase` ∈ `start` · `iteration-start` · `iteration-end` · `paused` · `end`; `iteration-end` carries `iteration`, `outcome`, `fault`, `stop_acked`, `postmortem`, `applied_0x70`, `duration_s`, `telemetry` (drainer stats: `ackSeq`, `lastSeq`, `records`, `blocks`, `bytes`, `gaps`, `dropped`, `droppedSession`, `errors`, `notStored`, …). **`dropped` is the controller's lifetime evicted-record counter from the block header** (1157 with `gaps: 0` in the sample = eviction before the drainer started, not in-run loss); `droppedSession` (2026-10-29) is its growth since the drainer's first block of the ring incarnation, the in-session loss, and what the Studio's once-a-minute stats line leads with ("evicted this session N (controller lifetime M)") | | `sd_diag` | `arena_studio.html:8543` | `Studio.setSdDiag()` and the v0.79 connect banner | `flags`, `in_force` (GET_SD_INFO byte 29), `label` | | `probe` | `js/studio-postmortem.js:112` | post-mortem after a controller fault | `name`, `phase`, `cmd`, `req`, `sd`, then `dt`, `resp`, `status`, `ok`, `decoded` — or `error`, or `skipped: "capability X absent"`. The health read appears here as `name:"health"` with `decoded` | -| `telemetry_dump` | `arena_studio.html:8580` | post-mortem | `summary {tag, records, blocks, survivedReboot, bootCount, dropped, gaps, heapCollision, tail[]}`, `raw_blocks[]` (hex) | +| `telemetry_dump` | `arena_studio.html:8580` | post-mortem | `summary {tag, records, blocks, survivedReboot, bootCount, dropped, droppedSession, gaps, heapCollision, tail[]}`, `raw_blocks[]` (hex) | | `crash_report` | `arena_studio.html:8554` | post-mortem, fw flag bit 3 | `present`, `fault`, `ret_addr`, `cfsr`, `hfsr`, `bfar`, `mmfar`, `raw_hex` (junk after a HalfKay reflash — known) | | `health` | `arena_studio.html:5587` | **Console `chealth` only** | `source:"console"`, `health` (decoded 0xCA) | | `analog_loopback_sweep` | `arena_studio.html:5303` | Console Analog In panel | `channel`, `rows`, `fit`, `verdict` | diff --git a/js/arena-telemetry.js b/js/arena-telemetry.js index 052d495f..33a57df5 100644 --- a/js/arena-telemetry.js +++ b/js/arena-telemetry.js @@ -294,7 +294,9 @@ blocks: 0, bytes: 0, gaps: 0, // seq discontinuities seen (should equal controller-reported drops) - dropped: 0, // controller's cumulative drop counter (last value) + dropped: 0, // controller's LIFETIME eviction counter (header value, since its boot) + droppedBase: null, // that counter at our first block of this ring incarnation + droppedSession: 0, // evictions since then = loss this drainer could have prevented errors: 0, notStored: 0, // rows the sink refused (ack withheld, will be re-read) duplicates: 0, // records re-delivered after a rearm (already stored, dropped) @@ -339,6 +341,7 @@ break; } const rx = now(); + let freshRing = false; // Ring incarnation check (review finding: an old ack after a power // cycle would free unseen records). A different boot_count with the // ring NOT marked survived ⇒ fresh ring: forget the cursor. Same @@ -346,7 +349,10 @@ if (peekNext) { peekNext = false; if (st.bootCount != null && block.bootCount !== st.bootCount) { - if (!block.survivedReboot) st.lastSeq = null; + if (!block.survivedReboot) { + st.lastSeq = null; + freshRing = true; + } st.incarnations++; } if (st.lastSeq != null) { @@ -361,6 +367,15 @@ st.bytes += block.bytes; st.lastTNowUs = block.tNowUs; st.dropped = block.dropped; + // The header counter is the controller's lifetime eviction count: it + // includes everything the ring evicted before this drainer ever asked + // (a Studio connected hours after boot sees thousands with gaps 0). + // Only the growth since our first block of this incarnation is loss + // the host could have prevented, so that is what the UI reports. + if (st.droppedBase == null || freshRing || block.dropped < st.droppedBase) { + st.droppedBase = block.dropped; + } + st.droppedSession = (block.dropped - st.droppedBase) >>> 0; st.bootCount = block.bootCount; st.survivedReboot = block.survivedReboot; st.heapCollision = block.disabledHeapCollision; @@ -417,6 +432,8 @@ function reset() { st.ackSeq = NO_ACK; st.lastSeq = null; + st.droppedBase = null; + st.droppedSession = 0; peekNext = false; } diff --git a/tests/test-arena-telemetry.js b/tests/test-arena-telemetry.js index 759d30c3..3440413b 100644 --- a/tests/test-arena-telemetry.js +++ b/tests/test-arena-telemetry.js @@ -341,6 +341,7 @@ function block(h, records) { const per = 3; const asked = []; let failNext = false; + let hdrDropped = 0; // the controller's lifetime eviction counter in the header const session = { async send(bytes) { const ack = @@ -365,7 +366,16 @@ function block(h, records) { const firstSeq = out.length ? (out[0][2] | (out[0][3] << 8) | (out[0][4] << 16) | (out[0][5] << 24)) >>> 0 : 0; - return block({ tNowUs: 999, firstSeq, more: ring.length > per, flags: 1 }, out); + return block( + { + tNowUs: 999, + firstSeq, + dropped: hdrDropped, + more: ring.length > per, + flags: 1 + }, + out + ); } }; const got = []; @@ -444,6 +454,27 @@ function block(h, records) { check('only the 2 new records counted', d.stats.records - recBefore, 2); d.reset(); check('reset forgets the cursor', [d.stats.ackSeq, d.stats.lastSeq], [0xffffffff, null]); + + // The header `dropped` is the controller's LIFETIME eviction count. A drainer that + // starts hours after boot sees thousands with no gaps: that is not in-session loss + // (ww18, 2026-10-29: "dropped 1929" read as a problem). Only growth since our first + // block counts as droppedSession. + const d3 = T.createDrainer({ session, wire: Wire, now: () => 42, onRecords: () => true }); + hdrDropped = 1929; + push(1); + await d3.drainOnce(); + check('lifetime counter kept verbatim', d3.stats.dropped, 1929); + check('nothing evicted since our first block', d3.stats.droppedSession, 0); + hdrDropped = 1931; + push(1); + await d3.drainOnce(); + check('growth since the first block is the session loss', d3.stats.droppedSession, 2); + d3.reset(); + check( + 'reset forgets the eviction baseline', + [d3.stats.droppedBase, d3.stats.droppedSession], + [null, 0] + ); } console.log('\n=== poller: single-flight, gated ==='); From f2ab4c0afd7ae56073963120adea6adc562d35ca Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:00:37 -0400 Subject: [PATCH 06/10] qc: the bergamo profile runs the public validation protocol (2P diagnostics moved to the private repo, LAB-227) Co-Authored-By: Claude Fable 5.1 --- rig/qc/profiles.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rig/qc/profiles.toml b/rig/qc/profiles.toml index 0fb5e481..82f3fe08 100644 --- a/rig/qc/profiles.toml +++ b/rig/qc/profiles.toml @@ -94,14 +94,17 @@ hosts = ["reiserlab-bergamo1"] envs = ["default"] rig_flags = ["--no-fictrac", "--scanimage"] studio_rig = "bergamo_g6_2x10_2p" -protocol = { key = "g6_2x10_2p_diagnostics", path = "protocols/g6_2x10_2p_diagnostics.yaml", duration_s = 60 } +# The 2P diagnostics protocol moved to the private rig repo (scanimage-2026-bergamo/protocols/, +# LAB-227, 2026-10-29); the tier's own validation protocol stands in until the QC runner can load +# a protocol from a private repo. Not in protocols/index.json: loaded from the tier's raw YAML. +protocol = { path = "protocols/validation/bench_closed_loop_2x10.yaml", duration_s = 25 } fictrac = { kind = "none" } requires = [["tcp", 5555, "ScanImage's JSON server (start ScanImage from a terminal MATLAB and enable the server)"]] precheck = { epochs = 0, epoch_s = 0, min_frames = 0 } reports = { closed_loop = false, holds = [] } checklist = [ { id = "fictrac", look = "No FicTrac on this rig: the gateway answers hello_ack and advertises the scanimage connector." }, - { id = "run", look = "Arena: the 2P diagnostics sequence (brightness ramp and steps, 1 Hz flash, sparse, 6 s all-off); the Run view shows the controller-settings assertion (panel mode, refresh, SPI, AO) with no manual Console steps. About 1 minute." }, + { id = "run", look = "Arena: the 2×10 validation protocol (two gratings, no closed loop on this rig); the Run view shows the controller-settings assertion (panel mode, refresh, SPI, AO) with no manual Console steps. About half a minute." }, { id = "reports", look = "runlog-check OK, bag closed." }, ] From a7dec3070056be08f841885156c0844a2127be2c Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:01:29 -0400 Subject: [PATCH 07/10] qc: bergamo stands in with the open-loop smoke protocol (the validation protocol needs FicTrac) Co-Authored-By: Claude Fable 5.1 --- rig/qc/profiles.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rig/qc/profiles.toml b/rig/qc/profiles.toml index 82f3fe08..6e8442b9 100644 --- a/rig/qc/profiles.toml +++ b/rig/qc/profiles.toml @@ -95,16 +95,16 @@ envs = ["default"] rig_flags = ["--no-fictrac", "--scanimage"] studio_rig = "bergamo_g6_2x10_2p" # The 2P diagnostics protocol moved to the private rig repo (scanimage-2026-bergamo/protocols/, -# LAB-227, 2026-10-29); the tier's own validation protocol stands in until the QC runner can load -# a protocol from a private repo. Not in protocols/index.json: loaded from the tier's raw YAML. -protocol = { path = "protocols/validation/bench_closed_loop_2x10.yaml", duration_s = 25 } +# LAB-227, 2026-10-29). The public open-loop smoke protocol (no plugins, ~50 s) stands in until the +# QC runner can load a protocol from a private repo; bench_closed_loop_2x10 needs FicTrac. +protocol = { key = "g6_2x10_smoke", path = "protocols/g6_2x10_smoke.yaml", duration_s = 50 } fictrac = { kind = "none" } requires = [["tcp", 5555, "ScanImage's JSON server (start ScanImage from a terminal MATLAB and enable the server)"]] precheck = { epochs = 0, epoch_s = 0, min_frames = 0 } reports = { closed_loop = false, holds = [] } checklist = [ { id = "fictrac", look = "No FicTrac on this rig: the gateway answers hello_ack and advertises the scanimage connector." }, - { id = "run", look = "Arena: the 2×10 validation protocol (two gratings, no closed loop on this rig); the Run view shows the controller-settings assertion (panel mode, refresh, SPI, AO) with no manual Console steps. About half a minute." }, + { id = "run", look = "Arena: the 2×10 smoke sequence (all on, all off, open-loop gratings, a frame jump, a repeated block); the run ends with outcome completed, no fault. About 50 s." }, { id = "reports", look = "runlog-check OK, bag closed." }, ] From 281efec94a50d3897576545f89ea7b0cce33119c Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:40:40 -0400 Subject: [PATCH 08/10] qc: hold checks SKIP when a fly turned the ball; rig.py handles SIGBREAK (ww18 rc1 QC findings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the first `pixi run qc` on ww18 (candidate 2026-10-29 rc1, 2026-10-29): 1. bench-check's hold checks assume a still ball. A live fly turned it −1108° in 8 s, the display followed FicTrac with 0.00 % idx mismatch, and the QC table said FAIL. New qclib.hold_verdict(): when ONLY hold lines failed and closed-loop-report shows an epoch with |net turn| >= 45°, the row is SKIP naming the turns; a hold miss on a still ball, or any other bench-check failure, stays FAIL. ww18's run/reports look text says so. Tests in test-rig-qc.py. 2. Teardown on Windows left the graph's nodes and the router as orphans ("graph stopped", then "ports free again" FAIL on 7447/8765/60000). qc.py stops rig.py with rig_nodes.supervise.signal_group(SIGINT), which on Windows is CTRL_BREAK_EVENT on the process group; Python delivers that as SIGBREAK, which rig.py did not handle, so it died before its `finally` stopped the children. rig.py now registers SIGBREAK where it exists (bridge_node and host_monitor already did). QC.md documents both. Co-Authored-By: Claude Fable 5.1 --- rig/QC.md | 6 ++++-- rig/nodes/rig.py | 11 +++++++++-- rig/qc/profiles.toml | 4 ++-- rig/qc/qc.py | 9 +++++++-- rig/qc/qclib.py | 30 ++++++++++++++++++++++++++++++ tests/test-rig-qc.py | 23 +++++++++++++++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/rig/QC.md b/rig/QC.md index d3d6a5b2..802812ee 100644 --- a/rig/QC.md +++ b/rig/QC.md @@ -62,7 +62,7 @@ steps (bridge Connect → Connect → Run experiment → confirm the NEXT dialog | 5 fictrac | FicTrac started (or found running); frames counted at the gateway; the simulator takes over when the camera gives none | FicTrac tracking; the printed frames/s | | 6 pre-check | `tests/studio_emulator.py`: the Studio's exact bridge conversation (hello_ack ≥ 3.4, behavior_v2 ack, an epoch of frames, export; on ww14 also the BIAS connector) — **a broken candidate fails here**, before any browser | all PASS | | 7 browser | Chrome (QC profile) on the tier's `arena_studio.html?mode=run&rig=…`; build SHA in the page = the candidate; footer; protocol loaded; no console errors; bridge connected with frames; **your click**; firmware + panels; **Run experiment** | the Studio; the arena during the run (the rig's `look` line says what the display should do) | -| 8 reports | `pixi run export` (closes the bag), `metadata.yaml`, `runlog-check.py`, `closed-loop-report.py` (idx mismatch ≤ 1 %), `bench-check.py` with the rig's holds + channel + build | the numbers | +| 8 reports | `pixi run export` (closes the bag), `metadata.yaml`, `runlog-check.py`, `closed-loop-report.py` (idx mismatch ≤ 1 %), `bench-check.py` with the rig's holds + channel + build. The hold checks assume a **still ball**: when only they fail and the closed-loop report shows the ball turned (a fly on it), the row is SKIP, not FAIL — the idx mismatch is the tracking evidence then | the numbers | | 9 teardown | FicTrac / simulator / graph stopped, Chrome closed, ports free again | the pass/fail table | At the end the script prints the table, writes `summary.md` and `validation-note.md` into the dated folder and @@ -96,7 +96,9 @@ after deleting by hand). The rig's own `rig\.pixi` is never re-synced by QC. - The QC Chrome profile holds no course token, so a run is **not** committed to the course repo; the log comes out through `pixi run export` into the QC folder instead. - Everything the script started is stopped in teardown (SIGINT → TERM → KILL, in reverse order) and the ports - are checked free; Ctrl-C does the same. + are checked free; Ctrl-C does the same. On Windows the "SIGINT" is CTRL_BREAK_EVENT on the child's process + group, which Python delivers as SIGBREAK — `rig.py` handles it (2026-10-29; before that the graph's nodes + and the router were left running as orphans and "ports free again" failed). ## Adding a rig diff --git a/rig/nodes/rig.py b/rig/nodes/rig.py index 1c046ac8..8a7a7141 100644 --- a/rig/nodes/rig.py +++ b/rig/nodes/rig.py @@ -142,8 +142,15 @@ def main(argv: list[str] | None = None) -> int: nodes.append(Child("sim", node_argv("rig_nodes.sims.fictrac_sim", *split_args(args.sim_args)))) stop = threading.Event() - for sig in (signal.SIGINT, signal.SIGTERM): - signal.signal(sig, lambda *_: stop.set()) + # SIGBREAK too: on Windows a parent (rig/qc/qc.py, the test harness, anything using + # rig_nodes.supervise.signal_group) stops this process with CTRL_BREAK_EVENT on its process + # group, which Python delivers as SIGBREAK, not SIGINT. Unhandled, it ends rig.py before the + # `finally` below runs, and every node + the router live on as orphans (ww18 QC, 2026-10-29: + # "graph stopped" with 7447/8765/60000 still bound). bridge_node and host_monitor handle it too. + for name in ("SIGINT", "SIGTERM", "SIGBREAK"): + sig = getattr(signal, name, None) + if sig is not None: + signal.signal(sig, lambda *_: stop.set()) print(f"[rig] RMW_IMPLEMENTATION={os.environ.get('RMW_IMPLEMENTATION')} router config {os.environ.get('ZENOH_ROUTER_CONFIG_URI')}", file=sys.stderr) rc = 0 diff --git a/rig/qc/profiles.toml b/rig/qc/profiles.toml index 6e8442b9..48199ad2 100644 --- a/rig/qc/profiles.toml +++ b/rig/qc/profiles.toml @@ -59,8 +59,8 @@ foxglove = { layout = "foxglove/arena-fictrac.json" } reports = { closed_loop = true, holds = ["epoch_a_frame25=25", "epoch_b_frame75=75"] } checklist = [ { id = "fictrac", look = "FicTrac window tracking the ball (≈ 96 fps on ww18); the gateway rate printed below matches." }, - { id = "run", look = "Arena: 3 s background, then the grating HOLDS at frame 25 while the ball is still; turn the ball and the grating follows (coupling −1); ~11 s in it jumps to frame 75 and holds again; background to finish. About 25 s." }, - { id = "reports", look = "runlog-check OK (4 pass / 0 fail), both epochs open on 25 / 75 with 0 % idx mismatch, bench-check PASS with channel + build = this candidate." }, + { id = "run", look = "Arena: 3 s background, then the grating HOLDS at frame 25 while the ball is still (bare or blocked ball); turn the ball and the grating follows (coupling −1); ~11 s in it jumps to frame 75 and holds again; background to finish. About 25 s. With a fly on the ball the grating follows the fly instead and the hold checks are skipped — the closed-loop idx mismatch is the evidence then." }, + { id = "reports", look = "runlog-check OK (4 pass / 0 fail), both epochs open on 25 / 75 with 0 % idx mismatch, bench-check PASS (SKIP on the holds when a fly turned the ball) with channel + build = this candidate." }, ] # ── ww14: okey-cokey camera rig — BIAS (FLIR BFS-U3-51S5M) + G6 four-color, no FicTrac ────────── diff --git a/rig/qc/qc.py b/rig/qc/qc.py index 83ff2076..80184974 100644 --- a/rig/qc/qc.py +++ b/rig/qc/qc.py @@ -812,6 +812,7 @@ def reports(self) -> None: else: self.results.add("runlog-check", "PASS" if r.returncode == 0 else "SKIP", ("no controller on this rig; " + " | ".join(tail))[-300:]) self.log.result(self.results.rows[-1]["verdict"], "runlog-check", self.results.rows[-1]["detail"]) + epochs_json: list = [] # closed-loop-report epochs, for the hold verdict below if self.rig["reports"].get("closed_loop"): r = run([self.py, scripts / "closed-loop-report.py", "--json", log], env=self.env, timeout=300) (rep_dir / "closed-loop-report.json").write_text(r.stdout, encoding="utf-8") @@ -822,6 +823,7 @@ def reports(self) -> None: epochs = rep.get("epochs", []) if isinstance(rep, dict) else [] mism = [e.get("idx_mismatch_fraction") for e in epochs if "idx_mismatch_fraction" in e] ok = bool(epochs) and all(m is not None and m <= 0.01 for m in mism) + epochs_json = epochs self.record("closed-loop-report: idx mismatch ≤ 1 %", ok, f"{len(epochs)} epoch(s), mismatch {mism}") except ValueError: self.record("closed-loop-report", False, (r.stderr or r.stdout)[-300:]) @@ -832,8 +834,11 @@ def reports(self) -> None: argv += ["--hold", h] r = run(argv, env=self.env, timeout=300) (rep_dir / "bench-check.txt").write_text(r.stdout + r.stderr, encoding="utf-8") - last = [line for line in r.stdout.splitlines() if line.strip()][-2:] - self.record("bench-check (holds, channel, build)", r.returncode == 0, " | ".join(last)[-300:]) + # A fly on the ball turns the hold checks into SKIP (qclib.hold_verdict), never into a FAIL + # of the candidate: the closed-loop report above is the tracking evidence then. + verdict, detail = qclib.hold_verdict(r.returncode, r.stdout, epochs_json) + self.results.add("bench-check (holds, channel, build)", verdict, detail) + self.log.result(verdict, "bench-check (holds, channel, build)", detail) self.look("reports") # ── 9 teardown ── diff --git a/rig/qc/qclib.py b/rig/qc/qclib.py index 86d5097d..616e91a8 100644 --- a/rig/qc/qclib.py +++ b/rig/qc/qclib.py @@ -241,6 +241,36 @@ def fictrac_config_for_qc(text: str, sock_port: int = 60000, sock_host: str = "1 # ── results ────────────────────────────────────────────────────────────────── +def hold_verdict(bench_rc: int, bench_stdout: str, epochs: list | None, turn_threshold_deg: float = 45.0) -> tuple[str, str]: + """Verdict + detail for the bench-check step from bench-check's exit code and output. + + The hold checks (" holds frame N") assume a STILL ball: with a fly on it the display + follows FicTrac away from the start frame by design, and only the closed-loop report's idx mismatch + says whether the candidate tracked (ww18, 2026-10-29: 0.00 % mismatch, holds "failed" with a fly + turning −1108° in 8 s). When bench-check failed ONLY on hold lines and some epoch's |net turn| + is at least `turn_threshold_deg`, the step is SKIP with the turns in the detail. Anything else + (a hold miss on a still ball, channel/build/completion failures) keeps bench-check's verdict. + """ + lines = [ln.strip() for ln in bench_stdout.splitlines() if ln.strip()] + last = " | ".join(lines[-2:])[-300:] + if bench_rc == 0: + return "PASS", last + fails = [ln for ln in lines if ln.startswith("FAIL")] + hold_fails = [ln for ln in fails if " holds frame " in ln] + turns: list[float] = [] + for e in epochs or []: + if isinstance(e, dict): + try: + turns.append(abs(float((e.get("fly") or {}).get("net_turn_deg") or 0.0))) + except (TypeError, ValueError): + pass + if fails and len(hold_fails) == len(fails) and any(t >= turn_threshold_deg for t in turns): + return "SKIP", (f"ball turned {', '.join(f'{t:.0f}°' for t in turns)} per epoch (a fly on the ball?): the hold checks " + f"assume a still ball, so they are skipped; the closed-loop idx mismatch is the tracking evidence | " + + hold_fails[0][:140]) + return "FAIL", last + + class Results: """Ordered pass/fail rows; `verdict` is PASS / FAIL / SKIP / LOOK (Michael's eyes).""" diff --git a/tests/test-rig-qc.py b/tests/test-rig-qc.py index ec2d4c2f..fa5f5c36 100644 --- a/tests/test-rig-qc.py +++ b/tests/test-rig-qc.py @@ -134,6 +134,29 @@ def main() -> int: check("validate command", cmd == 'cd /repo && pixi run tiers -- validate --rig ww18 --by "Michael" --notes-file "/qc/note.md"', cmd) check("validate command quotes a root with spaces", qclib.validate_command("/a b", "x", "y", "/n").startswith('cd "/a b" &&')) + print("== hold_verdict (bench-check with a fly on the ball)") + bench_ok = " PASS run completed\n PASS epoch_a_frame25 holds frame 25\n\n9/9 checks passed\n" + bench_holds = (" PASS run completed\n PASS channel is next\n PASS build is f49f34f\n" + " FAIL epoch_a_frame25 holds frame 25 — 13/772 frames at 25 (1.7%); most common [(30, 16), (32, 15)]\n" + " FAIL epoch_b_frame75 holds frame 75 — 7/772 frames at 75 (0.9%); most common [(78, 10), (110, 9)]\n" + "\n7/9 checks passed\n") + bench_build = " PASS run completed\n FAIL build is f49f34f — build=06bad57\n FAIL epoch_a_frame25 holds frame 25 — 1/700 frames at 25 (0.1%)\n\n7/9 checks passed\n" + fly = [{"condition": "epoch_a_frame25", "fly": {"net_turn_deg": -1107.9}, "idx_mismatch_fraction": 0.0}, + {"condition": "epoch_b_frame75", "fly": {"net_turn_deg": 140.6}, "idx_mismatch_fraction": 0.0}] + still = [{"condition": "epoch_a_frame25", "fly": {"net_turn_deg": 3.2}}, {"condition": "epoch_b_frame75", "fly": {"net_turn_deg": -1.0}}] + v, d = qclib.hold_verdict(0, bench_ok, fly) + check("hold_verdict: bench-check rc 0 is PASS whatever the ball did", v == "PASS" and "9/9" in d, f"{v} {d}") + v, d = qclib.hold_verdict(1, bench_holds, fly) + check("hold_verdict: only hold lines failed + the ball turned → SKIP naming the turns", v == "SKIP" and "1108°" in d and "141°" in d and "holds frame 25" in d, f"{v} {d}") + v, d = qclib.hold_verdict(1, bench_holds, still) + check("hold_verdict: hold miss on a still ball stays FAIL", v == "FAIL" and "7/9" in d, f"{v} {d}") + v, d = qclib.hold_verdict(1, bench_build, fly) + check("hold_verdict: a non-hold failure (build) stays FAIL even with a fly", v == "FAIL", f"{v} {d}") + v, d = qclib.hold_verdict(1, bench_holds, None) + check("hold_verdict: no closed-loop epochs → FAIL (no evidence of turning)", v == "FAIL", f"{v} {d}") + v, d = qclib.hold_verdict(1, bench_holds, [{"fly": {"net_turn_deg": "n/a"}}, {"fly": None}, "junk"]) + check("hold_verdict: tolerates malformed epochs", v == "FAIL", f"{v} {d}") + print("== qc.py") p = subprocess.run([sys.executable, str(QC / "qc.py"), "--help"], capture_output=True, text=True, timeout=60, cwd=str(ROOT / "rig")) check("qc.py --help under the root interpreter (stdlib-only module scope)", p.returncode == 0 and "--tier" in p.stdout and "--auto" in p.stdout, (p.stderr or p.stdout)[-300:]) From 9614a91a903fc1d96ad9b7b8de4a49d2368081f1 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:44:40 -0400 Subject: [PATCH 09/10] qc: the start-frame check is "epochs open on their start frame", not a whole-epoch hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Michael on the rc1 QC: with FicTrac live the ball can always move, so a check that the display HOLDS frame 25 / 75 for a whole epoch is the wrong test on a rig. What the two epochs exist to verify is the v0.91 start frame: the first post-tare index equals the trialParams frame_index (closed-loop-start-frame.md). closed-loop-report now emits `first_idx_after_tare` + `opens_on_start_frame` per epoch (and marks "200 @25 ✓" in the table); qc.py records "epochs open on their start frame" from it; the rest of the epoch is the existing idx-mismatch check. The ww18 profile drops its `holds` (bench-check still judges completion, arena acks, channel, build; `holds` stays available for a bare or blocked ball, with hold_verdict's SKIP when the ball turned). Tests in test-closed-loop-report.py cover the open on 25 / 75 and a ≤ 3.3 bridge opening on 0. Co-Authored-By: Claude Fable 5.1 --- rig/QC.md | 2 +- rig/qc/profiles.toml | 8 +++++--- rig/qc/qc.py | 17 ++++++++++++++--- scripts/closed-loop-report.py | 21 ++++++++++++++++++--- tests/test-closed-loop-report.py | 4 ++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/rig/QC.md b/rig/QC.md index 802812ee..e51710c4 100644 --- a/rig/QC.md +++ b/rig/QC.md @@ -62,7 +62,7 @@ steps (bridge Connect → Connect → Run experiment → confirm the NEXT dialog | 5 fictrac | FicTrac started (or found running); frames counted at the gateway; the simulator takes over when the camera gives none | FicTrac tracking; the printed frames/s | | 6 pre-check | `tests/studio_emulator.py`: the Studio's exact bridge conversation (hello_ack ≥ 3.4, behavior_v2 ack, an epoch of frames, export; on ww14 also the BIAS connector) — **a broken candidate fails here**, before any browser | all PASS | | 7 browser | Chrome (QC profile) on the tier's `arena_studio.html?mode=run&rig=…`; build SHA in the page = the candidate; footer; protocol loaded; no console errors; bridge connected with frames; **your click**; firmware + panels; **Run experiment** | the Studio; the arena during the run (the rig's `look` line says what the display should do) | -| 8 reports | `pixi run export` (closes the bag), `metadata.yaml`, `runlog-check.py`, `closed-loop-report.py` (idx mismatch ≤ 1 %), `bench-check.py` with the rig's holds + channel + build. The hold checks assume a **still ball**: when only they fail and the closed-loop report shows the ball turned (a fly on it), the row is SKIP, not FAIL — the idx mismatch is the tracking evidence then | the numbers | +| 8 reports | `pixi run export` (closes the bag), `metadata.yaml`, `runlog-check.py`, `closed-loop-report.py` (idx mismatch ≤ 1 %), `bench-check.py` with channel + build (+ the rig's `holds`, if any). On the FicTrac rigs the start-frame check is **"epochs open on their start frame"** from the closed-loop report (first post-tare index = the trialParams `frame_index`, 25 / 75): it works with a live fly on the ball, where a whole-epoch hold never can. `holds` (the display stays on one frame for the whole epoch) is for a bare or blocked ball only; when only hold lines fail and the ball turned, that row is SKIP | the numbers | | 9 teardown | FicTrac / simulator / graph stopped, Chrome closed, ports free again | the pass/fail table | At the end the script prints the table, writes `summary.md` and `validation-note.md` into the dated folder and diff --git a/rig/qc/profiles.toml b/rig/qc/profiles.toml index 48199ad2..5198f316 100644 --- a/rig/qc/profiles.toml +++ b/rig/qc/profiles.toml @@ -56,11 +56,13 @@ protocol = { path = "protocols/validation/bench_closed_loop_2x10.yaml", duration # `frames_probe_s`), the FicTrac simulator takes over and the report says SIM. fictrac = { kind = "real", cmd = ['C:\Lab\GitHub\fictrac\build\Release\fictrac.bat', 'C:\Lab\GitHub\fictrac\cshl\rig05_vfov28_2026-09-22\config.txt'], cwd = 'C:\Lab\GitHub\fictrac\cshl\rig05_vfov28_2026-09-22', process = "fictrac.exe", fallback = "sim" } foxglove = { layout = "foxglove/arena-fictrac.json" } -reports = { closed_loop = true, holds = ["epoch_a_frame25=25", "epoch_b_frame75=75"] } +# No whole-epoch holds: with FicTrac live the ball may always move. The epochs must OPEN on 25 / 75 +# (closed-loop-report `opens_on_start_frame`) and track with ≤ 1 % idx mismatch; `holds` is for a bare ball. +reports = { closed_loop = true, holds = [] } checklist = [ { id = "fictrac", look = "FicTrac window tracking the ball (≈ 96 fps on ww18); the gateway rate printed below matches." }, - { id = "run", look = "Arena: 3 s background, then the grating HOLDS at frame 25 while the ball is still (bare or blocked ball); turn the ball and the grating follows (coupling −1); ~11 s in it jumps to frame 75 and holds again; background to finish. About 25 s. With a fly on the ball the grating follows the fly instead and the hold checks are skipped — the closed-loop idx mismatch is the evidence then." }, - { id = "reports", look = "runlog-check OK (4 pass / 0 fail), both epochs open on 25 / 75 with 0 % idx mismatch, bench-check PASS (SKIP on the holds when a fly turned the ball) with channel + build = this candidate." }, + { id = "run", look = "Arena: 3 s background, then the grating OPENS at frame 25 and follows the ball (coupling −1; a still ball keeps it there); ~11 s in it opens at frame 75 and follows again; background to finish. About 25 s." }, + { id = "reports", look = "runlog-check OK (4 pass / 0 fail), both epochs open on 25 / 75 (first post-tare index) with 0 % idx mismatch, bench-check PASS with channel + build = this candidate." }, ] # ── ww14: okey-cokey camera rig — BIAS (FLIR BFS-U3-51S5M) + G6 four-color, no FicTrac ────────── diff --git a/rig/qc/qc.py b/rig/qc/qc.py index 80184974..0361c5df 100644 --- a/rig/qc/qc.py +++ b/rig/qc/qc.py @@ -825,10 +825,20 @@ def reports(self) -> None: ok = bool(epochs) and all(m is not None and m <= 0.01 for m in mism) epochs_json = epochs self.record("closed-loop-report: idx mismatch ≤ 1 %", ok, f"{len(epochs)} epoch(s), mismatch {mism}") + # THE start-frame check that works with a live fly on the ball: each epoch's first + # post-tare index is its trialParams frame_index (25 / 75 in bench_closed_loop_2x10). + # A whole-epoch "hold" cannot pass while FicTrac is live (the ball may always move). + sf = [e for e in epochs if isinstance(e, dict) and e.get("start_frame") is not None] + if sf: + opened = [e.get("opens_on_start_frame") for e in sf] + det = ", ".join(f"{e.get('condition')} {e.get('first_idx_after_tare')}→{e.get('start_frame')}" for e in sf) + self.record("epochs open on their start frame", all(o is True for o in opened), det) except ValueError: self.record("closed-loop-report", False, (r.stderr or r.stdout)[-300:]) holds = self.rig["reports"].get("holds") or [] - if holds and self.studio_ran: + if self.studio_ran: + # holds (whole-epoch "display stays on frame N") only for a bare / blocked ball; the live-rig + # profiles leave it empty and bench-check still judges completion, arena acks, channel, build. argv = [self.py, scripts / "bench-check.py", log, "--channel", self.channel, "--build", self.sha[:7]] for h in holds: argv += ["--hold", h] @@ -837,8 +847,9 @@ def reports(self) -> None: # A fly on the ball turns the hold checks into SKIP (qclib.hold_verdict), never into a FAIL # of the candidate: the closed-loop report above is the tracking evidence then. verdict, detail = qclib.hold_verdict(r.returncode, r.stdout, epochs_json) - self.results.add("bench-check (holds, channel, build)", verdict, detail) - self.log.result(verdict, "bench-check (holds, channel, build)", detail) + label = "bench-check (holds, channel, build)" if holds else "bench-check (completion, arena acks, channel, build)" + self.results.add(label, verdict, detail) + self.log.result(verdict, label, detail) self.look("reports") # ── 9 teardown ── diff --git a/scripts/closed-loop-report.py b/scripts/closed-loop-report.py index 7260a92e..cd983bdf 100644 --- a/scripts/closed-loop-report.py +++ b/scripts/closed-loop-report.py @@ -271,9 +271,9 @@ def analyze_epoch(log: dict, ep: dict, deg_per_frame: float, frame_dir: int, az0 if bias is None: bias_t0 = _latest_before([(t, t) for t, _ in log["bias"]], ep["start_ms"], ep["start_ms"]) bias = _latest_before(log["bias"], ep["start_ms"], {"type": "none", "amplitude": 0.0, "frequency": 0.0}) - _, hd0 = _last_within(log["tares"], ep["start_ms"], ep["start_ms"] + 1500.0) + tare_t, hd0 = _last_within(log["tares"], ep["start_ms"], ep["start_ms"] + 1500.0) if hd0 is None: - _, hd0 = _last_within(log["tares"], ep["start_ms"], ep["end_ms"]) + tare_t, hd0 = _last_within(log["tares"], ep["start_ms"], ep["end_ms"]) out = { "condition": ep.get("condition"), "start_s": round(ep["start_ms"] / 1000.0, 2), "duration_s": round((ep["end_ms"] - ep["start_ms"]) / 1000.0, 2), "rows": len(rows), @@ -287,6 +287,17 @@ def analyze_epoch(log: dict, ep: dict, deg_per_frame: float, frame_dir: int, az0 ms = [r[0] for r in rows] hd = [r[3] for r in rows] idx = [r[2] for r in rows] + if start_frame is not None: + # Does the epoch OPEN on its start frame? The first row at/after the tare has zero turn since + # the tare, so the bridge's index there is round(offset / pitch) = start_frame by construction + # (closed-loop-start-frame.md); a ≤ 3.3 bridge opens on frame 0 instead. This is the QC check + # that works with a live fly on the ball — the rest of the epoch is the idx mismatch below. + i0 = next((i for i, m in enumerate(ms) if tare_t is not None and m >= tare_t), 0) + first = idx[i0] if i0 < len(idx) and _num(idx[i0]) is not None else None + if first is not None: + d = (int(first) - int(start_frame)) % frames + out["first_idx_after_tare"] = int(first) + out["opens_on_start_frame"] = min(d, frames - d) <= 1 rel_hd = unwrap_deg(hd) dur_s = max((ms[-1] - ms[0]) / 1000.0, 1e-6) omegas = [(rel_hd[i] - rel_hd[i - 1]) / max((ms[i] - ms[i - 1]) / 1000.0, 1e-3) for i in range(1, len(rows))] @@ -363,7 +374,11 @@ def _kpitch(e): def _frames_label(e): """'200' — or '200 @25' when a bridge-3.4 start_frame opened the epoch off frame 0.""" - return f"{e['frames']}" if e.get("start_frame") is None else f"{e['frames']} @{e['start_frame']}" + if e.get("start_frame") is None: + return f"{e['frames']}" + opened = e.get("opens_on_start_frame") + mark = "" if opened is None else (" ✓" if opened else f" ✗ opened on {e.get('first_idx_after_tare')}") + return f"{e['frames']} @{e['start_frame']}{mark}" def _bias_label(b): diff --git a/tests/test-closed-loop-report.py b/tests/test-closed-loop-report.py index cbbe934e..e13c8e97 100644 --- a/tests/test-closed-loop-report.py +++ b/tests/test-closed-loop-report.py @@ -147,6 +147,8 @@ def synth_start_frame_log(path, epochs, ignore_start_frame=False, coupling=-1.0, check("three epochs", len(S), 3) check("start_frame per epoch (mod frames; none on a plain epoch)", [e["start_frame"] for e in S], [25, 75, None]) check("offset per epoch = start_frame × pitch", [e["offset_deg"] for e in S], [45.0, 135.0, 0.0]) + check("epochs open on their start frame (first post-tare idx; none on a plain epoch)", + [(e.get("first_idx_after_tare"), e.get("opens_on_start_frame")) for e in S], [(25, True), (75, True), (None, None)]) check("idx reconstruction exact on start_frame epochs (was 100 % before bridge-3.4 support)", [e["idx_mismatch_fraction"] for e in S], [0.0, 0.0, 0.0]) md = rep.to_markdown(rep.analyze(p_sf, 0.0, 1, 0.0)) @@ -157,6 +159,8 @@ def synth_start_frame_log(path, epochs, ignore_start_frame=False, coupling=-1.0, synth_start_frame_log(p_old, eps, ignore_start_frame=True) O = rep.analyze(p_old, 0.0, 1, 0.0)["epochs"] check("idx that ignored start_frame → mismatch on those epochs only", [e["idx_mismatch_fraction"] for e in O], [1.0, 1.0, 0.0]) + check("a ≤ 3.3 bridge opens on frame 0 → opens_on_start_frame False, with the frame it opened on", + [(e.get("first_idx_after_tare"), e.get("opens_on_start_frame")) for e in O], [(0, False), (0, False), (None, None)]) print("=== closed-loop-report on synthetic runs ===") From 41c1f51d4d2b426203db8413873919b9959c6521 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:45:14 -0400 Subject: [PATCH 10/10] =?UTF-8?q?release:=202026-10-29=20rc3=20=E2=80=94?= =?UTF-8?q?=20Arena=20Studio=20v0.101?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Candidate for Production. PRs: #286, #285, #288, #289. Release-Manifest: releases/2026-10-29.json --- arena_studio.html | 2 +- .../development/arena-studio-release-notes.md | 9 ++++ releases/2026-10-29.json | 48 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 releases/2026-10-29.json diff --git a/arena_studio.html b/arena_studio.html index 9e1c1e2e..883d0026 100644 --- a/arena_studio.html +++ b/arena_studio.html @@ -3426,7 +3426,7 @@

Import error