From 612f082c0e3e64c8f012e2c2b487dc082d5f22fc Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Wed, 28 Oct 2026 17:16:24 -0400 Subject: [PATCH 1/7] rig: pin rig-nodes 0.3.0, requires-pixi >= 0.80, macOS floor on the platform (#280, LAB-338) Manifest only; the re-lock with pixi 0.83 follows once the v0.3.0 channel is tagged. Co-Authored-By: Claude Fable 5.1 --- rig/pixi.toml | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/rig/pixi.toml b/rig/pixi.toml index 8f058117..2abdab34 100644 --- a/rig/pixi.toml +++ b/rig/pixi.toml @@ -23,14 +23,18 @@ [workspace] name = "rig" version = "0.1.0" -channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel"] -platforms = ["osx-arm64", "win-64", "linux-64"] -preview = ["pixi-build"] - +channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.3.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). -[system-requirements] -macos = "14.0" +# Declared on the platform (pixi ≥ 0.80 virtual packages), not as the deprecated [system-requirements]; pixi +# requires an entry carrying virtual packages to have a `name` distinct from its subdir, and this is the name +# pixi 0.80 itself wrote on ww18 when it rewrote the 0.69 lock (#280). +platforms = [{ name = "osx-arm64-macos-14-0", platform = "osx-arm64", macos = "14.0" }, "win-64", "linux-64"] +preview = ["pixi-build"] +# An older pixi must refuse rather than rewrite pixi.lock in an older format: the lock is written by +# pixi ≥ 0.80 (issue #280 — ww18 on 0.80 rewrote a 0.69 lock in place, 1095 lines, and left the checkout +# dirty). Rigs install with `pixi install --locked`. +requires-pixi = ">=0.80" [activation.env] # Every ROS 2 process in this workspace uses the Zenoh middleware. rmw_zenoh needs @@ -77,13 +81,16 @@ 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) and their simulators, the MCAP recorder and -# indexer, the CDR decoder, the Foxglove rewriter, the supervision primitives rig.py composes with. +# 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). # Written into every run's record by the gateway (hello_ack + /arena/session `packages`). -rig_nodes = "==0.2.1" +rig_nodes = "==0.3.0" # ── typed messages = the OPT-IN `typed` environment ─────────────────────────────────────── -# `pixi install -e typed` adds rig_interfaces (FicTracFrame, ArenaFrame, ConnectorReply) from the +# `pixi install -e typed` adds rig_interfaces (FicTracFrame, ArenaFrame, ConnectorReply, HostStats + its +# DiskStats/NetStats/GpuStats/ProcStats records) from the # SAME channel, built in rig-nodes' CI for win-64 / osx-arm64 / linux-64 — no compiler, no build # backend, no MAX_PATH (docs in rig-nodes: docs/typed-distribution-decision.md). Same solve group # as the default environment: since the package pins the 0.19 set too, the two environments share @@ -91,7 +98,7 @@ rig_nodes = "==0.2.1" # 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.2.1" +rig_interfaces = "==0.3.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 2218d39e6357deac8aac20aee007f5f4cd343c73 Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Wed, 28 Oct 2026 18:05:28 -0400 Subject: [PATCH 2/7] rig: re-lock on rig-nodes 0.3.0 with pixi 0.83; the channel pin in a `channel` feature (default/typed/viz), dev from git alone Channel read at the payload commit 5e20dee (the v0.3.0 tag sits one commit early; moving it is a one-liner for Michael). `dev` no longer includes `typed` and the rig_nodes pin is no longer a workspace dependency: one package name from a git source and the channel does not solve once the released version is ahead of rig-nodes main. Verified on the Mac with the sibling rig-nodes checkout renamed away: `pixi install --locked` (default + typed), check-typed lists the eight message types, `pixi run test` 59/59, `pixi run -e typed test` 62/62. Closes #280. LAB-338. Co-Authored-By: Claude Fable 5.1 --- rig/pixi.lock | 761 +++++++++++++++++++++++--------------------------- rig/pixi.toml | 24 +- 2 files changed, 369 insertions(+), 416 deletions(-) diff --git a/rig/pixi.lock b/rig/pixi.lock index f8689eed..c23ddf56 100644 --- a/rig/pixi.lock +++ b/rig/pixi.lock @@ -1,14 +1,27 @@ version: 7 platforms: - name: linux-64 -- name: osx-arm64 + virtual-packages: + - __unix=0=0 + - __linux=4.18 + - __glibc=2.28 + - __archspec=0=x86_64 +- name: osx-arm64-macos-14-0 + subdir: osx-arm64 + virtual-packages: + - __osx=14.0 + - __unix=0=0 + - __archspec=0=m1 - name: win-64 + virtual-packages: + - __win=10.0 + - __archspec=0=x86_64 environments: default: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -21,7 +34,7 @@ environments: - 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/cairo-1.18.6-h3c89d7e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.4-hff7ac6b_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/console_bridge-1.0.2-h924138e_1.tar.bz2 - 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/cryptography-50.0.2-py311hc91d8b8_0.conda @@ -45,7 +58,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.52-ha5ea40c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-15.0.0-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-hfc2019e_3.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 @@ -86,10 +99,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-platform-2.95.8-hf91fcae_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-posh-2.95.8-hb5873e6_5.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/libjpeg-turbo-3.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.1-h7cc23a3_0.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.68.1-h74cf4be_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/libpango-1.58.2-hce4e988_3.conda @@ -145,7 +158,7 @@ environments: - 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/urdfdom-6.0.2-hf2e7533_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.3-py312h89dfda2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-hebe6cf0_1.conda @@ -211,10 +224,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -448,8 +461,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - osx-arm64: + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.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 - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -489,10 +502,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -511,7 +524,7 @@ environments: - 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/cairo-1.18.6-h580cee5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.1-py312hc892d8b_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.4-hf91ab31_0.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/console_bridge-1.0.2-h3e96240_1.tar.bz2 - 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/cryptography-50.0.2-py311hc28ac56_0.conda @@ -534,7 +547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-15.0.0-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-h5ce3bf5_3.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/lerc-4.2.0-h1eee2c3_0.conda @@ -562,10 +575,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiceoryx-posh-2.95.8-hbfce10a_5.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/libjpeg-turbo-3.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.1-hb001647_0.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/liblzma-5.8.3-h8088a28_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h435687b_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/libpango-1.58.2-hc2ac4f2_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpango-devel-1.58.2-haa2d88b_3.conda @@ -848,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.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 @@ -888,10 +901,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -908,7 +921,7 @@ environments: - 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/cairo-1.18.6-h477c42c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.1-py312he06e257_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/console_bridge-1.0.2-h5362a0b_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py312h9df87ca_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-50.0.2-py311h67a9792_0.conda @@ -954,7 +967,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-11_hf9ab0e9_mkl.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/libpango-1.58.2-h02b4fad_3.conda @@ -1003,7 +1016,7 @@ environments: - 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/uncrustify-0.83.0-h5112557_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom-6.0.2-h37f4996_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_2.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 @@ -1247,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.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/v0.2.1/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -1266,7 +1279,7 @@ environments: - 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/cairo-1.18.6-h3c89d7e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.4-hff7ac6b_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/conda-gcc-specs-15.3.0-h8846f6e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/console_bridge-1.0.2-h924138e_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/cppcheck-2.22.0-py312h3cdd867_1.conda @@ -1296,7 +1309,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-15.3.0-hc6a0c74_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-15.3.0-h90d9265_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-15.0.0-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-hfc2019e_3.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 @@ -1337,10 +1350,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-platform-2.95.8-hf91fcae_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-posh-2.95.8-hb5873e6_5.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/libjpeg-turbo-3.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.1-h7cc23a3_0.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.68.1-h74cf4be_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/libpango-1.58.2-hce4e988_3.conda @@ -1398,7 +1411,7 @@ environments: - 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/urdfdom-6.0.2-hf2e7533_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.3-py312h89dfda2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-hebe6cf0_1.conda @@ -1467,10 +1480,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -1709,9 +1722,9 @@ 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[806fd8b9] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[d35d99f3] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - osx-arm64: + - 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 + 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 - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -1754,10 +1767,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -1784,7 +1797,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-21.1.8-default_hc11f16d_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-21.1.8-default_cfg_h7f0254a_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-21.1.8-default_hc11f16d_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.4-hf91ab31_0.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-21.1.8-hce30654_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt21-21.1.8-hdb3d66b_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/console_bridge-1.0.2-h3e96240_1.tar.bz2 @@ -1810,7 +1823,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-15.0.0-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-h5ce3bf5_3.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-956.6-llvm21_1_h5d6df6c_6.conda @@ -1845,12 +1858,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiceoryx-posh-2.95.8-hbfce10a_5.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/libjpeg-turbo-3.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.1-hb001647_0.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/libllvm21-21.1.8-h759d1ac_1.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/libnghttp2-1.68.1-h435687b_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/libpango-1.58.2-hc2ac4f2_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpango-devel-1.58.2-haa2d88b_3.conda @@ -2143,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[d2cb8de4] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[37bf213e] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + - 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 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 @@ -2184,10 +2197,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -2205,7 +2218,7 @@ environments: - 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/cairo-1.18.6-h477c42c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.1-py312he06e257_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/console_bridge-1.0.2-h5362a0b_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py312h9df87ca_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-50.0.2-py311h67a9792_0.conda @@ -2252,7 +2265,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-11_hf9ab0e9_mkl.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/libpango-1.58.2-h02b4fad_3.conda @@ -2302,12 +2315,12 @@ environments: - 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/uncrustify-0.83.0-h5112557_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom-6.0.2-h37f4996_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_2.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/vs2015_runtime-14.51.36247-h633cb9f_41.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_41.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_42.conda - conda: https://conda.anaconda.org/conda-forge/win-64/websockets-17.3-py312h5247d40_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-hd74fcf3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-hd74fcf3_1.conda @@ -2551,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[4fc1ccdb] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - - conda_source: rig_nodes[1e0a2c3d] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + - 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 typed: channels: - url: https://conda.anaconda.org/conda-forge/ - url: https://conda.anaconda.org/robostack-jazzy/ - - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -2570,7 +2583,7 @@ environments: - 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/cairo-1.18.6-h3c89d7e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.4-hff7ac6b_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/console_bridge-1.0.2-h924138e_1.tar.bz2 - 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/cryptography-50.0.2-py311hc91d8b8_0.conda @@ -2594,7 +2607,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.52-ha5ea40c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-15.0.0-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-hfc2019e_3.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 @@ -2635,10 +2648,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-platform-2.95.8-hf91fcae_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-posh-2.95.8-hb5873e6_5.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/libjpeg-turbo-3.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.1-h7cc23a3_0.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.68.1-h74cf4be_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/libpango-1.58.2-hce4e988_3.conda @@ -2694,7 +2707,7 @@ environments: - 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/urdfdom-6.0.2-hf2e7533_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.3-py312h89dfda2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-hebe6cf0_1.conda @@ -2760,10 +2773,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -2998,9 +3011,9 @@ 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/v0.2.1/channel/linux-64/rig_interfaces-0.2.1-hb0f4dca_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - osx-arm64: + - 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 + 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 - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -3040,10 +3053,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -3062,7 +3075,7 @@ environments: - 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/cairo-1.18.6-h580cee5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.1-py312hc892d8b_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.4-hf91ab31_0.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/console_bridge-1.0.2-h3e96240_1.tar.bz2 - 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/cryptography-50.0.2-py311hc28ac56_0.conda @@ -3085,7 +3098,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-15.0.0-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-h5ce3bf5_3.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/lerc-4.2.0-h1eee2c3_0.conda @@ -3113,10 +3126,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiceoryx-posh-2.95.8-hbfce10a_5.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/libjpeg-turbo-3.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.1-hb001647_0.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/liblzma-5.8.3-h8088a28_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h435687b_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/libpango-1.58.2-hc2ac4f2_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpango-devel-1.58.2-haa2d88b_3.conda @@ -3400,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/osx-arm64/rig_interfaces-0.2.1-h60d57d3_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/5e20dee381061832377d5a51c767aaf67a20c53c/channel/osx-arm64/rig_interfaces-0.3.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 @@ -3441,10 +3454,10 @@ environments: - 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.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -3461,7 +3474,7 @@ environments: - 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/cairo-1.18.6-h477c42c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.1-py312he06e257_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/console_bridge-1.0.2-h5362a0b_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py312h9df87ca_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-50.0.2-py311h67a9792_0.conda @@ -3507,7 +3520,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-11_hf9ab0e9_mkl.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/libpango-1.58.2-h02b4fad_3.conda @@ -3556,7 +3569,7 @@ environments: - 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/uncrustify-0.83.0-h5112557_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom-6.0.2-h37f4996_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_2.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 @@ -3801,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/win-64/rig_interfaces-0.2.1-ha41a7fa_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/5e20dee381061832377d5a51c767aaf67a20c53c/channel/win-64/rig_interfaces-0.3.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/v0.2.1/channel/ + - url: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -3821,7 +3834,7 @@ environments: - 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/cairo-1.18.6-h3c89d7e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.4-hff7ac6b_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/console_bridge-1.0.2-h924138e_1.tar.bz2 - 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/cryptography-50.0.2-py311hc91d8b8_0.conda @@ -3849,7 +3862,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.52-ha5ea40c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-15.0.0-ha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-hfc2019e_3.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 @@ -3897,12 +3910,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-platform-2.95.8-hf91fcae_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiceoryx-posh-2.95.8-hb5873e6_5.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/libjpeg-turbo-3.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.1-h7cc23a3_0.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/libllvm22-22.1.8-h474f4eb_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm23-23.1.4-h474f4eb_1.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.68.1-h74cf4be_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/libntlm-1.8-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda @@ -3977,7 +3990,7 @@ environments: - 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/urdfdom-6.0.2-hf2e7533_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.3-py312h89dfda2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda @@ -4052,10 +4065,10 @@ environments: - 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/pyqt-builder-1.18.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -4308,8 +4321,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - osx-arm64: + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.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 - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -4352,10 +4365,10 @@ environments: - 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/pyqt-builder-1.18.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -4375,7 +4388,7 @@ environments: - 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/cairo-1.18.6-h580cee5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.1-py312hc892d8b_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.4-hf91ab31_0.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/console_bridge-1.0.2-h3e96240_1.tar.bz2 - 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/cryptography-50.0.2-py311hc28ac56_0.conda @@ -4402,7 +4415,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-15.0.0-hce30654_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-h5ce3bf5_3.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/lerc-4.2.0-h1eee2c3_0.conda @@ -4434,12 +4447,12 @@ environments: - 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/libintl-devel-0.25.1-hae62183_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.1-hb001647_0.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/libllvm18-18.1.8-default_h8e162e0_12.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/libnghttp2-1.68.1-h435687b_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/libntlm-1.8-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.34-openmp_h5e6e99c_1.conda @@ -4753,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.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 @@ -4796,10 +4809,10 @@ environments: - 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/pyqt-builder-1.18.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.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-gil-3.12.16-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-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/rosdistro-1.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rospkg-1.6.3-pyh5ded981_0.conda @@ -4817,7 +4830,7 @@ environments: - 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/cairo-1.18.6-h477c42c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.1-py312he06e257_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/console_bridge-1.0.2-h5362a0b_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py312h9df87ca_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cryptography-50.0.2-py311h67a9792_0.conda @@ -4866,7 +4879,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.1-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-11_hf9ab0e9_mkl.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/libogg-1.3.5-h2466b09_1.conda @@ -4922,7 +4935,7 @@ environments: - 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/uncrustify-0.83.0-h5112557_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom-6.0.2-h37f4996_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_2.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 @@ -5184,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/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda + - conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/noarch/rig_nodes-0.3.0-pyh4616a5c_0.conda packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -5386,48 +5399,27 @@ packages: run_exports: {} size: 302937 timestamp: 1788485303634 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.3-hff7ac6b_0.conda - sha256: a6d21b5166cd7a44fa975e541f1abf851579a8ac3458e72e77125761da06c842 - md5: e9d08c8f9a02853bfc6f8c3a4b0b19a4 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.5-hff7ac6b_0.conda + sha256: 2fdc108bc51a8845fc1c25e1cb78f6781bdce439f637ff850c2fa9a5a92c56f6 + md5: e18e63b15ab8bf430536d4f76abef6ad depends: - - libgcc >=15 - __glibc >=2.17,<3.0.a0 + - libgcc >=15 - libstdcxx >=15 + - libzlib >=1.3.2,<2.0a0 - ncurses >=6.6,<7.0a0 + - libcurl >=8.23.0,<9.0a0 - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.8.1,<3.0a0 - liblzma >=5.8.3,<6.0a0 - - libcurl >=8.21.0,<9.0a0 + - libexpat >=2.9.0,<3.0a0 - rhash >=1.4.6,<2.0a0 - libuv >=1.52.1,<2.0a0 - - libzlib >=1.3.2,<2.0a0 - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD run_exports: {} - size: 26077721 - timestamp: 1787711119591 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-4.4.4-hff7ac6b_0.conda - sha256: 1c04749bc4a45d5712ba81d7bf38204f6f3a53803b8c223571e7a0a1e1dbbebf - md5: f58ffc242c3ecbf9753d1f7dc6b91276 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=15 - - libstdcxx >=15 - - ncurses >=6.6,<7.0a0 - - libexpat >=2.8.5,<3.0a0 - - libcurl >=8.22.0,<9.0a0 - - rhash >=1.4.6,<2.0a0 - - liblzma >=5.8.3,<6.0a0 - - libzlib >=1.3.2,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - - libuv >=1.52.1,<2.0a0 - - bzip2 >=1.0.8,<2.0a0 - license: BSD-3-Clause - license_family: BSD - run_exports: {} - size: 26080138 - timestamp: 1791001096509 + size: 26078149 + timestamp: 1793157027565 - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-gcc-specs-15.3.0-h8846f6e_7.conda sha256: 395a5928fc78e8545afdb4e9bf074cf90e37ff8287a1f34cd98c9350da6c3095 md5: da715e003b8f6e6737923cda77f67dbf @@ -6080,14 +6072,13 @@ packages: - libharfbuzz >=15.0.0 size: 11075 timestamp: 1792769341313 -- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda - sha256: 6d7e6e1286cb521059fe69696705100a03b006efb914ffe82a2ae97ecbae66b7 - md5: 129e404c5b001f3ef5581316971e3ea0 +- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-hfc2019e_3.conda + sha256: 60239c0d075243791360503c4397cecadecc923a71f585c1d08bd7bdc0cb0992 + md5: 49b16dd4ce2635d4c5d1a11b2908af47 license: GPL-2.0-or-later - license_family: GPL run_exports: {} - size: 17625 - timestamp: 1771539597968 + size: 15244 + timestamp: 1793205203043 - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda sha256: 9f07834f0c546ab14d885ce0366285f61f44e326c0edd1fc63b8294e113ae432 md5: 72a381cbad04f24b1c2a43ef707f45b4 @@ -6324,25 +6315,6 @@ packages: - libcups >=2.3.3,<2.4.0a0 size: 4518030 timestamp: 1770902209173 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda - sha256: 7d243f45a48f62cb8e3b871dd336137fe0fb90094a191756fb553508837f6338 - md5: 2c1e55d695b11525c760b486ac0be517 - depends: - - __glibc >=2.17,<3.0.a0 - - krb5 >=1.22.2,<1.23.0a0 - - libgcc >=14 - - libnghttp2 >=1.68.1,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.7,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl - license_family: MIT - run_exports: - weak: - - libcurl >=8.21.0,<9.0a0 - size: 478914 - timestamp: 1782802520033 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.23.0-ha042cf0_0.conda sha256: d2db14d80efb4f2e76e76561c633f1767f7543987f2bcb54a067a9d264d432ab md5: 0ad16151226df493aed967f0efce0e80 @@ -6797,20 +6769,20 @@ packages: - libiconv >=1.18,<2.0a0 size: 789471 timestamp: 1787033836207 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_1.conda - sha256: bba8538e6538ed58a8479b332337b96986561f975d06cfa2039a016c2d246ee4 - md5: 898d1c9793eaa52efc4727bd84d2e39a +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.1-h7cc23a3_0.conda + sha256: 2f7d092a3212884825c43814575225fde8a9190611bc0e38c55e0dae45e329ac + md5: 5fd2b2218f7c28c71e95738a4fffaf78 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=14 + - libgcc >=15 constrains: - jpeg <0.0.0a license: IJG AND BSD-3-Clause AND Zlib run_exports: weak: - - libjpeg-turbo >=3.2.0,<4.0a0 - size: 650434 - timestamp: 1785896381946 + - libjpeg-turbo >=3.2.1,<4.0a0 + size: 653905 + timestamp: 1793157024388 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda build_number: 11 sha256: 1ad3f43c9319ef398a8ef45989f4903a06e3965b51e77fc669a4c894168a8411 @@ -6889,25 +6861,25 @@ packages: run_exports: {} size: 92759 timestamp: 1786650399772 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h74cf4be_1.conda - sha256: 11a2f54a6f391e25738bce0023e6ef499600147bbcf21c1fa69d2e251b03cc6a - md5: 75d211f04ac87506f1f43420712a69fe +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.70.0-hc5e5977_0.conda + sha256: 9756c04ea017f18a3b0dfd8580564e6e6c13273191eb41a0e9f20a0fd3a9ec05 + md5: 1a2439373d982a874d6e3a50702099bd depends: - - __glibc >=2.17,<3.0.a0 - - c-ares >=1.34.8,<2.0a0 - - libev >=4.33,<4.34.0a0 - libev >=4.33,<5.0a0 - - libgcc >=15 - libstdcxx >=15 + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - openssl >=3.5.9,<4.0a0 + - c-ares >=1.34.9,<2.0a0 + - libev >=4.33,<4.34.0a0 - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT run_exports: weak: - - libnghttp2 >=1.68.1,<2.0a0 - size: 649974 - timestamp: 1787177490105 + - libnghttp2 >=1.70.0,<2.0a0 + size: 832943 + timestamp: 1793191292795 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 md5: d864d34357c3b65a4b731f78c0801dc4 @@ -8258,18 +8230,18 @@ packages: - urdfdom >=6.0.2,<7.0a0 size: 109685 timestamp: 1792669607977 -- conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_1.conda - sha256: 3cd3b0bda2828275bf24cfb1b20c8d304dc0213d801071341ec71b089ca2920c - md5: dc46bb96e69b340067324e235a180c1b +- conda: https://conda.anaconda.org/conda-forge/linux-64/urdfdom_headers-3.0.1-h4dbf13b_2.conda + sha256: ecd60c94da7a234288b4835ae42811dee756544fa33fcd1777abdebcb37131bc + md5: 7a7fd5f799e9f2d7894c8561d7ce92ca depends: + - __glibc >=2.17,<3.0.a0 - libstdcxx >=15 - libgcc >=15 - - __glibc >=2.17,<3.0.a0 license: BSD-3-Clause license_family: BSD run_exports: {} - size: 22402 - timestamp: 1787006267023 + size: 22413 + timestamp: 1793089272470 - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.13.6-h535ca38_0.conda sha256: bf95a4a9f5bf9c462a2f8c1844f5d7ca201e1829d6b530c1b2d586e9a065743a md5: b30bbbf08f32017f9bc857119b1636dc @@ -9058,9 +9030,9 @@ packages: run_exports: {} size: 4059 timestamp: 1762351264405 -- conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.32.4-pyh5ded981_0.conda - sha256: 7c26a93ef28b1bebadd012ca403b338e716c378485de783cee1a490d15d409a8 - md5: 29f56fdb658cdccdda59fd575765a19f +- conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.33.0-pyh5ded981_0.conda + sha256: c8d6e3b4fe83dbe1a78396bbb29bd6e982504f03c42994680d5d45f166a73d80 + md5: a1d32b260d637aeb2c237a47ee732944 depends: - packaging >=24.2 - pathspec >=0.10.1 @@ -9073,8 +9045,8 @@ packages: license: MIT license_family: MIT run_exports: {} - size: 63070 - timestamp: 1789948339002 + size: 62916 + timestamp: 1793191159729 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-10.0.0-pyh5ded981_0.conda sha256: 43151c94c02fa67e8105cfe64c8c27a1b260b22bae6ddbe9c33b56a9c302e459 md5: 8401f0ee3de84b5a549c47cf3038909b @@ -9429,26 +9401,25 @@ packages: run_exports: {} size: 2952282 timestamp: 1766858321453 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda - sha256: 430051d80765207a7d782b2b188230ba1489d35c6e75fd9903f76cb9fda4af16 - md5: 64c98a12c4e23eb238bf66bbecafdf3c +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.2.0-pyhc364b38_1.conda + sha256: a9cf3571e80a4e86992502068eb1d29a109e88345c39babf03b10450895056af + md5: f0cfded84ced11a9dfb3790c4c1c0b63 depends: - colorama - - pygments >=2.7.2 - python >=3.10 - - iniconfig >=1.0.1 - - packaging >=22 - - pluggy >=1.5,<2 - - tomli >=1 - exceptiongroup >=1 + - iniconfig >=2 + - packaging >=24 + - pluggy >=1.6,<2 + - pygments >=2.15 + - tomli >=2 - python constrains: - pytest-faulthandler >=2 license: MIT - license_family: MIT run_exports: {} - size: 306724 - timestamp: 1782127176429 + size: 326683 + timestamp: 1793214631903 - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-3.0.0-pyh5ded981_0.conda sha256: 8ad3e3fd71fa3881081a38d660cd9cc7bd18fd4eb99cee96225903e5e2eb9ae9 md5: d75c60076c9f2cfa4a459e9f17cecd93 @@ -9472,17 +9443,16 @@ packages: run_exports: {} size: 47268 timestamp: 1793014321349 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.5-pyh5ded981_0.conda - sha256: b66b783be882069a9f6c00bc44ee561fc3dfba21fe4bab6de31599caf3c63154 - md5: 4a0ae2a06b469e6f8ca95d0655e31c86 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.6-pyh5ded981_0.conda + sha256: bb29725d0d84f5e98f221f2f0afbad4a44cd67b016ab48388592c940bbd75a5e + md5: cddcce875e2021c3a5b9dbecffb70d92 depends: - python >=3.11 - python license: Apache-2.0 - license_family: APACHE run_exports: {} - size: 141379 - timestamp: 1791025923003 + size: 141245 + timestamp: 1793205938858 - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda build_number: 9 sha256: ee9c2922e07afc85fc82d5fa82c9ac2c79da3be3283a5e17bf2f2ae38dbd02fb @@ -10048,46 +10018,26 @@ packages: - libcxx >=23 size: 19216 timestamp: 1793018051621 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.3-hf91ab31_0.conda - sha256: bba84a688b3f1d29afc7f495ce55c6743f9b269d6d0af6c405a4fdfac8229022 - md5: b10a4aaff1177eb0055c6911cc77585a +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.5-h07a7c03_0.conda + sha256: ff1d978c97017efc1a9867543eb67c1d5374d13e62c21102b60f2f4d4961ee93 + md5: 155bbf5b54c5ca723fa708d793cb17b1 depends: - - libcxx >=21 - __osx >=11.0 - - ncurses >=6.6,<7.0a0 + - libcxx >=21 + - zstd >=1.5.7,<1.6.0a0 - libzlib >=1.3.2,<2.0a0 - bzip2 >=1.0.8,<2.0a0 - - rhash >=1.4.6,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - - libexpat >=2.8.1,<3.0a0 - liblzma >=5.8.3,<6.0a0 - - libcurl >=8.21.0,<9.0a0 - - libuv >=1.52.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - run_exports: {} - size: 20192243 - timestamp: 1787711168527 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-4.4.4-hf91ab31_0.conda - sha256: 0dd66618a37205201d1b522e7a7188b8bba8f3e4121496a93d8e5743c8549eff - md5: 1dd388142b3f596207223993dd6564ca - depends: - - __osx >=11.0 - - libcxx >=21 - - libexpat >=2.8.5,<3.0a0 + - libexpat >=2.9.0,<3.0a0 - rhash >=1.4.6,<2.0a0 - - libzlib >=1.3.2,<2.0a0 - - libuv >=1.52.1,<2.0a0 + - libcurl >=8.23.0,<9.0a0 - ncurses >=6.6,<7.0a0 - - libcurl >=8.22.0,<9.0a0 - - liblzma >=5.8.3,<6.0a0 - - bzip2 >=1.0.8,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 + - libuv >=1.52.1,<2.0a0 license: BSD-3-Clause license_family: BSD run_exports: {} - size: 20193229 - timestamp: 1791001164107 + size: 20195299 + timestamp: 1793157041166 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-21.1.8-hce30654_2.conda sha256: 624b8923052bf0cea5ae6ee422f18e73594f76cd4e9b7fc7565f51b256275c92 md5: 129de7382248e851078a1c4bbde92756 @@ -10590,14 +10540,13 @@ packages: - libharfbuzz >=15.0.0 size: 11071 timestamp: 1792769148252 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda - sha256: 46a4958f2f916c5938f2a6dc0709f78b175ece42f601d79a04e0276d55d25d07 - md5: cfb39109ac5fa8601eb595d66d5bf156 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-h5ce3bf5_3.conda + sha256: 6884de33550caede7ec4e387927d8da9c1f67220210c91098f96cedeb185c76a + md5: 7f0fcc2e0b004759715c8d30e168bbeb license: GPL-2.0-or-later - license_family: GPL run_exports: {} - size: 17616 - timestamp: 1771539622983 + size: 14700 + timestamp: 1793205222625 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda sha256: 6cdb5dee54c72e56ab189fb3ad33cb28533553d42590e7e831160248f4416a43 md5: a5efc0b42bb8b42e97d0a29ae3e3c187 @@ -10821,24 +10770,6 @@ packages: - libcompiler-rt >=23.1.4 size: 1383407 timestamp: 1792567052336 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda - sha256: ba74311dc4805af2ba1365d85814199a563cb09950f68b97fcd4e939dc090855 - md5: 27c5b464c5fbee7411aab3bc0195f9c2 - depends: - - __osx >=11.0 - - krb5 >=1.22.2,<1.23.0a0 - - libnghttp2 >=1.68.1,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.7,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl - license_family: MIT - run_exports: - weak: - - libcurl >=8.21.0,<9.0a0 - size: 410285 - timestamp: 1782802574409 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.23.0-h6651222_0.conda sha256: d9f60525b665e50da15c84fcb4e74e6b1f0a56ea4c89a02d4258f23c3fe1f02c md5: b955db4c41b9820eba0aa2c9a3c74a37 @@ -11181,9 +11112,9 @@ packages: - libintl >=0.25.1,<1.0a0 size: 41844 timestamp: 1790118714457 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_1.conda - sha256: 05006418f9392c9b723e8428808db106de0350a497832f95f921b85ff1072310 - md5: b2f8c8e5a7651a1d7c05404c3a159517 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.1-hb001647_0.conda + sha256: 957469c69b3b9c9e6ae48dc1267c5c9dfe0db9e1aefa5fa9abf9798224aa23c9 + md5: d420b10837f3b64259b1febf263b5f7a depends: - __osx >=11.0 constrains: @@ -11191,9 +11122,9 @@ packages: license: IJG AND BSD-3-Clause AND Zlib run_exports: weak: - - libjpeg-turbo >=3.2.0,<4.0a0 - size: 558459 - timestamp: 1785896382474 + - libjpeg-turbo >=3.2.1,<4.0a0 + size: 555606 + timestamp: 1793156969006 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-11_hd9741b5_openblas.conda build_number: 11 sha256: e7697df761eeb243272801a8c883dc1de6018a7f738746b443ef1396f0a78075 @@ -11285,24 +11216,24 @@ packages: run_exports: {} size: 73289 timestamp: 1786651074391 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h435687b_1.conda - sha256: 445440d8cccccb6585f71d1eb7c949d64a2af2ced5481d924621424ff3b6a398 - md5: ac9902dcbe0417f50cf95ab2bde062fa +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.70.0-h344cdd5_0.conda + sha256: 7ac62fd2b395f405cd80eb12a90b506172a1c7dc605e7665c8721e88bb1982d1 + md5: a1a6c349d8af2533f74183d2740fbc84 depends: + - libev >=4.33,<5.0a0 - __osx >=11.0 - - c-ares >=1.34.8,<2.0a0 - libcxx >=21 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - libzlib >=1.3.2,<2.0a0 - - openssl >=3.5.7,<4.0a0 + - libev >=4.33,<4.34.0a0 + - c-ares >=1.34.9,<2.0a0 + - openssl >=3.5.9,<4.0a0 license: MIT license_family: MIT run_exports: weak: - - libnghttp2 >=1.68.1,<2.0a0 - size: 567024 - timestamp: 1787177422467 + - libnghttp2 >=1.70.0,<2.0a0 + size: 681145 + timestamp: 1793191321189 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libntlm-1.8-h5505292_0.conda sha256: ea8c680924d957e12270dca549620327d5e986f23c4bd5f45627167ca6ef7a3b md5: c90c1d3bd778f5ec0d4bb4ef36cbd5b6 @@ -12679,24 +12610,24 @@ packages: run_exports: {} size: 344044 timestamp: 1788485380019 -- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda - sha256: 159c0e80f733a4ebcd24a9bf7fa6ad848a8e1c6ac441d5f2f63bafcfdff176ce - md5: 8a2288d93af982987191a1e778de756e +- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda + sha256: 1923ff4a092dfdacdcffa1ecb61fcfddd97852a3b09f013027b65ae1fb901126 + md5: d16138d4bb7c21323a08510c11009bc5 depends: - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 + - libcurl >=8.23.0,<9.0a0 + - bzip2 >=1.0.8,<2.0a0 - libuv >=1.52.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - liblzma >=5.8.3,<6.0a0 + - libexpat >=2.9.0,<3.0a0 - libzlib >=1.3.2,<2.0a0 - - libexpat >=2.8.5,<3.0a0 - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.22.0,<9.0a0 + - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD run_exports: {} - size: 17932458 - timestamp: 1791001114890 + size: 17933425 + timestamp: 1793157037107 - conda: https://conda.anaconda.org/conda-forge/win-64/console_bridge-1.0.2-h5362a0b_1.tar.bz2 sha256: 15dd8cd1735c9405ad04d9881c15650fb98bf8e71e5675e98898184e4a731ec6 md5: 47acc5c1cb921914270dd9fe47ac30db @@ -13486,9 +13417,9 @@ packages: - libintl >=0.22.5,<1.0a0 size: 42414 timestamp: 1787841194162 -- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda - sha256: df78ab4c0eecb3dd9331898f96baeed8e5ca1c363346332517abeb0b614b9a53 - md5: fc2c23bacefd1e733f1e1d6cd3b2aaeb +- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.1-hfd05255_0.conda + sha256: 195be7b2ff1eeff02b31cf366437a38d84560c2f6c4facd56b2969024f50957f + md5: e2b8d430d6cde5e940c0d02c257273e0 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 @@ -13498,9 +13429,9 @@ packages: license: IJG AND BSD-3-Clause AND Zlib run_exports: weak: - - libjpeg-turbo >=3.2.0,<4.0a0 - size: 990125 - timestamp: 1785896494014 + - libjpeg-turbo >=3.2.1,<4.0a0 + size: 990502 + timestamp: 1793156991591 - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-11_hf9ab0e9_mkl.conda build_number: 11 sha256: 460a2b9896bbee9a3cdfef26d1fdce056028291eef104564082de9e9f566bba6 @@ -13778,6 +13709,7 @@ packages: - msys2-conda-epoch <0.0a0 - pthreads-win32 <0.0a0 license: MIT + license_family: MIT run_exports: {} size: 34475 timestamp: 1793054490651 @@ -14481,9 +14413,9 @@ packages: - urdfdom >=6.0.2,<7.0a0 size: 100859 timestamp: 1792669615678 -- conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_1.conda - sha256: a9f4c347d22f2f32df2089eb6ed06396d7bdfe059072c05f7f35221945fcc9f2 - md5: ce03adcd93654386ec71ee99a025f1fe +- conda: https://conda.anaconda.org/conda-forge/win-64/urdfdom_headers-3.0.1-h477610d_2.conda + sha256: 4341a4c3702f8fb1c1d8e33983429a0ed650bae0680719883fb2de1d8d13a12a + md5: 2d376b786573f9ba8711d7ef232c54c2 depends: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 @@ -14491,8 +14423,8 @@ packages: license: BSD-3-Clause license_family: BSD run_exports: {} - size: 20090 - timestamp: 1787006296934 + size: 20101 + timestamp: 1793089271685 - conda: https://conda.anaconda.org/conda-forge/win-64/uv-0.13.6-hf7a7c96_0.conda sha256: a7c479be4d985294eed236afa899a154d0700b96565a4f408dba39ae83d01a70 md5: 5177960bc53ce73e7e6428514eec530c @@ -14554,9 +14486,9 @@ packages: run_exports: {} size: 21386 timestamp: 1785359368990 -- conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_41.conda - sha256: 9d7d1b43cf4af5a8e8b1646175c9f899ffbcab189a33ac41127a96e7bcf41af0 - md5: 04190e0ebd886433300ce9343ee98942 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_42.conda + sha256: 872123d66fbc41898bea34973cbf64544c41cdcd5123a00abe747c5f35b92898 + md5: 410be03ef3a0eb26e1b20cb90be5493c depends: - vswhere constrains: @@ -14570,8 +14502,8 @@ packages: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - size: 25462 - timestamp: 1785358620723 + size: 25374 + timestamp: 1793120002126 - conda: https://conda.anaconda.org/conda-forge/win-64/websockets-17.3-py312h5247d40_0.conda sha256: e8f05d398eca8dcaa75a573040502af03c219b90a9187a064c8203b6cc6f7f11 md5: a1f1125ec8f147436f374d681ed281d0 @@ -33230,9 +33162,9 @@ packages: license: Apache-2.0 OR BSD-3-Clause size: 21471 timestamp: 1791433174793 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/linux-64/rig_interfaces-0.2.1-hb0f4dca_0.conda - sha256: 9eb49568b01263b22a193915e0102e9dc1a0597a5f4d3f6267f400c77700a3be - md5: ea446b574ec41bcdc263d3ee929c9b41 +- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/5e20dee381061832377d5a51c767aaf67a20c53c/channel/linux-64/rig_interfaces-0.3.0-hb0f4dca_0.conda + sha256: 47aceda59b9b620865ebc5e265651dee277d5b8e1c75086f5f4d693b1e29564d + md5: e1ee21c44a8089aa7026d524704f3c2f depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33242,11 +33174,12 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 108827 - timestamp: 1793061689682 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/noarch/rig_nodes-0.2.1-pyh4616a5c_0.conda - sha256: e002109fbeb72c36489c850ddce0176df13e0e032b422b64ccf854465d5394a7 - md5: e6d1f0825cab19cfe6a05d73ce1ddc64 + 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 depends: - python >=3.12 - python * @@ -33258,12 +33191,14 @@ packages: - ros2-distro-mutex 0.19.* - mcap >=1.2 - websockets >=12 + - psutil license: MIT - size: 73173 - timestamp: 1793061824585 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/osx-arm64/rig_interfaces-0.2.1-h60d57d3_0.conda - sha256: 76339c539d4eaae37bb81d7efd84c5e6dfff438a89b722a6c57252b09ba9d32d - md5: 49fd4624aa1a863e595c8e809f4a6d92 + 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 depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33272,11 +33207,12 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 98935 - timestamp: 1793061701492 -- conda: https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.2.1/channel/win-64/rig_interfaces-0.2.1-ha41a7fa_0.conda - sha256: dfef21559ce477fd9d4ca7bbd4f9f24943b52405f82b79d075d2874202cb0d4f - md5: e4e5920b2ff153858701ac29e388fe06 + 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 depends: - ros-jazzy-rosidl-default-runtime - ros2-distro-mutex @@ -33287,9 +33223,10 @@ packages: - python_abi 3.12.* *_cp312 - numpy >=1.25,<3 license: LicenseRef-MIT - size: 110182 - timestamp: 1793061783743 -- conda_source: rig_interfaces[4fc1ccdb] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc + 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 build: ha41a7fa_0 subdir: win-64 @@ -33298,14 +33235,14 @@ packages: cxx_compiler: vs2022 target_platform: win-64 depends: + - 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 + - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - - numpy >=1.25,<3 license: LicenseRef-MIT build_packages: - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -33336,9 +33273,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -33349,7 +33286,7 @@ packages: - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.2-pyh5ded981_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/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py314h7ae7f31_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/git-2.56.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/git-lfs-3.8.1-ha70c05e_0.conda @@ -33393,7 +33330,7 @@ packages: - 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/vs2015_runtime-14.51.36247-h633cb9f_41.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_41.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_42.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros-jazzy-ament-cmake-2.5.6-h9490d1a_27.conda - conda: https://conda.anaconda.org/robostack-jazzy/win-64/ros-jazzy-ros-workspace-1.0.3-h9490d1a_27.conda @@ -33499,9 +33436,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -33512,7 +33449,7 @@ packages: - 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/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_10.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.4-hde52c71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-4.4.5-hde52c71_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cppcheck-2.22.0-py312h9df87ca_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gmock-1.18.0-h4f519af_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gtest-1.18.0-h477610d_1.conda @@ -33633,21 +33570,21 @@ 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[806fd8b9] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc +- 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 variants: target_platform: linux-64 depends: - - ros-jazzy-rosidl-default-runtime - - ros2-distro-mutex - - ros2-distro-mutex >=0.19.0,<0.20.0a0 - libgcc >=16 - libgcc >=16 - libstdcxx >=16 - - python_abi 3.12.* *_cp312 - 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 @@ -33655,7 +33592,7 @@ packages: - 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.4-hff7ac6b_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 @@ -33686,7 +33623,7 @@ packages: - 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.68.1-h74cf4be_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/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 @@ -33744,9 +33681,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -33837,16 +33774,17 @@ 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.3-hff7ac6b_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.21.0-hcf29cc6_1.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 @@ -33858,11 +33796,12 @@ packages: - 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.68.1-h74cf4be_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-h0737f62_101.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 @@ -33908,9 +33847,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -33997,19 +33936,19 @@ packages: - 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[d2cb8de4] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_interfaces&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc +- 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 - - libcxx >=23 - - python_abi 3.12.* *_cp312 - - numpy >=1.25,<3 license: LicenseRef-MIT build_packages: - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.2-pyhcf101f3_0.conda @@ -34041,9 +33980,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -34064,7 +34003,7 @@ packages: - 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.4-hf91ab31_0.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 @@ -34097,7 +34036,7 @@ packages: - 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.68.1-h435687b_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/libpython-3.14.10-h281bc5c_100_cp314.conda @@ -34233,9 +34172,9 @@ packages: - 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.1.1-pyhc364b38_2.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.5-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 @@ -34248,14 +34187,15 @@ packages: - 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/cmake-4.4.3-hf91ab31_0.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/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/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 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.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/libedit-3.1.20260512-pl5321h26f1114_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h1a92334_3.conda @@ -34267,10 +34207,11 @@ packages: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-he4c29f2_3.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/liblzma-5.8.3-h8088a28_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h435687b_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/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/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/libzlib-1.3.2-h8088a28_3.conda @@ -34364,7 +34305,7 @@ 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[1e0a2c3d] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc +- conda_source: rig_nodes[b5d6e649] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc version: 0.2.1 build: pyh4616a5c_0 subdir: noarch @@ -34372,66 +34313,21 @@ packages: variants: target_platform: noarch depends: - - python >=3.12 - - python * - - ros-jazzy-rclpy - - ros-jazzy-std-msgs - - ros-jazzy-rosbag2-py - - ros-jazzy-rosbag2-storage-mcap - - ros-jazzy-rosidl-runtime-py - - ros2-distro-mutex 0.19.* - mcap >=1.2 - - 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/editables-0.6-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.32.4-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_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/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_source: rig_nodes[37bf213e] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc - version: 0.2.1 - build: pyh4616a5c_0 - subdir: noarch - noarch: python - variants: - target_platform: noarch - depends: - - python >=3.12 - python * + - python >=3.12 - ros-jazzy-rclpy - - ros-jazzy-std-msgs - ros-jazzy-rosbag2-py - ros-jazzy-rosbag2-storage-mcap - ros-jazzy-rosidl-runtime-py + - ros-jazzy-std-msgs - ros2-distro-mutex 0.19.* - - mcap >=1.2 - 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/editables-0.6-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.32.4-pyh5ded981_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 - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.7.0-pyh5ded981_0.conda @@ -34453,7 +34349,7 @@ packages: - 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[d35d99f3] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc +- conda_source: rig_nodes[daef82bd] @ git+https://github.com/reiserlab/rig-nodes?subdirectory=src%2Frig_nodes&branch=main#75b99867f253b4b8f0fe90e2214ff9d9835cb6fc version: 0.2.1 build: pyh4616a5c_0 subdir: noarch @@ -34461,15 +34357,15 @@ packages: variants: target_platform: noarch depends: - - python >=3.12 + - mcap >=1.2 - python * + - python >=3.12 - ros-jazzy-rclpy - - ros-jazzy-std-msgs - ros-jazzy-rosbag2-py - ros-jazzy-rosbag2-storage-mcap - ros-jazzy-rosidl-runtime-py + - ros-jazzy-std-msgs - ros2-distro-mutex 0.19.* - - mcap >=1.2 - websockets >=12 license: MIT host_packages: @@ -34497,10 +34393,55 @@ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.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.32.4-pyh5ded981_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 + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_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/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 + build: pyh4616a5c_0 + subdir: noarch + noarch: python + variants: + target_platform: noarch + depends: + - mcap >=1.2 + - python * + - python >=3.12 + - ros-jazzy-rclpy + - ros-jazzy-rosbag2-py + - ros-jazzy-rosbag2-storage-mcap + - ros-jazzy-rosidl-runtime-py + - ros-jazzy-std-msgs + - ros2-distro-mutex 0.19.* + - 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/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 - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-1.1.1-pyhd8ed1ab_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/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 diff --git a/rig/pixi.toml b/rig/pixi.toml index 2abdab34..97e98349 100644 --- a/rig/pixi.toml +++ b/rig/pixi.toml @@ -23,7 +23,10 @@ [workspace] name = "rig" version = "0.1.0" -channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.3.0/channel"] +# 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"] # 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 @@ -85,7 +88,12 @@ mcap = ">=1.2" # 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). -# Written into every run's record by the gateway (hello_ack + /arena/session `packages`). +# 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" # ── typed messages = the OPT-IN `typed` environment ─────────────────────────────────────── @@ -140,10 +148,14 @@ ros-jazzy-rqt-console = "*" ros-jazzy-foxglove-bridge = "*" [environments] -default = { solve-group = "rig" } -typed = { features = ["typed"], solve-group = "rig" } -viz = { features = ["viz"], solve-group = "rig" } -dev = { features = ["dev", "typed"], solve-group = "dev" } +default = { features = ["channel"], solve-group = "rig" } +typed = { features = ["channel", "typed"], solve-group = "rig" } +viz = { features = ["channel", "viz"], solve-group = "rig" } +# `dev` without the `typed` feature: `typed` pins rig_interfaces to the released version from the channel, while +# `dev` builds it from the git source — one name from two sources does not solve once the release is ahead of +# `main` (0.3.0 vs 0.2.1 on 2026-10-28, "excluded because of strict channel priority"). The git source IS the +# typed package here, so nothing is lost. +dev = { features = ["dev"], solve-group = "dev" } [tasks] # ── hour one: middleware bring-up ─────────────────────────────────────────── From 3b636acb401a1cdb9a1dd5cf643ac2bc53638dba Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Wed, 28 Oct 2026 19:09:18 -0400 Subject: [PATCH 3/7] =?UTF-8?q?rig:=20`pixi=20run=20qc`=20=E2=80=94=20the?= =?UTF-8?q?=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 f2ab4c0afd7ae56073963120adea6adc562d35ca Mon Sep 17 00:00:00 2001 From: Michael B Reiser Date: Thu, 29 Oct 2026 16:00:37 -0400 Subject: [PATCH 4/7] 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 5/7] 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 6/7] 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 7/7] 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 ===")