From cb92a8a7e04e7ebc2fb1148ccb64b0acc17b0b02 Mon Sep 17 00:00:00 2001 From: juliaj Date: Mon, 16 Feb 2026 11:57:54 -0800 Subject: [PATCH 1/6] Refactor PythonExamples to add configuration and unit tests --- .github/workflows/ci.yml | 22 + .gitignore | 3 + .pre-commit-config.yaml | 33 + ArduinoExample/README.md | 2 - PythonExample/AmazingHand_Demo.py | 391 ++++--- PythonExample/AmazingHand_Demo_Both.py | 95 +- PythonExample/AmazingHand_FingerTest.py | 104 +- .../AmazingHand_Hand_FingerMiddlePos.py | 71 +- PythonExample/README.md | 61 +- PythonExample/common.py | 127 ++ PythonExample/config.toml | 81 ++ PythonExample/pyproject.toml | 17 + PythonExample/tests/__init__.py | 16 + PythonExample/tests/test_amazinghand_demo.py | 501 ++++++++ PythonExample/tests/test_common.py | 134 +++ PythonExample/tests/test_config.py | 52 + PythonExample/tests/test_finger_demos.py | 84 ++ README.md | 46 +- docs/contact.md | 10 +- docs/maintainer.md | 57 + pixi.lock | 1034 +++++++++++++++++ pixi.toml | 20 + 22 files changed, 2609 insertions(+), 352 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 PythonExample/common.py create mode 100644 PythonExample/config.toml create mode 100644 PythonExample/pyproject.toml create mode 100644 PythonExample/tests/__init__.py create mode 100644 PythonExample/tests/test_amazinghand_demo.py create mode 100644 PythonExample/tests/test_common.py create mode 100644 PythonExample/tests/test_config.py create mode 100644 PythonExample/tests/test_finger_demos.py create mode 100644 docs/maintainer.md create mode 100644 pixi.lock create mode 100644 pixi.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6fe7ab2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: {} + pull_request: {} + workflow_dispatch: + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: prefix-dev/setup-pixi@v0.9.4 + with: + cache: ${{ !env.ACT }} + + - name: Run pre-commit + run: pixi run pre-commit run --all-files + + - name: Unit tests + run: pixi run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a93d20e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +push.json +.pytest_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a501c31 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +# See https://pre-commit.com for usage. All hooks limited to PythonExample/. +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: ^PythonExample/ + - id: end-of-file-fixer + files: ^PythonExample/ + exclude: \.pytest_cache + - id: check-yaml + files: ^PythonExample/ + - id: check-added-large-files + args: ["--maxkb=1000"] + files: ^PythonExample/ + - id: check-merge-conflict + files: ^PythonExample/ + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + - id: ruff + args: [--fix] + files: ^PythonExample/ + + - repo: local + hooks: + - id: pytest + name: pytest + entry: bash -c 'cd PythonExample && PYTHONPATH= pixi run python -m pytest tests/ -v' + language: system + pass_filenames: false + files: ^PythonExample/ diff --git a/ArduinoExample/README.md b/ArduinoExample/README.md index d1bb082..df430b0 100644 --- a/ArduinoExample/README.md +++ b/ArduinoExample/README.md @@ -35,5 +35,3 @@ Rx arduino pin connected to Tx pin on TTLinker / Tx arduino pin connected to Rx ![Arduino_Mega_PortCom](../assets/Arduino_Mega.jpg) Rx and Tx pin used on arduino Mega are RX1 & TX1 (19 & 18) - - diff --git a/PythonExample/AmazingHand_Demo.py b/PythonExample/AmazingHand_Demo.py index 8db4c9c..3e9f12e 100644 --- a/PythonExample/AmazingHand_Demo.py +++ b/PythonExample/AmazingHand_Demo.py @@ -1,49 +1,95 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys import time import numpy as np -from rustypot import Scs0009PyController +from common import ( + create_controller, + default_serial_port, + get_demo_hand_config, + load_config, +) -#Side -Side = 2 # 1=> Right Hand // 2=> Left Hand +HAND_RIGHT = 1 +HAND_LEFT = 2 +_cfg = load_config() +_side_from_config = _cfg.get("hand_test_id") if _cfg.get("hand_test_id") is not None else _cfg.get("side") +_default_hand_used = _side_from_config is None +side = _side_from_config if _side_from_config is not None else HAND_LEFT +max_speed = _cfg.get("max_speed", 7) +close_speed = _cfg.get("close_speed", 3) -#Speed -MaxSpeed = 7 -CloseSpeed = 3 +try: + _hand = get_demo_hand_config(_cfg, side) +except ValueError as e: + print("Hand config error:", str(e)) + sys.exit(1) +side = _hand["side"] +servo_ids = _hand["servo_ids"] +middle_pos = _hand["middle_pos"] # degrees, one per joint (8 values) -#Fingers middle poses -MiddlePos = [-12, 2, 2, 5, -2, -8, 0, -15, 8] # replace values by your calibration results +c = None # Set in main() -# Servo IDs based on hand side -if Side == 1: # Right Hand - ServoIDs = [1, 2, 3, 4, 5, 6, 7, 8] # Index(1,2), Middle(3,4), Ring(5,6), Thumb(7,8) -else: # Left Hand (Side == 2) - ServoIDs = [15, 16, 13, 14, 11, 12, 17, 18] # Index(15,16), Middle(13,14), Ring(11,12), Thumb(17,18) +# Timings (s) +INTER_SERVO_DELAY = 0.0002 +POST_MOVE_DELAY = 0.005 +GESTURE_STEP_DELAY = 0.2 + +# Angles in pose / Move_* are degrees; converted to rad in _move_finger. +# max_speed / close_speed: controller unit for write_goal_speed (see rustypot/servo spec). -c = None # Will be initialized in main() def main(): - global c - + global c, side, servo_ids, middle_pos + # CLI overrides config side when given + parser = argparse.ArgumentParser(description="AmazingHand demo (one hand).") + parser.add_argument("--side", type=int, choices=(HAND_RIGHT, HAND_LEFT), default=None, help="1 = right, 2 = left (overrides config)") + args = parser.parse_args() + if _default_hand_used and args.side is None: + hand_name = "right" if side == HAND_RIGHT else "left" + print(f"Using default hand: hand_{side} ({hand_name})") + if args.side is not None: + try: + _hand = get_demo_hand_config(_cfg, args.side) + except ValueError as e: + print("Hand config error:", str(e)) + sys.exit(1) + side = _hand["side"] + servo_ids = _hand["servo_ids"] + middle_pos = _hand["middle_pos"] + + port = _cfg.get("port") or default_serial_port() try: - print("Initializing controller on COM3...") - c = Scs0009PyController( - serial_port="COM3", - baudrate=1000000, - timeout=2.5, - ) + print(f"Initializing controller on {port}...") + c = create_controller(timeout=_cfg.get("timeout")) print("Controller initialized successfully!") except RuntimeError as e: print(f"Failed to initialize controller: {e}") - print("Please check that the device is powered on and properly connected to COM3.") + print(f"Check that the device is powered on and connected to {port}.") return - - c.write_torque_enable(ServoIDs[0], 1) #1 = On / 2 = Off / 3 = Free - t0 = time.time() - while True: - t = time.time() - t0 + c.write_torque_enable(servo_ids[0], 1) # 1 = On, 2 = Off, 3 = Free + # Demo loop: run gesture sequence repeatedly + while True: print("OpenHand") OpenHand() time.sleep(0.5) @@ -73,7 +119,7 @@ def main(): print("Nonono") Nonono() time.sleep(0.5) - + print("OpenHand") OpenHand() time.sleep(0.3) @@ -121,195 +167,160 @@ def main(): def OpenHand(): - Move_Index (-35,35, MaxSpeed) - Move_Middle (-35,35, MaxSpeed) - Move_Ring (-35,35, MaxSpeed) - Move_Thumb (-35,35, MaxSpeed) + # Index, Middle, Ring, Thumb: open angles (same left/right) + pose = [(-35, 35)] * 4 + _apply_pose(pose, max_speed) + def CloseHand(): - Move_Index (90,-90, CloseSpeed) - Move_Middle (90,-90, CloseSpeed) - Move_Ring (90,-90, CloseSpeed) - Move_Thumb (90,-90, CloseSpeed+1) + pose = [(90, -90)] * 4 + # Thumb (finger 3) uses slightly higher close speed + for finger_idx, (a1, a2) in enumerate(pose): + speed = close_speed + 1 if finger_idx == 3 else close_speed + _move_finger(finger_idx, a1, a2, speed) + def OpenHand_Progressive(): - Move_Index (-35,35, MaxSpeed-2) - time.sleep(0.2) - Move_Middle (-35,35, MaxSpeed-2) - time.sleep(0.2) - Move_Ring (-35,35, MaxSpeed-2) - time.sleep(0.2) - Move_Thumb (-35,35, MaxSpeed-2) + # Open each finger in sequence with delay between + pose = [(-35, 35)] * 4 + speed = max_speed - 2 + for finger_idx, (a1, a2) in enumerate(pose): + _move_finger(finger_idx, a1, a2, speed) + time.sleep(GESTURE_STEP_DELAY) + def SpreadHand(): - if (Side==1): # Right Hand - Move_Index (4, 90, MaxSpeed) - Move_Middle (-32, 32, MaxSpeed) - Move_Ring (-90, -4, MaxSpeed) - Move_Thumb (-90, -4, MaxSpeed) - - if (Side==2): # Left Hand - Move_Index (-60, 0, MaxSpeed) - Move_Middle (-35, 35, MaxSpeed) - Move_Ring (-4, 90, MaxSpeed) - Move_Thumb (-4, 90, MaxSpeed) + # Right: fingers splayed out; left: mirror angles + if side == HAND_RIGHT: + pose = [(4, 90), (-32, 32), (-90, -4), (-90, -4)] + else: + pose = [(-60, 0), (-35, 35), (-4, 90), (-4, 90)] + _apply_pose(pose, max_speed) + def ClenchHand(): - if (Side==1): # Right Hand - Move_Index (-60, 0, MaxSpeed) - Move_Middle (-35, 35, MaxSpeed) - Move_Ring (0, 70, MaxSpeed) - Move_Thumb (-4, 90, MaxSpeed) - - if (Side==2): # Left Hand - Move_Index (0, 60, MaxSpeed) - Move_Middle (-35, 35, MaxSpeed) - Move_Ring (-70, 0, MaxSpeed) - Move_Thumb (-90, -4, MaxSpeed) + # Right: curl in one direction; left: mirror + if side == HAND_RIGHT: + pose = [(-60, 0), (-35, 35), (0, 70), (-4, 90)] + else: + pose = [(0, 60), (-35, 35), (-70, 0), (-90, -4)] + _apply_pose(pose, max_speed) + def Index_Pointing(): - Move_Index (-40, 40, MaxSpeed) - Move_Middle (90, -90, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (90, -90, MaxSpeed) + # Index extended, others closed (same left/right) + pose = [(-40, 40), (90, -90), (90, -90), (90, -90)] + _apply_pose(pose, max_speed) + def Nonono(): - Index_Pointing() - for i in range(3) : - time.sleep(0.2) - Move_Index (-10, 80, MaxSpeed) - time.sleep(0.2) - Move_Index (-80, 10, MaxSpeed) + # Index pointing, then wag index 3 times, then open + Index_Pointing() + for _ in range(3): + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-10, 80, max_speed) + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-80, 10, max_speed) + Move_Index(-35, 35, max_speed) + time.sleep(0.4) - Move_Index (-35, 35, MaxSpeed) - time.sleep(0.4) def Perfect(): - if (Side==1): #Right Hand - Move_Index (50, -50, MaxSpeed) - Move_Middle (0, -0, MaxSpeed) - Move_Ring (-20, 20, MaxSpeed) - Move_Thumb (65, 12, MaxSpeed) - + # OK sign: thumb and index circle; middle/ring slight curl + if side == HAND_RIGHT: + pose = [(50, -50), (0, 0), (-20, 20), (65, 12)] + else: + pose = [(50, -50), (0, 0), (-20, 20), (-12, -65)] + _apply_pose(pose, max_speed) - if (Side==2): #Left Hand - Move_Index (50, -50, MaxSpeed) - Move_Middle (0, -0, MaxSpeed) - Move_Ring (-20, 20, MaxSpeed) - Move_Thumb (-12, -65, MaxSpeed) def Victory(): - if (Side==1): #Right Hand - Move_Index (-15, 65, MaxSpeed) - Move_Middle (-65, 15, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (90, -90, MaxSpeed) - + # Index and middle extended; ring and thumb closed + if side == HAND_RIGHT: + pose = [(-15, 65), (-65, 15), (90, -90), (90, -90)] + else: + pose = [(-65, 15), (-15, 65), (90, -90), (90, -90)] + _apply_pose(pose, max_speed) - if (Side==2): #Left Hand - Move_Index (-65, 15, MaxSpeed) - Move_Middle (-15, 65, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (90, -90, MaxSpeed) def Pinched(): - if (Side==1): #Right Hand - Move_Index (90, -90, MaxSpeed) - Move_Middle (90, -90, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (0, -75, MaxSpeed) - - if (Side==2): #Left Hand - Move_Index (90, -90, MaxSpeed) - Move_Middle (90, -90, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (75, 5, MaxSpeed) + # Index/middle/ring closed; thumb to pinch + if side == HAND_RIGHT: + pose = [(90, -90), (90, -90), (90, -90), (0, -75)] + else: + pose = [(90, -90), (90, -90), (90, -90), (75, 5)] + _apply_pose(pose, max_speed) + def Scissors(): - Victory() - if (Side==1): #Right Hand - for i in range(3): - time.sleep(0.2) - Move_Index (-50, 20, MaxSpeed) - Move_Middle (-20, 50, MaxSpeed) - - time.sleep(0.2) - Move_Index (-15, 65, MaxSpeed) - Move_Middle (-65, 15, MaxSpeed) - - - if (Side==2): #Left Hand - for i in range(3): - time.sleep(0.2) - Move_Index (-20, 50, MaxSpeed) - Move_Middle (-50, 20, MaxSpeed) - - time.sleep(0.2) - Move_Index (-65, 15, MaxSpeed) - Move_Middle (-15, 65, MaxSpeed) + # Victory then open/close index and middle 3 times + Victory() + if side == HAND_RIGHT: + for _ in range(3): + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-50, 20, max_speed) + Move_Middle(-20, 50, max_speed) + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-15, 65, max_speed) + Move_Middle(-65, 15, max_speed) + else: + for _ in range(3): + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-20, 50, max_speed) + Move_Middle(-50, 20, max_speed) + time.sleep(GESTURE_STEP_DELAY) + Move_Index(-65, 15, max_speed) + Move_Middle(-15, 65, max_speed) + def Fuck(): + # Middle extended; index/ring closed; thumb orientation by side + if side == HAND_RIGHT: + pose = [(90, -90), (-35, 35), (90, -90), (0, -75)] + else: + pose = [(90, -90), (-35, 35), (90, -90), (75, 0)] + _apply_pose(pose, max_speed) - if (Side==1): #Right Hand - Move_Index (90, -90, MaxSpeed) - Move_Middle (-35, 35, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (0, -75, MaxSpeed) - - if (Side==2): #Left Hand - Move_Index (90, -90, MaxSpeed) - Move_Middle (-35, 35, MaxSpeed) - Move_Ring (90, -90, MaxSpeed) - Move_Thumb (75, 0, MaxSpeed) - -def Move_Index (Angle_1,Angle_2,Speed): - - c.write_goal_speed(ServoIDs[0], Speed) - time.sleep(0.0002) - c.write_goal_speed(ServoIDs[1], Speed) - time.sleep(0.0002) - Pos_1 = np.deg2rad(MiddlePos[0]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos[1]+Angle_2) - c.write_goal_position(ServoIDs[0], Pos_1) - c.write_goal_position(ServoIDs[1], Pos_2) - time.sleep(0.005) - -def Move_Middle(Angle_1,Angle_2,Speed): - c.write_goal_speed(ServoIDs[2], Speed) - time.sleep(0.0002) - c.write_goal_speed(ServoIDs[3], Speed) - time.sleep(0.0002) - Pos_1 = np.deg2rad(MiddlePos[2]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos[3]+Angle_2) - c.write_goal_position(ServoIDs[2], Pos_1) - c.write_goal_position(ServoIDs[3], Pos_2) - time.sleep(0.005) - -def Move_Ring(Angle_1,Angle_2,Speed): - c.write_goal_speed(ServoIDs[4], Speed) - time.sleep(0.0002) - c.write_goal_speed(ServoIDs[5], Speed) - time.sleep(0.0002) - Pos_1 = np.deg2rad(MiddlePos[4]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos[5]+Angle_2) - c.write_goal_position(ServoIDs[4], Pos_1) - c.write_goal_position(ServoIDs[5], Pos_2) - time.sleep(0.005) - -def Move_Thumb(Angle_1,Angle_2,Speed): - c.write_goal_speed(ServoIDs[6], Speed) - time.sleep(0.0002) - c.write_goal_speed(ServoIDs[7], Speed) - time.sleep(0.0002) - Pos_1 = np.deg2rad(MiddlePos[6]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos[7]+Angle_2) - c.write_goal_position(ServoIDs[6], Pos_1) - c.write_goal_position(ServoIDs[7], Pos_2) - time.sleep(0.005) +def _move_finger(finger_idx, angle_1_deg, angle_2_deg, speed_controller_unit): + """Move one finger (0=Index, 1=Middle, 2=Ring, 3=Thumb). + Angles in degrees; speed_controller_unit passed to write_goal_speed (see rustypot/servo spec).""" + i, j = 2 * finger_idx, 2 * finger_idx + 1 + c.write_goal_speed(servo_ids[i], speed_controller_unit) + time.sleep(INTER_SERVO_DELAY) + c.write_goal_speed(servo_ids[j], speed_controller_unit) + time.sleep(INTER_SERVO_DELAY) + c.write_goal_position(servo_ids[i], np.deg2rad(middle_pos[i] + angle_1_deg)) + c.write_goal_position(servo_ids[j], np.deg2rad(middle_pos[j] + angle_2_deg)) + time.sleep(POST_MOVE_DELAY) -if __name__ == '__main__': - main() + +def _apply_pose(pose, speed_controller_unit): + """Apply a pose to all fingers. pose: list of 4 (angle_1_deg, angle_2_deg) per finger. + speed_controller_unit: value for write_goal_speed (controller unit, not RPM).""" + for finger_idx, (a1, a2) in enumerate(pose): + _move_finger(finger_idx, a1, a2, speed_controller_unit) + + +def Move_Index(angle_1_deg, angle_2_deg, speed_controller_unit): + # Finger 0: Index (servo_ids 0, 1). Angles in degrees; speed in controller unit. + _move_finger(0, angle_1_deg, angle_2_deg, speed_controller_unit) +def Move_Middle(angle_1_deg, angle_2_deg, speed_controller_unit): + # Finger 1: Middle (servo_ids 2, 3). Angles in degrees; speed in controller unit. + _move_finger(1, angle_1_deg, angle_2_deg, speed_controller_unit) + +def Move_Ring(angle_1_deg, angle_2_deg, speed_controller_unit): + # Finger 2: Ring (servo_ids 4, 5). Angles in degrees; speed in controller unit. + _move_finger(2, angle_1_deg, angle_2_deg, speed_controller_unit) + + +def Move_Thumb(angle_1_deg, angle_2_deg, speed_controller_unit): + # Finger 3: Thumb (servo_ids 6, 7). Angles in degrees; speed in controller unit. + _move_finger(3, angle_1_deg, angle_2_deg, speed_controller_unit) + + +if __name__ == '__main__': + main() diff --git a/PythonExample/AmazingHand_Demo_Both.py b/PythonExample/AmazingHand_Demo_Both.py index 157011d..286667f 100644 --- a/PythonExample/AmazingHand_Demo_Both.py +++ b/PythonExample/AmazingHand_Demo_Both.py @@ -1,3 +1,20 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time import numpy as np @@ -25,13 +42,10 @@ def main(): - - c.write_torque_enable(1, 1) # (Lowest ID , #1 = On / 2 = Off / 3 = Free ) - t0 = time.time() - while True: - t = time.time() - t0 + c.write_torque_enable(1, 1) # (Lowest ID , #1 = On / 2 = Off / 3 = Free ) + while True: OpenHand() time.sleep(0.5) @@ -53,7 +67,7 @@ def main(): time.sleep(0.4) Nonono() time.sleep(0.5) - + OpenHand() time.sleep(0.3) @@ -88,9 +102,9 @@ def OpenHand(): Move_Ring (-35,35, MaxSpeed, 1) Move_Ring (-35,35, MaxSpeed, 2) Move_Thumb (-35,35, MaxSpeed, 1) - Move_Thumb (-35,35, MaxSpeed, 2) - - + Move_Thumb (-35,35, MaxSpeed, 2) + + def CloseHand(): Move_Index (90,-90, CloseSpeed, 1) Move_Index (90,-90, CloseSpeed, 2) @@ -117,29 +131,29 @@ def OpenHand_Progressive(): Move_Thumb (-35,35, MaxSpeed-2, 1) Move_Thumb (-35,35, MaxSpeed-2, 2) - -def SpreadHand(): + +def SpreadHand(): Move_Index (4, 90, MaxSpeed, 1) Move_Index (-90, 0, MaxSpeed, 2) Move_Middle (-32, 32, MaxSpeed, 1) Move_Middle (-32, 32, MaxSpeed, 2) Move_Ring (-90, -4, MaxSpeed, 1) Move_Ring (-4, 90, MaxSpeed, 2) - Move_Thumb (-90, -4, MaxSpeed, 1) - Move_Thumb (-4, 90, MaxSpeed, 2) + Move_Thumb (-90, -4, MaxSpeed, 1) + Move_Thumb (-4, 90, MaxSpeed, 2) + - -def ClenchHand(): +def ClenchHand(): Move_Index (-60, 0, MaxSpeed, 1) Move_Index (0, 60, MaxSpeed, 2) Move_Middle (-35, 35, MaxSpeed, 1) Move_Middle (-35, 35, MaxSpeed, 2) Move_Ring (0, 70, MaxSpeed, 1) Move_Ring (-70, 0, MaxSpeed, 2) - Move_Thumb (-4, 90, MaxSpeed, 1) + Move_Thumb (-4, 90, MaxSpeed, 1) Move_Thumb (-90, -4, MaxSpeed, 2) - + def Index_Pointing(): Move_Index (-40, 40, MaxSpeed, 1) @@ -148,9 +162,9 @@ def Index_Pointing(): Move_Middle (90, -90, MaxSpeed, 2) Move_Ring (90, -90, MaxSpeed, 1) Move_Ring (90, -90, MaxSpeed, 2) - Move_Thumb (90, -90, MaxSpeed, 1) + Move_Thumb (90, -90, MaxSpeed, 1) Move_Thumb (90, -90, MaxSpeed, 2) - + def Nonono(): Index_Pointing() for i in range(3) : @@ -160,13 +174,13 @@ def Nonono(): time.sleep(0.2) Move_Index (-80, 10, MaxSpeed, 1) Move_Index (-80, 10, MaxSpeed, 2) - + Move_Index (-35, 35, MaxSpeed, 1) Move_Index (-35, 35, MaxSpeed, 2) time.sleep(0.4) - - - + + + def Perfect(): Move_Index (55, -55, MaxSpeed-3, 1) Move_Index (55, -55, MaxSpeed-3, 2) @@ -174,9 +188,9 @@ def Perfect(): Move_Middle (0, -0, MaxSpeed, 2) Move_Ring (-20, 20, MaxSpeed, 1) Move_Ring (-20, 20, MaxSpeed, 2) - Move_Thumb (85, 10, MaxSpeed, 1) + Move_Thumb (85, 10, MaxSpeed, 1) Move_Thumb (-10, -85, MaxSpeed, 2) - + def Victory(): Move_Index (-15, 65, MaxSpeed, 1) @@ -185,7 +199,7 @@ def Victory(): Move_Middle (-15, 65, MaxSpeed, 2) Move_Ring (90, -90, MaxSpeed, 1) Move_Ring (90, -90, MaxSpeed, 2) - Move_Thumb (90, -90, MaxSpeed, 1) + Move_Thumb (90, -90, MaxSpeed, 1) Move_Thumb (90, -90, MaxSpeed, 2) @@ -196,25 +210,25 @@ def Pinched(): Move_Middle (90, -90, MaxSpeed, 2) Move_Ring (90, -90, MaxSpeed, 1) Move_Ring (90, -90, MaxSpeed, 2) - Move_Thumb (5, -75, MaxSpeed, 1) + Move_Thumb (5, -75, MaxSpeed, 1) Move_Thumb (75, -5, MaxSpeed, 2) def Scissors(): - - Victory() - for i in range(3): + + Victory() + for i in range(3): time.sleep(0.2) Move_Index (-50, 20, MaxSpeed, 1) Move_Middle (-20, 50, MaxSpeed, 1) Move_Index (-20, 50, MaxSpeed, 2) Move_Middle (-50, 20, MaxSpeed, 2) - + time.sleep(0.2) Move_Index (-15, 65, MaxSpeed, 1) Move_Middle (-65, 15, MaxSpeed, 1) Move_Index (-65, 15, MaxSpeed, 2) Move_Middle (-15, 65, MaxSpeed, 2) - + def Fuck(): Move_Index (90, -90, MaxSpeed, 1) @@ -223,7 +237,7 @@ def Fuck(): Move_Middle (-35, 35, MaxSpeed, 2) Move_Ring (90, -90, MaxSpeed, 1) Move_Ring (90, -90, MaxSpeed, 2) - Move_Thumb (5, -75, MaxSpeed, 1) + Move_Thumb (5, -75, MaxSpeed, 1) Move_Thumb (75, -5, MaxSpeed, 2) @@ -236,7 +250,7 @@ def Move_Index (Angle_1,Angle_2,Speed, Hand): c.write_goal_speed(2, Speed) time.sleep(0.0002) Pos_1 = np.deg2rad(MiddlePos_1[0]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos_1[1]+Angle_2) + Pos_2 = np.deg2rad(MiddlePos_1[1]+Angle_2) c.write_goal_position(1, Pos_1) c.write_goal_position(2, Pos_2) time.sleep(0.0002) @@ -247,12 +261,12 @@ def Move_Index (Angle_1,Angle_2,Speed, Hand): c.write_goal_speed(12, Speed) time.sleep(0.0002) Pos_1 = np.deg2rad(MiddlePos_2[0]+Angle_1) - Pos_2 = np.deg2rad(MiddlePos_2[1]+Angle_2) + Pos_2 = np.deg2rad(MiddlePos_2[1]+Angle_2) c.write_goal_position(11, Pos_1) c.write_goal_position(12, Pos_2) time.sleep(0.0002) -def Move_Middle(Angle_1,Angle_2,Speed, Hand): +def Move_Middle(Angle_1,Angle_2,Speed, Hand): if (Hand==1): #Right hand finger c.write_goal_speed(3, Speed) time.sleep(0.0002) @@ -275,7 +289,7 @@ def Move_Middle(Angle_1,Angle_2,Speed, Hand): time.sleep(0.0002) def Move_Ring(Angle_1,Angle_2,Speed, Hand): - if (Hand==1): #Right hand finger + if (Hand==1): #Right hand finger c.write_goal_speed(5, Speed) time.sleep(0.0002) c.write_goal_speed(6, Speed) @@ -298,7 +312,7 @@ def Move_Ring(Angle_1,Angle_2,Speed, Hand): time.sleep(0.0002) def Move_Thumb(Angle_1,Angle_2,Speed, Hand): - if (Hand==1): #Right hand finger + if (Hand==1): #Right hand finger c.write_goal_speed(7, Speed) time.sleep(0.0002) c.write_goal_speed(8, Speed) @@ -320,10 +334,7 @@ def Move_Thumb(Angle_1,Angle_2,Speed, Hand): c.write_goal_position(18, Pos_2) time.sleep(0.0002) - + if __name__ == '__main__': main() - - - diff --git a/PythonExample/AmazingHand_FingerTest.py b/PythonExample/AmazingHand_FingerTest.py index 7422875..75f2e4a 100644 --- a/PythonExample/AmazingHand_FingerTest.py +++ b/PythonExample/AmazingHand_FingerTest.py @@ -1,75 +1,67 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time import numpy as np -from rustypot import Scs0009PyController - +from common import create_controller, load_config -ID_1 = 13 #Change to servo ID you want to calibrate -ID_2 = 14 #Change to servo ID you want to calibrate -MiddlePos_1 = 5 #Middle position for servo ID_1 -MiddlePos_2 = -2 #Middle position for servo ID_2 +# Config-driven; 1 = On, 2 = Off, 3 = Free for torque +TORQUE_ENABLE = 1 +FINGER_SPEED = 6 +DEG_CLOSE_OFFSET = 90 +DEG_OPEN_OFFSET = 30 +_cfg = load_config() +_ids = _cfg.get("finger_test_servo_ids", [13, 14]) +_mid = _cfg.get("finger_test_middle_pos", [5, -2]) +id_1, id_2 = _ids[0], _ids[1] +middle_pos_1, middle_pos_2 = _mid[0], _mid[1] +controller = create_controller(timeout=_cfg.get("timeout", 0.5)) -c = Scs0009PyController( - serial_port="COM3", - baudrate=1000000, - timeout=0.5, - ) -def main(): - - c.write_torque_enable(ID_1, 1) - c.write_torque_enable(ID_2, 1) - #1 = On / 2 = Off / 3 = Free - +def main(): + """Run finger open/close loop using two servos from config.""" + controller.write_torque_enable(id_1, TORQUE_ENABLE) + controller.write_torque_enable(id_2, TORQUE_ENABLE) while True: - - - CloseFinger() + close_finger() time.sleep(3) - - - OpenFinger() + open_finger() time.sleep(1) - #c.sync_write_raw_goal_position([1,2], [50,50]) - #time.sleep(1) - - #a=c.read_present_position(1) - #b=c.read_present_position(2) - #a=np.rad2deg(a) - #b=np.rad2deg(b) - #print(f'{a} {b}') - #time.sleep(0.001) - - -def CloseFinger (): - - c.write_goal_speed(ID_1, 6) # Set speed for ID_6 to 6 => Max Speed - c.write_goal_speed(ID_2, 6) # Set speed for ID_6 to 6 => Max Speed - Pos_1 = np.deg2rad(MiddlePos_1+90) - Pos_2 = np.deg2rad(MiddlePos_2-90) - c.write_goal_position(ID_1, Pos_1) - c.write_goal_position(ID_2, Pos_2) +def close_finger(): + """Drive both servos to closed position (middle + offset).""" + controller.write_goal_speed(id_1, FINGER_SPEED) + controller.write_goal_speed(id_2, FINGER_SPEED) + controller.write_goal_position(id_1, np.deg2rad(middle_pos_1 + DEG_CLOSE_OFFSET)) + controller.write_goal_position(id_2, np.deg2rad(middle_pos_2 - DEG_CLOSE_OFFSET)) time.sleep(0.01) -def OpenFinger(): - c.write_goal_speed(ID_1, 6) # Set speed for ID_1 to 6 => Max Speed - c.write_goal_speed(ID_2, 6) # Set speed for ID_1 to 6 => Max Speed - Pos_1 = np.deg2rad(MiddlePos_1-30) - Pos_2 = np.deg2rad(MiddlePos_2+30) - c.write_goal_position(ID_1, Pos_1) - c.write_goal_position(ID_2, Pos_2) +def open_finger(): + """Drive both servos to open position (middle - offset).""" + controller.write_goal_speed(id_1, FINGER_SPEED) + controller.write_goal_speed(id_2, FINGER_SPEED) + controller.write_goal_position(id_1, np.deg2rad(middle_pos_1 - DEG_OPEN_OFFSET)) + controller.write_goal_position(id_2, np.deg2rad(middle_pos_2 + DEG_OPEN_OFFSET)) time.sleep(0.01) - - - - -if __name__ == '__main__': +if __name__ == "__main__": main() - - diff --git a/PythonExample/AmazingHand_Hand_FingerMiddlePos.py b/PythonExample/AmazingHand_Hand_FingerMiddlePos.py index 1a30da1..d12a63d 100644 --- a/PythonExample/AmazingHand_Hand_FingerMiddlePos.py +++ b/PythonExample/AmazingHand_Hand_FingerMiddlePos.py @@ -1,49 +1,54 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import time import numpy as np -from rustypot import Scs0009PyController - +from common import create_controller, load_config -ID_1 = 13 #Change to servo ID you want to calibrate -ID_2 = 14 #Change to servo ID you want to calibrate -MiddlePos_1 = 5 #Middle position for servo ID_1 -MiddlePos_2 = -2 #Middle position for servo ID_2 +# Config-driven; 1 = On for torque +TORQUE_ENABLE = 1 +FINGER_SPEED = 6 +_cfg = load_config() +_ids = _cfg.get("finger_test_servo_ids", [13, 14]) +_mid = _cfg.get("finger_test_middle_pos", [5, -2]) +id_1, id_2 = _ids[0], _ids[1] +middle_pos_1, middle_pos_2 = _mid[0], _mid[1] +controller = create_controller(timeout=_cfg.get("timeout", 0.5)) -c = Scs0009PyController( - serial_port="COM3", - baudrate=1000000, - timeout=0.5, - ) def main(): - - - c.write_torque_enable(ID_1, 1) - c.write_torque_enable(ID_2, 1) - #1 = On / 2 = Off / 3 = Free - + """Run loop holding both servos at middle position from config.""" + controller.write_torque_enable(id_1, TORQUE_ENABLE) + controller.write_torque_enable(id_2, TORQUE_ENABLE) while True: - - ServosInMiddle() + servos_in_middle() time.sleep(3) - - -def ServosInMiddle (): - - c.write_goal_speed(ID_1, 6) # Set speed for ID_1 to 6 => Max Speed - c.write_goal_speed(ID_2, 6) # Set speed for ID_1 to 6 => Max Speed - Pos_1 = np.deg2rad(MiddlePos_1) - Pos_2 = np.deg2rad(MiddlePos_2) - c.write_goal_position(ID_1, Pos_1) - c.write_goal_position(ID_2, Pos_2) +def servos_in_middle(): + """Drive both servos to their configured middle positions.""" + controller.write_goal_speed(id_1, FINGER_SPEED) + controller.write_goal_speed(id_2, FINGER_SPEED) + controller.write_goal_position(id_1, np.deg2rad(middle_pos_1)) + controller.write_goal_position(id_2, np.deg2rad(middle_pos_2)) time.sleep(0.01) - -if __name__ == '__main__': +if __name__ == "__main__": main() - - diff --git a/PythonExample/README.md b/PythonExample/README.md index 295016d..cc88ced 100644 --- a/PythonExample/README.md +++ b/PythonExample/README.md @@ -1,4 +1,63 @@ +# Python Examples + Tutorial for settings IDs with Feetech Software, and Serial bus driver : [https://www.robot-maker.com/forum/tutorials/article/168-brancher-et-controler-le-servomoteur-feetech-sts3032-360/] -Use Feetech software to set IDs : [https://github.com/Robot-Maker-SAS/FeetechServo/tree/main/feetech%20debug%20tool%20master/FD1.9.8.2)] +Use Feetech software to set IDs : [https://github.com/Robot-Maker-SAS/FeetechServo/tree/main/feetech%20debug%20tool%20master/FD1.9.8.2)] + +## Setup Environment + +**Install pixi** (if not already installed): + +```bash +curl -fsSL https://pixi.sh/install.sh | bash +``` + +Restart your shell or run `source ~/.bashrc` (or the equivalent for your shell) so the `pixi` command is available. + +**With pixi (recommended)** +From the repository root (where `pixi.toml` lives): + +```bash +pixi install # one time setup +# Optional: export AMAZINGHAND_TEAM=julia +pixi run python PythonExample/AmazingHand_FingerTest.py +``` + +To run another script, replace the filename, e.g. `PythonExample/AmazingHand_Hand_FingerMiddlePos.py` or `PythonExample/AmazingHand_Demo.py`. + +Set `AMAZINGHAND_TEAM` to choose config (e.g. `export AMAZINGHAND_TEAM=krishan`). Edit `PythonExample/config.toml` for serial port and finger settings. + +The hand must be connected via USB and the serial port in `config.toml` must match your system (e.g. `COM3` on Windows, `/dev/ttyUSB0` or `/dev/ttyACM0` on Linux). Leave `port = ""` to use the default for your OS. If the port is wrong or the device is unplugged, the script will fail with "No such file or directory". + +## Run Python Examples + +### Hand Demo + +Runs a loop of gestures (open/close, spread, point, victory, etc.) on one hand. Which hand is controlled by config (`hand_test_id` or `side` in `config.toml`) or by `AMAZINGHAND_TEAM`. Override from the command line with `--side`: + +```bash +pixi run python PythonExample/AmazingHand_Demo.py +# Right hand (1) or left hand (2): +pixi run python PythonExample/AmazingHand_Demo.py --side 1 +``` + +Ensure the chosen hand's servo IDs and middle positions are set in `config.toml` for your team (see `hand_1_*` / `hand_2_*`). + +## Run Unit Tests + +From the repository root: + +```bash +pixi run test +``` + +## Pre-commit (optional) + +To run lint and tests before each commit, install the git hooks: + +```bash +pixi run pre-commit install +``` + +Hooks are limited to `PythonExample/`. Run manually with `pixi run pre-commit run --all-files`. diff --git a/PythonExample/common.py b/PythonExample/common.py new file mode 100644 index 0000000..a7d2f5c --- /dev/null +++ b/PythonExample/common.py @@ -0,0 +1,127 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +from pathlib import Path + +from rustypot import Scs0009PyController + +try: + import tomllib +except ImportError: + tomllib = None + +_CONFIG_PATH = Path(__file__).resolve().parent / "config.toml" +_TEAM_ENV = "AMAZINGHAND_TEAM" +# Section names in config.toml are [team_], e.g. [team_julia], [team_krishan]. +_CONFIG_TEAM_SECTION_PREFIX = "team_" + +_DEFAULTS = { + "port": "", + "baudrate": 1000000, + "timeout": 0.5, +} + + +def default_serial_port(): + """Return default serial port for current OS (Linux: /dev/ttyUSB0, Windows: COM3).""" + if sys.platform == "win32": + return "COM3" + return "/dev/ttyUSB0" + + +def get_team(): + """Return current team name (e.g. 'julia', 'krishan'). From AMAZINGHAND_TEAM env; default 'julia'. + Accepts either the short name or the full section name; strips config section prefix so it matches [team_] in config.toml.""" + raw = (os.environ.get(_TEAM_ENV) or "julia").strip().lower() + return raw.removeprefix(_CONFIG_TEAM_SECTION_PREFIX) + + +def load_config(team=None, path=None): + """Load section [team_] from config.toml. Returns dict with port, baudrate, timeout and all other section keys.""" + p = path or _CONFIG_PATH + if not p.exists() or tomllib is None: + return _DEFAULTS.copy() + with open(p, "rb") as f: + data = tomllib.load(f) + name = (team or get_team()).strip().lower() + section_key = f"{_CONFIG_TEAM_SECTION_PREFIX}{name}" + section = data.get(section_key, {}) + out = { + "port": (section.get("port") or _DEFAULTS["port"]) or "", + "baudrate": section.get("baudrate", _DEFAULTS["baudrate"]), + "timeout": section.get("timeout", _DEFAULTS["timeout"]), + } + for k, v in section.items(): + if k not in out: + out[k] = v + return out + + +def create_controller(team=None, serial_port=None, baudrate=None, timeout=None): + """Create Scs0009PyController. Section from AMAZINGHAND_TEAM or team=; explicit args override.""" + cfg = load_config(team=team) + port = serial_port if serial_port is not None else (cfg["port"] or default_serial_port()) + br = baudrate if baudrate is not None else cfg["baudrate"] + to = timeout if timeout is not None else cfg["timeout"] + return Scs0009PyController(serial_port=port, baudrate=br, timeout=to) + + +# Demo script: one hand (side 1 = right, side 2 = left). Servo IDs and middle poses per finger. +# Layout is isolated in _parse_hand_section so config shape (flat vs nested) can change later. + + +def _parse_hand_section(cfg, side): + """Extract servo_ids and middle_pos for one hand from team config. side 1 = hand_1, 2 = hand_2. + Missing or empty entries default to [0, 0] per finger. Returns (servo_ids, middle_pos).""" + prefix = "hand_1" if side == 1 else "hand_2" + servo_ids = [] + middle_pos = [] + for name in ("index", "middle", "ring", "thumb"): + ids = cfg.get(f"{prefix}_{name}_servo_ids", []) + mid = cfg.get(f"{prefix}_{name}_middle_pos", []) + servo_ids.extend(ids if len(ids) == 2 else [0, 0]) + middle_pos.extend(mid if len(mid) == 2 else [0, 0]) + return servo_ids, middle_pos + + +def get_demo_hand_config(cfg, side, config_path=None): + """Return dict with servo_ids, middle_pos, and side (1 or 2) for one hand. + Raises ValueError with a friendly message if the hand is not configured (all zeros).""" + servo_ids, middle_pos = _parse_hand_section(cfg, side) + if len(servo_ids) != 8 or len(middle_pos) != 8: + prefix = "hand_1" if side == 1 else "hand_2" + raise ValueError( + f"config must define {prefix}_*_servo_ids and {prefix}_*_middle_pos for index, middle, ring, thumb " + f"(8 servo IDs and 8 middle positions); got {len(servo_ids)} ids and {len(middle_pos)} positions" + ) + if all(s == 0 for s in servo_ids): + prefix = "hand_1" if side == 1 else "hand_2" + hand_name = "right" if side == 1 else "left" + keys = ", ".join( + f"{prefix}_{name}_{suffix}" + for name in ("index", "middle", "ring", "thumb") + for suffix in ("servo_ids", "middle_pos") + ) + path_hint = f" Edit {config_path or _CONFIG_PATH} in your team section." + raise ValueError( + f"{prefix} ({hand_name} hand) is not configured. " + f"Set: {keys}.{path_hint} " + f"Or run with --side {3 - side} to use the other hand." + ) + return {"servo_ids": servo_ids, "middle_pos": middle_pos, "side": side} diff --git a/PythonExample/config.toml b/PythonExample/config.toml new file mode 100644 index 0000000..19718db --- /dev/null +++ b/PythonExample/config.toml @@ -0,0 +1,81 @@ +# Per-team serial settings. Set AMAZINGHAND_TEAM=julia or AMAZINGHAND_TEAM=krishan to choose. +# Leave port empty to use OS default (COM3 on Windows, /dev/ttyUSB0 on Linux). +# Finger config: each finger has servo_ids (2) and middle_pos (2). hand_1 = right, hand_2 = left. +# Demo uses one hand (side 1 = hand_1, side 2 = hand_2). Demo_Both uses both. +# Side: 1 = right hand, 2 = left hand. Speed: max_speed (open), close_speed (close). + +# Finger test: two servos for calibration / FingerTest / Hand_FingerMiddlePos scripts. +# finger_test_servo_ids = [id1, id2], finger_test_middle_pos = [deg1, deg2]. +# Hand demo: hand_test_id = 1 or 2 selects which hand to use (overrides side when set). + +[team_julia] +port = "/dev/ttyACM0" +baudrate = 1000000 +timeout = 0.5 +side = 1 +max_speed = 7 +close_speed = 3 + +# testing finger calibration +finger_test_servo_ids = [6, 5] +finger_test_middle_pos = [-3, 8] + +# hand demo testing: hand_test_id selects which hand (overrides side when set). +# hand_test_id 1 => hand_1 (right), hand_test_id 2 => hand_2 (left). +hand_test_id = 1 + +# hand_1 (right) +hand_1_index_servo_ids = [1, 2] +hand_1_index_middle_pos = [-2, 0] +hand_1_middle_servo_ids = [3, 4] +hand_1_middle_middle_pos = [1,2] +hand_1_ring_servo_ids = [6,5] +hand_1_ring_middle_pos = [-3,8] +hand_1_thumb_servo_ids = [8,7] +hand_1_thumb_middle_pos = [8,-8] + +# hand_2 (left) – required for Demo (side=2) and Demo_Both +hand_2_index_servo_ids = [0, 0] +hand_2_index_middle_pos = [0, 0] +hand_2_middle_servo_ids = [0, 0] +hand_2_middle_middle_pos = [2, 5] +hand_2_ring_servo_ids = [0, 0] +hand_2_ring_middle_pos = [-2, -8] +hand_2_thumb_servo_ids = [0, 0] +hand_2_thumb_middle_pos = [0, -15] + +[team_krishan] +port = "COM3" +baudrate = 1000000 +timeout = 2.5 +side = 2 +max_speed = 7 +close_speed = 3 + +# testing finger calibration +finger_test_servo_ids = [13, 14] +finger_test_middle_pos = [5, -2] + +# hand demo testing: hand_test_id selects which hand (overrides side when set). +# hand_test_id 1 => hand_1 (right), hand_test_id 2 => hand_2 (left). +hand_test_id = 2 + +# hand_1 (right) +hand_1_index_servo_ids = [1, 2] +hand_1_index_middle_pos = [3, 0] +hand_1_middle_servo_ids = [3, 4] +hand_1_middle_middle_pos = [-8, -13] +hand_1_ring_servo_ids = [5, 6] +hand_1_ring_middle_pos = [2, -5] +hand_1_thumb_servo_ids = [7, 8] +hand_1_thumb_middle_pos = [-12, -5] + +# hand_2 (left) – required for Demo (side=2) and Demo_Both +hand_2_index_servo_ids = [11, 12] +hand_2_index_middle_pos = [3, -3] +hand_2_middle_servo_ids = [13, 14] +hand_2_middle_middle_pos = [-1, -10] +hand_2_ring_servo_ids = [15, 16] +hand_2_ring_middle_pos = [5, 2] +hand_2_thumb_servo_ids = [17, 18] +hand_2_thumb_middle_pos = [-7, 3] diff --git a/PythonExample/pyproject.toml b/PythonExample/pyproject.toml new file mode 100644 index 0000000..b260370 --- /dev/null +++ b/PythonExample/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "amazinghand-python-example" +version = "0.1.0" +description = "Python examples and demos for Amazing Hand (Pollen Robotics)" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "numpy>=1.20", + "rustypot", +] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = [] diff --git a/PythonExample/tests/__init__.py b/PythonExample/tests/__init__.py new file mode 100644 index 0000000..ae0403e --- /dev/null +++ b/PythonExample/tests/__init__.py @@ -0,0 +1,16 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/PythonExample/tests/test_amazinghand_demo.py b/PythonExample/tests/test_amazinghand_demo.py new file mode 100644 index 0000000..629488c --- /dev/null +++ b/PythonExample/tests/test_amazinghand_demo.py @@ -0,0 +1,501 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import sys +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +import numpy as np + +_EXAMPLE_ROOT = Path(__file__).resolve().parent.parent +if str(_EXAMPLE_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLE_ROOT)) + +sys.modules["rustypot"] = MagicMock() +sys.modules["rustypot"].Scs0009PyController = MagicMock() + + +_DEMO_CONFIG_LEFT = { + "side": 2, + "port": "", + "baudrate": 1000000, + "timeout": 0.5, + "max_speed": 7, + "close_speed": 3, + "hand_2_index_servo_ids": [15, 16], + "hand_2_index_middle_pos": [-12, 2], + "hand_2_middle_servo_ids": [13, 14], + "hand_2_middle_middle_pos": [2, 5], + "hand_2_ring_servo_ids": [11, 12], + "hand_2_ring_middle_pos": [-2, -8], + "hand_2_thumb_servo_ids": [17, 18], + "hand_2_thumb_middle_pos": [0, -15], +} + +_DEMO_CONFIG_RIGHT = { + "side": 1, + "port": "", + "baudrate": 1000000, + "timeout": 0.5, + "max_speed": 7, + "close_speed": 3, + "hand_1_index_servo_ids": [1, 2], + "hand_1_index_middle_pos": [-2, 0], + "hand_1_middle_servo_ids": [3, 4], + "hand_1_middle_middle_pos": [1, 2], + "hand_1_ring_servo_ids": [6, 5], + "hand_1_ring_middle_pos": [-3, 8], + "hand_1_thumb_servo_ids": [8, 7], + "hand_1_thumb_middle_pos": [8, -8], +} + + +def _reload_demo(config=None): + config = config or _DEMO_CONFIG_LEFT + name = "AmazingHand_Demo" + if name in sys.modules: + del sys.modules[name] + with patch("common.load_config", return_value=config): + return importlib.import_module(name) + + +class TestAmazingHandDemoServoIDs(unittest.TestCase): + """ServoIDs and constants for default (Left Hand, Side=2).""" + + def test_servo_ids_left_hand(self): + mod = _reload_demo() + self.assertEqual(mod.side, 2) + self.assertEqual( + mod.servo_ids, + [15, 16, 13, 14, 11, 12, 17, 18], + ) + + def test_middle_pos_length(self): + mod = _reload_demo() + self.assertEqual(len(mod.middle_pos), 8) + + +class TestMoveFingerFunctions(unittest.TestCase): + """Move_Index, Move_Middle, Move_Ring, Move_Thumb use correct ServoIDs and MiddlePos indices.""" + + def setUp(self): + self.mock_c = MagicMock() + self.mod = _reload_demo() + self.mod.c = self.mock_c + + def test_move_index_uses_servos_0_1_and_middle_pos_0_1(self): + self.mod.Move_Index(10, -20, 5) + self.mock_c.write_goal_speed.assert_any_call(15, 5) + self.mock_c.write_goal_speed.assert_any_call(16, 5) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 10) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + (-20)) + ) + + def test_move_middle_uses_servos_2_3_and_middle_pos_2_3(self): + self.mod.Move_Middle(30, -40, 6) + self.mock_c.write_goal_speed.assert_any_call(13, 6) + self.mock_c.write_goal_speed.assert_any_call(14, 6) + self.mock_c.write_goal_position.assert_any_call( + 13, np.deg2rad(self.mod.middle_pos[2] + 30) + ) + self.mock_c.write_goal_position.assert_any_call( + 14, np.deg2rad(self.mod.middle_pos[3] + (-40)) + ) + + def test_move_ring_uses_servos_4_5_and_middle_pos_4_5(self): + self.mod.Move_Ring(-50, 60, 7) + self.mock_c.write_goal_speed.assert_any_call(11, 7) + self.mock_c.write_goal_speed.assert_any_call(12, 7) + self.mock_c.write_goal_position.assert_any_call( + 11, np.deg2rad(self.mod.middle_pos[4] + (-50)) + ) + self.mock_c.write_goal_position.assert_any_call( + 12, np.deg2rad(self.mod.middle_pos[5] + 60) + ) + + def test_move_thumb_uses_servos_6_7_and_middle_pos_6_7(self): + self.mod.Move_Thumb(0, -90, 4) + self.mock_c.write_goal_speed.assert_any_call(17, 4) + self.mock_c.write_goal_speed.assert_any_call(18, 4) + self.mock_c.write_goal_position.assert_any_call( + 17, np.deg2rad(self.mod.middle_pos[6] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 18, np.deg2rad(self.mod.middle_pos[7] + (-90)) + ) + + +class TestAmazingHandDemoOpenClose(unittest.TestCase): + """OpenHand and CloseHand call controller with expected speeds and positions.""" + + def setUp(self): + self.mock_c = MagicMock() + self.mod = _reload_demo() + self.mod.c = self.mock_c + + def test_open_hand_writes_all_fingers_open(self): + self.mod.OpenHand() + # Index (15,16): -35, 35 -> rad(MiddlePos[0]-35), rad(MiddlePos[1]+35) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-35)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 35) + ) + # All 8 servos get speed and position + self.assertEqual(self.mock_c.write_goal_speed.call_count, 8) + self.assertEqual(self.mock_c.write_goal_position.call_count, 8) + + def test_close_hand_writes_all_fingers_closed(self): + self.mod.CloseHand() + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + (-90)) + ) + self.assertEqual(self.mock_c.write_goal_speed.call_count, 8) + self.assertEqual(self.mock_c.write_goal_position.call_count, 8) + + +class TestAmazingHandDemoGestures(unittest.TestCase): + """Index_Pointing, SpreadHand (left), and one composite gesture.""" + + def setUp(self): + self.mock_c = MagicMock() + self.mod = _reload_demo() + self.mod.c = self.mock_c + + def test_index_pointing_index_open_others_closed(self): + self.mod.Index_Pointing() + # Index open: -40, 40 + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-40)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 40) + ) + # Middle/Ring/Thumb closed (90, -90) + self.mock_c.write_goal_position.assert_any_call( + 13, np.deg2rad(self.mod.middle_pos[2] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 14, np.deg2rad(self.mod.middle_pos[3] + (-90)) + ) + + def test_spread_hand_left_uses_expected_angles(self): + self.mod.SpreadHand() + # Left: Index(-60,0), Middle(-35,35), Ring(-4,90), Thumb(-4,90) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-60)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 11, np.deg2rad(self.mod.middle_pos[4] + (-4)) + ) + self.mock_c.write_goal_position.assert_any_call( + 12, np.deg2rad(self.mod.middle_pos[5] + 90) + ) + + def test_victory_left_calls_controller(self): + self.mod.Victory() + # Index and Middle extended; Ring/Thumb closed + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-65)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 15) + ) + self.assertEqual(self.mock_c.write_goal_position.call_count, 8) + + @patch("AmazingHand_Demo.time.sleep") + def test_open_hand_progressive_orders_index_middle_ring_thumb(self, mock_sleep): + self.mod.OpenHand_Progressive() + # Each finger: (-35, 35), speed MaxSpeed-2 = 5 + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-35)) + ) + self.mock_c.write_goal_position.assert_any_call( + 13, np.deg2rad(self.mod.middle_pos[2] + (-35)) + ) + self.mock_c.write_goal_position.assert_any_call( + 11, np.deg2rad(self.mod.middle_pos[4] + (-35)) + ) + self.mock_c.write_goal_position.assert_any_call( + 17, np.deg2rad(self.mod.middle_pos[6] + (-35)) + ) + self.assertEqual(self.mock_c.write_goal_position.call_count, 8) + + def test_clench_hand_left_uses_expected_angles(self): + self.mod.ClenchHand() + # Left: Index(0,60), Middle(-35,35), Ring(-70,0), Thumb(-90,-4) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 60) + ) + self.mock_c.write_goal_position.assert_any_call( + 11, np.deg2rad(self.mod.middle_pos[4] + (-70)) + ) + self.mock_c.write_goal_position.assert_any_call( + 18, np.deg2rad(self.mod.middle_pos[7] + (-4)) + ) + + def test_perfect_left_uses_expected_angles(self): + self.mod.Perfect() + # Left: Index(50,-50), Middle(0,0), Ring(-20,20), Thumb(-12,-65) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 50) + ) + self.mock_c.write_goal_position.assert_any_call( + 13, np.deg2rad(self.mod.middle_pos[2] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 17, np.deg2rad(self.mod.middle_pos[6] + (-12)) + ) + self.mock_c.write_goal_position.assert_any_call( + 18, np.deg2rad(self.mod.middle_pos[7] + (-65)) + ) + + def test_pinched_left_uses_expected_angles(self): + self.mod.Pinched() + # Left: Index/Middle/Ring (90,-90), Thumb (75,5) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 18, np.deg2rad(self.mod.middle_pos[7] + 5) + ) + + def test_fuck_left_uses_expected_angles(self): + self.mod.Fuck() + # Left: Index/Ring (90,-90), Middle (-35,35), Thumb (75,0) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 14, np.deg2rad(self.mod.middle_pos[3] + 35) + ) + self.mock_c.write_goal_position.assert_any_call( + 18, np.deg2rad(self.mod.middle_pos[7] + 0) + ) + + @patch("AmazingHand_Demo.time.sleep") + def test_nonono_calls_index_pointing_then_wag_then_open(self, mock_sleep): + self.mod.Nonono() + # Index_Pointing: Index (-40,40), others closed + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-40)) + ) + # Wag: (-10,80) and (-80,10) each 3 times; then final open (-35,35) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-10)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 80) + ) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-80)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 10) + ) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-35)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 35) + ) + + @patch("AmazingHand_Demo.time.sleep") + def test_scissors_left_calls_victory_then_alternating_index_middle(self, mock_sleep): + self.mod.Scissors() + # Victory: Index (-65,15), Middle (-15,65), Ring/Thumb closed + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-65)) + ) + self.mock_c.write_goal_position.assert_any_call( + 14, np.deg2rad(self.mod.middle_pos[3] + 65) + ) + # Left loop 3x: Index (-20,50) then (-65,15); Middle (-50,20) then (-15,65) + self.mock_c.write_goal_position.assert_any_call( + 15, np.deg2rad(self.mod.middle_pos[0] + (-20)) + ) + self.mock_c.write_goal_position.assert_any_call( + 16, np.deg2rad(self.mod.middle_pos[1] + 50) + ) + self.mock_c.write_goal_position.assert_any_call( + 13, np.deg2rad(self.mod.middle_pos[2] + (-50)) + ) + self.mock_c.write_goal_position.assert_any_call( + 14, np.deg2rad(self.mod.middle_pos[3] + 20) + ) + + +class TestAmazingHandDemoGesturesRightHand(unittest.TestCase): + """Right-hand (Side=1) branches of gestures. Uses same pattern as left-hand tests.""" + + def setUp(self): + self.mock_c = MagicMock() + self.mod = _reload_demo(_DEMO_CONFIG_RIGHT) + self.mod.c = self.mock_c + + def test_servo_ids_right_hand(self): + self.assertEqual(self.mod.side, 1) + self.assertEqual(self.mod.servo_ids, [1, 2, 3, 4, 6, 5, 8, 7]) + + def test_spread_hand_right_uses_expected_angles(self): + self.mod.SpreadHand() + # Right: Index(4,90), Middle(-32,32), Ring(-90,-4), Thumb(-90,-4) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + 4) + ) + self.mock_c.write_goal_position.assert_any_call( + 2, np.deg2rad(self.mod.middle_pos[1] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 6, np.deg2rad(self.mod.middle_pos[4] + (-90)) + ) + self.mock_c.write_goal_position.assert_any_call( + 8, np.deg2rad(self.mod.middle_pos[6] + (-90)) + ) + + def test_clench_hand_right_uses_expected_angles(self): + self.mod.ClenchHand() + # Right: Index(-60,0), Middle(-35,35), Ring(0,70), Thumb(-4,90). Ring = ServoIDs[4], [5] = 6, 5. + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + (-60)) + ) + self.mock_c.write_goal_position.assert_any_call( + 6, np.deg2rad(self.mod.middle_pos[4] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 5, np.deg2rad(self.mod.middle_pos[5] + 70) + ) + self.mock_c.write_goal_position.assert_any_call( + 8, np.deg2rad(self.mod.middle_pos[6] + (-4)) + ) + + def test_perfect_right_uses_expected_angles(self): + self.mod.Perfect() + # Right: Index(50,-50), Middle(0,0), Ring(-20,20), Thumb(65,12) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + 50) + ) + self.mock_c.write_goal_position.assert_any_call( + 8, np.deg2rad(self.mod.middle_pos[6] + 65) + ) + self.mock_c.write_goal_position.assert_any_call( + 7, np.deg2rad(self.mod.middle_pos[7] + 12) + ) + + def test_victory_right_uses_expected_angles(self): + self.mod.Victory() + # Right: Index(-15,65), Middle(-65,15), Ring/Thumb (90,-90) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + (-15)) + ) + self.mock_c.write_goal_position.assert_any_call( + 2, np.deg2rad(self.mod.middle_pos[1] + 65) + ) + self.mock_c.write_goal_position.assert_any_call( + 3, np.deg2rad(self.mod.middle_pos[2] + (-65)) + ) + self.mock_c.write_goal_position.assert_any_call( + 4, np.deg2rad(self.mod.middle_pos[3] + 15) + ) + self.assertEqual(self.mock_c.write_goal_position.call_count, 8) + + def test_pinched_right_uses_expected_angles(self): + self.mod.Pinched() + # Right: Index/Middle/Ring (90,-90), Thumb (0,-75) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 8, np.deg2rad(self.mod.middle_pos[6] + 0) + ) + self.mock_c.write_goal_position.assert_any_call( + 7, np.deg2rad(self.mod.middle_pos[7] + (-75)) + ) + + def test_fuck_right_uses_expected_angles(self): + self.mod.Fuck() + # Right: Index/Ring (90,-90), Middle(-35,35), Thumb(0,-75) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + 90) + ) + self.mock_c.write_goal_position.assert_any_call( + 4, np.deg2rad(self.mod.middle_pos[3] + 35) + ) + self.mock_c.write_goal_position.assert_any_call( + 7, np.deg2rad(self.mod.middle_pos[7] + (-75)) + ) + + @patch("AmazingHand_Demo.time.sleep") + def test_scissors_right_calls_victory_then_alternating_index_middle(self, mock_sleep): + self.mod.Scissors() + # Victory right: Index(-15,65), Middle(-65,15) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + (-15)) + ) + # Right loop 3x: Index(-50,20) then (-15,65); Middle(-20,50) then (-65,15) + self.mock_c.write_goal_position.assert_any_call( + 1, np.deg2rad(self.mod.middle_pos[0] + (-50)) + ) + self.mock_c.write_goal_position.assert_any_call( + 2, np.deg2rad(self.mod.middle_pos[1] + 20) + ) + self.mock_c.write_goal_position.assert_any_call( + 3, np.deg2rad(self.mod.middle_pos[2] + (-20)) + ) + self.mock_c.write_goal_position.assert_any_call( + 4, np.deg2rad(self.mod.middle_pos[3] + 50) + ) + + +class TestAmazingHandDemoMain(unittest.TestCase): + """main() behavior (init failure path only; loop not run).""" + + def test_main_returns_on_controller_init_failure(self): + mod = _reload_demo() + with patch("sys.argv", ["AmazingHand_Demo.py"]): + with patch("AmazingHand_Demo.create_controller", side_effect=RuntimeError("no device")): + mod.main() + self.assertIsNone(mod.c) + + def test_main_sets_controller_on_success(self): + mod = _reload_demo() + fake = MagicMock() + with patch("sys.argv", ["AmazingHand_Demo.py"]): + with patch("AmazingHand_Demo.create_controller", return_value=fake): + with patch("time.sleep", side_effect=SystemExit(0)): + with patch("time.time", return_value=0.0): + try: + mod.main() + except SystemExit: + pass + self.assertIs(mod.c, fake) + + +if __name__ == "__main__": + unittest.main() diff --git a/PythonExample/tests/test_common.py b/PythonExample/tests/test_common.py new file mode 100644 index 0000000..bd56e87 --- /dev/null +++ b/PythonExample/tests/test_common.py @@ -0,0 +1,134 @@ +# Original authors: Pollen Robotics, AmazingHand authors. +# See: https://github.com/pollen-robotics/AmazingHand +# +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +# PythonExample root: one level up from tests/ +_EXAMPLE_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_EXAMPLE_ROOT)) + +# Allow tests to run without rustypot (hardware) installed +sys.modules["rustypot"] = MagicMock() +sys.modules["rustypot"].Scs0009PyController = MagicMock() + +import common # noqa: E402 + + +class TestDefaultSerialPort(unittest.TestCase): + def test_returns_string(self): + self.assertIsInstance(common.default_serial_port(), str) + + def test_platform_value(self): + port = common.default_serial_port() + if sys.platform == "win32": + self.assertEqual(port, "COM3") + else: + self.assertEqual(port, "/dev/ttyUSB0") + + +class TestGetTeam(unittest.TestCase): + def test_default_is_julia(self): + with patch.dict(os.environ, {"AMAZINGHAND_TEAM": ""}, clear=False): + self.assertEqual(common.get_team(), "julia") + + def test_env_team_used(self): + with patch.dict(os.environ, {"AMAZINGHAND_TEAM": "krishan"}, clear=False): + self.assertEqual(common.get_team(), "krishan") + + def test_normalized_lower(self): + with patch.dict(os.environ, {"AMAZINGHAND_TEAM": " Krishan "}, clear=False): + self.assertEqual(common.get_team(), "krishan") + + +class TestLoadConfig(unittest.TestCase): + def test_missing_file_returns_defaults(self): + cfg = common.load_config(path=Path("/nonexistent/config.toml")) + self.assertEqual(cfg["baudrate"], 1000000) + self.assertEqual(cfg["timeout"], 0.5) + self.assertEqual(cfg["port"], "") + + def test_existing_config_team_section(self): + config_path = _EXAMPLE_ROOT / "config.toml" + if not config_path.exists(): + self.skipTest("config.toml not found") + cfg = common.load_config(team="krishan", path=config_path) + self.assertIn("port", cfg) + self.assertIn("baudrate", cfg) + self.assertIn("timeout", cfg) + self.assertEqual(cfg["port"], "COM3") + self.assertEqual(cfg["timeout"], 2.5) + + def test_team_julia_section(self): + config_path = _EXAMPLE_ROOT / "config.toml" + if not config_path.exists(): + self.skipTest("config.toml not found") + cfg = common.load_config(team="julia", path=config_path) + self.assertEqual(cfg["port"], "/dev/ttyACM0") + self.assertEqual(cfg["timeout"], 0.5) + + +class TestCreateController(unittest.TestCase): + @patch("common.Scs0009PyController") + def test_called_with_config_values(self, mock_controller): + if not (_EXAMPLE_ROOT / "config.toml").exists(): + self.skipTest("config.toml not found") + with patch("common.load_config") as mock_load: + mock_load.return_value = {"port": "COM9", "baudrate": 1000000, "timeout": 1.0} + common.create_controller(team="krishan") + mock_controller.assert_called_once_with( + serial_port="COM9", baudrate=1000000, timeout=1.0 + ) + + @patch("common.Scs0009PyController") + def test_explicit_args_override_config(self, mock_controller): + with patch("common.load_config") as mock_load: + mock_load.return_value = {"port": "COM9", "baudrate": 1000000, "timeout": 1.0} + common.create_controller(serial_port="/dev/ttyUSB1", timeout=0.1) + mock_controller.assert_called_once_with( + serial_port="/dev/ttyUSB1", baudrate=1000000, timeout=0.1 + ) + + +class TestGetDemoHandConfig(unittest.TestCase): + def test_returns_servo_ids_and_middle_pos_when_complete(self): + cfg = { + "hand_2_index_servo_ids": [15, 16], + "hand_2_index_middle_pos": [-12, 2], + "hand_2_middle_servo_ids": [13, 14], + "hand_2_middle_middle_pos": [2, 5], + "hand_2_ring_servo_ids": [11, 12], + "hand_2_ring_middle_pos": [-2, -8], + "hand_2_thumb_servo_ids": [17, 18], + "hand_2_thumb_middle_pos": [0, -15], + } + out = common.get_demo_hand_config(cfg, 2) + self.assertEqual(out["servo_ids"], [15, 16, 13, 14, 11, 12, 17, 18]) + self.assertEqual(out["middle_pos"], [-12, 2, 2, 5, -2, -8, 0, -15]) + + def test_raises_when_hand_config_incomplete(self): + cfg = {"hand_2_index_servo_ids": [], "hand_2_index_middle_pos": []} + with self.assertRaises(ValueError) as ctx: + common.get_demo_hand_config(cfg, 2) + self.assertIn("not configured", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/PythonExample/tests/test_config.py b/PythonExample/tests/test_config.py new file mode 100644 index 0000000..db48a6c --- /dev/null +++ b/PythonExample/tests/test_config.py @@ -0,0 +1,52 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import unittest +from pathlib import Path + +EXAMPLE_ROOT = Path(__file__).resolve().parent.parent +CONFIG_PATH = EXAMPLE_ROOT / "config.toml" + + +class TestConfigStructure(unittest.TestCase): + """Check config.toml exists and has expected team sections and serial keys.""" + + def test_config_file_exists(self): + self.assertTrue(CONFIG_PATH.exists(), f"config.toml not found at {CONFIG_PATH}") + + def test_config_has_team_sections(self): + if sys.version_info < (3, 11): + self.skipTest("tomllib requires Python 3.11+") + import tomllib + with open(CONFIG_PATH, "rb") as f: + data = tomllib.load(f) + self.assertIn("team_julia", data) + self.assertIn("team_krishan", data) + + def test_team_sections_have_serial_keys(self): + if sys.version_info < (3, 11): + self.skipTest("tomllib requires Python 3.11+") + import tomllib + with open(CONFIG_PATH, "rb") as f: + data = tomllib.load(f) + for team in ("team_julia", "team_krishan"): + section = data[team] + self.assertIn("port", section, f"{team} missing port") + self.assertIn("baudrate", section, f"{team} missing baudrate") + self.assertIn("timeout", section, f"{team} missing timeout") + + +if __name__ == "__main__": + unittest.main() diff --git a/PythonExample/tests/test_finger_demos.py b/PythonExample/tests/test_finger_demos.py new file mode 100644 index 0000000..2aff75b --- /dev/null +++ b/PythonExample/tests/test_finger_demos.py @@ -0,0 +1,84 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import sys +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import numpy as np + +_EXAMPLE_ROOT = Path(__file__).resolve().parent.parent +if str(_EXAMPLE_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLE_ROOT)) + +sys.modules["rustypot"] = MagicMock() +sys.modules["rustypot"].Scs0009PyController = MagicMock() + + +_FINGER_CONFIG = { + "finger_test_servo_ids": [13, 14], + "finger_test_middle_pos": [5, -2], + "timeout": 0.5, +} + + +def _reload_with_mock(module_name, mock_controller): + if module_name in sys.modules: + del sys.modules[module_name] + with patch("common.create_controller", return_value=mock_controller), patch( + "common.load_config", return_value=_FINGER_CONFIG + ): + mod = importlib.import_module(module_name) + return mod + + +class TestAmazingHandFingerTest(unittest.TestCase): + """Unit tests for AmazingHand_FingerTest (CloseFinger / OpenFinger).""" + + def test_close_finger_calls_controller_with_expected_positions(self): + mock_c = MagicMock() + mod = _reload_with_mock("AmazingHand_FingerTest", mock_c) + mod.close_finger() + mock_c.write_goal_speed.assert_any_call(13, 6) + mock_c.write_goal_speed.assert_any_call(14, 6) + mock_c.write_goal_position.assert_any_call(13, np.deg2rad(5 + 90)) + mock_c.write_goal_position.assert_any_call(14, np.deg2rad(-2 - 90)) + + def test_open_finger_calls_controller_with_expected_positions(self): + mock_c = MagicMock() + mod = _reload_with_mock("AmazingHand_FingerTest", mock_c) + mod.open_finger() + mock_c.write_goal_speed.assert_any_call(13, 6) + mock_c.write_goal_speed.assert_any_call(14, 6) + mock_c.write_goal_position.assert_any_call(13, np.deg2rad(5 - 30)) + mock_c.write_goal_position.assert_any_call(14, np.deg2rad(-2 + 30)) + + +class TestAmazingHandHandFingerMiddlePos(unittest.TestCase): + """Unit tests for AmazingHand_Hand_FingerMiddlePos (ServosInMiddle).""" + + def test_servos_in_middle_calls_controller_with_expected_positions(self): + mock_c = MagicMock() + mod = _reload_with_mock("AmazingHand_Hand_FingerMiddlePos", mock_c) + mod.servos_in_middle() + mock_c.write_goal_speed.assert_any_call(13, 6) + mock_c.write_goal_speed.assert_any_call(14, 6) + mock_c.write_goal_position.assert_any_call(13, np.deg2rad(5)) + mock_c.write_goal_position.assert_any_call(14, np.deg2rad(-2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/README.md b/README.md index 7decc13..26eec93 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Amazing Hand is: [AmazingHand_Overview](/docs/AmazingHand_Overview.pdf) ![Hand Overview](assets/Hand_Overview.jpg) -Each finger is driven by a parallel mechanism. +Each finger is driven by a parallel mechanism. That means 2x small Feetech SCS0009 servos are used to move each finger in flexion/extension and abduction/adduction ![Finger Overview](assets/Finger_Overview.jpg) @@ -56,10 +56,10 @@ Up to you ! - [BOM (Bill Of Materials)](#bom-bill-of-materials) - [CAD Files and Onshape document](#cad-files-and-onshape-document) - [Assembly Guide](#assembly-guide) - - [Run_basic_Demo](#run-basic-demo) + - [Run_basic_Demo](#run-basic-demo) - [Disclaimer](#disclaimer) -- [AmazingHand advanced Demo](#amazinghand-advanced-demo) -- [Don't want to build it by yourself? Kits are available!](#don't-want-to-build-it-by-yourself-?) +- [AmazingHand advanced Demo](#amazinghand-advanced-demo) +- [Don't want to build it by yourself? Kits are available!](#don't-want-to-build-it-by-yourself-?) - [Project Updates & Community](#project-updates-&-community) - [To Do List](#to-do-list) - [FAQ](#faq) @@ -69,43 +69,43 @@ Up to you ! # Build Resources ## BOM (Bill Of Materials) -List of all needed components is available here: -[AmazingHand BOM](https://docs.google.com/spreadsheets/d/1QH2ePseqXjAhkWdS9oBYAcHPrxaxkSRCgM_kOK0m52E/edit?gid=1269903342#gid=1269903342) +List of all needed components is available here: +[AmazingHand BOM](https://docs.google.com/spreadsheets/d/1QH2ePseqXjAhkWdS9oBYAcHPrxaxkSRCgM_kOK0m52E/edit?gid=1269903342#gid=1269903342) ![BOM](assets/BOM.jpg) And remember to add control choice cost (2 options detailed previously) -Details for custom 3D printed parts are here: +Details for custom 3D printed parts are here: [3Dprinted parts](https://docs.google.com/spreadsheets/d/1QH2ePseqXjAhkWdS9oBYAcHPrxaxkSRCgM_kOK0m52E/edit?gid=2050623549#gid=2050623549) ![3Dparts](assets/3Dparts.jpg) Here is a guide explaining how to print all the needed custom parts: [=> 3D Printing Guide](/docs/AmazingHand_3DprintingTips.pdf) -![3DPrint_example](/assets/3DPrint.jpg) +![3DPrint_example](/assets/3DPrint.jpg) ## CAD Files and Onshape document -STL and Steps files can be found [here](https://github.com/pollen-robotics/AmazingHand/tree/main/cad) +STL and Steps files can be found [here](https://github.com/pollen-robotics/AmazingHand/tree/main/cad) Note that fingers are the same if you want to build a left hand, but some parts are symmetrical. Specific right hand parts are preceded by an "R", and other left hand parts by an "L". ![Heart](/assets/Heart.jpg) -Everyone can access the Onshape document too: -[Link Onshape](https://cad.onshape.com/documents/430ff184cf3dd9557aaff2be/w/e3658b7152c139971d22c688/e/bd399bf1860732c6c6a2ee45?renderMode=0&uiState=6867fd3ef773466d059edf0c) +Everyone can access the Onshape document too: +[Link Onshape](https://cad.onshape.com/documents/430ff184cf3dd9557aaff2be/w/e3658b7152c139971d22c688/e/bd399bf1860732c6c6a2ee45?renderMode=0&uiState=6867fd3ef773466d059edf0c) Note that predefined positions are available in the "named position" tooling, with corresponding servo angles -![Onshape&Named_Pos](/assets/Named_Pos.jpg) +![Onshape&Named_Pos](/assets/Named_Pos.jpg) ## Assembly Guide -Assembly guide for the Amazing Hand in combination with standard components in the BOM is here: -[=> Assembly Guide](/docs/AmazingHand_Assembly.pdf) -![Assembly_example](/assets/Assembly.jpg) +Assembly guide for the Amazing Hand in combination with standard components in the BOM is here: +[=> Assembly Guide](/docs/AmazingHand_Assembly.pdf) +![Assembly_example](/assets/Assembly.jpg) You will need a simple program / script to calibrate each finger, available here: - With Python & Waveshare serial bus driver: [here](https://github.com/pollen-robotics/AmazingHand/tree/main/PythonExample) @@ -125,10 +125,10 @@ You will need an external power supply to be able to power the 8 actuators insid If you don't have one already, a simple external power supply could be a DC/DC 220V -> 5V / 2A adapter with jack connector. Check on the BOM list: -[AmazingHand BOM](https://docs.google.com/spreadsheets/d/1QH2ePseqXjAhkWdS9oBYAcHPrxaxkSRCgM_kOK0m52E/edit?gid=1269903342#gid=1269903342) +[AmazingHand BOM](https://docs.google.com/spreadsheets/d/1QH2ePseqXjAhkWdS9oBYAcHPrxaxkSRCgM_kOK0m52E/edit?gid=1269903342#gid=1269903342) - Python script: "AmazingHand_Demo.py" [here](https://github.com/pollen-robotics/AmazingHand/tree/main/ArduinoExample) - + - Arduino program: "AmazingHand_Demo.ino" [here](https://github.com/pollen-robotics/AmazingHand/tree/main/PythonExample) @@ -178,9 +178,9 @@ https://shop.wowrobo.com/products/amazing-hand-the-open-source-robotic-hand-kit # Project Updates & Community ## Updates from community -- ### Amazing Base for the amazing hand : +- ### Amazing Base for the amazing hand : ![Base](assets/Base.jpg) -STL or Step file can be found [here](https://github.com/pollen-robotics/AmazingHand/tree/main/cad) +STL or Step file can be found [here](https://github.com/pollen-robotics/AmazingHand/tree/main/cad) - ### Specific Chinese BOM available here : [Chinese BOM](https://docs.google.com/spreadsheets/d/1fHZiTky79vyZwICj5UGP2c_RiuLLm89K8HrB3vpb2h4/edit?gid=837395814#gid=837395814) @@ -204,7 +204,7 @@ Or directly on onshape workspace, folder "community update" ## To Do List - Design small custom pcb with serial hub and power supply functions, to fit everything in the hand -- Test with prehensile tasks +- Test with prehensile tasks => Add smarter behaviour for closing hand, based on available motors feedbacks - Study possibility to have 4 different fingers length, or add a 5th finger - Study possibility to use STS3032 Feetech motors instead of SCS0009 @@ -218,14 +218,14 @@ WIP ## Contact -You can reach public discord channel here : +You can reach public discord channel here : [Discord AmazingHand](https://discord.com/channels/519098054377340948/1395021147346698300) -Or +Or [Contact me or Pollen Robotics](/docs/contact.md) ## Thank you Huge thanks to those who have contributed to this project so far: - [Steve N'Guyen](https://github.com/SteveNguyen) for beta testing, Feetech motors integration in Rustypot, Mujoco/Mink and hand tracking demo -- [Pierre Rouanet](https://github.com/pierre-rouanet) for Feetech motors integration in pypot +- [Pierre Rouanet](https://github.com/pierre-rouanet) for Feetech motors integration in pypot - [Augustin Crampette](https://fr.linkedin.com/in/augustin-crampette) & [Matthieu Lapeyre](https://www.linkedin.com/in/matthieulapeyre/) for open discussions and mechanical advices diff --git a/docs/contact.md b/docs/contact.md index 3b74667..17bd6ef 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -1,6 +1,6 @@ # Contact -If you have any questions or suggestions, feel free to contact us at the following addresses: +If you have any questions or suggestions, feel free to contact us at the following addresses: For general mechanical questions : [jeremy.laville@pollen-robotics.com](mailto:jeremy.laville@pollen-robotics.com) @@ -8,11 +8,11 @@ For general mechanical questions : For specific Hand tracking Demo : [steve.nguyen@pollen-robotics.com](mailto:steve.nguyen@pollen-robotics.com) -Reach me on social networks: -- [LinkedIn](https://fr.linkedin.com/in/jeremy-laville-1038b176) +Reach me on social networks: +- [LinkedIn](https://fr.linkedin.com/in/jeremy-laville-1038b176) -Or reach pollen robotics: +Or reach pollen robotics: - [contact@pollen-robotics.com](mailto:contact@pollen-robotics.com) -- [YouTube](https://www.youtube.com/@PollenRobotics/featured) +- [YouTube](https://www.youtube.com/@PollenRobotics/featured) - [LinkedIn](https://www.linkedin.com/company/pollen-robotics/) - [X](https://x.com/pollenrobotics/) diff --git a/docs/maintainer.md b/docs/maintainer.md new file mode 100644 index 0000000..d83b39c --- /dev/null +++ b/docs/maintainer.md @@ -0,0 +1,57 @@ +# Maintainer Guide + +## Run CI Workflow Locally + +From the repository root, run the same steps as CI: + +**1. Run pre-commit on PythonExample only** + +```bash +pixi run pre-commit run --files $(find PythonExample -type f) +``` + +**2. Run unit tests** + +```bash +pixi run test +``` + +To run pre-commit on all files (hooks still only process PythonExample per config): + +```bash +pixi run pre-commit run --all-files +``` + +## Optional: Full GitHub Actions Locally + +Use [act](https://github.com/nektos/act) (requires Docker) to run the workflow. + +Install act (Linux): + +```bash +curl -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash +``` + +The script installs to `./bin/act` in the current directory. To make it available system-wide: + +```bash +sudo mv ./bin/act /usr/local/bin/ +``` + +On first run, act prompts for a default image. Choose Medium (~500MB); it works with most actions including setup-pixi. + +Use `workflow_dispatch` (the CI workflow skips checkout when running under act and uses your local workspace): + +```bash +act workflow_dispatch +``` + +Or simulate push/PR (may fail with "reference not found" if the branch is not on the remote): + +```bash +act push +# or +act pull_request +``` + + diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..9a41085 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,1034 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - 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_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_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/libsqlite-3.51.2-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.12-hd63d673_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/69/7d/5cd09ada4c979035348188efdb454df14ea032e4ebaa8745d579a56b0cf6/rustypot-1.4.2-cp312-cp312-manylinux_2_24_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.2-hb1525cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.2-h10b116e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.12-h91f4b29_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/ec/6b/bfa2e5a16d7b9977c6cf65acf50eb2beb90199df8ef70abb6766d41b7d2b/rustypot-1.4.2-cp312-cp312-manylinux_2_24_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.2-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.12-h0159041_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.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/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/11/cb/60e4be7b67dd8e0adfe73d0d30c11977ca1a2639ce0b852d880852cae4bc/rustypot-1.4.2-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 + md5: 468fd3bb9e1f671d36c2cbc677e56f1d + depends: + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + purls: [] + size: 28926 + timestamp: 1770939656741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 + md5: 51a19bba1b8ebfb60df25cde030b7ebc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260341 + timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + sha256: d2a296aa0b5f38ed9c264def6cf775c0ccb0f110ae156fcde322f3eccebf2e01 + md5: 2921ac0b541bf37c69e66bd6d9a43bca + depends: + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 192536 + timestamp: 1757437302703 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 + md5: 1077e9333c41ff0be8edd1a5ec0ddace + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 55977 + timestamp: 1757437738856 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda + sha256: 4ddcb01be03f85d3db9d881407fb13a673372f1b9fac9c836ea441893390e049 + md5: 84d389c9eee640dda3d26fc5335c67d8 + depends: + - __win + license: ISC + purls: [] + size: 147139 + timestamp: 1767500904211 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 + md5: bddacf101bb4dd0e51811cb69c7790e2 + depends: + - __unix + license: ISC + purls: [] + size: 146519 + timestamp: 1767500828366 +- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + name: cfgv + version: 3.5.0 + sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + name: colorama + version: 0.4.6 + sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + name: distlib + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + name: filelock + version: 3.24.2 + sha256: 667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + sha256: 142a722072fa96cf16ff98eaaf641f54ab84744af81754c292cb81e0881c0329 + md5: 186a18e3ba246eccfc7cff00cd19a870 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12728445 + timestamp: 1767969922681 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.2-hb1525cb_0.conda + sha256: 09f7f9213eb68e7e4291cd476e72b37f3ded99ed957528567f32f5ba6b611043 + md5: 15b35dc33e185e7d2aac1cfcd6778627 + depends: + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12852963 + timestamp: 1767975394622 +- pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + name: identify + version: 2.6.16 + sha256: 391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 + requires_dist: + - ukkonen ; extra == 'license' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + name: iniconfig + version: 2.3.0 + sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 + md5: 12bd9a3f089ee6c9266a37dab82afabd + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 725507 + timestamp: 1770267139900 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + sha256: 44527364aa333be631913451c32eb0cae1e09343827e9ce3ccabd8d962584226 + md5: 35b2ae7fadf364b8e5fb8185aaeb80e5 + depends: + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-aarch64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 875924 + timestamp: 1770267209884 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f + md5: 8b09ae86839581147ef2e5c5e229d164 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 76643 + timestamp: 1763549731408 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda + sha256: cc2581a78315418cc2e0bb2a273d37363203e79cefe78ba6d282fed546262239 + md5: b414e36fbb7ca122030276c75fa9c34a + depends: + - libgcc >=14 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 76201 + timestamp: 1763549910086 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda + sha256: 844ab708594bdfbd7b35e1a67c379861bcd180d6efe57b654f482ae2f7f5c21e + md5: 8c9e4f1a0e688eef2e95711178061a0f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 70137 + timestamp: 1763550049107 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 + md5: 2f364feefb6a7c00423e80dcb12db62a + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 55952 + timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + sha256: 43860222cf3abf04ded0cf24541a105aa388e0e1d4d6ca46258e186d4e87ae3e + md5: 3c281169ea25b987311400d7a7e28445 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_17 + - libgomp 15.2.0 he0feb66_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1040478 + timestamp: 1770252533873 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_17.conda + sha256: 3709e656917d282b5d3d78928a7a101bd638aaa9d17fb8689e6f95428d519056 + md5: 0d842d2053b95a6dbb83554948e7cbfe + depends: + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.2.0 h8acb6b2_17 + - libgcc-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 622154 + timestamp: 1770252127670 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda + sha256: bdfe50501e4a2d904a5eae65a7ae26e2b7a29b473ab084ad55d96080b966502e + md5: 1478bfa85224a65ab096d69ffd2af1e5 + depends: + - libgcc 15.2.0 he0feb66_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27541 + timestamp: 1770252546553 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_17.conda + sha256: 308bd5fe9cd4b19b08c07437a626cf02a1d70a43216d68469d17003aece63202 + md5: bcd31f4a57798bd158dfc0647fa77ca3 + depends: + - libgcc 15.2.0 h8acb6b2_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27555 + timestamp: 1770252134574 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + sha256: b961b5dd9761907a7179678b58a69bb4fc16b940eb477f635aea3aec0a3f17a6 + md5: 51b78c6a757575c0d12f4401ffc67029 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603334 + timestamp: 1770252441199 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_17.conda + sha256: ffa6169ef78e5333ecb08152141932c5f8ca4f4d3742b1a5eec67173e70b907a + md5: 0ad9074c91b35dbf8b58bc68a9f9bda0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 587183 + timestamp: 1770252042141 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb + md5: c7c83eecbb72d88b940c249af56c8b17 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 113207 + timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + sha256: 843c46e20519651a3e357a8928352b16c5b94f4cd3d5481acc48be2e93e8f6a3 + md5: 96944e3c92386a12755b94619bae0b35 + depends: + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 125916 + timestamp: 1768754941722 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + sha256: f25bf293f550c8ed2e0c7145eb404324611cfccff37660869d97abf526eb957c + md5: ba0bfd4c3cf73f299ffe46ff0eaeb8e3 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 106169 + timestamp: 1768752763559 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + sha256: c0dc4d84198e3eef1f37321299e48e2754ca83fd12e6284754e3cb231357c3a5 + md5: d5d58b2dc3e57073fe22303f5fed4db7 + depends: + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 34831 + timestamp: 1750274211 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + sha256: 04596fcee262a870e4b7c9807224680ff48d4d0cc0dac076a602503d3dc6d217 + md5: da5be73701eecd0e8454423fd6ffcf30 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 942808 + timestamp: 1768147973361 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.2-h10b116e_0.conda + sha256: 5f8230ccaf9ffaab369adc894ef530699e96111dac0a8ff9b735a871f8ba8f8b + md5: 4e3ba0d5d192f99217b85f07a0761e64 + depends: + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 944688 + timestamp: 1768147991301 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.2-hf5d6505_0.conda + sha256: 756478128e3e104bd7e7c3ce6c1b0efad7e08c7320c69fdc726e039323c63fbb + md5: 903979414b47d777d548e5f0165e6cd8 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1291616 + timestamp: 1768148278261 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + sha256: 50c48cd3716a2e58e8e2e02edc78fef2d08fffe1e3b1ed40eb5f87e7e2d07889 + md5: 24c2fe35fa45cd71214beba6f337c071 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_17 + constrains: + - libstdcxx-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852406 + timestamp: 1770252584235 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_17.conda + sha256: fe425c07aa3116e7d5f537efe010d545bb43ac120cb565fbfb5ece8cb725aa80 + md5: 500d275cb616d81b5d9828aedffc4bc3 + depends: + - libgcc 15.2.0 h8acb6b2_17 + constrains: + - libstdcxx-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5542688 + timestamp: 1770252159760 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee + md5: db409b7c1720428638e7c0d509d3e1b5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40311 + timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + sha256: c37a8e89b700646f3252608f8368e7eb8e2a44886b92776e57ad7601fc402a11 + md5: cf2861212053d05f27ec49c3784ff8bb + depends: + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 43453 + timestamp: 1766271546875 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda + sha256: 6b46c397644091b8a26a3048636d10b989b1bf266d4be5e9474bf763f828f41f + md5: b4df5d7d4b63579d081fd3a4cf99740e + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 114269 + timestamp: 1702724369203 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 + md5: 08aad7cbe9f5a6b460d0976076b6ae64 + depends: + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 66657 + timestamp: 1727963199518 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 55476 + timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 + md5: 182afabe009dc78d8b73100255ee6868 + depends: + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 926034 + timestamp: 1738196018799 +- pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + name: nodeenv + version: 1.10.0 + sha256: 5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl + name: numpy + version: 2.4.2 + sha256: 209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: numpy + version: 2.4.2 + sha256: 6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.4.2 + sha256: 9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c + md5: f61eb8cd60ff9057122a3d338b99c00f + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + sha256: 7f8048c0e75b2620254218d72b4ae7f14136f1981c5eb555ef61645a9344505f + md5: 25f5885f11e8b1f075bccf4a2da91c60 + depends: + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3692030 + timestamp: 1769557678657 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + sha256: 53a5ad2e5553b8157a91bb8aa375f78c5958f77cb80e9d2ce59471ea8e5c0bd6 + md5: eb585509b815415bc964b2c7e11c7eb3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9343023 + timestamp: 1769557547888 +- pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + name: packaging + version: '26.0' + sha256: b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl + name: platformdirs + version: 4.9.2 + sha256: 9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + name: pre-commit + version: 4.5.1 + sha256: 3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 + requires_dist: + - cfgv>=2.0.0 + - identify>=1.0.0 + - nodeenv>=0.11.1 + - pyyaml>=5.1 + - virtualenv>=20.10.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + name: pygments + version: 2.19.2 + sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + name: pytest + version: 9.0.2 + sha256: 711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1.0.1 + - packaging>=22 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.12-hd63d673_2_cpython.conda + build_number: 2 + sha256: 6621befd6570a216ba94bc34ec4618e4f3777de55ad0adc15fc23c28fadd4d1a + md5: c4540d3de3fa228d9fa95e31f8e97f89 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31457785 + timestamp: 1769472855343 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.12-h91f4b29_2_cpython.conda + build_number: 2 + sha256: b67569e1d6ce065e1d246a38a0c92bcc9f43cf003a6d67a57e7db7240714c5ce + md5: b75f79be54a1422f1259bb7d198e8697 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 13798340 + timestamp: 1769471112 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.12-h0159041_2_cpython.conda + build_number: 2 + sha256: 5937ab50dfeb979f7405132f73e836a29690f21162308b95b240b8037aa99975 + md5: 068897f82240d69580c2d93f93b56ff5 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 15829087 + timestamp: 1769470991307 +- pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl + name: pyyaml + version: 6.0.3 + sha256: 5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: pyyaml + version: 6.0.3 + sha256: ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: pyyaml + version: 6.0.3 + sha256: 9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 + md5: 3d49cad61f829f4f0e0611547a9cda12 + depends: + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 357597 + timestamp: 1765815673644 +- pypi: https://files.pythonhosted.org/packages/11/cb/60e4be7b67dd8e0adfe73d0d30c11977ca1a2639ce0b852d880852cae4bc/rustypot-1.4.2-cp312-cp312-win_amd64.whl + name: rustypot + version: 1.4.2 + sha256: d5310a6394cfcb47ff9759484783aba33fe5f2684948d5308d207f78ee999bb9 + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/69/7d/5cd09ada4c979035348188efdb454df14ea032e4ebaa8745d579a56b0cf6/rustypot-1.4.2-cp312-cp312-manylinux_2_24_x86_64.whl + name: rustypot + version: 1.4.2 + sha256: a40f2431fecd92684490f304c15a940c27b022c18b246a0fd62cfbdeb40a8d84 + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ec/6b/bfa2e5a16d7b9977c6cf65acf50eb2beb90199df8ef70abb6766d41b7d2b/rustypot-1.4.2-cp312-cp312-manylinux_2_24_aarch64.whl + name: rustypot + version: 1.4.2 + sha256: ae4a379886ce710bf052c45ab06cffcd30302654c8d706e135c5fa22220bd1af + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + sha256: e25c314b52764219f842b41aea2c98a059f06437392268f09b03561e4f6e5309 + md5: 7fc6affb9b01e567d2ef1d05b84aa6ed + depends: + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3368666 + timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 +- pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + name: virtualenv + version: 20.37.0 + sha256: 5d3951c32d57232ae3569d4de4cc256c439e045135ebf43518131175d9be435d + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.24.2,<4 ; python_full_version >= '3.10' + - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - furo>=2023.7.26 ; extra == 'docs' + - pre-commit-uv>=4.1.4 ; extra == 'docs' + - proselint>=0.13 ; extra == 'docs' + - sphinx>=7.1.2,!=7.3 ; extra == 'docs' + - sphinx-argparse>=0.4 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.6.2 ; extra == 'docs' + - sphinx-copybutton>=0.5.2 ; extra == 'docs' + - sphinx-inline-tabs>=2025.12.21.14 ; extra == 'docs' + - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' + - towncrier>=23.6 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'test' + - coverage-enable-subprocess>=1 ; extra == 'test' + - coverage>=7.2.7 ; extra == 'test' + - flaky>=3.7 ; extra == 'test' + - packaging>=23.1 ; extra == 'test' + - pytest-env>=0.8.2 ; extra == 'test' + - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') + - pytest-mock>=3.11.1 ; extra == 'test' + - pytest-randomly>=3.12 ; extra == 'test' + - pytest-timeout>=2.1 ; extra == 'test' + - pytest-xdist>=3.5 ; extra == 'test' + - pytest>=7.4 ; extra == 'test' + - setuptools>=68 ; extra == 'test' + - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 + md5: c3655f82dcea2aa179b291e7099c1fcc + depends: + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 614429 + timestamp: 1764777145593 diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..3edd70b --- /dev/null +++ b/pixi.toml @@ -0,0 +1,20 @@ +[workspace] +name = "amazinghand" +version = "0.1.0" +description = "Amazing Hand - Python examples and demos (Pollen Robotics)" +channels = ["conda-forge"] +# Ubuntu (x86_64), Windows (x64), Raspberry Pi (aarch64) +platforms = ["linux-64", "win-64", "linux-aarch64"] + +[dependencies] +python = "3.12.*" + +[pypi-dependencies] +numpy = ">=1.20" +rustypot = "*" +pytest = "*" +pre-commit = "*" + +[tasks] +# PYTHONPATH= avoids loading system/ROS pytest plugins when run locally +test = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" From eb5869ef6c01bd4be49d3f1e50c7c479dff19300 Mon Sep 17 00:00:00 2001 From: juliaj Date: Thu, 19 Feb 2026 16:44:24 -0800 Subject: [PATCH 2/6] Handtracking with webcam --- .github/workflows/ci.yml | 7 +- .gitignore | 10 + Demo/AHControl/README.md | 36 + Demo/AHControl/config/r_hand_team_julia.toml | 42 + Demo/Cargo.lock | 4324 ++++++++++++++++++ Demo/HandTracking/HandTracking/main.py | 269 +- Demo/README.md | 41 +- Demo/dataflow_tracking_real_team_julia.yml | 27 + Demo/tests/__init__.py | 0 Demo/tests/test_hand_tracking.py | 103 + FORK.md | 7 + pixi.lock | 2509 +++++++++- pixi.toml | 23 +- 13 files changed, 7210 insertions(+), 188 deletions(-) create mode 100644 Demo/AHControl/config/r_hand_team_julia.toml create mode 100644 Demo/Cargo.lock create mode 100644 Demo/dataflow_tracking_real_team_julia.yml create mode 100644 Demo/tests/__init__.py create mode 100644 Demo/tests/test_hand_tracking.py create mode 100644 FORK.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fe7ab2..c9936fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,5 +18,8 @@ jobs: - name: Run pre-commit run: pixi run pre-commit run --all-files - - name: Unit tests - run: pixi run test + - name: PythonExample unit tests + run: pixi run test-python-example + + - name: Demo unit tests + run: pixi run test-demo diff --git a/.gitignore b/.gitignore index a93d20e..01c8096 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ *.pyc push.json .pytest_cache/ + +# Python package metadata (pip install -e) +*.egg-info/ + +# Rust build output +target/ + +# Dora / local output and logs +Demo/out/ +out/ diff --git a/Demo/AHControl/README.md b/Demo/AHControl/README.md index 395762a..fcff035 100644 --- a/Demo/AHControl/README.md +++ b/Demo/AHControl/README.md @@ -8,3 +8,39 @@ In this file you can set the motors ID, and angle offsets for each finger. - *goto*: to move a single motor to a given position. `cargo run --bin=goto -- -h` for a list of parameters - *get_zeros*: to help you set the motor zeros, it sets the motors in the compliant mode and write the TOML file to the console. `cargo run --bin=get_zeros -- -h` for a list of parameters - *set_zeros*: to move the hand in the "zero" position according to the config file. `cargo run --bin=set_zeros -- -h` for a list of parameters + +# Calibration steps + +1. Verify IDs: run `set_zeros` or `goto` per motor; if the wrong finger moves, the ID in config does not match the physical motor. Fix with `change_id`, or use [Feetech debug tool](https://github.com/Robot-Maker-SAS/FeetechServo/tree/main/feetech%20debug%20tool%20master/FD1.9.8.2) to scan the bus. + +- Expected result for `set_zeros`: the hand moves all fingers to their zero pose (from config offsets), holds ~1 second, then relaxes (torque off). If the wrong finger moves for a given config entry, the motor IDs are mismatched. + +2. Calibrate offsets: run `get_zeros`. Put motors in compliant mode, manually position each finger to its zero pose, press Enter. Copy the printed TOML into your config file (e.g. `Demo/AHControl/config/r_hand_julia.toml`). + +# Commands + +Run from the project root. If you use pixi for Rust, run `pixi shell` first so `cargo` is in PATH. Set `PORT` to your serial device (e.g. `/dev/ttyACM0` on Linux, `COM3` on Windows). Set `CONFIG_PREFIX` to your config name (e.g. `team_julia`, `team_krishan`); the config file is `Demo/AHControl/config/r_hand_${CONFIG_PREFIX}.toml`. Copy `r_hand.toml` to `r_hand_.toml` if needed. + +Linux: + +```bash +export PORT=/dev/ttyACM0 +export CONFIG_PREFIX="team_julia" +export CFG="Demo/AHControl/config/r_hand_${CONFIG_PREFIX}.toml" +cargo run --manifest-path Demo/Cargo.toml --bin=set_zeros -- --serialport $PORT --config $CFG +cargo run --manifest-path Demo/Cargo.toml --bin=goto -- --serialport $PORT --id 1 --pos 0.0 +cargo run --manifest-path Demo/Cargo.toml --bin=change_id -- --serialport $PORT --old-id 1 --new-id 2 +cargo run --manifest-path Demo/Cargo.toml --bin=get_zeros -- --serialport $PORT --config $CFG +``` + +Windows (PowerShell): + +```powershell +$PORT = "COM3" +$CONFIG_PREFIX = "team_krishan" +$CFG = "Demo/AHControl/config/r_hand_$CONFIG_PREFIX.toml" +cargo run --manifest-path Demo/Cargo.toml --bin=set_zeros -- --serialport $PORT --config $CFG +cargo run --manifest-path Demo/Cargo.toml --bin=goto -- --serialport $PORT --id 1 --pos 0.0 +cargo run --manifest-path Demo/Cargo.toml --bin=change_id -- --serialport $PORT --old-id 1 --new-id 2 +cargo run --manifest-path Demo/Cargo.toml --bin=get_zeros -- --serialport $PORT --config $CFG +``` diff --git a/Demo/AHControl/config/r_hand_team_julia.toml b/Demo/AHControl/config/r_hand_team_julia.toml new file mode 100644 index 0000000..5f7ce2b --- /dev/null +++ b/Demo/AHControl/config/r_hand_team_julia.toml @@ -0,0 +1,42 @@ +# Config for the right index finger1 +[Fingers] +[[motors]] +finger_name="r_finger1" +motor1.id = 1 +motor1.offset = 0.12217304763960307 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 2 +motor2.offset = 0.08726646259971647 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name="r_finger2" +motor1.id = 3 +motor1.offset = 0.0 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 4 +motor2.offset = 0.12217304763960307 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name="r_finger3" +motor1.id = 5 +motor1.offset = 0.08726646259971647 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 6 +motor2.offset = 0.12217304763960307 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name="r_finger4" +motor1.id = 7 +motor1.offset = 0.0 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 8 +motor2.offset = 0.12217304763960307 +motor2.invert = false +motor2.model = "SCS0009" diff --git a/Demo/Cargo.lock b/Demo/Cargo.lock new file mode 100644 index 0000000..7ff4301 --- /dev/null +++ b/Demo/Cargo.lock @@ -0,0 +1,4324 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "AHControl" +version = "0.1.0" +dependencies = [ + "arrow_convert", + "clap", + "dora-node-api", + "eyre", + "facet", + "facet-pretty", + "facet-toml", + "rustypot", + "serialport", + "toml", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +dependencies = [ + "serde", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "ariadne" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f" +dependencies = [ + "unicode-width", + "yansi", +] + +[[package]] +name = "arrow" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" +dependencies = [ + "arrow-arith 54.3.1", + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-cast 54.3.1", + "arrow-csv", + "arrow-data 54.3.1", + "arrow-ipc", + "arrow-json", + "arrow-ord 54.3.1", + "arrow-row 54.3.1", + "arrow-schema 54.3.1", + "arrow-select 54.3.1", + "arrow-string 54.3.1", +] + +[[package]] +name = "arrow" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f15b4c6b148206ff3a2b35002e08929c2462467b62b9c02036d9c34f9ef994" +dependencies = [ + "arrow-arith 55.2.0", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-cast 55.2.0", + "arrow-data 55.2.0", + "arrow-ord 55.2.0", + "arrow-row 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "arrow-string 55.2.0", +] + +[[package]] +name = "arrow-arith" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "chrono", + "num", +] + +[[package]] +name = "arrow-arith" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30feb679425110209ae35c3fbf82404a39a4c0436bb3ec36164d8bffed2a4ce4" +dependencies = [ + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" +dependencies = [ + "ahash", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-array" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70732f04d285d49054a48b72c54f791bb3424abae92d27aafdf776c98af161c8" +dependencies = [ + "ahash", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "169b1d5d6cb390dd92ce582b06b23815c7953e9dfaaea75556e89d890d19993d" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "arrow-select 54.3.1", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-cast" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f12eccc3e1c05a766cafb31f6a60a46c2f8efec9b74c6e0648766d30686af8" +dependencies = [ + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1644877d8bc9a0ef022d9153dc29375c2bda244c39aec05a91d0e87ccf77995f" +dependencies = [ + "arrow-array 54.3.1", + "arrow-cast 54.3.1", + "arrow-schema 54.3.1", + "chrono", + "csv", + "csv-core", + "lazy_static", + "regex", +] + +[[package]] +name = "arrow-data" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" +dependencies = [ + "arrow-buffer 54.3.1", + "arrow-schema 54.3.1", + "half", + "num", +] + +[[package]] +name = "arrow-data" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de1ce212d803199684b658fc4ba55fb2d7e87b213de5af415308d2fee3619c2" +dependencies = [ + "arrow-buffer 55.2.0", + "arrow-schema 55.2.0", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ff528658b521e33905334723b795ee56b393dbe9cf76c8b1f64b648c65a60c" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee5b4ca98a7fb2efb9ab3309a5d1c88b5116997ff93f3147efdc1062a6158e9" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-cast 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "chrono", + "half", + "indexmap 2.13.0", + "lexical-core", + "memchr", + "num", + "serde", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "arrow-select 54.3.1", +] + +[[package]] +name = "arrow-ord" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6506e3a059e3be23023f587f79c82ef0bcf6d293587e3272d20f2d30b969b5a7" +dependencies = [ + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", +] + +[[package]] +name = "arrow-row" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "half", +] + +[[package]] +name = "arrow-row" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bf7393166beaf79b4bed9bfdf19e97472af32ce5b6b48169d321518a08cae2" +dependencies = [ + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "half", +] + +[[package]] +name = "arrow-schema" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" +dependencies = [ + "serde", +] + +[[package]] +name = "arrow-schema" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7686986a3bf2254c9fb130c623cdcb2f8e1f15763e7c71c310f0834da3d292" + +[[package]] +name = "arrow-select" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" +dependencies = [ + "ahash", + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "num", +] + +[[package]] +name = "arrow-select" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2b45757d6a2373faa3352d02ff5b54b098f5e21dccebc45a21806bc34501e5" +dependencies = [ + "ahash", + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "num", +] + +[[package]] +name = "arrow-string" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" +dependencies = [ + "arrow-array 54.3.1", + "arrow-buffer 54.3.1", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "arrow-select 54.3.1", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "arrow-string" +version = "55.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0377d532850babb4d927a06294314b316e23311503ed580ec6ce6a0158f49d40" +dependencies = [ + "arrow-array 55.2.0", + "arrow-buffer 55.2.0", + "arrow-data 55.2.0", + "arrow-schema 55.2.0", + "arrow-select 55.2.0", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "arrow_convert" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad95fe1f29f3e2779202bb86563ee042e17502b569e46e6107ef9bf3f6aad584" +dependencies = [ + "arrow 55.2.0", + "arrow_convert_derive", + "chrono", + "err-derive", + "half", +] + +[[package]] +name = "arrow_convert_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4fd4416bd62e3bbd3b84d3ff4837e106e49b295adda89625aa6ea32893bc94" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "dora-arrow-convert" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af1790d82745638304d85ec3161dc03f0857f139829e31b3518b13cd159cda" +dependencies = [ + "arrow 54.3.1", + "chrono", + "eyre", + "half", + "num", +] + +[[package]] +name = "dora-core" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255a1fcbe74b0caa20e83d132a7dbcad3b20769263eb6b748d91dd6c56354bce" +dependencies = [ + "dora-message", + "dunce", + "eyre", + "fs_extra", + "itertools", + "log", + "once_cell", + "schemars", + "serde", + "serde-with-expand-env", + "serde_json", + "serde_yaml", + "splitty", + "tokio", + "tracing", + "uuid", + "which", +] + +[[package]] +name = "dora-message" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e02396e7fd23f14f0bcdb852a746e0a261004701aa4058fba5f2cb7c428cd3e" +dependencies = [ + "aligned-vec", + "arrow-data 54.3.1", + "arrow-schema 54.3.1", + "bincode", + "eyre", + "log", + "once_cell", + "schemars", + "semver", + "serde", + "serde-with-expand-env", + "serde_yaml", + "tokio", + "uhlc", + "uuid", +] + +[[package]] +name = "dora-metrics" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d192d5b9ccefd5089b379a251e8c332e870237dffab5f8d420248f031a63e2" +dependencies = [ + "eyre", + "opentelemetry 0.29.1", + "opentelemetry-otlp", + "opentelemetry-system-metrics", + "opentelemetry_sdk 0.29.0", +] + +[[package]] +name = "dora-node-api" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a6bbd54b585aeb2bab3cd460fcbf522ad539f793879f8b4ae750ebd7f6de76" +dependencies = [ + "aligned-vec", + "arrow 54.3.1", + "bincode", + "dora-arrow-convert", + "dora-core", + "dora-message", + "dora-metrics", + "dora-tracing", + "eyre", + "flume", + "futures", + "futures-concurrency", + "futures-timer", + "serde_json", + "serde_yaml", + "shared-memory-server", + "shared_memory_extended", + "tokio", + "tracing", +] + +[[package]] +name = "dora-tracing" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b3c71657809ba0772bdb00a0d289ccd4dc2da6a6cd736dd2d5d00550f803b2" +dependencies = [ + "eyre", + "opentelemetry 0.23.0", + "opentelemetry-jaeger", + "opentelemetry_sdk 0.23.0", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "err-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "facet" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be4cf803e7c34ab73217e3c05b6c382b8a70a5948a9282ed9d64f62c2a03a90" +dependencies = [ + "facet-core", + "facet-macros", + "static_assertions", +] + +[[package]] +name = "facet-core" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0dfc311334e2fad887baaebdc3773c05bbbddc286bef7c9c87cfb3fb4235fb" +dependencies = [ + "bitflags 2.11.0", + "impls", +] + +[[package]] +name = "facet-macros" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10707410fd2c4f6b779fa04eac1a25d2907322dfb31039f16829c7996236768" +dependencies = [ + "facet-core", + "facet-macros-emit", +] + +[[package]] +name = "facet-macros-emit" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c496a4d3bd731444d278cf1178302d854c46dd04661e79a67d926b63d60a4cbb" +dependencies = [ + "facet-macros-parse", + "quote", +] + +[[package]] +name = "facet-macros-parse" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e81c2f638023b13c3ab6e73b29be6fec44bdced5e418839c215a4b81afc6e2" +dependencies = [ + "proc-macro2", + "quote", + "unsynn", +] + +[[package]] +name = "facet-pretty" +version = "0.23.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afac2c6825ba52c68d474574b397f08c2abc47e869a24091f34c77603849baa3" +dependencies = [ + "facet-core", + "facet-reflect", +] + +[[package]] +name = "facet-reflect" +version = "0.27.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61652b0cd54648b83633b74819a6156b77013e0d0695cc70a6566e75870f6c27" +dependencies = [ + "bitflags 2.11.0", + "facet-core", + "owo-colors", +] + +[[package]] +name = "facet-serialize" +version = "0.24.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5838b88d5e7fb752a88afa61144b8c7be9fd80e7bb12ed579a733485900789d8" +dependencies = [ + "facet-core", + "facet-reflect", + "log", +] + +[[package]] +name = "facet-toml" +version = "0.25.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bda1f0ea90577b8c50754ed0a73ae83ceeeb483a2a4552fa4ad3bf0350dbbec" +dependencies = [ + "ariadne", + "facet-core", + "facet-reflect", + "facet-serialize", + "log", + "num-traits", + "owo-colors", + "toml_edit 0.22.27", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impls" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a46645bbd70538861a90d0f26c31537cdf1e44aae99a794fb75a664b70951bc" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.11.0", + "libc", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "serde_core", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opentelemetry" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror 1.0.69", +] + +[[package]] +name = "opentelemetry" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry 0.29.1", + "reqwest", + "tracing", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501b471b67b746d9a07d4c29f8be00f952d1a2eca356922ede0098cbaddff19f" +dependencies = [ + "async-trait", + "futures-core", + "futures-util", + "opentelemetry 0.23.0", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk 0.23.0", + "thrift", + "tokio", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" +dependencies = [ + "futures-core", + "http", + "opentelemetry 0.29.1", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk 0.29.0", + "prost", + "reqwest", + "thiserror 2.0.18", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" +dependencies = [ + "opentelemetry 0.29.1", + "opentelemetry_sdk 0.29.0", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" + +[[package]] +name = "opentelemetry-system-metrics" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff095ac36df870a11380877fb7e9b1e7529abfe994fd06a6d6d17ca1c77d30b" +dependencies = [ + "eyre", + "opentelemetry 0.29.1", + "sysinfo", + "tokio", + "tracing", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "lazy_static", + "once_cell", + "opentelemetry 0.23.0", + "ordered-float 4.6.0", + "percent-encoding", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry 0.29.1", + "percent-encoding", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.116", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw_sync_2" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f067b45fa17e31d15636789c2638bd562da5496d498876cf0495df78f7e4fdcb" +dependencies = [ + "cfg-if", + "libc", + "nix 0.23.2", + "rand 0.8.5", + "winapi", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustypot" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e608135ce018f7b1ad6771f488a66eb0ea44c088e4c2584c89fa9f2e6eed9f1" +dependencies = [ + "clap", + "env_logger", + "log", + "num_enum", + "paste", + "proc-macro2", + "serialport", + "signal-hook", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.116", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-with-expand-env" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888d884a3be3a209308d0b66f1918ff18f60e93db837259e53ea7d8dd14e7e98" +dependencies = [ + "serde", + "shellexpand", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serialport" +version = "4.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f60a586160667241d7702c420fc223939fb3c0bb8d3fac84f78768e8970dee" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "quote", + "scopeguard", + "unescaper", + "windows-sys 0.52.0", +] + +[[package]] +name = "shadow_counted" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65da48d447333cebe1aadbdd3662f3ba56e76e67f53bc46f3dd5f67c74629d6b" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-memory-server" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2712bb95a9726b80a00cab3fc76db35221c691ef7a4d7d44c35e4bae0d43bed2" +dependencies = [ + "bincode", + "eyre", + "raw_sync_2", + "serde", + "shared_memory_extended", + "tracing", +] + +[[package]] +name = "shared_memory_extended" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004d7ece9a3be64f85471d50967710b0a146144225bed5f0abd0514a3bed086f" +dependencies = [ + "cfg-if", + "libc", + "nix 0.26.4", + "rand 0.8.5", + "win-sys", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "splitty" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db70a1e6827e4d71c655b606caf1346862c38ae52ab4f58c32635e7c7aedd67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "sysinfo" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "windows 0.57.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "log", + "ordered-float 2.10.1", + "threadpool", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f68803492bf28ab40aeccaecc7021096bd256baf7ca77c3d425d89b35a7be4e4" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry 0.23.0", + "opentelemetry_sdk 0.23.0", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "uhlc" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d291a7454d390b753ef68df8145da18367e32883ec2fa863959f0aefb915cdb" +dependencies = [ + "hex", + "humantime", + "lazy_static", + "log", + "serde", + "spin", + "uuid", +] + +[[package]] +name = "unescaper" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unsynn" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940603a9e25cf11211cc43b81f4fcad2b8ab4df291ca855f32c40e1ac22d5bc" +dependencies = [ + "fxhash", + "mutants", + "proc-macro2", + "shadow_counted", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.116", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "win-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7b128a98c1cfa201b09eb49ba285887deb3cbe7466a98850eb1adabb452be5" +dependencies = [ + "windows 0.34.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.116", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.116", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure 0.13.2", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Demo/HandTracking/HandTracking/main.py b/Demo/HandTracking/HandTracking/main.py index 02c9d53..7e9fc15 100644 --- a/Demo/HandTracking/HandTracking/main.py +++ b/Demo/HandTracking/HandTracking/main.py @@ -1,216 +1,145 @@ -import argparse -import os -import time - import cv2 import numpy as np import pyarrow as pa from dora import Node import mediapipe as mp -from scipy.spatial.transform import Rotation mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles mp_hands = mp.solutions.hands -# https://mediapipe.readthedocs.io/en/latest/solutions/hands.html - -def process_img(hand_proc, image): - image.flags.writeable = False - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - results = hand_proc.process(image) - # img_width,img_height,_ =image.shape - # Draw the hand annotations on the image. - image.flags.writeable = True - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - r_res=None - l_res=None - if results.multi_hand_landmarks: - - # print('Handedness:', results.multi_handedness) - # print(results.multi_hand_world_landmarks) - - for index,handedness_classif in enumerate(results.multi_handedness): - if handedness_classif.classification[0].score>0.8: #let's considere only one right hand - - - # for hand_landmarks in results.multi_hand_landmarks: - # tip_x=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].x - # tip_y=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].y - # tip_z=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].z-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].z - # print(f'TIP: {tip_x} {tip_y} {tip_z}') - # mp_drawing.draw_landmarks( - # image, - # hand_landmarks, - # mp_hands.HAND_CONNECTIONS, - # mp_drawing_styles.get_default_hand_landmarks_style(), - # mp_drawing_styles.get_default_hand_connections_style()) - - - hand_landmarks=results.multi_hand_world_landmarks[index] #metric - # hand_landmarks=results.multi_hand_landmarks[index] #normalized - hand_landmarks_norm=results.multi_hand_landmarks[index] #normalized - - - - - - tip1_x=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].x - tip1_y=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].y - tip1_z=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].z-hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].z - - tip2_x=hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].x-hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].x - tip2_y=hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].y-hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].y - tip2_z=hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].z-hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].z - - tip3_x=hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].x-hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].x - tip3_y=hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].y-hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].y - tip3_z=hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].z-hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].z - - tip4_x=hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP].x-hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_MCP].x - tip4_y=hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP].y-hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_MCP].y - tip4_z=hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP].z-hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_MCP].z - - - # tip1_x=hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x-hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].x - # tip1_y=hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y-hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].y - # tip1_z=hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].z-hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].z - # tip2_x=hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].x-hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].x - # tip2_y=hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].y-hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].y - # tip2_z=hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].z-hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].z +def _landmark_vec(landmarks, idx): + """Return landmark at idx as numpy array [x, y, z].""" + lm = landmarks.landmark[idx] + return np.array([lm.x, lm.y, lm.z]) - # tip3_x=hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].x-hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].x - # tip3_y=hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].y-hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].y - # tip3_z=hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_TIP].z-hand_landmarks_norm.landmark[mp_hands.HandLandmark.RING_FINGER_MCP].z - # tip4_x=hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_TIP].x-hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_MCP].x - # tip4_y=hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_TIP].y-hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_MCP].y - # tip4_z=hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_TIP].z-hand_landmarks_norm.landmark[mp_hands.HandLandmark.THUMB_MCP].z +def _tip_vector(landmarks, tip_idx, mcp_idx): + """Return vector from MCP to tip for a finger.""" + tip = _landmark_vec(landmarks, tip_idx) + mcp = _landmark_vec(landmarks, mcp_idx) + return tip - mcp +def _hand_frame(landmarks_norm, is_right): + """Build hand frame: origin at wrist, z toward middle MCP, x normal to palm.""" + origin = _landmark_vec(landmarks_norm, mp_hands.HandLandmark.WRIST) + mid_mcp = _landmark_vec(landmarks_norm, mp_hands.HandLandmark.MIDDLE_FINGER_MCP) + unit_z = mid_mcp - origin + unit_z = unit_z / np.linalg.norm(unit_z) + if is_right: + vec_y = _landmark_vec(landmarks_norm, mp_hands.HandLandmark.PINKY_MCP) - origin + else: + vec_y = _landmark_vec(landmarks_norm, mp_hands.HandLandmark.INDEX_FINGER_MCP) - origin + unit_x = np.cross(vec_y, unit_z) + unit_x = unit_x / np.linalg.norm(unit_x) + unit_y = np.cross(unit_z, unit_x) - # tip1_x=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x - # tip1_y=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y - # tip1_z=hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].z + R = np.array([unit_x, -unit_y, unit_z]).reshape((3, 3)) + return R - # print(f'TIP: {tip_x} {tip_y} {tip_z} ({hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x} {hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y} {hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].z})') - mp_drawing.draw_landmarks( - image, - hand_landmarks_norm, - mp_hands.HAND_CONNECTIONS, - mp_drawing_styles.get_default_hand_landmarks_style(), - mp_drawing_styles.get_default_hand_connections_style()) - - - #define a new hand frame centered at marker WRIST (n°0) with z along the vector (WRIST,MIDDLE_FINGER_MCP) (0,9) and x is the "third dimension" normal to the plan of the palm (WRIST,MIDDLE_FINGER_MCP)x(WRIST,PINKY_MCP) - # origin=np.array([hand_landmarks.landmark[mp_hands.HandLandmark.WRIST].x,hand_landmarks.landmark[mp_hands.HandLandmark.WRIST].y,hand_landmarks.landmark[mp_hands.HandLandmark.WRIST].z]) - # mid_mcp=np.array([hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].x,hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].y,hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].z]) - # unit_z=mid_mcp-origin - # unit_z=unit_z/np.linalg.norm(unit_z) - # pinky_mcp=np.array([hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_MCP].x,hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_MCP].y,hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_MCP].z]) - - #rotate everything in a hand referential - origin=np.array([hand_landmarks_norm.landmark[mp_hands.HandLandmark.WRIST].x,hand_landmarks_norm.landmark[mp_hands.HandLandmark.WRIST].y,hand_landmarks_norm.landmark[mp_hands.HandLandmark.WRIST].z]) #wrist base as the origin - mid_mcp=np.array([hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].x,hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].y,hand_landmarks_norm.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_MCP].z]) #base of the middle finger - unit_z=mid_mcp-origin #z is unit vector from base of wrist toward base of middle finger - unit_z=unit_z/np.linalg.norm(unit_z) - pinky_mcp=np.array([hand_landmarks_norm.landmark[mp_hands.HandLandmark.PINKY_MCP].x,hand_landmarks_norm.landmark[mp_hands.HandLandmark.PINKY_MCP].y,hand_landmarks_norm.landmark[mp_hands.HandLandmark.PINKY_MCP].z]) #base of the pinky finger - - index_mcp=np.array([hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].x,hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].y,hand_landmarks_norm.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP].z]) #base of the index finger - +def _process_hand(hand_proc, image): + """Process image, draw landmarks, extract hand poses. Returns (image, r_res, l_res).""" + image.flags.writeable = False + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + results = hand_proc.process(image) + image.flags.writeable = True + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - # print(f"ORIGIN: {origin} MID: {mid_mcp}") + r_res = None + l_res = None - if handedness_classif.classification[0].label=='Right': - vec_towards_y=pinky_mcp-origin #vector from wrist base towards pinky base - if handedness_classif.classification[0].label=='Left': - vec_towards_y=index_mcp-origin #vector from wrist base towards pinky base - # unit_x=np.cross(unit_z,vec_towards_y) - # vec_towards_y=pinky_mcp-origin #vector from wrist base towards pinky base + if not results.multi_hand_landmarks: + return image, r_res, l_res - unit_x=np.cross(vec_towards_y,unit_z) #we say unit x is the cross product of z and the vector towards pinky + for index, handedness in enumerate(results.multi_handedness): + if handedness.classification[0].score <= 0.8: + continue - unit_x=unit_x/np.linalg.norm(unit_x) + is_right = handedness.classification[0].label == "Right" + hand_landmarks = results.multi_hand_world_landmarks[index] + hand_landmarks_norm = results.multi_hand_landmarks[index] - unit_y=np.cross(unit_z,unit_x) - # unit_y=np.cross(unit_x,unit_z) + tip1 = _tip_vector(hand_landmarks, mp_hands.HandLandmark.INDEX_FINGER_TIP, mp_hands.HandLandmark.INDEX_FINGER_MCP) + tip2 = _tip_vector(hand_landmarks, mp_hands.HandLandmark.MIDDLE_FINGER_TIP, mp_hands.HandLandmark.MIDDLE_FINGER_MCP) + tip3 = _tip_vector(hand_landmarks, mp_hands.HandLandmark.RING_FINGER_TIP, mp_hands.HandLandmark.RING_FINGER_MCP) + tip4 = _tip_vector(hand_landmarks, mp_hands.HandLandmark.THUMB_TIP, mp_hands.HandLandmark.THUMB_MCP) + R = _hand_frame(hand_landmarks_norm, is_right) + tip1_rot = R @ tip1 + tip2_rot = R @ tip2 + tip3_rot = R @ tip3 + tip4_rot = R @ tip4 + mp_drawing.draw_landmarks( + image, + hand_landmarks_norm, + mp_hands.HAND_CONNECTIONS, + mp_drawing_styles.get_default_hand_landmarks_style(), + mp_drawing_styles.get_default_hand_connections_style(), + ) + tips = {"tip1": tip1_rot, "tip2": tip2_rot, "tip3": tip3_rot, "tip4": tip4_rot} + prefix = "r" if is_right else "l" + result = {f"{prefix}_{k}": v for k, v in tips.items()} + if is_right: + r_res = [result] + else: + l_res = [result] - if handedness_classif.classification[0].label=='Right': - # A=np.array([unit_x,unit_y,unit_z]).reshape((3,3)) - R=np.array([unit_x,-unit_y,unit_z]).reshape((3,3)) #-y because of mirror? - if handedness_classif.classification[0].label=='Left': - R=np.array([unit_x,-unit_y,unit_z]).reshape((3,3)) #-y because of mirror? - tip1=R@np.array([tip1_x,tip1_y,tip1_z]) - tip2=R@np.array([tip2_x,tip2_y,tip2_z]) - tip3=R@np.array([tip3_x,tip3_y,tip3_z]) - tip4=R@np.array([tip4_x,tip4_y,tip4_z]) + return image, r_res, l_res - # scale=0.01 - # image = cv2.drawFrameAxes(image, K, disto, rotV, origin, scale) - # res=[{'r_tip1': [tip1_x,tip1_y,tip1_z],'r_tip2': [tip2_x,tip2_y,tip2_z],'r_tip3': [tip3_x,tip3_y,tip3_z],'r_tip4': [tip4_x,tip4_y,tip4_z]}] - if handedness_classif.classification[0].label=='Right': - r_res=[{'r_tip1': tip1,'r_tip2': tip2,'r_tip3': tip3,'r_tip4': tip4}] - # print(f"RIGHT: {tip1_x:.3f} {tip1_y:.3f} {tip1_z:.3f} => {tip1}. {unit_x} {unit_y} {unit_z}") - elif handedness_classif.classification[0].label=='Left': - l_res=[{'l_tip1': tip1,'l_tip2': tip2,'l_tip3': tip3,'l_tip4': tip4}] - # print(f"LEFT: {tip1_x:.3f} {tip1_y:.3f} {tip1_z:.3f} => {tip1}. {unit_x} {unit_y} {unit_z}") - # Flip the image horizontally for a selfie-view display. - return image,r_res,l_res -# cv2.imshow('MediaPipe Hands', cv2.flip(image, 1)) +def process_img(hand_proc, image): + """Process image for hand tracking. Returns (image, r_res, l_res).""" + return _process_hand(hand_proc, image) def main(): - node = Node() - - - pa.array([]) # initialize pyarrow array cap = cv2.VideoCapture(0) + if not cap.isOpened(): + raise RuntimeError("Camera 0 failed to open. Check device, permissions, or try another index.") + w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + print(f"Camera opened: {w}x{h}") with mp_hands.Hands( - model_complexity=0, - min_detection_confidence=0.5, - min_tracking_confidence=0.5) as hands: - - - + model_complexity=0, + min_detection_confidence=0.5, + min_tracking_confidence=0.5, + ) as hands: for event in node: - - event_type = event["type"] - - if event_type == "INPUT": - event_id = event["id"] - - if event_id == "tick": - ret, frame = cap.read() - - if not ret: - continue - - frame = cv2.flip(frame, 1) - #process - frame,r_res,l_res=process_img(hands,frame) - - if r_res is not None: - node.send_output('r_hand_pos',pa.array(r_res)) - if l_res is not None: - node.send_output('l_hand_pos',pa.array(l_res)) - # cv2.imshow('MediaPipe Hands', cv2.flip(frame, 1)) - cv2.imshow('MediaPipe Hands', frame) + if event["type"] == "INPUT" and event["id"] == "tick": + ret, frame = cap.read() + if not ret: + print("Camera read failed") + continue + + frame = cv2.flip(frame, 1) + frame, r_res, l_res = process_img(hands, frame) + + if r_res is not None: + node.send_output("r_hand_pos", pa.array(r_res)) + if l_res is not None: + node.send_output("l_hand_pos", pa.array(l_res)) + + try: + cv2.imshow("MediaPipe Hands", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break + except cv2.error as e: + raise RuntimeError( + f"Display error (headless?): {e}. " + "Set DISPLAY or use X11 forwarding." + ) from e - - elif event_type == "ERROR": + elif event["type"] == "ERROR": raise RuntimeError(event["error"]) diff --git a/Demo/README.md b/Demo/README.md index 25f03a5..63d470d 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -1,6 +1,44 @@ # Example control for the Pollen Robotics "AmazingHand" (a.k.a. AH!) -## How to use: +## Running with pixi + +Prerequisites: install [Pixi](https://pixi.prefix.dev/latest/installation/). Rust is needed for real hardware demos (AHControl). + +From the AmazingHand repository root: + +```bash +pixi install +pixi run dora up # start daemon (in a separate terminal) +``` + +Webcam hand tracking (simulation only): + +```bash +pixi run dora build Demo/dataflow_tracking_simu.yml # once +pixi run dora run Demo/dataflow_tracking_simu.yml +``` + +Webcam hand tracking (real hardware): + +```bash +pixi run dora build Demo/dataflow_tracking_real.yml # once +pixi run dora run Demo/dataflow_tracking_real_team_julia.yml +``` + +The config file is set in `Demo/dataflow_tracking_real.yml` (hand_controller `args`: change `--config AHControl/config/...` and `--serialport` as needed). + +Linux: add your user to the `dialout` group for serial port access: `sudo usermod -a -G dialout $USER` (log out and back in). If your hand is on a different port (e.g. `/dev/ttyUSB0`), edit `Demo/dataflow_tracking_real.yml` and change the `--serialport` arg. + +Finger angle control (simulation): + +```bash +pixi run dora build Demo/dataflow_angle_simu.yml # once +pixi run dora run Demo/dataflow_angle_simu.yml +``` + +The dataflow `build` steps install HandTracking and AHSimulation in editable mode and build AHControl when needed. + +## How to use (uv): - Install Rust: https://www.rust-lang.org/tools/install - Install uv: https://docs.astral.sh/uv/getting-started/installation/ - Install dora-rs: https://dora-rs.ai/docs/guides/Installation/installing @@ -34,3 +72,4 @@ You can use the software tools located in [AHControl](AHControl). - [AHControl](AHControl) contains a dora-rs node to control the motors, along with some useful tools to configure them. - [AHSimulation](AHSimulation) contains a dora-rs node to simulate the hand and get the inverse kinematics. - [HandTracking](HandTracking) contains a dora-rs node to track hands from a webcam and use it as target to control AH!. + diff --git a/Demo/dataflow_tracking_real_team_julia.yml b/Demo/dataflow_tracking_real_team_julia.yml new file mode 100644 index 0000000..f61bd0b --- /dev/null +++ b/Demo/dataflow_tracking_real_team_julia.yml @@ -0,0 +1,27 @@ +# Note: Using 'python -m pip' instead of 'pip' to ensure pixi environment's pip is used. +# Direct 'pip' command may resolve to system pip which is externally-managed (PEP 668). +nodes: + - id: hand_tracker + build: python -m pip install -e HandTracking + path: HandTracking/HandTracking/main.py + inputs: + tick: dora/timer/millis/50 + outputs: + - r_hand_pos + + - id: r_hand_simulation + build: python -m pip install -e AHSimulation + path: AHSimulation/AHSimulation/mj_mink_right.py + inputs: + r_hand_pos: hand_tracker/r_hand_pos + tick: dora/timer/millis/2 + tick_ctrl: dora/timer/millis/10 + outputs: + - mj_r_joints_pos + + - id: hand_controller + build: cargo build -p AHControl + path: target/debug/AHControl #--serialport /dev/ttyACM0 --config AHControl/config/r_hand.toml + args: --serialport /dev/ttyACM0 --config AHControl/config/r_hand_team_julia.toml + inputs: + mj_r_joints_pos: r_hand_simulation/mj_r_joints_pos diff --git a/Demo/tests/__init__.py b/Demo/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Demo/tests/test_hand_tracking.py b/Demo/tests/test_hand_tracking.py new file mode 100644 index 0000000..1b5ce4a --- /dev/null +++ b/Demo/tests/test_hand_tracking.py @@ -0,0 +1,103 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from pathlib import Path +from unittest.mock import MagicMock + +import numpy as np + +# HandTracking package: Demo/HandTracking/HandTracking/ +_DEMO_ROOT = Path(__file__).resolve().parent.parent +_HANDTRACKING_ROOT = _DEMO_ROOT / "HandTracking" +sys.path.insert(0, str(_HANDTRACKING_ROOT)) + +import mediapipe as mp +from HandTracking.main import _hand_frame, _landmark_vec, _tip_vector, process_img + +mp_hands = mp.solutions.hands + + +def _make_landmarks(landmark_dict): + """Create mock landmarks: landmark_dict maps HandLandmark idx -> (x, y, z).""" + class Landmark: + pass + + class Landmarks: + def __init__(self, d): + self.landmark = {} + for idx, (x, y, z) in d.items(): + lm = Landmark() + lm.x, lm.y, lm.z = x, y, z + self.landmark[idx] = lm + + return Landmarks(landmark_dict) + + +class TestLandmarkVec: + def test_returns_numpy_array(self): + lm = _make_landmarks({0: (1.0, 2.0, 3.0)}) + out = _landmark_vec(lm, 0) + np.testing.assert_array_equal(out, [1.0, 2.0, 3.0]) + + def test_different_indices(self): + lm = _make_landmarks({5: (0.1, 0.2, 0.3), 9: (-1.0, 0.0, 1.0)}) + np.testing.assert_array_equal(_landmark_vec(lm, 5), [0.1, 0.2, 0.3]) + np.testing.assert_array_equal(_landmark_vec(lm, 9), [-1.0, 0.0, 1.0]) + + +class TestTipVector: + def test_tip_minus_mcp(self): + lm = _make_landmarks({ + mp_hands.HandLandmark.INDEX_FINGER_TIP: (1.0, 2.0, 3.0), + mp_hands.HandLandmark.INDEX_FINGER_MCP: (0.0, 1.0, 2.0), + }) + out = _tip_vector(lm, mp_hands.HandLandmark.INDEX_FINGER_TIP, mp_hands.HandLandmark.INDEX_FINGER_MCP) + np.testing.assert_array_equal(out, [1.0, 1.0, 1.0]) + + +class TestHandFrame: + def test_returns_3x3_rotation_matrix(self): + # Wrist at origin, middle MCP along z, pinky MCP for y + lm = _make_landmarks({ + mp_hands.HandLandmark.WRIST: (0.0, 0.0, 0.0), + mp_hands.HandLandmark.MIDDLE_FINGER_MCP: (0.0, 0.0, 1.0), + mp_hands.HandLandmark.PINKY_MCP: (0.0, 1.0, 0.0), + mp_hands.HandLandmark.INDEX_FINGER_MCP: (0.0, -1.0, 0.0), + }) + R = _hand_frame(lm, is_right=True) + assert R.shape == (3, 3) + # R should be orthogonal + np.testing.assert_array_almost_equal(R @ R.T, np.eye(3)) + + def test_left_hand_uses_index_mcp_for_y(self): + lm = _make_landmarks({ + mp_hands.HandLandmark.WRIST: (0.0, 0.0, 0.0), + mp_hands.HandLandmark.MIDDLE_FINGER_MCP: (0.0, 0.0, 1.0), + mp_hands.HandLandmark.PINKY_MCP: (1.0, 0.0, 0.0), + mp_hands.HandLandmark.INDEX_FINGER_MCP: (0.0, 1.0, 0.0), + }) + R = _hand_frame(lm, is_right=False) + assert R.shape == (3, 3) + np.testing.assert_array_almost_equal(R @ R.T, np.eye(3)) + + +class TestProcessImg: + def test_no_hands_returns_none_results(self): + mock_proc = MagicMock() + mock_proc.process.return_value = MagicMock(multi_hand_landmarks=None) + img = np.zeros((100, 100, 3), dtype=np.uint8) + _, r_res, l_res = process_img(mock_proc, img.copy()) + assert r_res is None + assert l_res is None diff --git a/FORK.md b/FORK.md new file mode 100644 index 0000000..997c671 --- /dev/null +++ b/FORK.md @@ -0,0 +1,7 @@ +# Fork Notice + +This repository is a fork of [pollen-robotics/AmazingHand](https://github.com/pollen-robotics/AmazingHand). + +We no longer track or merge from upstream. The fork has diverged to meet our engineering needs: refactoring for effective unit testing, maintainability, and integration with our tooling (pixi, cross-platform support). These changes make regular merges impractical. + +If you need specific fixes from upstream, port them manually rather than merging. diff --git a/pixi.lock b/pixi.lock index 9a41085..a86af6b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -10,86 +10,308 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45.1-default_hfdba357_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-he420e7e_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_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/libsanitizer-15.2.0-h90f66d4_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.2.0-hd446a21_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.12-hd63d673_2_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.93.1-h53717f1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-unknown-linux-gnu-1.93.1-h2c6d0dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/84/f8a89433fd182a850aa8db9c0cfffc9d2ebf7347b34eb5e203c8488459a2/daqp-0.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/af/78aaff4eff5bc850041035673971925ee2650a7ea1173d33ad35bd7d056c/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/da/77/20163e4df61f2c8e9e36619b2e23f4bec073885be46774709b0c58a5159d/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/89/0dd938e6ed65ee994a49351a13aceaea46235ffbc1db5444d9ba3a279814/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz + - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/11/73/07c6dcbb322f86e2b8526e0073456dbdd2813d5351f772f882123c985fda/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/15/6f/64c5dd93f1d19de05709e5c6ac66f6abe89d280838b434e9075f94e30b65/mink-1.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c8/8a/229e4db3692be55532e155e2ca6a1363752243ee79df0e7e22ba00f716cf/mujoco-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/0a/8c35465fb5bd481d4299534d9d1dce8bf71474d4edc569133fb4b721f3bc/onshape_to_robot-1.8.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/ba/f6/3c645c21358079097201090de7c30d110f5ec3fa01008e3ee81b0a77a354/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/64/72ae344963db87a15e8566b8c4f1408080ad5746c8e1c32f8d04911074bb/quadprog-0.1.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/7d/5cd09ada4c979035348188efdb454df14ea032e4ebaa8745d579a56b0cf6/rustypot-1.4.2-cp312-cp312-manylinux_2_24_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/b3/4b9580d62e1245df52e8516cf3e404ff39cc72634d2d749d47b1dada4161/uv-0.10.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/394801755d4c8684b655d35c665aea7836ec68320304f62ab3c94395b442/virtualenv-20.38.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.45.1-default_h5f4c503_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-15.2.0-hcedddb3_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.2-hb1525cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_17.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.2.0-h55c397f_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-15.2.0-he19c465_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.2-h10b116e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_17.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-15.2.0-ha7b1723_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.12-h91f4b29_2_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rust-1.93.1-h6cf38e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-unknown-linux-gnu-1.93.1-hbe8e118_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/2a/b0a896f1aa8618fae09acf508f0bb0192022383dbf73bea76143345167c8/daqp-0.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/59/d5/64a3e1dadfa72aed14d9dc1a2e11b4acbef9dfb850ad3eeed1293b63ea26/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/2a/e66c6cbf19f05ec38ecab7ca3d0ecdb3b5a9450050087c12b6e1d3025699/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/92/40d4f0acecb3d6f7078b9eb468e524778a3497d0882c7ecf80509c10b7d3/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz + - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/f4/da/dfed8db260b3fbe4e24ac17dda32c55787643a656d8d4e78c55bc847efa8/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9c/71/3b219f6563b5a1a4e7bda894d7314b9eea1a5ec53d5cc3975b7951c6e84a/mink-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b8/7b/c1612ec68d98e5f3dbc5b8a21ff5d40ab52409fcc89ea7afc8a197983297/mujoco-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/0a/8c35465fb5bd481d4299534d9d1dce8bf71474d4edc569133fb4b721f3bc/onshape_to_robot-1.8.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/f3/11/10c46e9527c4591d5264117debd8fe0e21bb23dbf378ce760add6b1e85b6/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/60/fb9b56d425c5b3495bb92683c28f954aedb609ae62f14c81a1b5621ad759/quadprog-0.1.13.tar.gz + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/6b/bfa2e5a16d7b9977c6cf65acf50eb2beb90199df8ef70abb6766d41b7d2b/rustypot-1.4.2-cp312-cp312-manylinux_2_24_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/4e/058976e2a5513f11954e09595a1821d5db1819e96e00bafded19c6a470e9/uv-0.10.4-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/394801755d4c8684b655d35c665aea7836ec68320304f62ab3c94395b442/virtualenv-20.38.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.12-h18782d2_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.93.1-h4ff7c5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-apple-darwin-1.93.1-hf6ec828_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/08/041461120e16bab05ea163afdb88fab452aefa9cbeadb9b91b3ec4f23635/daqp-0.7.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/59/8f/c5e43fc1a9bf27cd34cd98d5ad48ce18429851ae67977ae5be8f2ab0eccf/dora_rs-0.3.13-cp37-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/19/1e/2c9be5ca5a99bee5150cc76ff654f67c8d0e4b85e64f9cdd81d4ec138cf0/dora_rs_cli-0.3.13-cp37-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/76/e89fd547f292663d8ce11b3247cd653a220e0d3cedbdbd094f0a8460d735/jaxlib-0.9.0.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz + - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2d/df/be410905b9757de4b00891dd34236d96e6db150b624f28cc27cd90c74564/mediapipe-0.10.14-cp312-cp312-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c7/3d/97e4b9736f7d983d87f872eb80bbdc256a903ae1e4481f24bf3c317e376e/mink-1.1.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/e1/d4/d0032323f58a9b8080b8464c6aade8d5ac2e101dbed1de64a38b3913b446/mujoco-3.5.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/0a/8c35465fb5bd481d4299534d9d1dce8bf71474d4edc569133fb4b721f3bc/onshape_to_robot-1.8.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/7e/4c/a45c96b9fe90b2c48ee604f5176eb7deb46ce7c2e87c8d819d2945dbcab6/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/de8d3911e44699abc4e3ce835e69c3db76525af8018026f9bce61be69a43/quadprog-0.1.13-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/d8/e91cfb6fde9b37fb9884e0ed0b6becc9b2d98293e8c3255579999a8cc734/rustypot-1.4.2-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl + - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/91/c4ddf7e55e05394967615050cc364a999157a44c008d0e1e9db2ed49a11c/uv-0.10.4-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/394801755d4c8684b655d35c665aea7836ec68320304f62ab3c94395b442/virtualenv-20.38.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-h4c7d964_0.conda @@ -99,30 +321,88 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.2-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.12-h0159041_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rust-1.93.1-hf8d6059_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-pc-windows-msvc-1.93.1-h17fc481_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.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/vc-14.3-h41ae7f8_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda + - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/72/3bbbb5c5da4b982f420000ee4465b46b35d802bfbc73c1f3b772585c8b86/daqp-0.7.2-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/72/7e6881a2c9d0513e65e93f6f8343c5e66b46bee1e1de0d4d39d5fd3d1b7e/dora_rs-0.3.13-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/eb/29/005e12a00e4475c30185f4a3998fa82370eba689a8133227ef591924ee37/dora_rs_cli-0.3.13-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/02/265e5ccadd65fee2f0716431573d9e512e5c6aecb23f478a7a92053cf219/jaxlib-0.9.0.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz + - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f0/26/d228fe6e9f2060dde7f7db738968bcd603e9340f064351655b5b2652a664/mediapipe-0.10.14-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/99/7f/cfba1085a845ce188259d25c72d3180c4f7431c566125f6b171f2fd62d06/mink-1.1.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/02/37/527d83610b878f27c01dd762e0e41aaa62f095c607f0500ac7f724a2c7a5/mujoco-3.5.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/0a/8c35465fb5bd481d4299534d9d1dce8bf71474d4edc569133fb4b721f3bc/onshape_to_robot-1.8.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/d9/98/a03f69ff6fb86a67d584ecc990d85a95e6930b96e3f39ad1f8e019cb8ada/opencv_contrib_python-4.13.0.92-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/11/4f8b99099215f6e7f2c8ca9756c590e561d1d3093fedb51a960dc609e347/quadprog-0.1.13-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/cb/60e4be7b67dd8e0adfe73d0d30c11977ca1a2639ce0b852d880852cae4bc/rustypot-1.4.2-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/dd/1900452678d46f6a649ab8167bededb02500b0561fc9f69e1f52607895c7/uv-0.10.4-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/d7/394801755d4c8684b655d35c665aea7836ec68320304f62ab3c94395b442/virtualenv-20.38.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -149,6 +429,40 @@ packages: purls: [] size: 28926 timestamp: 1770939656741 +- pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl + name: absl-py + version: 2.4.0 + sha256: 88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + name: attrs + version: 25.4.0 + sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.45.1-default_hfdba357_101.conda + sha256: 74341b26a2b9475dc14ba3cf12432fcd10a23af285101883e720216d81d44676 + md5: 83aa53cb3f5fc849851a84d777a60551 + depends: + - ld_impl_linux-64 2.45.1 default_hbd61a6d_101 + - sysroot_linux-64 + - zstd >=1.5.7,<1.6.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 3744895 + timestamp: 1770267152681 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.45.1-default_h5f4c503_101.conda + sha256: e90ab42a5225dc1eaa6e4e7201cd7b8ed52dad6ec46814be7e5a4039433ae85c + md5: df6e1dc38cbe5642350fa09d4a1d546b + depends: + - ld_impl_linux-aarch64 2.45.1 default_h1979696_101 + - sysroot_linux-aarch64 + - zstd >=1.5.7,<1.6.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 4741684 + timestamp: 1770267224406 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 md5: 51a19bba1b8ebfb60df25cde030b7ebc @@ -170,6 +484,16 @@ packages: purls: [] size: 192536 timestamp: 1757437302703 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 md5: 1077e9333c41ff0be8edd1a5ec0ddace @@ -200,25 +524,661 @@ packages: purls: [] size: 146519 timestamp: 1767500828366 +- pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + name: certifi + version: 2026.1.4 + sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: cffi + version: 2.0.0 + sha256: 3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: cffi + version: 2.0.0 + sha256: b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl + name: cffi + version: 2.0.0 + sha256: 8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl + name: cffi + version: 2.0.0 + sha256: da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl name: cfgv version: 3.5.0 sha256: a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl + name: charset-normalizer + version: 3.4.4 + sha256: a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: charset-normalizer + version: 3.4.4 + sha256: b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.4 + sha256: 11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl + name: charset-normalizer + version: 3.4.4 + sha256: 0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl name: colorama version: 0.4.6 sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz + name: commentjson + version: 0.9.0 + sha256: 42f9f231d97d93aff3286a4dc0de39bfd91ae823d1d9eba9fa901fe0c7113dd4 + requires_dist: + - lark-parser>=0.7.1,<0.8.0 +- pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl + name: contourpy + version: 1.3.3 + sha256: 8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: 556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: contourpy + version: 1.3.3 + sha256: 92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + name: cycler + version: 0.12.1 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 + requires_dist: + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2f/2a/b0a896f1aa8618fae09acf508f0bb0192022383dbf73bea76143345167c8/daqp-0.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: daqp + version: 0.7.2 + sha256: 5bc97b1f8b9ecd3bc1e5b8c0edc839314e6fd9e13061637a664d003e9b7ef7db +- pypi: https://files.pythonhosted.org/packages/97/84/f8a89433fd182a850aa8db9c0cfffc9d2ebf7347b34eb5e203c8488459a2/daqp-0.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: daqp + version: 0.7.2 + sha256: 7d27e010ef42405f5979a93ca75c2c2f323a2b34247aacb4328fb446c61058d3 +- pypi: https://files.pythonhosted.org/packages/9e/08/041461120e16bab05ea163afdb88fab452aefa9cbeadb9b91b3ec4f23635/daqp-0.7.2-cp312-cp312-macosx_11_0_arm64.whl + name: daqp + version: 0.7.2 + sha256: 9da7a69e89b5d26302cbfb049f132981fe1c2f2a59a788e46a7b53df242b4828 +- pypi: https://files.pythonhosted.org/packages/ea/72/3bbbb5c5da4b982f420000ee4465b46b35d802bfbc73c1f3b772585c8b86/daqp-0.7.2-cp312-cp312-win_amd64.whl + name: daqp + version: 0.7.2 + sha256: 49d66d0c92f309eda5acbbbb3b970f2ff90ec93e0f7450ced75fd2df83c12e3b - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl name: distlib version: 0.4.0 sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/1a/af/78aaff4eff5bc850041035673971925ee2650a7ea1173d33ad35bd7d056c/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl + name: dora-rs + version: 0.3.13 + sha256: f82aa30346899b06dd9971ca8eb90829b0651580592e0aa149b7c7fa575a501d + requires_dist: + - pyarrow + - pyyaml>=6.0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/59/8f/c5e43fc1a9bf27cd34cd98d5ad48ce18429851ae67977ae5be8f2ab0eccf/dora_rs-0.3.13-cp37-abi3-macosx_11_0_arm64.whl + name: dora-rs + version: 0.3.13 + sha256: c53a4fc0490553e9a4a17be043d2d17e30c6533f08694e1781659dc143a6e5a3 + requires_dist: + - pyarrow + - pyyaml>=6.0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/59/d5/64a3e1dadfa72aed14d9dc1a2e11b4acbef9dfb850ad3eeed1293b63ea26/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl + name: dora-rs + version: 0.3.13 + sha256: 73f4c758b1a26a7495ed258e1ed90126aa843ab35dece3f89e4ba951f963a9e3 + requires_dist: + - pyarrow + - pyyaml>=6.0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/cb/72/7e6881a2c9d0513e65e93f6f8343c5e66b46bee1e1de0d4d39d5fd3d1b7e/dora_rs-0.3.13-cp37-abi3-win_amd64.whl + name: dora-rs + version: 0.3.13 + sha256: e88a2cbe7a8d16a3dd576f9449dabb5f89e6a6a7f280cea7e2733ff852838728 + requires_dist: + - pyarrow + - pyyaml>=6.0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/19/1e/2c9be5ca5a99bee5150cc76ff654f67c8d0e4b85e64f9cdd81d4ec138cf0/dora_rs_cli-0.3.13-cp37-abi3-macosx_11_0_arm64.whl + name: dora-rs-cli + version: 0.3.13 + sha256: 609ea61953c3793f08bd429ef4da06150112ce7ba53f385ea74b4425e9df555b + requires_dist: + - dora-rs>=0.3.9 + - uv + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/90/2a/e66c6cbf19f05ec38ecab7ca3d0ecdb3b5a9450050087c12b6e1d3025699/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl + name: dora-rs-cli + version: 0.3.13 + sha256: 14e0d0c8033db45d130bdfec15f1832d7110fb106c0dcdfe159dfa8bf89b1485 + requires_dist: + - dora-rs>=0.3.9 + - uv + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/da/77/20163e4df61f2c8e9e36619b2e23f4bec073885be46774709b0c58a5159d/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl + name: dora-rs-cli + version: 0.3.13 + sha256: b496cafe3887c3c449c51163644608f6f5588da87bbc2625510f0d241919e83b + requires_dist: + - dora-rs>=0.3.9 + - uv + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/eb/29/005e12a00e4475c30185f4a3998fa82370eba689a8133227ef591924ee37/dora_rs_cli-0.3.13-cp37-abi3-win_amd64.whl + name: dora-rs-cli + version: 0.3.13 + sha256: 022b031f1416c4ae0155e61f832f7fddfb822c1a50b223e3b2bb4a96fe915f84 + requires_dist: + - dora-rs>=0.3.9 + - uv + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl + name: etils + version: 1.13.0 + sha256: d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb + requires_dist: + - etils[array-types] ; extra == 'all' + - etils[eapp] ; extra == 'all' + - etils[ecolab] ; extra == 'all' + - etils[edc] ; extra == 'all' + - etils[enp] ; extra == 'all' + - etils[epath] ; extra == 'all' + - etils[epath-gcs] ; extra == 'all' + - etils[epath-s3] ; extra == 'all' + - etils[epy] ; extra == 'all' + - etils[etqdm] ; extra == 'all' + - etils[etree] ; extra == 'all' + - etils[etree-dm] ; extra == 'all' + - etils[etree-jax] ; extra == 'all' + - etils[etree-tf] ; extra == 'all' + - etils[enp] ; extra == 'array-types' + - pytest ; extra == 'dev' + - pytest-subtests ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyink ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - chex ; extra == 'dev' + - fiddle ; extra == 'dev' + - torch ; extra == 'dev' + - optree ; extra == 'dev' + - tensorflow-datasets ; extra == 'dev' + - pydantic ; extra == 'dev' + - sphinx-apitree[ext] ; extra == 'docs' + - etils[dev,all] ; extra == 'docs' + - absl-py ; extra == 'eapp' + - simple-parsing ; extra == 'eapp' + - etils[epy] ; extra == 'eapp' + - jupyter ; extra == 'ecolab' + - numpy ; extra == 'ecolab' + - mediapy ; extra == 'ecolab' + - packaging ; extra == 'ecolab' + - protobuf ; extra == 'ecolab' + - etils[enp] ; extra == 'ecolab' + - etils[epy] ; extra == 'ecolab' + - etils[etree] ; extra == 'ecolab' + - etils[epy] ; extra == 'edc' + - numpy ; extra == 'enp' + - einops ; extra == 'enp' + - etils[epy] ; extra == 'enp' + - fsspec ; extra == 'epath' + - importlib-resources ; extra == 'epath' + - typing-extensions ; extra == 'epath' + - zipp ; extra == 'epath' + - etils[epy] ; extra == 'epath' + - gcsfs ; extra == 'epath-gcs' + - etils[epath] ; extra == 'epath-gcs' + - s3fs ; extra == 'epath-s3' + - etils[epath] ; extra == 'epath-s3' + - typing-extensions ; extra == 'epy' + - absl-py ; extra == 'etqdm' + - tqdm ; extra == 'etqdm' + - etils[epy] ; extra == 'etqdm' + - etils[array-types] ; extra == 'etree' + - etils[epy] ; extra == 'etree' + - etils[enp] ; extra == 'etree' + - etils[etqdm] ; extra == 'etree' + - dm-tree ; extra == 'etree-dm' + - etils[etree] ; extra == 'etree-dm' + - jax[cpu] ; extra == 'etree-jax' + - etils[etree] ; extra == 'etree-jax' + - tensorflow ; extra == 'etree-tf' + - etils[etree] ; extra == 'etree-tf' + - etils[ecolab] ; extra == 'lazy-imports' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl name: filelock version: 3.24.2 sha256: 667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl + name: filelock + version: 3.24.3 + sha256: 426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl + name: flatbuffers + version: 25.12.19 + sha256: 7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 +- pypi: https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: fonttools + version: 4.61.1 + sha256: 15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl + name: fonttools + version: 4.61.1 + sha256: f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl + name: fonttools + version: 4.61.1 + sha256: 497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + name: fonttools + version: 4.61.1 + sha256: 10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.45.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.45.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + name: fsspec + version: 2026.2.0 + sha256: 98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-15.2.0-he420e7e_18.conda + sha256: a088cfd3ae6fa83815faa8703bc9d21cc915f17bd1b51aac9c16ddf678da21e4 + md5: cf56b6d74f580b91fd527e10d9a2e324 + depends: + - binutils_impl_linux-64 >=2.45 + - libgcc >=15.2.0 + - libgcc-devel_linux-64 15.2.0 hcc6f6b0_118 + - libgomp >=15.2.0 + - libsanitizer 15.2.0 h90f66d4_18 + - libstdcxx >=15.2.0 + - libstdcxx-devel_linux-64 15.2.0 hd446a21_118 + - sysroot_linux-64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 81814135 + timestamp: 1771378369317 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-15.2.0-hcedddb3_18.conda + sha256: 12919d985a6c6787872699c7a3c295dad07f4084f2d850e9c7fe592ee0a6806b + md5: 761a75d8c098913bc1186b26588051e0 + depends: + - binutils_impl_linux-aarch64 >=2.45 + - libgcc >=15.2.0 + - libgcc-devel_linux-aarch64 15.2.0 h55c397f_118 + - libgomp >=15.2.0 + - libsanitizer 15.2.0 he19c465_18 + - libstdcxx >=15.2.0 + - libstdcxx-devel_linux-aarch64 15.2.0 ha7b1723_118 + - sysroot_linux-aarch64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 73516504 + timestamp: 1771378256368 +- pypi: https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl + name: glfw + version: 2.10.0 + sha256: ce6724bb7cb3d0543dcba17206dce909f94176e68220b8eafee72e9f92bcf542 + requires_dist: + - glfw-preview ; extra == 'preview' +- pypi: https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl + name: glfw + version: 2.10.0 + sha256: 23936202a107039b5372f0b88ae1d11080746aa1c78910a45d4a0c4cf408cfaa + requires_dist: + - glfw-preview ; extra == 'preview' +- pypi: https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl + name: glfw + version: 2.10.0 + sha256: 823c0bd7770977d4b10e0ed0aef2f3682276b7c88b8b65cfc540afce5951392f + requires_dist: + - glfw-preview ; extra == 'preview' +- pypi: https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl + name: glfw + version: 2.10.0 + sha256: 27c9e9a2d5e1dc3c9e3996171d844d9df9a5a101e797cb94cce217b7afcf8fd9 + requires_dist: + - glfw-preview ; extra == 'preview' - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda sha256: 142a722072fa96cf16ff98eaaf641f54ab84744af81754c292cb81e0881c0329 md5: 186a18e3ba246eccfc7cff00cd19a870 @@ -242,6 +1202,16 @@ packages: purls: [] size: 12852963 timestamp: 1767975394622 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda + sha256: d4cefbca587429d1192509edc52c88de52bc96c2447771ddc1f8bee928aed5ef + md5: 1e93aca311da0210e660d2247812fa02 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12358010 + timestamp: 1767970350308 - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl name: identify version: 2.6.16 @@ -249,11 +1219,152 @@ packages: requires_dist: - ukkonen ; extra == 'license' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + name: idna + version: '3.11' + sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl + name: importlib-resources + version: 6.5.2 + sha256: 789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec + requires_dist: + - zipp>=3.1.0 ; python_full_version < '3.10' + - pytest>=6,!=8.1.* ; extra == 'test' + - zipp>=3.17 ; extra == 'test' + - jaraco-test>=5.4 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl name: iniconfig version: 2.3.0 sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl + name: jax + version: 0.9.0.1 + sha256: 3baeaec6dc853394c272eb38a35ffba1972d67cf55d07a76bdb913bcd867e2ca + requires_dist: + - jaxlib<=0.9.0.1,>=0.9.0.1 + - ml-dtypes>=0.5.0 + - numpy>=2.0 + - opt-einsum + - scipy>=1.13 + - jaxlib==0.9.0.1 ; extra == 'minimum-jaxlib' + - jaxlib==0.8.2 ; extra == 'ci' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'tpu' + - libtpu==0.0.34.* ; extra == 'tpu' + - requests ; extra == 'tpu' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda' + - jax-cuda12-plugin[with-cuda]<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda12' + - jax-cuda12-plugin[with-cuda]<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda12' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda13' + - jax-cuda13-plugin[with-cuda]<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda13' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda12-local' + - jax-cuda12-plugin<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda12-local' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda13-local' + - jax-cuda13-plugin<=0.9.0.1,>=0.9.0.1 ; extra == 'cuda13-local' + - jaxlib<=0.9.0.1,>=0.9.0.1 ; extra == 'rocm' + - jax-rocm7-plugin<=0.9.0.1,>=0.9.0.1 ; extra == 'rocm' + - kubernetes ; extra == 'k8s' + - xprof ; extra == 'xprof' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/1d/89/0dd938e6ed65ee994a49351a13aceaea46235ffbc1db5444d9ba3a279814/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_x86_64.whl + name: jaxlib + version: 0.9.0.1 + sha256: e0e4a0a24ef98ec021b913991fbda09aeb96481b1bc0e5300a0339aad216b226 + requires_dist: + - scipy>=1.13 + - numpy>=2.0 + - ml-dtypes>=0.5.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/bb/02/265e5ccadd65fee2f0716431573d9e512e5c6aecb23f478a7a92053cf219/jaxlib-0.9.0.1-cp312-cp312-win_amd64.whl + name: jaxlib + version: 0.9.0.1 + sha256: 08733d1431238a7cf9108338ab7be898b97181cba0eef53f2f9fd3de17d20adb + requires_dist: + - scipy>=1.13 + - numpy>=2.0 + - ml-dtypes>=0.5.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c1/92/40d4f0acecb3d6f7078b9eb468e524778a3497d0882c7ecf80509c10b7d3/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_aarch64.whl + name: jaxlib + version: 0.9.0.1 + sha256: 5ea8ebd62165b6f18f89b02fab749e02f5c584c2a1c703f04592d4d803f9e981 + requires_dist: + - scipy>=1.13 + - numpy>=2.0 + - ml-dtypes>=0.5.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c8/76/e89fd547f292663d8ce11b3247cd653a220e0d3cedbdbd094f0a8460d735/jaxlib-0.9.0.1-cp312-cp312-macosx_11_0_arm64.whl + name: jaxlib + version: 0.9.0.1 + sha256: 3707bf0a58410da7c053c15ec6efee1fe12e70361416e055e4109b8041f4119b + requires_dist: + - scipy>=1.13 + - numpy>=2.0 + - ml-dtypes>=0.5.0 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a + md5: 86d9cba083cd041bfbf242a01a7a1999 + constrains: + - sysroot_linux-64 ==2.28 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 1278712 + timestamp: 1765578681495 +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda + sha256: 5d224bf4df9bac24e69de41897c53756108c5271a0e5d2d2f66fd4e2fbc1d84b + md5: bb3b7cad9005f2cbf9d169fb30263f3e + constrains: + - sysroot_linux-aarch64 ==2.28 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 1248134 + timestamp: 1765578613607 +- pypi: https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + name: kiwisolver + version: 1.4.9 + sha256: 67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.4.9 + sha256: 4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.4.9 + sha256: f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl + name: kiwisolver + version: 1.4.9 + sha256: f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz + name: lark-parser + version: 0.7.8 + sha256: 26215ebb157e6fb2ee74319aa4445b9f3b7e456e26be215ce19fdaaa901c20a4 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 md5: 12bd9a3f089ee6c9266a37dab82afabd @@ -304,6 +1415,18 @@ packages: purls: [] size: 76201 timestamp: 1763549910086 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + sha256: 03887d8080d6a8fe02d75b80929271b39697ecca7628f0657d7afaea87761edf + md5: a92e310ae8dfc206ff449f362fc4217f + depends: + - __osx >=11.0 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 68199 + timestamp: 1771260020767 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.3-hac47afa_0.conda sha256: 844ab708594bdfbd7b35e1a67c379861bcd180d6efe57b654f482ae2f7f5c21e md5: 8c9e4f1a0e688eef2e95711178061a0f @@ -339,6 +1462,16 @@ packages: purls: [] size: 55952 timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 md5: 720b39f5ec0610457b725eb3f396219a @@ -378,6 +1511,26 @@ packages: purls: [] size: 622154 timestamp: 1770252127670 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_118.conda + sha256: af69fc5852908d26e5b630b270982ac792506551dd6af1614bf0370dd5ab5746 + md5: 5d3a96d55f1be45fef88ee23155effd9 + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3085932 + timestamp: 1771378098166 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.2.0-h55c397f_118.conda + sha256: 661e29553769ceb5874eb1ed6c00263fcd36fac9f5fe0fee65d5e5cac3187ff3 + md5: 42284981c315916d916fb3156b8d5b9e + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 2364690 + timestamp: 1771378032404 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda sha256: bdfe50501e4a2d904a5eae65a7ae26e2b7a29b473ab084ad55d96080b966502e md5: 1478bfa85224a65ab096d69ffd2af1e5 @@ -439,6 +1592,17 @@ packages: purls: [] size: 125916 timestamp: 1768754941722 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + sha256: 7bfc7ffb2d6a9629357a70d4eadeadb6f88fa26ebc28f606b1c1e5e5ed99dc7e + md5: 009f0d956d7bfb00de86901d16e486c7 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 92242 + timestamp: 1768752982486 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda sha256: f25bf293f550c8ed2e0c7145eb404324611cfccff37660869d97abf526eb957c md5: ba0bfd4c3cf73f299ffe46ff0eaeb8e3 @@ -473,6 +1637,29 @@ packages: purls: [] size: 34831 timestamp: 1750274211 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-15.2.0-h90f66d4_18.conda + sha256: 0329e23d54a567c259adc962a62172eaa55e6ca33c105ef67b4f3cdb4ef70eaa + md5: ff754fbe790d4e70cf38aea3668c3cb3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 + - libstdcxx >=15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 8095113 + timestamp: 1771378289674 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-15.2.0-he19c465_18.conda + sha256: 10c42c4e12972088cf0d5f57393f83e6727ad31bdb38ae46935641861f394698 + md5: 589c6fc3e744df871bbbf703f1e6ce98 + depends: + - libgcc >=15.2.0 + - libstdcxx >=15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 7164557 + timestamp: 1771378185265 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda sha256: 04596fcee262a870e4b7c9807224680ff48d4d0cc0dac076a602503d3dc6d217 md5: da5be73701eecd0e8454423fd6ffcf30 @@ -496,6 +1683,17 @@ packages: purls: [] size: 944688 timestamp: 1768147991301 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda + sha256: 6e9b9f269732cbc4698c7984aa5b9682c168e2a8d1e0406e1ff10091ca046167 + md5: 4b0bf313c53c3e89692f020fb55d5f2c + depends: + - __osx >=11.0 + - icu >=78.2,<79.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 909777 + timestamp: 1768148320535 - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.51.2-hf5d6505_0.conda sha256: 756478128e3e104bd7e7c3ce6c1b0efad7e08c7320c69fdc726e039323c63fbb md5: 903979414b47d777d548e5f0165e6cd8 @@ -532,6 +1730,26 @@ packages: purls: [] size: 5542688 timestamp: 1770252159760 +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-15.2.0-hd446a21_118.conda + sha256: 138ee40ba770abf4556ee9981879da9e33299f406a450831b48c1c397d7d0833 + md5: a50630d1810916fc252b2152f1dc9d6d + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 20669511 + timestamp: 1771378139786 +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-15.2.0-ha7b1723_118.conda + sha256: 52afca5e24e0bbc840cf9c28b440dea2cebc4500e97084a38cdd27fdc8a3e57c + md5: 99ea26f70c5e380294e760e8bdbaddff + depends: + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 17628403 + timestamp: 1771378058765 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee md5: db409b7c1720428638e7c0d509d3e1b5 @@ -596,6 +1814,18 @@ packages: purls: [] size: 66657 timestamp: 1727963199518 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 md5: 41fbfac52c601159df6c01f875de31b9 @@ -610,6 +1840,339 @@ packages: purls: [] size: 55476 timestamp: 1727963768015 +- pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + name: loop-rate-limiters + version: 1.2.0 + sha256: 482f720f409e05dfca8b7b63df180217afa9a51564def681853cb7370a020b74 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: matplotlib + version: 3.10.8 + sha256: 24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl + name: matplotlib + version: 3.10.8 + sha256: dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.8 + sha256: 3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.8 + sha256: b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/11/73/07c6dcbb322f86e2b8526e0073456dbdd2813d5351f772f882123c985fda/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: mediapipe + version: 0.10.14 + sha256: 9b1e72d754cd9e1b4b88d80ec9ead2f1cbe8424b7f883d3bda53341b982a9f8b + requires_dist: + - absl-py + - attrs>=19.1.0 + - flatbuffers>=2.0 + - jax + - jaxlib + - matplotlib + - numpy + - opencv-contrib-python + - protobuf>=4.25.3,<5 + - sounddevice>=0.4.4 +- pypi: https://files.pythonhosted.org/packages/2d/df/be410905b9757de4b00891dd34236d96e6db150b624f28cc27cd90c74564/mediapipe-0.10.14-cp312-cp312-macosx_11_0_universal2.whl + name: mediapipe + version: 0.10.14 + sha256: aa2298c1886716cde6bd7ce96aba1505d67d52edaba856f6c2e1ab905de52b0d + requires_dist: + - absl-py + - attrs>=19.1.0 + - flatbuffers>=2.0 + - jax + - jaxlib + - matplotlib + - numpy + - opencv-contrib-python + - protobuf>=4.25.3,<5 + - sounddevice>=0.4.4 +- pypi: https://files.pythonhosted.org/packages/f0/26/d228fe6e9f2060dde7f7db738968bcd603e9340f064351655b5b2652a664/mediapipe-0.10.14-cp312-cp312-win_amd64.whl + name: mediapipe + version: 0.10.14 + sha256: ebb8350e860c8e00b7c84d71e15090fc3ac4cc9d4249892f85fb35011590e372 + requires_dist: + - absl-py + - attrs>=19.1.0 + - flatbuffers>=2.0 + - jax + - jaxlib + - matplotlib + - numpy + - opencv-contrib-python + - protobuf>=4.25.3,<5 + - sounddevice>=0.4.4 +- pypi: https://files.pythonhosted.org/packages/f4/da/dfed8db260b3fbe4e24ac17dda32c55787643a656d8d4e78c55bc847efa8/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: mediapipe + version: 0.10.14 + sha256: 3e30ed2f39f925924de58adeec6a8e61ba6beb6959325dc0d8a1aa487a74377b + requires_dist: + - absl-py + - attrs>=19.1.0 + - flatbuffers>=2.0 + - jax + - jaxlib + - matplotlib + - numpy + - opencv-contrib-python + - protobuf>=4.25.3,<5 + - sounddevice>=0.4.4 +- pypi: https://files.pythonhosted.org/packages/15/6f/64c5dd93f1d19de05709e5c6ac66f6abe89d280838b434e9075f94e30b65/mink-1.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: mink + version: 1.1.0 + sha256: 3d11676be6d08ccf7943be6c8afa1ff64fa6be3fbab633fcc2e3f1cd1ef004b1 + requires_dist: + - mujoco>=3.3.6 + - qpsolvers[daqp]>=4.3.1 + - typing-extensions + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/99/7f/cfba1085a845ce188259d25c72d3180c4f7431c566125f6b171f2fd62d06/mink-1.1.0-cp312-cp312-win_amd64.whl + name: mink + version: 1.1.0 + sha256: 22efa22b554d8e1076890afbb3a7223d2ca249fae97d631d10fbbf71bc2c3c56 + requires_dist: + - mujoco>=3.3.6 + - qpsolvers[daqp]>=4.3.1 + - typing-extensions + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/9c/71/3b219f6563b5a1a4e7bda894d7314b9eea1a5ec53d5cc3975b7951c6e84a/mink-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: mink + version: 1.1.0 + sha256: 9b1f8426e6e39976a9863e832680583945a9ccb644d6f96c7fa40462d7859965 + requires_dist: + - mujoco>=3.3.6 + - qpsolvers[daqp]>=4.3.1 + - typing-extensions + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/c7/3d/97e4b9736f7d983d87f872eb80bbdc256a903ae1e4481f24bf3c317e376e/mink-1.1.0-cp312-cp312-macosx_11_0_arm64.whl + name: mink + version: 1.1.0 + sha256: de76f557b2f667abbd1305a5074f051a6e66d439df53e48ada5359915e122fc1 + requires_dist: + - mujoco>=3.3.6 + - qpsolvers[daqp]>=4.3.1 + - typing-extensions + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: ml-dtypes + version: 0.5.4 + sha256: 9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff + requires_dist: + - numpy>=1.21 + - numpy>=1.21.2 ; python_full_version >= '3.10' + - numpy>=1.23.3 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=2.1.0 ; python_full_version >= '3.13' + - absl-py ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: ml-dtypes + version: 0.5.4 + sha256: a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 + requires_dist: + - numpy>=1.21 + - numpy>=1.21.2 ; python_full_version >= '3.10' + - numpy>=1.23.3 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=2.1.0 ; python_full_version >= '3.13' + - absl-py ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl + name: ml-dtypes + version: 0.5.4 + sha256: a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac + requires_dist: + - numpy>=1.21 + - numpy>=1.21.2 ; python_full_version >= '3.10' + - numpy>=1.23.3 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=2.1.0 ; python_full_version >= '3.13' + - absl-py ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl + name: ml-dtypes + version: 0.5.4 + sha256: c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 + requires_dist: + - numpy>=1.21 + - numpy>=1.21.2 ; python_full_version >= '3.10' + - numpy>=1.23.3 ; python_full_version >= '3.11' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - numpy>=2.1.0 ; python_full_version >= '3.13' + - absl-py ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pylint>=2.6.0 ; extra == 'dev' + - pyink ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/02/37/527d83610b878f27c01dd762e0e41aaa62f095c607f0500ac7f724a2c7a5/mujoco-3.5.0-cp312-cp312-win_amd64.whl + name: mujoco + version: 3.5.0 + sha256: 4b3a62af174ab59b9b6d816dca0786b7fd85ac081d6c2a931a2b22dd6e821f50 + requires_dist: + - absl-py + - etils[epath] + - glfw + - numpy + - pyopengl + - absl-py ; extra == 'sysid' + - colorama ; extra == 'sysid' + - imageio[ffmpeg] ; extra == 'sysid' + - jinja2 ; extra == 'sysid' + - matplotlib ; extra == 'sysid' + - plotly ; extra == 'sysid' + - pyyaml ; extra == 'sysid' + - scipy ; extra == 'sysid' + - tabulate ; extra == 'sysid' + - typing-extensions ; extra == 'sysid' + - usd-core ; extra == 'usd' + - pillow ; extra == 'usd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b8/7b/c1612ec68d98e5f3dbc5b8a21ff5d40ab52409fcc89ea7afc8a197983297/mujoco-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: mujoco + version: 3.5.0 + sha256: 12bfb2bb70f760e0d51fd59f3c43b2906c7660a23954fd717321da52ba85a617 + requires_dist: + - absl-py + - etils[epath] + - glfw + - numpy + - pyopengl + - absl-py ; extra == 'sysid' + - colorama ; extra == 'sysid' + - imageio[ffmpeg] ; extra == 'sysid' + - jinja2 ; extra == 'sysid' + - matplotlib ; extra == 'sysid' + - plotly ; extra == 'sysid' + - pyyaml ; extra == 'sysid' + - scipy ; extra == 'sysid' + - tabulate ; extra == 'sysid' + - typing-extensions ; extra == 'sysid' + - usd-core ; extra == 'usd' + - pillow ; extra == 'usd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c8/8a/229e4db3692be55532e155e2ca6a1363752243ee79df0e7e22ba00f716cf/mujoco-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: mujoco + version: 3.5.0 + sha256: 66fe37276644c28fab497929c55580725de81afc6d511a40cc27525a8dd99efa + requires_dist: + - absl-py + - etils[epath] + - glfw + - numpy + - pyopengl + - absl-py ; extra == 'sysid' + - colorama ; extra == 'sysid' + - imageio[ffmpeg] ; extra == 'sysid' + - jinja2 ; extra == 'sysid' + - matplotlib ; extra == 'sysid' + - plotly ; extra == 'sysid' + - pyyaml ; extra == 'sysid' + - scipy ; extra == 'sysid' + - tabulate ; extra == 'sysid' + - typing-extensions ; extra == 'sysid' + - usd-core ; extra == 'usd' + - pillow ; extra == 'usd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e1/d4/d0032323f58a9b8080b8464c6aade8d5ac2e101dbed1de64a38b3913b446/mujoco-3.5.0-cp312-cp312-macosx_11_0_arm64.whl + name: mujoco + version: 3.5.0 + sha256: 94cf4285b46bc2d74fbe86e39a93ecfb3b0e584477fff7e38d293d47b88576e7 + requires_dist: + - absl-py + - etils[epath] + - glfw + - numpy + - pyopengl + - absl-py ; extra == 'sysid' + - colorama ; extra == 'sysid' + - imageio[ffmpeg] ; extra == 'sysid' + - jinja2 ; extra == 'sysid' + - matplotlib ; extra == 'sysid' + - plotly ; extra == 'sysid' + - pyyaml ; extra == 'sysid' + - scipy ; extra == 'sysid' + - tabulate ; extra == 'sysid' + - typing-extensions ; extra == 'sysid' + - usd-core ; extra == 'usd' + - pillow ; extra == 'usd' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -629,11 +2192,25 @@ packages: purls: [] size: 926034 timestamp: 1738196018799 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl name: nodeenv version: 1.10.0 sha256: 5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl + name: numpy + version: 2.4.2 + sha256: 40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1 + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl name: numpy version: 2.4.2 @@ -649,6 +2226,113 @@ packages: version: 2.4.2 sha256: 9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl + name: numpy-stl + version: 3.2.0 + sha256: 697c81b107231362460aaedf4647e81ba54f54f59c896772fea7904c9c439da5 + requires_dist: + - numpy + - python-utils>=3.4.5 + - mock ; extra == 'docs' + - sphinx ; extra == 'docs' + - python-utils ; extra == 'docs' + - cov-core ; extra == 'tests' + - coverage ; extra == 'tests' + - docutils ; extra == 'tests' + - execnet ; extra == 'tests' + - numpy ; extra == 'tests' + - cython ; extra == 'tests' + - pep8 ; extra == 'tests' + - py ; extra == 'tests' + - pyflakes ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cache ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - python-utils ; extra == 'tests' + - sphinx ; extra == 'tests' + - flake8 ; extra == 'tests' + - wheel ; extra == 'tests' + requires_python: '>3.9.0' +- pypi: https://files.pythonhosted.org/packages/86/0a/8c35465fb5bd481d4299534d9d1dce8bf71474d4edc569133fb4b721f3bc/onshape_to_robot-1.8.1.tar.gz + name: onshape-to-robot + version: 1.8.1 + sha256: 2b9d8569a440b61294b38b62bbef681d390d30722e30c5f69b5f2dd59049dbbb + requires_dist: + - numpy + - requests + - commentjson + - colorama>=0.4.6 + - numpy-stl + - transforms3d + - python-dotenv + - pymeshlab ; extra == 'pymeshlab' + - pybullet ; extra == 'pybullet' + - mujoco ; extra == 'mujoco' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7e/4c/a45c96b9fe90b2c48ee604f5176eb7deb46ce7c2e87c8d819d2945dbcab6/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl + name: opencv-contrib-python + version: 4.13.0.92 + sha256: 53c8ab81376210dda5836307eb6bda7266f39a3820a9a070c7131510ba815fe1 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/ba/f6/3c645c21358079097201090de7c30d110f5ec3fa01008e3ee81b0a77a354/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl + name: opencv-contrib-python + version: 4.13.0.92 + sha256: fc5ee50e2be9d40e913536f7f20cc6f87f25d8e413ebb32a3335ab6edf245d3e + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/d9/98/a03f69ff6fb86a67d584ecc990d85a95e6930b96e3f39ad1f8e019cb8ada/opencv_contrib_python-4.13.0.92-cp37-abi3-win_amd64.whl + name: opencv-contrib-python + version: 4.13.0.92 + sha256: cb694dcf76bb2c8d7fa573fc1a99339e8b6640194d7778381e74cc3445369e45 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/f3/11/10c46e9527c4591d5264117debd8fe0e21bb23dbf378ce760add6b1e85b6/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl + name: opencv-contrib-python + version: 4.13.0.92 + sha256: a3c54377c5cf9c45d9b1a207df26dc8fe4f1042d07036cb17d80930c04b25d97 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl + name: opencv-python + version: 4.13.0.92 + sha256: bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl + name: opencv-python + version: 4.13.0.92 + sha256: 423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl + name: opencv-python + version: 4.13.0.92 + sha256: caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl + name: opencv-python + version: 4.13.0.92 + sha256: 620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5 + requires_dist: + - numpy<2.0 ; python_full_version < '3.9' + - numpy>=2 ; python_full_version >= '3.9' + requires_python: '>=3.6' - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c md5: f61eb8cd60ff9057122a3d338b99c00f @@ -672,6 +2356,17 @@ packages: purls: [] size: 3692030 timestamp: 1769557678657 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + sha256: 361f5c5e60052abc12bdd1b50d7a1a43e6a6653aab99a2263bf2288d709dcf67 + md5: f4f6ad63f98f64191c3e77c5f5f29d76 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3104268 + timestamp: 1769556384749 - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda sha256: 53a5ad2e5553b8157a91bb8aa375f78c5958f77cb80e9d2ce59471ea8e5c0bd6 md5: eb585509b815415bc964b2c7e11c7eb3 @@ -685,11 +2380,164 @@ packages: purls: [] size: 9343023 timestamp: 1769557547888 -- pypi: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl - name: packaging - version: '26.0' - sha256: b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +- pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl + name: opt-einsum + version: 3.4.0 + sha256: 69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 +- pypi: https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl + name: pillow + version: 12.1.1 + sha256: 21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: pillow + version: 12.1.1 + sha256: a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl + name: pillow + version: 12.1.1 + sha256: adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.1.1 + sha256: 7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.0.1-pyh8b19718_0.conda + sha256: 8e1497814a9997654ed7990a79c054ea5a42545679407acbc6f7e809c73c9120 + md5: 67bdec43082fd8a9cffb9484420b39a2 + depends: + - python >=3.10,<3.13.0a0 + - setuptools + - wheel + license: MIT + license_family: MIT + purls: + - pkg:pypi/pip?source=compressed-mapping + size: 1181790 + timestamp: 1770270305795 - pypi: https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl name: platformdirs version: 4.9.2 @@ -717,6 +2565,51 @@ packages: - pyyaml>=5.1 - virtualenv>=20.10.0 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl + name: protobuf + version: 4.25.8 + sha256: bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl + name: protobuf + version: 4.25.8 + sha256: ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl + name: protobuf + version: 4.25.8 + sha256: 9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl + name: protobuf + version: 4.25.8 + sha256: 83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl + name: pyarrow + version: 23.0.1 + sha256: 813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl + name: pyarrow + version: 23.0.1 + sha256: 86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl + name: pyarrow + version: 23.0.1 + sha256: f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl + name: pyarrow + version: 23.0.1 + sha256: 07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + name: pycparser + version: '3.0' + sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments version: 2.19.2 @@ -724,6 +2617,18 @@ packages: requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl + name: pyopengl + version: 3.1.10 + sha256: 794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl name: pytest version: 9.0.2 @@ -799,6 +2704,29 @@ packages: purls: [] size: 13798340 timestamp: 1769471112 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.12-h18782d2_2_cpython.conda + build_number: 2 + sha256: 765e5d0f92dabc8c468d078a4409490e08181a6f9be6f5d5802a4e3131b9a69c + md5: e198b8f74b12292d138eb4eceb004fa3 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 12953358 + timestamp: 1769472376612 - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.12-h0159041_2_cpython.conda build_number: 2 sha256: 5937ab50dfeb979f7405132f73e836a29690f21162308b95b240b8037aa99975 @@ -822,11 +2750,53 @@ packages: purls: [] size: 15829087 timestamp: 1769470991307 +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + name: python-dotenv + version: 1.2.1 + sha256: b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 + requires_dist: + - click>=5.0 ; extra == 'cli' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl + name: python-utils + version: 3.9.1 + sha256: 0273d7363c7ad4b70999b2791d5ba6b55333d6f7a4e4c8b6b39fb82b5fab4613 + requires_dist: + - typing-extensions>3.10.0.2 + - loguru ; extra == 'loguru' + - mock ; extra == 'docs' + - sphinx ; extra == 'docs' + - python-utils ; extra == 'docs' + - ruff ; extra == 'tests' + - pyright ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-mypy ; extra == 'tests' + - pytest-asyncio ; extra == 'tests' + - sphinx ; extra == 'tests' + - types-setuptools ; extra == 'tests' + - loguru ; extra == 'tests' + - loguru-mypy ; extra == 'tests' + - mypy-ipython ; extra == 'tests' + - blessings ; extra == 'tests' + requires_python: '>=3.9.0' - pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl name: pyyaml version: 6.0.3 sha256: 5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl + name: pyyaml + version: 6.0.3 + sha256: fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: pyyaml version: 6.0.3 @@ -837,6 +2807,62 @@ packages: version: 6.0.3 sha256: 9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl + name: qpsolvers + version: 4.8.2 + sha256: 66cad899705b5ba009c6a280b2702c5f413e25c69beec2c6bcad72307fb22dd1 + requires_dist: + - numpy>=1.15.4 + - scipy>=1.2.0 + - clarabel>=0.4.1 ; extra == 'clarabel' + - cvxopt>=1.2.6 ; extra == 'cvxopt' + - daqp>=0.5.1 ; extra == 'daqp' + - ecos>=2.0.8 ; extra == 'ecos' + - gurobipy>=9.5.2 ; extra == 'gurobi' + - highspy>=1.1.2.dev3 ; extra == 'highs' + - jaxopt>=0.8.3 ; extra == 'jaxopt' + - kvxopt>=1.3.2 ; extra == 'kvxopt' + - cvxopt>=1.2.6 ; extra == 'mosek' + - mosek>=10.0.40 ; extra == 'mosek' + - qpsolvers[clarabel,cvxopt,daqp,ecos,highs,jaxopt,osqp,piqp,proxqp,qpalm,quadprog,scs,sip,qpax] ; extra == 'open-source-solvers' + - osqp>=0.6.2 ; extra == 'osqp' + - piqp>=0.2.2 ; extra == 'piqp' + - proxsuite>=0.2.9 ; extra == 'proxqp' + - qpalm>=1.2.1 ; extra == 'qpalm' + - qpax>=0.0.9 ; extra == 'qpax' + - quadprog>=0.1.11 ; extra == 'quadprog' + - scs>=3.2.0 ; extra == 'scs' + - sip-python>=0.0.2 ; extra == 'sip' + - qpsolvers[cvxopt,daqp,ecos,highs,piqp,proxqp,qpalm,sip] ; extra == 'wheels-only' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/10/5e/de8d3911e44699abc4e3ce835e69c3db76525af8018026f9bce61be69a43/quadprog-0.1.13-cp312-cp312-macosx_11_0_arm64.whl + name: quadprog + version: 0.1.13 + sha256: 8628ea1b1910c0fbfc828fca36ebc76a8c7352fdef782b5971aee1f4db08ba38 + requires_dist: + - numpy + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/20/64/72ae344963db87a15e8566b8c4f1408080ad5746c8e1c32f8d04911074bb/quadprog-0.1.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: quadprog + version: 0.1.13 + sha256: 7e2cb007ecf649fb2a614a1954359d534682d7339c3834df30aee7321ae1f7bb + requires_dist: + - numpy + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/47/11/4f8b99099215f6e7f2c8ca9756c590e561d1d3093fedb51a960dc609e347/quadprog-0.1.13-cp312-cp312-win_amd64.whl + name: quadprog + version: 0.1.13 + sha256: d3d8d774d6436829ea4a11eeffe4a1a72375df17c9e9a7a48973372e64c826ad + requires_dist: + - numpy + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/60/fb9b56d425c5b3495bb92683c28f954aedb609ae62f14c81a1b5621ad759/quadprog-0.1.13.tar.gz + name: quadprog + version: 0.1.13 + sha256: 9d6dd32f2762f29b840fb83741d11e527ddf48745f63b79caad0e530b4a6a0ff + requires_dist: + - numpy + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 md5: d7d95fc8287ea7bf33e0e7116d2b95ec @@ -860,6 +2886,126 @@ packages: purls: [] size: 357597 timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + name: requests + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.21.1,<3 + - certifi>=2017.4.17 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.93.1-h53717f1_0.conda + sha256: 21e067aabf5863eee02fc5681a6d56de888abea6eaa521abf756b707c0dbcd39 + md5: f05b79be5b5f13fecc79af60892bb1c4 + depends: + - __glibc >=2.17,<3.0.a0 + - gcc_impl_linux-64 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + - rust-std-x86_64-unknown-linux-gnu 1.93.1 h2c6d0dc_0 + - sysroot_linux-64 >=2.17 + license: MIT + license_family: MIT + purls: [] + size: 177821736 + timestamp: 1771008968072 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rust-1.93.1-h6cf38e9_0.conda + sha256: 0b3f1c4d552092345f32d4723adfbb77aba02175615cda40454963f4136cba3d + md5: 27e0439ad967c39b26045a52764abc91 + depends: + - gcc_impl_linux-aarch64 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + - rust-std-aarch64-unknown-linux-gnu 1.93.1 hbe8e118_0 + - sysroot_linux-aarch64 >=2.17 + license: MIT + license_family: MIT + purls: [] + size: 139979043 + timestamp: 1771009599403 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.93.1-h4ff7c5d_0.conda + sha256: 5c7bf3200b072752878281fee69077a6731d491562e03ddaa46df0058ff9d706 + md5: 9d63a676e2138373a5520b820f34a3ab + depends: + - rust-std-aarch64-apple-darwin 1.93.1 hf6ec828_0 + license: MIT + license_family: MIT + purls: [] + size: 181513569 + timestamp: 1771008421760 +- conda: https://conda.anaconda.org/conda-forge/win-64/rust-1.93.1-hf8d6059_0.conda + sha256: ca85fab88197407d0a240eb43ca9c454e00068d7b8cd75c0e88e3ba00ea64116 + md5: 426d6deb734b4e00a7e413ddf281f1dd + depends: + - rust-std-x86_64-pc-windows-msvc 1.93.1 h17fc481_0 + license: MIT + license_family: MIT + purls: [] + size: 199507365 + timestamp: 1771011101657 +- conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-apple-darwin-1.93.1-hf6ec828_0.conda + sha256: 738563b1144c277822379194e7cf3980327f2b22fc0f46a20e432c177d6fa156 + md5: 7ad7ce45df53349e3bc817ed384496fd + depends: + - __unix + constrains: + - rust >=1.93.1,<1.93.2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 35165708 + timestamp: 1771008221089 +- conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-unknown-linux-gnu-1.93.1-hbe8e118_0.conda + sha256: 82dbb0788d7896a9270ebc17712d41a1391fb2bebaed7847ce66985e6cbd6c92 + md5: 62fbbf47f940ed4c722a0cf156d86ba0 + depends: + - __unix + constrains: + - rust >=1.93.1,<1.93.2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 38356812 + timestamp: 1771009097984 +- conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-pc-windows-msvc-1.93.1-h17fc481_0.conda + sha256: 02604af1baf2fcef0184ca3dd1e78dde5a87982f3193fc2fc0bbd5e752597a77 + md5: 68a38a410f3652df865e3d536e52e541 + depends: + - __win + constrains: + - rust >=1.93.1,<1.93.2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 28799384 + timestamp: 1771010950507 +- conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-unknown-linux-gnu-1.93.1-h2c6d0dc_0.conda + sha256: fb979ded3c378074457af671a4f7317e0e24ba9393a4ace33d7d87efe055752e + md5: b202bf8a5113a75a130ca5f6f111dda7 + depends: + - __unix + constrains: + - rust >=1.93.1,<1.93.2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 38400767 + timestamp: 1771008857576 - pypi: https://files.pythonhosted.org/packages/11/cb/60e4be7b67dd8e0adfe73d0d30c11977ca1a2639ce0b852d880852cae4bc/rustypot-1.4.2-cp312-cp312-win_amd64.whl name: rustypot version: 1.4.2 @@ -874,6 +3020,13 @@ packages: requires_dist: - pytest ; extra == 'tests' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/99/d8/e91cfb6fde9b37fb9884e0ed0b6becc9b2d98293e8c3255579999a8cc734/rustypot-1.4.2-cp312-cp312-macosx_11_0_arm64.whl + name: rustypot + version: 1.4.2 + sha256: fbd72500f9834fa1304c00d82df4c85477bd239415d79f737672acb6aef2baff + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/ec/6b/bfa2e5a16d7b9977c6cf65acf50eb2beb90199df8ef70abb6766d41b7d2b/rustypot-1.4.2-cp312-cp312-manylinux_2_24_aarch64.whl name: rustypot version: 1.4.2 @@ -881,6 +3034,246 @@ packages: requires_dist: - pytest ; extra == 'tests' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: scipy + version: 1.17.0 + sha256: 5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl + name: scipy + version: 1.17.0 + sha256: 0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.0 + sha256: 9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl + name: scipy + version: 1.17.0 + sha256: 88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + sha256: fd7201e38e38bf7f25818d624ca8da97b8998957ca9ae3fb7fdc9c17e6b25fcd + md5: 1d00d46c634177fc8ede8b99d6089239 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=compressed-mapping + size: 637506 + timestamp: 1770634745653 +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl + name: sounddevice + version: 0.5.5 + sha256: 30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f + requires_dist: + - cffi + - numpy ; extra == 'numpy' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl + name: sounddevice + version: 0.5.5 + sha256: 05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722 + requires_dist: + - cffi + - numpy ; extra == 'numpy' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl + name: sounddevice + version: 0.5.5 + sha256: cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519 + requires_dist: + - cffi + - numpy ; extra == 'numpy' + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + sha256: c47299fe37aebb0fcf674b3be588e67e4afb86225be4b0d452c7eb75c086b851 + md5: 13dc3adbc692664cd3beabd216434749 + depends: + - __glibc >=2.28 + - kernel-headers_linux-64 4.18.0 he073ed8_9 + - tzdata + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 24008591 + timestamp: 1765578833462 +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda + sha256: 1bd2db6b2e451247bab103e4a0128cf6c7595dd72cb26d70f7fadd9edd1d1bc3 + md5: fdf07ab944a222ff28c754914fdb0740 + depends: + - __glibc >=2.28 + - kernel-headers_linux-aarch64 4.18.0 h05a177a_9 + - tzdata + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL + purls: [] + size: 23644746 + timestamp: 1765578629426 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac md5: cffd3bdd58090148f4cfcd831f4b26ab @@ -908,6 +3301,17 @@ packages: purls: [] size: 3368666 timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 md5: 0481bfd9814bf525bd4b3ee4b51494c4 @@ -920,6 +3324,18 @@ packages: purls: [] size: 3526350 timestamp: 1769460339384 +- pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + name: transforms3d + version: 0.4.2 + sha256: 1c70399d9e9473ecc23311fd947f727f7c69ed0b063244828c383aa1aefa5941 + requires_dist: + - numpy>=1.15 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + name: typing-extensions + version: 4.15.0 + sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c md5: ad659d0a2b3e47e38d829aa8cad2d610 @@ -937,6 +3353,37 @@ packages: purls: [] size: 694692 timestamp: 1756385147981 +- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + name: urllib3 + version: 2.6.3 + sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/31/dd/1900452678d46f6a649ab8167bededb02500b0561fc9f69e1f52607895c7/uv-0.10.4-py3-none-win_amd64.whl + name: uv + version: 0.10.4 + sha256: 4a1c595cf692fa611019a7ad9bf4b0757fccd0a3f838ca05e53db82912ddaa39 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/63/91/c4ddf7e55e05394967615050cc364a999157a44c008d0e1e9db2ed49a11c/uv-0.10.4-py3-none-macosx_11_0_arm64.whl + name: uv + version: 0.10.4 + sha256: 751959135a62f006ef51f3fcc5d02ec67986defa0424d470cce0918eede36a55 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bd/4e/058976e2a5513f11954e09595a1821d5db1819e96e00bafded19c6a470e9/uv-0.10.4-py3-none-manylinux_2_28_aarch64.whl + name: uv + version: 0.10.4 + sha256: 8437e56a7d0f8ecd7421e8b84024dd8153179b8f1371ca1bd66b79fa7fb4c2c1 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f2/b3/4b9580d62e1245df52e8516cf3e404ff39cc72634d2d749d47b1dada4161/uv-0.10.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: uv + version: 0.10.4 + sha256: 82978155e571f2ac3dd57077bd746bfe41b65fa19accc3c92d1f09632cd36c63 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a md5: 1e610f2416b6acdd231c5f573d754a0f @@ -974,10 +3421,10 @@ packages: purls: [] size: 115235 timestamp: 1767320173250 -- pypi: https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/42/d7/394801755d4c8684b655d35c665aea7836ec68320304f62ab3c94395b442/virtualenv-20.38.0-py3-none-any.whl name: virtualenv - version: 20.37.0 - sha256: 5d3951c32d57232ae3569d4de4cc256c439e045135ebf43518131175d9be435d + version: 20.38.0 + sha256: d6e78e5889de3a4742df2d3d44e779366325a90cf356f15621fddace82431794 requires_dist: - distlib>=0.3.7,<1 - filelock>=3.24.2,<4 ; python_full_version >= '3.10' @@ -1011,6 +3458,42 @@ packages: - setuptools>=68 ; extra == 'test' - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda + sha256: d6cf2f0ebd5e09120c28ecba450556ce553752652d91795442f0e70f837126ae + md5: bdbd7385b4a67025ac2dba4ef8cb6a8f + depends: + - packaging >=24.0 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wheel?source=hash-mapping + size: 31858 + timestamp: 1769139207397 +- pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + name: zipp + version: 3.23.0 + sha256: 071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-itertools ; extra == 'test' + - jaraco-functools ; extra == 'test' + - more-itertools ; extra == 'test' + - big-o ; extra == 'test' + - pytest-ignore-flaky ; extra == 'test' + - jaraco-test ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 diff --git a/pixi.toml b/pixi.toml index 3edd70b..14f6c1f 100644 --- a/pixi.toml +++ b/pixi.toml @@ -4,17 +4,36 @@ version = "0.1.0" description = "Amazing Hand - Python examples and demos (Pollen Robotics)" channels = ["conda-forge"] # Ubuntu (x86_64), Windows (x64), Raspberry Pi (aarch64) -platforms = ["linux-64", "win-64", "linux-aarch64"] +platforms = ["linux-64", "win-64", "linux-aarch64", "osx-arm64"] [dependencies] python = "3.12.*" +pip = "*" +rust = "*" + +[target.linux-64.activation.env] +# Use system GCC for Rust linking; conda's toolchain can fail with __libc_csu_* symbols on Linux +CC = "/usr/bin/gcc" +CXX = "/usr/bin/g++" +CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "/usr/bin/gcc" [pypi-dependencies] numpy = ">=1.20" rustypot = "*" pytest = "*" pre-commit = "*" +# Demo/HandTracking, Demo/AHSimulation +opencv-python = ">=4.8.0" +mediapipe = ">=0.10.14,<=0.10.15" +dora-rs-cli = ">=0.3.11,<=0.3.13" +dora-rs = ">=0.3.11,<=0.3.13" +loop-rate-limiters = ">=1.1.2" +mink = ">=0.0.11" +mujoco = ">=3.3.2" +onshape-to-robot = ">=1.7.5" +qpsolvers = { version = ">=4.7.1", extras = ["quadprog"] } [tasks] # PYTHONPATH= avoids loading system/ROS pytest plugins when run locally -test = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" +test-python-example = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" +test-demo = "cd Demo && python -m pytest tests/ -v" From 7e9c6127a4e2c1a431fcb0c6f07fc07bf2749f89 Mon Sep 17 00:00:00 2001 From: juliaj Date: Fri, 20 Feb 2026 14:14:46 -0800 Subject: [PATCH 3/6] Refactor to consolidate configuration for both rust code and python code --- .github/workflows/ci.yml | 3 + Demo/AHControl/README.md | 19 +- Demo/AHControl/src/bin/get_zeros.rs | 43 +-- Demo/AHControl/src/bin/set_zeros.rs | 46 +-- Demo/AHControl/src/lib.rs | 344 ++++++++++++++++++ Demo/AHControl/src/main.rs | 67 +--- Demo/README.md | 16 +- Demo/dataflow_tracking_real_team_julia.yml | 9 +- PythonExample/README.md | 13 +- PythonExample/common.py | 140 ++++--- PythonExample/config.toml | 81 ----- .../tests/test_canonical_config_contract.py | 87 +++++ PythonExample/tests/test_common.py | 96 +++-- PythonExample/tests/test_config.py | 70 +++- config/calibration/l_hand_team_krishan.toml | 18 + config/calibration/r_hand_team_julia.toml | 18 + config/calibration/r_hand_team_krishan.toml | 18 + config/hand_geometry.toml | 4 + config/profiles.toml | 28 ++ docs/canonical_hand_config_design.md | 80 ++++ pixi.toml | 1 + 21 files changed, 860 insertions(+), 341 deletions(-) create mode 100644 Demo/AHControl/src/lib.rs delete mode 100644 PythonExample/config.toml create mode 100644 PythonExample/tests/test_canonical_config_contract.py create mode 100644 config/calibration/l_hand_team_krishan.toml create mode 100644 config/calibration/r_hand_team_julia.toml create mode 100644 config/calibration/r_hand_team_krishan.toml create mode 100644 config/hand_geometry.toml create mode 100644 config/profiles.toml create mode 100644 docs/canonical_hand_config_design.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9936fb..022422a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,6 @@ jobs: - name: Demo unit tests run: pixi run test-demo + + - name: AHControl (Rust) unit tests + run: pixi run test-ahcontrol diff --git a/Demo/AHControl/README.md b/Demo/AHControl/README.md index fcff035..dfea942 100644 --- a/Demo/AHControl/README.md +++ b/Demo/AHControl/README.md @@ -1,7 +1,6 @@ # Motor control node -The motor configuration is set in a TOML file (cf. [r_hand.toml](config/r_hand.toml)). -In this file you can set the motors ID, and angle offsets for each finger. +Use canonical calibration from the repo `config/calibration/` (recommended): e.g. `--config ../../config/calibration/r_hand_team_julia.toml` when running from Demo/AHControl. See [canonical_hand_config_design.md](../../docs/canonical_hand_config_design.md). Finger order is read from `config/hand_geometry.toml` when using a calibration file under `config/calibration/`. Alternatively, a legacy TOML under AHControl `config/` (e.g. [r_hand.toml](config/r_hand.toml)) is still supported for motor IDs and angle offsets. # Tools - *change_id*: to help you change the id of a motor. `cargo run --bin=change_id -- -h` for a list of parameters @@ -9,24 +8,19 @@ In this file you can set the motors ID, and angle offsets for each finger. - *get_zeros*: to help you set the motor zeros, it sets the motors in the compliant mode and write the TOML file to the console. `cargo run --bin=get_zeros -- -h` for a list of parameters - *set_zeros*: to move the hand in the "zero" position according to the config file. `cargo run --bin=set_zeros -- -h` for a list of parameters -# Calibration steps +# Calibration -1. Verify IDs: run `set_zeros` or `goto` per motor; if the wrong finger moves, the ID in config does not match the physical motor. Fix with `change_id`, or use [Feetech debug tool](https://github.com/Robot-Maker-SAS/FeetechServo/tree/main/feetech%20debug%20tool%20master/FD1.9.8.2) to scan the bus. - -- Expected result for `set_zeros`: the hand moves all fingers to their zero pose (from config offsets), holds ~1 second, then relaxes (torque off). If the wrong finger moves for a given config entry, the motor IDs are mismatched. - -2. Calibrate offsets: run `get_zeros`. Put motors in compliant mode, manually position each finger to its zero pose, press Enter. Copy the printed TOML into your config file (e.g. `Demo/AHControl/config/r_hand_julia.toml`). +Full procedure (verify IDs, create calibration file, get_zeros, set_zeros, run with profile) is in [docs/canonical_hand_config_design.md](../../docs/canonical_hand_config_design.md) under "Calibration procedures". # Commands -Run from the project root. If you use pixi for Rust, run `pixi shell` first so `cargo` is in PATH. Set `PORT` to your serial device (e.g. `/dev/ttyACM0` on Linux, `COM3` on Windows). Set `CONFIG_PREFIX` to your config name (e.g. `team_julia`, `team_krishan`); the config file is `Demo/AHControl/config/r_hand_${CONFIG_PREFIX}.toml`. Copy `r_hand.toml` to `r_hand_.toml` if needed. +Run from the project root. If you use pixi for Rust, run `pixi shell` first. Set `PORT` to your serial device (e.g. `/dev/ttyACM0` on Linux, `COM3` on Windows). For canonical calibration use a file under `config/calibration/` (e.g. `config/calibration/r_hand_team_julia.toml`). Linux: ```bash export PORT=/dev/ttyACM0 -export CONFIG_PREFIX="team_julia" -export CFG="Demo/AHControl/config/r_hand_${CONFIG_PREFIX}.toml" +export CFG="config/calibration/r_hand_team_julia.toml" cargo run --manifest-path Demo/Cargo.toml --bin=set_zeros -- --serialport $PORT --config $CFG cargo run --manifest-path Demo/Cargo.toml --bin=goto -- --serialport $PORT --id 1 --pos 0.0 cargo run --manifest-path Demo/Cargo.toml --bin=change_id -- --serialport $PORT --old-id 1 --new-id 2 @@ -37,8 +31,7 @@ Windows (PowerShell): ```powershell $PORT = "COM3" -$CONFIG_PREFIX = "team_krishan" -$CFG = "Demo/AHControl/config/r_hand_$CONFIG_PREFIX.toml" +$CFG = "config/calibration/r_hand_team_krishan.toml" cargo run --manifest-path Demo/Cargo.toml --bin=set_zeros -- --serialport $PORT --config $CFG cargo run --manifest-path Demo/Cargo.toml --bin=goto -- --serialport $PORT --id 1 --pos 0.0 cargo run --manifest-path Demo/Cargo.toml --bin=change_id -- --serialport $PORT --old-id 1 --new-id 2 diff --git a/Demo/AHControl/src/bin/get_zeros.rs b/Demo/AHControl/src/bin/get_zeros.rs index 1ed0f96..165aa67 100644 --- a/Demo/AHControl/src/bin/get_zeros.rs +++ b/Demo/AHControl/src/bin/get_zeros.rs @@ -1,42 +1,10 @@ use clap::Parser; - use eyre::{eyre, Result}; -use facet::Facet; use facet_pretty::FacetPretty; use rustypot::servo; use std::io; -use std::{error::Error, time::Duration}; - -use std::{fs, thread}; - -// use std::io::Read; -#[derive(Debug, Facet)] -struct Fingers { - #[allow(dead_code)] // Disable dead code warning for the entire struct - motors: Vec, -} - -#[derive(Debug, Facet)] -struct Motors { - #[allow(dead_code)] // Disable dead code warning for the entire struct - finger_name: String, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor1: Motor, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor2: Motor, -} - -#[derive(Debug, Facet)] -struct Motor { - #[allow(dead_code)] - id: u8, - #[allow(dead_code)] - offset: f64, - #[allow(dead_code)] - invert: bool, - #[allow(dead_code)] - model: String, -} +use std::path::Path; +use std::{error::Error, time::Duration, thread}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -58,10 +26,9 @@ fn main() -> Result<(), Box> { let baudrate: u32 = args.baudrate; let configfile: String = args.config; println!("Opening {:?}", configfile); - let toml_str = fs::read_to_string(configfile).expect("Failed to read config file"); - - let mut motors_conf: Fingers = - facet_toml::from_str(&toml_str).expect("Failed to deserialize config file"); + let mut motors_conf: AHControl::Fingers = + AHControl::load_fingers_from_path(Path::new(&configfile)) + .map_err(|e| eyre!("config load failed: {}", e))?; // println!("{}", motors_conf.pretty()); let serial_port = serialport::new(serialport, baudrate) diff --git a/Demo/AHControl/src/bin/set_zeros.rs b/Demo/AHControl/src/bin/set_zeros.rs index f373059..7bf8cae 100644 --- a/Demo/AHControl/src/bin/set_zeros.rs +++ b/Demo/AHControl/src/bin/set_zeros.rs @@ -1,42 +1,9 @@ use clap::Parser; - use eyre::{eyre, Result}; -use rustypot::servo; -use std::{error::Error, time::Duration}; - -use facet::Facet; use facet_pretty::FacetPretty; - -use std::{fs, thread}; - -// use std::io::Read; -#[derive(Debug, Facet)] -struct Fingers { - #[allow(dead_code)] // Disable dead code warning for the entire struct - motors: Vec, -} - -#[derive(Debug, Facet)] -struct Motors { - #[allow(dead_code)] // Disable dead code warning for the entire struct - finger_name: String, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor1: Motor, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor2: Motor, -} - -#[derive(Debug, Facet)] -struct Motor { - #[allow(dead_code)] - id: u8, - #[allow(dead_code)] - offset: f64, - #[allow(dead_code)] - invert: bool, - #[allow(dead_code)] - model: String, -} +use rustypot::servo; +use std::path::Path; +use std::{error::Error, time::Duration, thread}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -58,10 +25,9 @@ fn main() -> Result<(), Box> { let baudrate: u32 = args.baudrate; let configfile: String = args.config; println!("Opening {:?}", configfile); - let toml_str = fs::read_to_string(configfile).expect("Failed to read config file"); - - let motors_conf: Fingers = - facet_toml::from_str(&toml_str).expect("Failed to deserialize config file"); + let motors_conf: AHControl::Fingers = + AHControl::load_fingers_from_path(Path::new(&configfile)) + .map_err(|e| eyre!("config load failed: {}", e))?; println!("{}", motors_conf.pretty()); let serial_port = serialport::new(serialport, baudrate) diff --git a/Demo/AHControl/src/lib.rs b/Demo/AHControl/src/lib.rs new file mode 100644 index 0000000..def6610 --- /dev/null +++ b/Demo/AHControl/src/lib.rs @@ -0,0 +1,344 @@ +// Copyright (C) Julia Jia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(dead_code)] // Facet-derived struct fields are used by (de)serialization, not always in this crate + +use facet::Facet; +use std::fs; +use std::path::{Path, PathBuf}; + +const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0; + +/// Fallback finger order when geometry file is missing or not used. +const DEFAULT_FINGER_ORDER: &[&str] = &["index", "middle", "ring", "thumb"]; + +fn default_finger_order() -> Vec { + DEFAULT_FINGER_ORDER.iter().map(ToString::to_string).collect() +} + +#[derive(Debug, Clone, Facet)] +pub struct Fingers { + pub motors: Vec, +} + +#[derive(Debug, Clone, Facet)] +pub struct Motors { + pub finger_name: String, + pub motor1: Motor, + pub motor2: Motor, +} + +#[derive(Debug, Clone, Facet)] +pub struct Motor { + pub id: u8, + pub offset: f64, + pub invert: bool, + pub model: String, +} + +/// Load finger order from hand_geometry.toml (key "fingers" = array of strings). +fn load_finger_order_from_path(geometry_path: &Path) -> Result, String> { + let s = fs::read_to_string(geometry_path).map_err(|e| e.to_string())?; + load_finger_order_from_str(&s) +} + +fn load_finger_order_from_str(geometry_toml: &str) -> Result, String> { + let value: toml::Value = toml::from_str(geometry_toml).map_err(|e| e.to_string())?; + let root = value.as_table().ok_or("hand_geometry: expected top-level table")?; + let arr = root + .get("fingers") + .and_then(|v| v.as_array()) + .ok_or("hand_geometry: missing key 'fingers'")?; + let order: Vec = arr + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + if order.is_empty() { + return Err("hand_geometry: 'fingers' must be non-empty".into()); + } + Ok(order) +} + +/// Maps anatomical name (index, middle, ring, thumb) to legacy finger_name (r_finger1..4 or l_finger1..4). +/// Legacy names are used by: main.rs (metadata.parameters key), get_zeros.rs, set_zeros.rs, +/// AHSimulation/mj_mink_right.py, mj_mink_left.py (metadata keys), and legacy TOML configs (r_hand.toml, 2hands.toml). +/// TODO: refactor consumers to use anatomical names and remove this mapping. +fn anatomical_to_legacy_name(anatomical: &str, right_hand: bool) -> String { + let prefix = if right_hand { "r_finger" } else { "l_finger" }; + match anatomical { + "index" => format!("{}1", prefix), + "middle" => format!("{}2", prefix), + "ring" => format!("{}3", prefix), + "thumb" => format!("{}4", prefix), + _ => format!("{}_{}", prefix, anatomical), + } +} + +fn parse_finger_section( + root: &toml::map::Map, + name: &str, + right_hand: bool, +) -> Result { + let section = root + .get(name) + .and_then(|v| v.as_table()) + .ok_or_else(|| format!("canonical calibration missing section [{}]", name))?; + let ids: Vec = section + .get("ids") + .and_then(|v| v.as_array()) + .ok_or_else(|| format!("[{}] missing ids", name))? + .iter() + .filter_map(|v| v.as_integer()) + .collect(); + let rest_deg: Vec = section + .get("rest_deg") + .and_then(|v| v.as_array()) + .ok_or_else(|| format!("[{}] missing rest_deg", name))? + .iter() + .map(|v| v.as_float().unwrap_or_else(|| v.as_integer().unwrap_or(0) as f64)) + .collect(); + if ids.len() < 2 || rest_deg.len() < 2 { + return Err(format!("[{}] needs ids and rest_deg with at least 2 elements", name)); + } + let motor = |id: i64, rest: f64| Motor { + id: id as u8, + offset: rest * DEG_TO_RAD, + invert: false, + model: "SCS0009".to_string(), + }; + Ok(Motors { + finger_name: anatomical_to_legacy_name(name, right_hand), + motor1: motor(ids[0], rest_deg[0]), + motor2: motor(ids[1], rest_deg[1]), + }) +} + +/// Build Fingers from canonical calibration TOML (tables per finger with ids and rest_deg). +/// right_hand: true for r_finger1..4, false for l_finger1..4. +fn parse_canonical_calibration( + toml_str: &str, + finger_order: &[String], + right_hand: bool, +) -> Result { + let value: toml::Value = toml::from_str(toml_str).map_err(|e| e.to_string())?; + let root = value.as_table().ok_or("expected top-level table")?; + let motors: Vec = finger_order + .iter() + .map(|name| parse_finger_section(root, name, right_hand)) + .collect::, _>>()?; + Ok(Fingers { motors }) +} + +/// Detect if the TOML is canonical format (has [index] with ids). +fn is_canonical_format(toml_str: &str) -> bool { + let value: toml::Value = match toml::from_str(toml_str) { + Ok(v) => v, + Err(_) => return false, + }; + let root = match value.as_table() { + Some(t) => t, + None => return false, + }; + root.get("index") + .and_then(|v| v.as_table()) + .and_then(|t| t.get("ids")) + .is_some() +} + +/// Resolve hand_geometry.toml path from a calibration file path. +/// E.g. config/calibration/r_hand.toml -> config/hand_geometry.toml; config/r_hand.toml -> config/hand_geometry.toml. +pub fn geometry_path_for_calibration(cal_path: &Path) -> PathBuf { + let parent = cal_path.parent().unwrap_or_else(|| Path::new(".")); + let config_dir = if parent.file_name().map(|n| n == "calibration").unwrap_or(false) { + parent.parent().unwrap_or(parent) + } else { + parent + }; + config_dir.join("hand_geometry.toml") +} + +/// Infer right hand from calibration path (e.g. l_hand_*.toml -> false, r_hand_*.toml or other -> true). +fn right_hand_from_path(path: &Path) -> bool { + path.file_name() + .and_then(|n| n.to_str()) + .map(|s| !s.starts_with("l_hand")) + .unwrap_or(true) +} + +fn finger_order_from_geometry_path(geometry_path: &Path) -> Vec { + load_finger_order_from_path(geometry_path).unwrap_or_else(|_| default_finger_order()) +} + +/// Load Fingers from a config file. Supports legacy (facet) and canonical calibration TOML. +/// For canonical format, finger order is read from hand_geometry.toml next to the config dir (derived from calibration path). +pub fn load_fingers_from_path(path: &Path) -> Result { + let toml_str = fs::read_to_string(path).map_err(|e| e.to_string())?; + if is_canonical_format(&toml_str) { + let geometry_path = geometry_path_for_calibration(path); + let order = finger_order_from_geometry_path(&geometry_path); + let right_hand = right_hand_from_path(path); + parse_canonical_calibration(&toml_str, &order, right_hand) + } else { + load_fingers_from_str(&toml_str, None) + } +} + +/// Load Fingers from TOML string. Supports legacy (facet) and canonical calibration format. +/// For canonical format, pass geometry_path to load finger order from hand_geometry.toml; if None, uses DEFAULT_FINGER_ORDER. +/// When loading from string, hand side is unknown so right_hand is assumed (r_finger1..4). +pub fn load_fingers_from_str(toml_str: &str, geometry_path: Option<&Path>) -> Result { + if is_canonical_format(toml_str) { + let order = geometry_path + .map(finger_order_from_geometry_path) + .unwrap_or_else(default_finger_order); + parse_canonical_calibration(toml_str, &order, true) + } else { + facet_toml::from_str(toml_str).map_err(|e| e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CANONICAL_SAMPLE: &str = r#" +[index] +ids = [1, 2] +rest_deg = [-2, 0] + +[middle] +ids = [3, 4] +rest_deg = [1, 2] + +[ring] +ids = [6, 5] +rest_deg = [-3, 8] + +[thumb] +ids = [8, 7] +rest_deg = [8, -8] +"#; + + const LEGACY_SAMPLE: &str = r#" +[Fingers] +[[motors]] +finger_name = "r_finger1" +motor1.id = 1 +motor1.offset = 0.12 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 2 +motor2.offset = 0.08 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name = "r_finger2" +motor1.id = 3 +motor1.offset = 0.0 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 4 +motor2.offset = 0.12 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name = "r_finger3" +motor1.id = 5 +motor1.offset = 0.08 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 6 +motor2.offset = 0.12 +motor2.invert = false +motor2.model = "SCS0009" +[[motors]] +finger_name = "r_finger4" +motor1.id = 7 +motor1.offset = 0.0 +motor1.invert = false +motor1.model = "SCS0009" +motor2.id = 8 +motor2.offset = 0.12 +motor2.invert = false +motor2.model = "SCS0009" +"#; + + #[test] + fn test_is_canonical_format_detects_canonical() { + assert!(is_canonical_format(CANONICAL_SAMPLE)); + } + + #[test] + fn test_is_canonical_format_rejects_legacy() { + assert!(!is_canonical_format(LEGACY_SAMPLE)); + } + + fn parse_canonical_sample(right_hand: bool) -> Fingers { + let order = default_finger_order(); + parse_canonical_calibration(CANONICAL_SAMPLE, &order, right_hand).unwrap() + } + + #[test] + fn test_parse_canonical_produces_four_fingers() { + let f = parse_canonical_sample(true); + assert_eq!(f.motors.len(), 4); + } + + #[test] + fn test_parse_canonical_finger_names_and_ids() { + let f = parse_canonical_sample(true); + assert_eq!(f.motors[0].finger_name, "r_finger1"); + assert_eq!(f.motors[0].motor1.id, 1); + assert_eq!(f.motors[0].motor2.id, 2); + assert_eq!(f.motors[3].finger_name, "r_finger4"); + assert_eq!(f.motors[3].motor1.id, 8); + assert_eq!(f.motors[3].motor2.id, 7); + } + + #[test] + fn test_parse_canonical_rest_deg_to_radians() { + let f = parse_canonical_sample(true); + let eps = 1e-9; + assert!((f.motors[0].motor1.offset - (-2.0_f64).to_radians()).abs() < eps); + assert!((f.motors[0].motor2.offset - 0.0_f64).abs() < eps); + assert!((f.motors[0].motor1.offset - (-2.0 * std::f64::consts::PI / 180.0)).abs() < eps); + } + + #[test] + fn test_load_fingers_from_str_canonical() { + let f = load_fingers_from_str(CANONICAL_SAMPLE, None).unwrap(); + assert_eq!(f.motors.len(), 4); + assert_eq!(f.motors[0].motor1.id, 1); + } + + #[test] + fn test_load_fingers_from_str_legacy() { + let f = load_fingers_from_str(LEGACY_SAMPLE, None).unwrap(); + assert_eq!(f.motors.len(), 4); + assert_eq!(f.motors[0].finger_name, "r_finger1"); + assert_eq!(f.motors[0].motor1.id, 1); + } + + #[test] + fn test_parse_canonical_missing_section_fails() { + let bad = r#"[index] +ids = [1, 2] +rest_deg = [-2, 0] +"#; + let order = default_finger_order(); + let r = parse_canonical_calibration(bad, &order, true); + assert!(r.is_err()); + assert!(r.unwrap_err().contains("middle")); + } +} diff --git a/Demo/AHControl/src/main.rs b/Demo/AHControl/src/main.rs index bf0130b..a0bd023 100644 --- a/Demo/AHControl/src/main.rs +++ b/Demo/AHControl/src/main.rs @@ -1,50 +1,12 @@ use clap::Parser; -// use dora_node_api::{dora_core::config::NodeId, DoraNode, Event}; - -// use dora_node_api::IntoArrow; use dora_node_api::{self, arrow::array::Array, DoraNode, Event, Parameter}; use eyre::{eyre, Result}; use rustypot::servo; -use std::{error::Error, time::Duration}; +use std::path::Path; +use std::{error::Error, time::Duration, thread}; -use facet::Facet; +use AHControl::Fingers; use facet_pretty::FacetPretty; -// use std::{collections::HashMap, path::PathBuf, sync::Arc}; -// use std::error::Error; -// use arrow_convert::{ -// deserialize::TryIntoCollection, serialize::TryIntoArrow, ArrowDeserialize, ArrowField, -// ArrowSerialize, -// }; -use std::{fs, thread}; - -// use std::io::Read; -#[derive(Debug, Facet)] -struct Fingers { - #[allow(dead_code)] // Disable dead code warning for the entire struct - motors: Vec, -} - -#[derive(Debug, Facet)] -struct Motors { - #[allow(dead_code)] // Disable dead code warning for the entire struct - finger_name: String, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor1: Motor, - #[allow(dead_code)] // Disable dead code warning for the entire struct - motor2: Motor, -} - -#[derive(Debug, Facet)] -struct Motor { - #[allow(dead_code)] - id: u8, - #[allow(dead_code)] - offset: f64, - #[allow(dead_code)] - invert: bool, - #[allow(dead_code)] - model: String, -} #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -66,10 +28,8 @@ fn main() -> Result<(), Box> { let baudrate: u32 = args.baudrate; let configfile: String = args.config; println!("Opening {:?}", configfile); - let toml_str = fs::read_to_string(configfile).expect("Failed to read config file"); - - let motors_conf: Fingers = - facet_toml::from_str(&toml_str).expect("Failed to deserialize config file"); + let motors_conf: Fingers = AHControl::load_fingers_from_path(Path::new(&configfile)) + .map_err(|e| eyre!("config load failed: {}", e))?; println!("{}", motors_conf.pretty()); let serial_port = serialport::new(serialport, baudrate) @@ -133,13 +93,6 @@ fn main() -> Result<(), Box> { buffer[finger1_idx[0] as usize], buffer[finger1_idx[1] as usize] ); - // controller.sync_write_goal_position( - // &[finger.motor1.id, finger.motor2.id], - // &[ - // buffer[finger1_idx[0] as usize] + finger.motor1.offset, - // buffer[finger1_idx[1] as usize] + finger.motor2.offset, - // ], - // )?; motors_ids.push(finger.motor1.id); motors_ids.push(finger.motor2.id); @@ -155,16 +108,6 @@ fn main() -> Result<(), Box> { } motors_goalpos.push(m2goal); - // controller.write_goal_position( - // finger.motor1.id, - // buffer[finger1_idx[0] as usize] + finger.motor1.offset, - // )?; - // thread::sleep(Duration::from_millis(10)); - // controller.write_goal_position( - // finger.motor2.id, - // buffer[finger1_idx[1] as usize] + finger.motor2.offset, - // )?; - // thread::sleep(Duration::from_millis(10)); } } controller.sync_write_goal_position(&motors_ids, &motors_goalpos)?; diff --git a/Demo/README.md b/Demo/README.md index 63d470d..81426e7 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -18,14 +18,21 @@ pixi run dora build Demo/dataflow_tracking_simu.yml # once pixi run dora run Demo/dataflow_tracking_simu.yml ``` -Webcam hand tracking (real hardware): +Webcam hand tracking (real hardware, new config): ```bash -pixi run dora build Demo/dataflow_tracking_real.yml # once +pixi run dora build Demo/dataflow_tracking_real_team_julia.yml # once pixi run dora run Demo/dataflow_tracking_real_team_julia.yml ``` -The config file is set in `Demo/dataflow_tracking_real.yml` (hand_controller `args`: change `--config AHControl/config/...` and `--serialport` as needed). +Same demo using previous config + +```bash +pixi run dora build Demo/dataflow_tracking_real.yml # once +pixi run dora run Demo/dataflow_tracking_real.yml +``` + +The hand config is set in the dataflow YAML (hand_controller `args`): change `--config` and `--serialport` as needed. You can use a legacy file under `AHControl/config/r_hand*.toml` or the repo canonical calibration (e.g. `--config config/calibration/r_hand_team_julia.toml` when running from repo root). See [AHControl](AHControl/README.md) and [canonical_hand_config_design.md](docs/canonical_hand_config_design.md) for details. Linux: add your user to the `dialout` group for serial port access: `sudo usermod -a -G dialout $USER` (log out and back in). If your hand is on a different port (e.g. `/dev/ttyUSB0`), edit `Demo/dataflow_tracking_real.yml` and change the `--serialport` arg. @@ -63,8 +70,7 @@ The dataflow `build` steps install HandTracking and AHSimulation in editable mod ![Fingers naming](docs/r_hand.png "Fingers naming for each hand") -Be sure to adapt the configuration file [r_hand.toml](AHControl/config/r_hand.toml) for your particular hand. -You can use the software tools located in [AHControl](AHControl). +Adapt the hand configuration for your setup: either the legacy [r_hand.toml](AHControl/config/r_hand.toml) (and copies like `r_hand_team_julia.toml`) under AHControl/config, or the repo canonical calibration files under `config/calibration/` (see [canonical_hand_config_design.md](../docs/canonical_hand_config_design.md)). Use the tools in [AHControl](AHControl) to calibrate. ## Details diff --git a/Demo/dataflow_tracking_real_team_julia.yml b/Demo/dataflow_tracking_real_team_julia.yml index f61bd0b..ab381dc 100644 --- a/Demo/dataflow_tracking_real_team_julia.yml +++ b/Demo/dataflow_tracking_real_team_julia.yml @@ -1,5 +1,6 @@ -# Note: Using 'python -m pip' instead of 'pip' to ensure pixi environment's pip is used. -# Direct 'pip' command may resolve to system pip which is externally-managed (PEP 668). +# Uses canonical calibration (config/calibration/). Dora spawns nodes with cwd = Demo/, +# so path is relative to Demo: ../config/calibration/. +# See docs/canonical_hand_config_design.md. nodes: - id: hand_tracker build: python -m pip install -e HandTracking @@ -21,7 +22,7 @@ nodes: - id: hand_controller build: cargo build -p AHControl - path: target/debug/AHControl #--serialport /dev/ttyACM0 --config AHControl/config/r_hand.toml - args: --serialport /dev/ttyACM0 --config AHControl/config/r_hand_team_julia.toml + path: target/debug/AHControl + args: --serialport /dev/ttyACM0 --config ../config/calibration/r_hand_team_julia.toml inputs: mj_r_joints_pos: r_hand_simulation/mj_r_joints_pos diff --git a/PythonExample/README.md b/PythonExample/README.md index cc88ced..777f9ac 100644 --- a/PythonExample/README.md +++ b/PythonExample/README.md @@ -20,36 +20,35 @@ From the repository root (where `pixi.toml` lives): ```bash pixi install # one time setup -# Optional: export AMAZINGHAND_TEAM=julia +# Optional: export AMAZINGHAND_PROFILE=team_julia pixi run python PythonExample/AmazingHand_FingerTest.py ``` To run another script, replace the filename, e.g. `PythonExample/AmazingHand_Hand_FingerMiddlePos.py` or `PythonExample/AmazingHand_Demo.py`. -Set `AMAZINGHAND_TEAM` to choose config (e.g. `export AMAZINGHAND_TEAM=krishan`). Edit `PythonExample/config.toml` for serial port and finger settings. - -The hand must be connected via USB and the serial port in `config.toml` must match your system (e.g. `COM3` on Windows, `/dev/ttyUSB0` or `/dev/ttyACM0` on Linux). Leave `port = ""` to use the default for your OS. If the port is wrong or the device is unplugged, the script will fail with "No such file or directory". +Set `AMAZINGHAND_PROFILE` to choose a profile (e.g. `team_julia`, `team_krishan`). Profiles and hand calibration live in `config/profiles.toml` and `config/calibration/`; see [canonical_hand_config_design.md](../docs/canonical_hand_config_design.md). The hand must be connected via USB; the profile's `port` must match your system (e.g. `COM3` on Windows, `/dev/ttyACM0` on Linux). If the port is wrong or the device is unplugged, the script will fail with "No such file or directory". ## Run Python Examples ### Hand Demo -Runs a loop of gestures (open/close, spread, point, victory, etc.) on one hand. Which hand is controlled by config (`hand_test_id` or `side` in `config.toml`) or by `AMAZINGHAND_TEAM`. Override from the command line with `--side`: +Runs a loop of gestures (open/close, spread, point, victory, etc.) on one hand. Which hand is controlled by the profile's `hand_test_id` or `side`, or override with `--side`: ```bash +# export AMAZINGHAND_PROFILE=team_julia pixi run python PythonExample/AmazingHand_Demo.py # Right hand (1) or left hand (2): pixi run python PythonExample/AmazingHand_Demo.py --side 1 ``` -Ensure the chosen hand's servo IDs and middle positions are set in `config.toml` for your team (see `hand_1_*` / `hand_2_*`). +Set `AMAZINGHAND_PROFILE` (e.g. `team_julia`); the profile loads servo IDs and rest/middle positions from `config/calibration/`. See [canonical_hand_config_design.md](../docs/canonical_hand_config_design.md). ## Run Unit Tests From the repository root: ```bash -pixi run test +pixi run test-demo ``` ## Pre-commit (optional) diff --git a/PythonExample/common.py b/PythonExample/common.py index a7d2f5c..8a0f9ef 100644 --- a/PythonExample/common.py +++ b/PythonExample/common.py @@ -26,10 +26,10 @@ except ImportError: tomllib = None -_CONFIG_PATH = Path(__file__).resolve().parent / "config.toml" -_TEAM_ENV = "AMAZINGHAND_TEAM" -# Section names in config.toml are [team_], e.g. [team_julia], [team_krishan]. -_CONFIG_TEAM_SECTION_PREFIX = "team_" +_REPO_ROOT = Path(__file__).resolve().parent.parent +_CANONICAL_CONFIG_ROOT = _REPO_ROOT / "config" +_PROFILE_ENV = "AMAZINGHAND_PROFILE" +_FINGER_ORDER = ("index", "middle", "ring", "thumb") _DEFAULTS = { "port": "", @@ -45,37 +45,18 @@ def default_serial_port(): return "/dev/ttyUSB0" -def get_team(): - """Return current team name (e.g. 'julia', 'krishan'). From AMAZINGHAND_TEAM env; default 'julia'. - Accepts either the short name or the full section name; strips config section prefix so it matches [team_] in config.toml.""" - raw = (os.environ.get(_TEAM_ENV) or "julia").strip().lower() - return raw.removeprefix(_CONFIG_TEAM_SECTION_PREFIX) +def load_config(profile=None, config_root=None): + """Load canonical config (profiles + calibration). Returns dict with port, baudrate, timeout, hand_* keys. + profile: from AMAZINGHAND_PROFILE env or default 'team_julia'. config_root: repo config/ dir.""" + return load_config_canonical( + profile=profile or os.environ.get(_PROFILE_ENV), + config_root=config_root or _CANONICAL_CONFIG_ROOT, + ) -def load_config(team=None, path=None): - """Load section [team_] from config.toml. Returns dict with port, baudrate, timeout and all other section keys.""" - p = path or _CONFIG_PATH - if not p.exists() or tomllib is None: - return _DEFAULTS.copy() - with open(p, "rb") as f: - data = tomllib.load(f) - name = (team or get_team()).strip().lower() - section_key = f"{_CONFIG_TEAM_SECTION_PREFIX}{name}" - section = data.get(section_key, {}) - out = { - "port": (section.get("port") or _DEFAULTS["port"]) or "", - "baudrate": section.get("baudrate", _DEFAULTS["baudrate"]), - "timeout": section.get("timeout", _DEFAULTS["timeout"]), - } - for k, v in section.items(): - if k not in out: - out[k] = v - return out - - -def create_controller(team=None, serial_port=None, baudrate=None, timeout=None): - """Create Scs0009PyController. Section from AMAZINGHAND_TEAM or team=; explicit args override.""" - cfg = load_config(team=team) +def create_controller(profile=None, serial_port=None, baudrate=None, timeout=None): + """Create Scs0009PyController from canonical config. Set AMAZINGHAND_PROFILE or pass profile=.""" + cfg = load_config(profile=profile) port = serial_port if serial_port is not None else (cfg["port"] or default_serial_port()) br = baudrate if baudrate is not None else cfg["baudrate"] to = timeout if timeout is not None else cfg["timeout"] @@ -113,15 +94,92 @@ def get_demo_hand_config(cfg, side, config_path=None): if all(s == 0 for s in servo_ids): prefix = "hand_1" if side == 1 else "hand_2" hand_name = "right" if side == 1 else "left" - keys = ", ".join( - f"{prefix}_{name}_{suffix}" - for name in ("index", "middle", "ring", "thumb") - for suffix in ("servo_ids", "middle_pos") - ) - path_hint = f" Edit {config_path or _CONFIG_PATH} in your team section." + path_hint = " Set AMAZINGHAND_PROFILE and ensure that profile references a calibration file in config/calibration/." raise ValueError( f"{prefix} ({hand_name} hand) is not configured. " - f"Set: {keys}.{path_hint} " - f"Or run with --side {3 - side} to use the other hand." + f"{path_hint} Or run with --side {3 - side} to use the other hand." ) return {"servo_ids": servo_ids, "middle_pos": middle_pos, "side": side} + + +# Canonical config: load from config/hand_geometry.toml, config/profiles.toml + config/calibration/*.toml. + + +def _load_hand_geometry(config_root): + """Load shared finger order from config/hand_geometry.toml. Returns tuple of finger names or None if missing.""" + if tomllib is None: + return None + path = config_root / "hand_geometry.toml" + if not path.exists(): + return None + with open(path, "rb") as f: + data = tomllib.load(f) + fingers = data.get("fingers") + if isinstance(fingers, list) and len(fingers) >= 4: + return tuple(str(x) for x in fingers[:4]) + return None + + +def _load_canonical_calibration(calibration_name, config_root): + """Load one calibration file. Returns dict with keys [finger].ids and [finger].rest_deg for each finger.""" + if not calibration_name: + return None + path = config_root / "calibration" / f"{calibration_name}.toml" + if not path.exists() or tomllib is None: + return None + with open(path, "rb") as f: + return tomllib.load(f) + + +def _calibration_to_hand_flat(cal, hand_prefix, finger_order=None): + """Turn calibration dict into hand_1_* or hand_2_* flat keys. finger_order from hand_geometry.toml or default.""" + order = finger_order or _FINGER_ORDER + out = {} + for name in order: + section = cal.get(name, {}) + ids = section.get("ids", [0, 0])[:2] + rest = section.get("rest_deg", [0, 0])[:2] + out[f"{hand_prefix}_{name}_servo_ids"] = ids if len(ids) == 2 else [0, 0] + out[f"{hand_prefix}_{name}_middle_pos"] = rest if len(rest) == 2 else [0, 0] + return out + + +def load_config_canonical(profile=None, config_root=None): + """Load canonical config (profiles + calibration) and return dict in same shape as load_config(). + Use this when AMAZINGHAND_PROFILE is set or when migrating to single source of truth. + profile: name of profile (e.g. 'team_julia', 'team_krishan'); default from AMAZINGHAND_PROFILE or 'team_julia'. + config_root: path to repo config/ directory; default _CANONICAL_CONFIG_ROOT.""" + root = config_root or _CANONICAL_CONFIG_ROOT + profiles_path = root / "profiles.toml" + if not profiles_path.exists() or tomllib is None: + return _DEFAULTS.copy() + with open(profiles_path, "rb") as f: + data = tomllib.load(f) + name = (profile or os.environ.get(_PROFILE_ENV) or "team_julia").strip().lower() + section = data.get("profile", {}).get(name, {}) + if not section: + return _DEFAULTS.copy() + out = { + "port": (section.get("port") or _DEFAULTS["port"]) or "", + "baudrate": section.get("baudrate", _DEFAULTS["baudrate"]), + "timeout": section.get("timeout", _DEFAULTS["timeout"]), + } + for k, v in section.items(): + if k not in out: + out[k] = v + finger_order = _load_hand_geometry(root) or _FINGER_ORDER + right_cal = _load_canonical_calibration(section.get("right_hand_calibration", ""), root) + if right_cal: + out.update(_calibration_to_hand_flat(right_cal, "hand_1", finger_order)) + else: + for name in finger_order: + out[f"hand_1_{name}_servo_ids"] = [0, 0] + out[f"hand_1_{name}_middle_pos"] = [0, 0] + left_cal = _load_canonical_calibration(section.get("left_hand_calibration", ""), root) + if left_cal: + out.update(_calibration_to_hand_flat(left_cal, "hand_2", finger_order)) + else: + for name in finger_order: + out[f"hand_2_{name}_servo_ids"] = [0, 0] + out[f"hand_2_{name}_middle_pos"] = [0, 0] + return out diff --git a/PythonExample/config.toml b/PythonExample/config.toml deleted file mode 100644 index 19718db..0000000 --- a/PythonExample/config.toml +++ /dev/null @@ -1,81 +0,0 @@ -# Per-team serial settings. Set AMAZINGHAND_TEAM=julia or AMAZINGHAND_TEAM=krishan to choose. -# Leave port empty to use OS default (COM3 on Windows, /dev/ttyUSB0 on Linux). -# Finger config: each finger has servo_ids (2) and middle_pos (2). hand_1 = right, hand_2 = left. -# Demo uses one hand (side 1 = hand_1, side 2 = hand_2). Demo_Both uses both. -# Side: 1 = right hand, 2 = left hand. Speed: max_speed (open), close_speed (close). - -# Finger test: two servos for calibration / FingerTest / Hand_FingerMiddlePos scripts. -# finger_test_servo_ids = [id1, id2], finger_test_middle_pos = [deg1, deg2]. -# Hand demo: hand_test_id = 1 or 2 selects which hand to use (overrides side when set). - -[team_julia] -port = "/dev/ttyACM0" -baudrate = 1000000 -timeout = 0.5 -side = 1 -max_speed = 7 -close_speed = 3 - -# testing finger calibration -finger_test_servo_ids = [6, 5] -finger_test_middle_pos = [-3, 8] - -# hand demo testing: hand_test_id selects which hand (overrides side when set). -# hand_test_id 1 => hand_1 (right), hand_test_id 2 => hand_2 (left). -hand_test_id = 1 - -# hand_1 (right) -hand_1_index_servo_ids = [1, 2] -hand_1_index_middle_pos = [-2, 0] -hand_1_middle_servo_ids = [3, 4] -hand_1_middle_middle_pos = [1,2] -hand_1_ring_servo_ids = [6,5] -hand_1_ring_middle_pos = [-3,8] -hand_1_thumb_servo_ids = [8,7] -hand_1_thumb_middle_pos = [8,-8] - -# hand_2 (left) – required for Demo (side=2) and Demo_Both -hand_2_index_servo_ids = [0, 0] -hand_2_index_middle_pos = [0, 0] -hand_2_middle_servo_ids = [0, 0] -hand_2_middle_middle_pos = [2, 5] -hand_2_ring_servo_ids = [0, 0] -hand_2_ring_middle_pos = [-2, -8] -hand_2_thumb_servo_ids = [0, 0] -hand_2_thumb_middle_pos = [0, -15] - -[team_krishan] -port = "COM3" -baudrate = 1000000 -timeout = 2.5 -side = 2 -max_speed = 7 -close_speed = 3 - -# testing finger calibration -finger_test_servo_ids = [13, 14] -finger_test_middle_pos = [5, -2] - -# hand demo testing: hand_test_id selects which hand (overrides side when set). -# hand_test_id 1 => hand_1 (right), hand_test_id 2 => hand_2 (left). -hand_test_id = 2 - -# hand_1 (right) -hand_1_index_servo_ids = [1, 2] -hand_1_index_middle_pos = [3, 0] -hand_1_middle_servo_ids = [3, 4] -hand_1_middle_middle_pos = [-8, -13] -hand_1_ring_servo_ids = [5, 6] -hand_1_ring_middle_pos = [2, -5] -hand_1_thumb_servo_ids = [7, 8] -hand_1_thumb_middle_pos = [-12, -5] - -# hand_2 (left) – required for Demo (side=2) and Demo_Both -hand_2_index_servo_ids = [11, 12] -hand_2_index_middle_pos = [3, -3] -hand_2_middle_servo_ids = [13, 14] -hand_2_middle_middle_pos = [-1, -10] -hand_2_ring_servo_ids = [15, 16] -hand_2_ring_middle_pos = [5, 2] -hand_2_thumb_servo_ids = [17, 18] -hand_2_thumb_middle_pos = [-7, 3] diff --git a/PythonExample/tests/test_canonical_config_contract.py b/PythonExample/tests/test_canonical_config_contract.py new file mode 100644 index 0000000..bbe031a --- /dev/null +++ b/PythonExample/tests/test_canonical_config_contract.py @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Regression tests for canonical hand config design. +PythonExample loads hand config from config/profiles.toml and config/calibration/ only. +""" + +import sys +import unittest +from pathlib import Path + +_EXAMPLE_ROOT = Path(__file__).resolve().parent.parent +if str(_EXAMPLE_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLE_ROOT)) + + +class TestAdapterOutputContract(unittest.TestCase): + """Contract for adapter output: any config passed to get_demo_hand_config must have these keys and produce 8 ids, 8 middle_pos.""" + + def test_demo_hand_config_expected_keys(self): + """Document the keys required so an adapter can emit valid config for get_demo_hand_config.""" + from unittest.mock import MagicMock + sys.modules["rustypot"] = MagicMock() + sys.modules["rustypot"].Scs0009PyController = MagicMock() + import common + required_keys = [] + for name in ("index", "middle", "ring", "thumb"): + required_keys.append(f"hand_1_{name}_servo_ids") + required_keys.append(f"hand_1_{name}_middle_pos") + cfg = {k: [1, 2] if "servo_ids" in k else [0, 0] for k in required_keys} + cfg["hand_1_index_servo_ids"] = [10, 11] + cfg["hand_1_index_middle_pos"] = [-1, 1] + out = common.get_demo_hand_config(cfg, 1) + self.assertEqual(len(out["servo_ids"]), 8) + self.assertEqual(len(out["middle_pos"]), 8) + self.assertEqual(out["servo_ids"][0], 10) + self.assertEqual(out["servo_ids"][1], 11) + self.assertEqual(out["middle_pos"][0], -1) + self.assertEqual(out["middle_pos"][1], 1) + + +class TestLoadConfigCanonical(unittest.TestCase): + """Canonical adapter must emit same shape as load_config() for profile.team_julia.""" + + def setUp(self): + from unittest.mock import MagicMock + if "rustypot" not in sys.modules: + sys.modules["rustypot"] = MagicMock() + sys.modules["rustypot"].Scs0009PyController = MagicMock() + import common as _common + self.common = _common + + def test_canonical_profile_julia_emits_hand_1_like_legacy(self): + """load_config_canonical(profile='team_julia') must yield hand_1 servo_ids and middle_pos from calibration.""" + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("canonical config/ not found") + cfg = self.common.load_config_canonical(profile="team_julia", config_root=config_root) + out = self.common.get_demo_hand_config(cfg, 1) + self.assertEqual(out["servo_ids"], [1, 2, 3, 4, 6, 5, 8, 7]) + self.assertEqual(out["middle_pos"], [-2, 0, 1, 2, -3, 8, 8, -8]) + + def test_canonical_profile_team_krishan_emits_hand_2(self): + """load_config_canonical(profile='team_krishan') must yield hand_2 from left calibration.""" + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("canonical config/ not found") + cfg = self.common.load_config_canonical(profile="team_krishan", config_root=config_root) + out = self.common.get_demo_hand_config(cfg, 2) + self.assertEqual(out["servo_ids"], [11, 12, 13, 14, 15, 16, 17, 18]) + self.assertEqual(out["middle_pos"], [3, -3, -1, -10, 5, 2, -7, 3]) + + +if __name__ == "__main__": + unittest.main() diff --git a/PythonExample/tests/test_common.py b/PythonExample/tests/test_common.py index bd56e87..f19e23e 100644 --- a/PythonExample/tests/test_common.py +++ b/PythonExample/tests/test_common.py @@ -44,55 +44,40 @@ def test_platform_value(self): self.assertEqual(port, "/dev/ttyUSB0") -class TestGetTeam(unittest.TestCase): - def test_default_is_julia(self): - with patch.dict(os.environ, {"AMAZINGHAND_TEAM": ""}, clear=False): - self.assertEqual(common.get_team(), "julia") - - def test_env_team_used(self): - with patch.dict(os.environ, {"AMAZINGHAND_TEAM": "krishan"}, clear=False): - self.assertEqual(common.get_team(), "krishan") - - def test_normalized_lower(self): - with patch.dict(os.environ, {"AMAZINGHAND_TEAM": " Krishan "}, clear=False): - self.assertEqual(common.get_team(), "krishan") - - class TestLoadConfig(unittest.TestCase): - def test_missing_file_returns_defaults(self): - cfg = common.load_config(path=Path("/nonexistent/config.toml")) + def test_missing_profiles_returns_defaults(self): + """When config_root has no profiles.toml, load_config returns defaults (no hand data).""" + cfg = common.load_config(config_root=Path("/nonexistent")) self.assertEqual(cfg["baudrate"], 1000000) self.assertEqual(cfg["timeout"], 0.5) self.assertEqual(cfg["port"], "") - def test_existing_config_team_section(self): - config_path = _EXAMPLE_ROOT / "config.toml" - if not config_path.exists(): - self.skipTest("config.toml not found") - cfg = common.load_config(team="krishan", path=config_path) + def test_canonical_profile_team_julia(self): + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("config/profiles.toml not found") + cfg = common.load_config(profile="team_julia", config_root=config_root) self.assertIn("port", cfg) self.assertIn("baudrate", cfg) self.assertIn("timeout", cfg) - self.assertEqual(cfg["port"], "COM3") - self.assertEqual(cfg["timeout"], 2.5) - - def test_team_julia_section(self): - config_path = _EXAMPLE_ROOT / "config.toml" - if not config_path.exists(): - self.skipTest("config.toml not found") - cfg = common.load_config(team="julia", path=config_path) self.assertEqual(cfg["port"], "/dev/ttyACM0") self.assertEqual(cfg["timeout"], 0.5) + def test_canonical_profile_team_krishan(self): + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("config/profiles.toml not found") + cfg = common.load_config(profile="team_krishan", config_root=config_root) + self.assertEqual(cfg["port"], "COM3") + self.assertEqual(cfg["timeout"], 2.5) + class TestCreateController(unittest.TestCase): @patch("common.Scs0009PyController") def test_called_with_config_values(self, mock_controller): - if not (_EXAMPLE_ROOT / "config.toml").exists(): - self.skipTest("config.toml not found") with patch("common.load_config") as mock_load: mock_load.return_value = {"port": "COM9", "baudrate": 1000000, "timeout": 1.0} - common.create_controller(team="krishan") + common.create_controller(profile="team_krishan") mock_controller.assert_called_once_with( serial_port="COM9", baudrate=1000000, timeout=1.0 ) @@ -123,12 +108,59 @@ def test_returns_servo_ids_and_middle_pos_when_complete(self): self.assertEqual(out["servo_ids"], [15, 16, 13, 14, 11, 12, 17, 18]) self.assertEqual(out["middle_pos"], [-12, 2, 2, 5, -2, -8, 0, -15]) + def test_hand_1_finger_order_index_middle_ring_thumb(self): + """Regression: get_demo_hand_config must return ids and middle_pos in order index, middle, ring, thumb (2 per finger).""" + cfg = { + "hand_1_index_servo_ids": [1, 2], + "hand_1_index_middle_pos": [-2, 0], + "hand_1_middle_servo_ids": [3, 4], + "hand_1_middle_middle_pos": [1, 2], + "hand_1_ring_servo_ids": [6, 5], + "hand_1_ring_middle_pos": [-3, 8], + "hand_1_thumb_servo_ids": [8, 7], + "hand_1_thumb_middle_pos": [8, -8], + } + out = common.get_demo_hand_config(cfg, 1) + self.assertEqual(out["servo_ids"], [1, 2, 3, 4, 6, 5, 8, 7]) + self.assertEqual(out["middle_pos"], [-2, 0, 1, 2, -3, 8, 8, -8]) + def test_raises_when_hand_config_incomplete(self): cfg = {"hand_2_index_servo_ids": [], "hand_2_index_middle_pos": []} with self.assertRaises(ValueError) as ctx: common.get_demo_hand_config(cfg, 2) self.assertIn("not configured", str(ctx.exception)) + def test_raises_when_wrong_length(self): + """Regression: 8 servo IDs and 8 middle_pos required; wrong length must raise with clear message.""" + with patch.object(common, "_parse_hand_section") as mock_parse: + mock_parse.return_value = ([1] * 7, [0] * 8) + with self.assertRaises(ValueError) as ctx: + common.get_demo_hand_config({}, 1) + self.assertIn("8 servo IDs", str(ctx.exception)) + self.assertIn("7", str(ctx.exception)) + + def test_load_config_and_get_demo_hand_config_team_julia_side_1(self): + """Regression: load_config(profile=team_julia) + get_demo_hand_config(cfg, 1) must succeed.""" + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("config/profiles.toml not found") + cfg = common.load_config(profile="team_julia", config_root=config_root) + out = common.get_demo_hand_config(cfg, 1) + self.assertEqual(len(out["servo_ids"]), 8) + self.assertEqual(len(out["middle_pos"]), 8) + self.assertFalse(all(s == 0 for s in out["servo_ids"])) + + def test_load_config_uses_AMAZINGHAND_PROFILE(self): + """When AMAZINGHAND_PROFILE is set, load_config() returns that profile's config.""" + config_root = _EXAMPLE_ROOT.parent / "config" + if not (config_root / "profiles.toml").exists(): + self.skipTest("config/profiles.toml not found") + with patch.dict(os.environ, {"AMAZINGHAND_PROFILE": "team_julia"}, clear=False): + cfg = common.load_config(config_root=config_root) + out = common.get_demo_hand_config(cfg, 1) + self.assertEqual(out["servo_ids"], [1, 2, 3, 4, 6, 5, 8, 7]) + self.assertEqual(out["middle_pos"], [-2, 0, 1, 2, -3, 8, 8, -8]) + if __name__ == "__main__": unittest.main() diff --git a/PythonExample/tests/test_config.py b/PythonExample/tests/test_config.py index db48a6c..9dbbee9 100644 --- a/PythonExample/tests/test_config.py +++ b/PythonExample/tests/test_config.py @@ -17,36 +17,70 @@ from pathlib import Path EXAMPLE_ROOT = Path(__file__).resolve().parent.parent -CONFIG_PATH = EXAMPLE_ROOT / "config.toml" +REPO_CONFIG = EXAMPLE_ROOT.parent / "config" -class TestConfigStructure(unittest.TestCase): - """Check config.toml exists and has expected team sections and serial keys.""" +class TestCanonicalConfigStructure(unittest.TestCase): + """Check config/profiles.toml and config/calibration/ exist and have expected layout.""" - def test_config_file_exists(self): - self.assertTrue(CONFIG_PATH.exists(), f"config.toml not found at {CONFIG_PATH}") + def test_config_dir_exists(self): + self.assertTrue(REPO_CONFIG.is_dir(), f"config/ not found at {REPO_CONFIG}") - def test_config_has_team_sections(self): + def test_profiles_toml_exists(self): + path = REPO_CONFIG / "profiles.toml" + self.assertTrue(path.exists(), f"profiles.toml not found at {path}") + + def test_profiles_have_expected_sections(self): if sys.version_info < (3, 11): self.skipTest("tomllib requires Python 3.11+") import tomllib - with open(CONFIG_PATH, "rb") as f: + path = REPO_CONFIG / "profiles.toml" + if not path.exists(): + self.skipTest("profiles.toml not found") + with open(path, "rb") as f: data = tomllib.load(f) - self.assertIn("team_julia", data) - self.assertIn("team_krishan", data) + profile = data.get("profile", {}) + self.assertIn("team_julia", profile, "profile.team_julia required") + self.assertIn("team_krishan", profile, "profile.team_krishan required") - def test_team_sections_have_serial_keys(self): + def test_profiles_have_connection_and_calibration(self): if sys.version_info < (3, 11): self.skipTest("tomllib requires Python 3.11+") import tomllib - with open(CONFIG_PATH, "rb") as f: + path = REPO_CONFIG / "profiles.toml" + if not path.exists(): + self.skipTest("profiles.toml not found") + with open(path, "rb") as f: data = tomllib.load(f) - for team in ("team_julia", "team_krishan"): - section = data[team] - self.assertIn("port", section, f"{team} missing port") - self.assertIn("baudrate", section, f"{team} missing baudrate") - self.assertIn("timeout", section, f"{team} missing timeout") + for name in ("team_julia", "team_krishan"): + section = data.get("profile", {}).get(name, {}) + self.assertIn("port", section, f"{name} missing port") + self.assertIn("baudrate", section, f"{name} missing baudrate") + self.assertIn("timeout", section, f"{name} missing timeout") + self.assertIn("right_hand_calibration", section, f"{name} missing right_hand_calibration") + def test_team_julia_calibration_file_exists(self): + """Profile team_julia references a calibration file that must exist.""" + if sys.version_info < (3, 11): + self.skipTest("tomllib requires Python 3.11+") + import tomllib + path = REPO_CONFIG / "profiles.toml" + if not path.exists(): + self.skipTest("profiles.toml not found") + with open(path, "rb") as f: + data = tomllib.load(f) + cal = data.get("profile", {}).get("team_julia", {}).get("right_hand_calibration", "") + if not cal: + self.skipTest("team_julia has no right_hand_calibration") + cal_path = REPO_CONFIG / "calibration" / f"{cal}.toml" + self.assertTrue(cal_path.exists(), f"Calibration file {cal_path} not found") -if __name__ == "__main__": - unittest.main() + def test_calibration_has_hand_1_keys_for_demo(self): + """Canonical load for team_julia must yield hand_1_* so get_demo_hand_config(side=1) works.""" + if not (REPO_CONFIG / "profiles.toml").exists(): + self.skipTest("profiles.toml not found") + import common + cfg = common.load_config(profile="team_julia", config_root=REPO_CONFIG) + for name in ("index", "middle", "ring", "thumb"): + self.assertIn(f"hand_1_{name}_servo_ids", cfg, f"hand_1_{name}_servo_ids missing") + self.assertIn(f"hand_1_{name}_middle_pos", cfg, f"hand_1_{name}_middle_pos missing") diff --git a/config/calibration/l_hand_team_krishan.toml b/config/calibration/l_hand_team_krishan.toml new file mode 100644 index 0000000..f930bb5 --- /dev/null +++ b/config/calibration/l_hand_team_krishan.toml @@ -0,0 +1,18 @@ +# Per-physical-hand calibration: left hand (team_krishan). +# Logical role -> servo IDs and rest position (degrees). + +[index] +ids = [11, 12] +rest_deg = [3, -3] + +[middle] +ids = [13, 14] +rest_deg = [-1, -10] + +[ring] +ids = [15, 16] +rest_deg = [5, 2] + +[thumb] +ids = [17, 18] +rest_deg = [-7, 3] diff --git a/config/calibration/r_hand_team_julia.toml b/config/calibration/r_hand_team_julia.toml new file mode 100644 index 0000000..9d6e14b --- /dev/null +++ b/config/calibration/r_hand_team_julia.toml @@ -0,0 +1,18 @@ +# Per-physical-hand calibration: right hand (team_julia). +# Logical role -> servo IDs and rest position (degrees). Anatomical order: index, middle, ring, thumb. + +[index] +ids = [1, 2] +rest_deg = [-2, 0] + +[middle] +ids = [3, 4] +rest_deg = [1, 2] + +[ring] +ids = [6, 5] +rest_deg = [-3, 8] + +[thumb] +ids = [8, 7] +rest_deg = [8, -8] diff --git a/config/calibration/r_hand_team_krishan.toml b/config/calibration/r_hand_team_krishan.toml new file mode 100644 index 0000000..a5aa5fd --- /dev/null +++ b/config/calibration/r_hand_team_krishan.toml @@ -0,0 +1,18 @@ +# Per-physical-hand calibration: right hand (team_krishan). +# Logical role -> servo IDs and rest position (degrees). + +[index] +ids = [1, 2] +rest_deg = [3, 0] + +[middle] +ids = [3, 4] +rest_deg = [-8, -13] + +[ring] +ids = [5, 6] +rest_deg = [2, -5] + +[thumb] +ids = [7, 8] +rest_deg = [-12, -5] diff --git a/config/hand_geometry.toml b/config/hand_geometry.toml new file mode 100644 index 0000000..0e0302c --- /dev/null +++ b/config/hand_geometry.toml @@ -0,0 +1,4 @@ +# Shared hand geometry for both hands. No physical servo IDs. +# Same structure for right and left. Rest positions come from calibration per physical hand. + +fingers = ["index", "middle", "ring", "thumb"] diff --git a/config/profiles.toml b/config/profiles.toml new file mode 100644 index 0000000..082b2a0 --- /dev/null +++ b/config/profiles.toml @@ -0,0 +1,28 @@ +# Named profiles for canonical hand config. Set AMAZINGHAND_PROFILE or pass profile name to load_config_canonical. +# Each profile references calibration files (without .toml) and connection options. + +[profile.team_julia] +right_hand_calibration = "r_hand_team_julia" +left_hand_calibration = "" +port = "/dev/ttyACM0" +baudrate = 1000000 +timeout = 0.5 +side = 1 +max_speed = 7 +close_speed = 3 +finger_test_servo_ids = [6, 5] +finger_test_middle_pos = [-3, 8] +hand_test_id = 1 + +[profile.team_krishan] +right_hand_calibration = "r_hand_team_krishan" +left_hand_calibration = "l_hand_team_krishan" +port = "COM3" +baudrate = 1000000 +timeout = 2.5 +side = 2 +max_speed = 7 +close_speed = 3 +finger_test_servo_ids = [13, 14] +finger_test_middle_pos = [5, -2] +hand_test_id = 2 diff --git a/docs/canonical_hand_config_design.md b/docs/canonical_hand_config_design.md new file mode 100644 index 0000000..f237fb1 --- /dev/null +++ b/docs/canonical_hand_config_design.md @@ -0,0 +1,80 @@ +# Canonical Hand Configuration Design (Proposal) + +A single source of truth for hand configuration. + +## Problem + +- Hand config (servo IDs, rest/middle positions, offsets) lives in two places: Demo/AHControl (e.g. `r_hand.toml`) and hardcoded in PythonExample. Values can drift. +- Servo IDs are only known after assembly and testing, so assignment is per physical hand. +- There is no support for multiple setups (e.g. team_julia, team_krishan). + +## Goals + +- One canonical definition per logical hand (right, left) so AHControl, PythonExample, and others read the same data. +- Support multiple profiles (connection + which hands) without duplicating hand geometry. +- Use logical roles everywhere; physical servo IDs only in per-physical-hand calibration files. + +## Proposed Structure + +1. Logical roles + +The hand is defined by a fixed set of logical roles (e.g. index_m0, index_m1, middle_m0, middle_m1, ring_m0, ring_m1, thumb_m0, thumb_m1). Application code and shared config refer only to these roles. Physical servo IDs appear only in calibration files. + +2. Shared hand geometry (one source of truth) + +One file defines the structure common to both hands: which roles exist (finger order). Geometry is identical for right and left, so one file suffices. No physical servo IDs. Implemented as `config/hand_geometry.toml` (key `fingers = ["index", "middle", "ring", "thumb"]`). AHControl and the Python canonical adapter read it; rest positions stay in per-physical-hand calibration. + +3. Per-physical-hand calibration + +After assembly and testing, one small file (or section) per physical hand contains only: +- Mapping: logical role → physical servo ID +- Rest/middle position or offset per role + +Rust (radians) and Python (degrees) can derive units from this single rest value at load time or via a thin adapter. + +4. Named profiles + +A main config (e.g. for PythonExample and/or launchers) uses named profiles (e.g. `[profile.team_julia]`, `[profile.team_krishan]`) that specify only: +- Connection: port, baudrate, timeout, etc. +- Which physical hand calibration to use for right and left (e.g. by file name or id) +- Demo-specific options (side, hand_test_id, speeds, etc.) + +Select the active profile by env var, CLI flag, or a key like `active_profile = "team_krishan"`. + +## Layout + +- Format and location: TOML. Single file `config/hand_geometry.toml` (key `fingers`); calibration under `config/calibration/`; profiles in `config/profiles.toml`. +- Naming: use anatomical role names (index, middle, ring, thumb) in the canonical config and in application-facing APIs. Each stack keeps internal names (e.g. r_finger1) only inside adapters that map from canonical roles. +- Loader strategy: small adapters in AHControl and PythonExample. Each adapter reads the canonical TOML (shared geometry + chosen calibration) and emits the shape that stack expects; no shared cross-language loader initially. AHControl resolves the geometry path from the calibration file path (e.g. `config/calibration/foo.toml` → `config/hand_geometry.toml`), so no fixed paths and it works for any user or clone. + +- `config/hand_geometry.toml`: shared hand geometry (finger order for both hands); no IDs. +- `config/calibration/`: one file per physical hand (e.g. `r_hand_team_julia.toml`, `l_hand_team_krishan.toml`) containing role → id and rest/middle per role. +- `config/profiles.toml`: profiles that reference calibration files and set connection/demo options. + +AHControl loads shared hand geometry plus the chosen calibration file(s). PythonExample does the same. Both resolve logical roles to IDs at load time. + +## Calibration procedures + +Zero, rest, and middle position all refer to the same thing: the reference pose you choose for each joint. In calibration TOML this is stored as `rest_deg` per finger; in Python demos it appears as `middle_pos`. The canonical adapter maps `rest_deg` to `hand_*_*_middle_pos` so there is a single source of truth. + +get_zeros (AHControl binary): Used after assembly to record the current physical pose as the reference. Connect the hand, run with the chosen calibration file (e.g. `config/calibration/r_hand_team_julia.toml`). Torque is turned off; you move the hand into the desired zero/rest pose, press Enter, and the tool reads each motor’s present position and overwrites the rest values in memory. It then prints the updated TOML in canonical form (`[index]`, `[middle]`, etc. with `ids` and `rest_deg`). Save that output as the calibration file so that file remains the single source of truth. get_zeros should take a path under `config/calibration/` and resolve geometry from the same config root. + +set_zeros (AHControl binary): Moves all motors to the rest pose defined in the given calibration file. Use it to verify or to bring the hand to the same zero pose without manually moving it. It should also use canonical calibration files under `config/calibration/`. + +Profiles in `config/profiles.toml` only point at which calibration file to use (`right_hand_calibration`, `left_hand_calibration`). The calibration tools do not need to read profiles; they operate on the calibration file that a profile references. Optionally, profile options like `finger_test_servo_ids` and `finger_test_middle_pos` can be derived from the selected hand’s calibration for one finger to avoid duplicating rest values. + +Recommended steps: + +0. Verify motor IDs: Run `set_zeros` or `goto` with your calibration file and port. If the wrong finger moves for a given config entry, the ID in the calibration file does not match the physical motor. Fix with the AHControl `change_id` binary, or use the [Feetech debug tool](https://github.com/Robot-Maker-SAS/FeetechServo/tree/main/feetech%20debug%20tool%20master/FD1.9.8.2) to scan the bus. Expected result for `set_zeros`: the hand moves all fingers to their zero pose (from config), holds about 1 second, then relaxes (torque off). + +1. Create or copy a calibration file (e.g. `config/calibration/r_hand_team_julia.toml`) with one section per finger: `[index]`, `[middle]`, `[ring]`, `[thumb]`. Each section must have `ids = [id1, id2]` (physical servo IDs for that finger) and `rest_deg = [d1, d2]` (initial values; can be zeros). Use the same port and baudrate as in your profile. + +2. get_zeros — record the current physical pose as the reference: From the repo root, run the AHControl get_zeros binary with the calibration file and connection args (e.g. `--config config/calibration/r_hand_team_julia.toml --serialport /dev/ttyACM0`). When the hand goes compliant, move it into the desired zero/rest pose (e.g. open, relaxed), then press Enter. The tool reads each motor position and prints a full canonical TOML. Save the printed TOML into the same calibration file (or a new file under `config/calibration/`), then add or update a profile in `config/profiles.toml` to reference that file. + +3. set_zeros — move the hand to the stored rest pose: Run the set_zeros binary with the same calibration file and port/baudrate. The hand moves all joints to the `rest_deg` values in that file. Use this to verify calibration or to reset the hand to zero without moving it by hand. + +4. Run demos or AHControl with a profile: set `AMAZINGHAND_PROFILE` (or pass the profile name to your loader) to the profile that references the calibration file you used. PythonExample and AHControl will load that calibration as the single source of truth for IDs and rest/middle positions. + +## Alternative: self-contained profiles + +Each profile could stay self-contained (hand_1, hand_2, connection in one section) with duplicated geometry. That avoids refactoring to shared geometry and calibration files but keeps two sources of truth and more duplication. diff --git a/pixi.toml b/pixi.toml index 14f6c1f..7d0d214 100644 --- a/pixi.toml +++ b/pixi.toml @@ -37,3 +37,4 @@ qpsolvers = { version = ">=4.7.1", extras = ["quadprog"] } # PYTHONPATH= avoids loading system/ROS pytest plugins when run locally test-python-example = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" test-demo = "cd Demo && python -m pytest tests/ -v" +test-ahcontrol = "cargo test --manifest-path Demo/Cargo.toml -p AHControl" From 97ba0964724ca94befbf77e3639a98db6e913b4b Mon Sep 17 00:00:00 2001 From: juliaj Date: Fri, 20 Feb 2026 14:56:08 -0800 Subject: [PATCH 4/6] Refactor mj_mink files --- .github/workflows/ci.yml | 3 + Demo/AHControl/src/main.rs | 8 - .../AHSimulation/mj_mink_common.py | 241 +++++++++++++ .../AHSimulation/AHSimulation/mj_mink_left.py | 323 +----------------- .../AHSimulation/mj_mink_right.py | 322 +---------------- Demo/tests/test_mj_mink.py | 147 ++++++++ pixi.toml | 1 + 7 files changed, 398 insertions(+), 647 deletions(-) create mode 100644 Demo/AHSimulation/AHSimulation/mj_mink_common.py create mode 100644 Demo/tests/test_mj_mink.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 022422a..3b9c5c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,8 @@ jobs: - name: Demo unit tests run: pixi run test-demo + - name: Demo AHSimulation (mj_mink) unit tests + run: pixi run test-demo-mj-mink + - name: AHControl (Rust) unit tests run: pixi run test-ahcontrol diff --git a/Demo/AHControl/src/main.rs b/Demo/AHControl/src/main.rs index a0bd023..a81c4ab 100644 --- a/Demo/AHControl/src/main.rs +++ b/Demo/AHControl/src/main.rs @@ -86,14 +86,6 @@ fn main() -> Result<(), Box> { if let Some(Parameter::ListInt(finger1_idx)) = metadata.parameters.get(&finger.finger_name) { - println!( - "metadata: name: {:?} idx {:?} data: {:?} {:?}", - finger.finger_name, - finger1_idx, - buffer[finger1_idx[0] as usize], - buffer[finger1_idx[1] as usize] - ); - motors_ids.push(finger.motor1.id); motors_ids.push(finger.motor2.id); diff --git a/Demo/AHSimulation/AHSimulation/mj_mink_common.py b/Demo/AHSimulation/AHSimulation/mj_mink_common.py new file mode 100644 index 0000000..90ea7ea --- /dev/null +++ b/Demo/AHSimulation/AHSimulation/mj_mink_common.py @@ -0,0 +1,241 @@ +"""Shared Mujoco client for left/right hand simulation nodes.""" + +import argparse +import os +import sys +import time +from pathlib import Path + +import mujoco +import mujoco.viewer +import numpy as np +import pyarrow as pa +from dora import Node + +import mink +from loop_rate_limiters import RateLimiter + +ROOT_PATH = Path(os.path.dirname(os.path.abspath(__file__))).parent + +# Per-tip (dx, dy, dz) for pos: result = x*1.5+dx, y*1.5+dy, z*1.5+dz +MOCAP_POS_OFFSETS = { + "right": [ + (-0.025, 0.022, 0.098), + (-0.025, -0.009, 0.092), + (-0.025, -0.040, 0.082), + (0.024, 0.019, 0.017), + ], + "left": [ + (0.025, -0.022, 0.098), + (0.025, 0.009, 0.092), + (0.025, 0.040, 0.082), + (0.024, -0.019, 0.017), + ], +} + + +class Client: + """Mujoco client for one hand (left or right).""" + + def __init__(self, side, mode="pos"): + if side not in ("left", "right"): + raise ValueError(f"side must be 'left' or 'right', got {side!r}") + self.side = side + self.prefix = "r_" if side == "right" else "l_" + scene_dir = "AH_Right" if side == "right" else "AH_Left" + self.joints_output = f"mj_{self.prefix[0]}_joints_pos" + self.hand_pos_id = f"{self.prefix}hand_pos" + self.hand_quat_id = f"{self.prefix}hand_quat" + self.mocap_offsets = MOCAP_POS_OFFSETS[side] + self.tip_names = [f"{self.prefix}tip{i}" for i in range(1, 5)] + self.finger_keys = [f"{self.prefix}finger{i}" for i in range(1, 5)] + + self.model = mujoco.MjModel.from_xml_path( + f"{ROOT_PATH}/AHSimulation/{scene_dir}/mjcf/scene.xml" + ) + self.configuration = mink.Configuration(self.model) + self.posture_task = mink.PostureTask(self.model, cost=1e-2) + + pos_cost = 1.0 if mode == "pos" else 0.0 + ori_cost = 0.0 if mode == "pos" else 1.0 + self.task1 = mink.FrameTask( + frame_name="tip1", frame_type="site", + position_cost=pos_cost, orientation_cost=ori_cost, lm_damping=1.0, + ) + self.task2 = mink.FrameTask( + frame_name="tip2", frame_type="site", + position_cost=pos_cost, orientation_cost=ori_cost, lm_damping=1.0, + ) + self.task3 = mink.FrameTask( + frame_name="tip3", frame_type="site", + position_cost=pos_cost, orientation_cost=ori_cost, lm_damping=1.0, + ) + self.task4 = mink.FrameTask( + frame_name="tip4", frame_type="site", + position_cost=pos_cost, orientation_cost=ori_cost, lm_damping=1.0, + ) + if mode not in ("pos", "quat"): + raise ValueError(f"unknown mode: {mode}") + eq_task = mink.EqualityConstraintTask(self.model, cost=1000.0) + self.tasks = [ + eq_task, + self.posture_task, + self.task1, + self.task2, + self.task3, + self.task4, + ] + + self.model = self.configuration.model + self.data = self.configuration.data + self.solver = "quadprog" + self.motor_pos = [] + self.metadata = [] + self.node = Node() + + def run(self): + with mujoco.viewer.launch_passive(self.model, self.data) as viewer: + rate = RateLimiter(frequency=1000.0) + self.configuration.update_from_keyframe("zero") + self.posture_task.set_target_from_configuration(self.configuration) + mink.move_mocap_to_frame(self.model, self.data, "finger1_target", "tip1", "site") + mink.move_mocap_to_frame(self.model, self.data, "finger2_target", "tip2", "site") + mink.move_mocap_to_frame(self.model, self.data, "finger3_target", "tip3", "site") + mink.move_mocap_to_frame(self.model, self.data, "finger4_target", "tip4", "site") + + for event in self.node: + event_type = event["type"] + if event_type == "INPUT": + event_id = event["id"] + if event_id == "tick": + if not viewer.is_running(): + break + step_start = time.time() + self.task1.set_target( + mink.SE3.from_mocap_name(self.model, self.data, "finger1_target") + ) + self.task2.set_target( + mink.SE3.from_mocap_name(self.model, self.data, "finger2_target") + ) + self.task3.set_target( + mink.SE3.from_mocap_name(self.model, self.data, "finger3_target") + ) + self.task4.set_target( + mink.SE3.from_mocap_name(self.model, self.data, "finger4_target") + ) + vel = mink.solve_ik( + self.configuration, self.tasks, rate.dt, self.solver, 1e-5 + ) + self.configuration.integrate_inplace(vel, rate.dt) + + f1_m1 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger1_motor1") + f1_m2 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger1_motor2") + f2_m1 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger2_motor1") + f2_m2 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger2_motor2") + f3_m1 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger3_motor1") + f3_m2 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger3_motor2") + f4_m1 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger4_motor1") + f4_m2 = mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, "finger4_motor2") + self.metadata = dict(event["metadata"]) + for i, key in enumerate(self.finger_keys): + self.metadata[key] = [2 * i, 2 * i + 1] + self.motor_pos = np.zeros(8) + self.motor_pos[self.metadata[self.finger_keys[0]]] = np.array( + [self.data.joint(f1_m1).qpos[0], self.data.joint(f1_m2).qpos[0]] + ) + self.motor_pos[self.metadata[self.finger_keys[1]]] = np.array( + [self.data.joint(f2_m1).qpos[0], self.data.joint(f2_m2).qpos[0]] + ) + self.motor_pos[self.metadata[self.finger_keys[2]]] = np.array( + [self.data.joint(f3_m1).qpos[0], self.data.joint(f3_m2).qpos[0]] + ) + self.motor_pos[self.metadata[self.finger_keys[3]]] = np.array( + [self.data.joint(f4_m1).qpos[0], self.data.joint(f4_m2).qpos[0]] + ) + + viewer.sync() + time_until_next_step = self.model.opt.timestep - (time.time() - step_start) + if time_until_next_step > 0: + time.sleep(time_until_next_step) + + elif event_id == "pull_position": + self.pull_position(self.node, event["metadata"]) + elif event_id == "tick_ctrl": + if len(self.metadata) > 0: + self.node.send_output( + self.joints_output, pa.array(self.motor_pos), self.metadata + ) + elif event_id == "pull_velocity": + self.pull_velocity(self.node, event["metadata"]) + elif event_id == "pull_current": + self.pull_current(self.node, event["metadata"]) + elif event_id == "write_goal_position": + self.write_goal_position(event["value"]) + elif event_id == self.hand_pos_id: + self.write_mocap_pos(event["value"]) + elif event_id == self.hand_quat_id: + self.write_mocap_quat(event["value"]) + elif event_id == "end": + break + + elif event_type == "STOP": + break + elif event_type == "ERROR": + raise ValueError("An error occurred in the dataflow: " + event["error"]) + + self.node.send_output("end", pa.array([])) + + def pull_position(self, node, metadata): + pass + + def pull_velocity(self, node, metadata): + pass + + def pull_current(self, node, metadata): + pass + + def write_goal_position(self, goal_position_with_joints): + joints = goal_position_with_joints.field("joints") + goal_position = goal_position_with_joints.field("values") + for i, joint in enumerate(joints): + self.data.joint(joint.as_py()).qpos[0] = goal_position[i].as_py() + + def write_mocap_pos(self, hand): + hand0 = hand[0] + scale = 1.5 + for i, tip in enumerate(self.tip_names): + if tip in hand0: + x, y, z = hand0[tip].values + dx, dy, dz = self.mocap_offsets[i] + self.data.mocap_pos[i] = [ + x.as_py() * scale + dx, + y.as_py() * scale + dy, + z.as_py() * scale + dz, + ] + + def write_mocap_quat(self, hand): + hand0 = hand[0] + for i, tip in enumerate(self.tip_names): + if tip in hand0: + w, x, y, z = hand0[tip].values + self.data.mocap_quat[i] = [ + w.as_py(), x.as_py(), y.as_py(), z.as_py(), + ] + + +def run_main(side): + parser = argparse.ArgumentParser() + parser.add_argument( + "-m", "--mode", type=str, choices=["pos", "quat"], default="pos", + help="control mode: pos=position, quat=quaternion", + ) + args = parser.parse_args() + client = Client(side=side, mode=args.mode) + try: + client.run() + except KeyboardInterrupt: + sys.exit(0) + except RuntimeError as e: + if "event stream" in str(e) or "subscribe failed" in str(e) or "exited before" in str(e): + sys.exit(0) + raise diff --git a/Demo/AHSimulation/AHSimulation/mj_mink_left.py b/Demo/AHSimulation/AHSimulation/mj_mink_left.py index 6ec598c..152d7ff 100644 --- a/Demo/AHSimulation/AHSimulation/mj_mink_left.py +++ b/Demo/AHSimulation/AHSimulation/mj_mink_left.py @@ -1,323 +1,6 @@ -"""Mujoco Client: This node is used to represent simulated robot, it can be used to read virtual positions, or can be controlled.""" - -import argparse - -import os -import time - -import mujoco -import mujoco.viewer -import pyarrow as pa -from dora import Node - -import mink -from loop_rate_limiters import RateLimiter -from mink.contrib import TeleopMocap -from pathlib import Path -import numpy as np - -ROOT_PATH = Path(os.path.dirname(os.path.abspath(__file__))).parent - - -class Client: - """TODO: Add docstring.""" - - def __init__(self, mode='pos'): - """TODO: Add docstring.""" - - - self.model = mujoco.MjModel.from_xml_path( - f"{ROOT_PATH}/AHSimulation/AH_Left/mjcf/scene.xml" - ) - # self.data=mujoco.MjData(self.model) - - - self.configuration = mink.Configuration(self.model) - - self.posture_task = mink.PostureTask(self.model, cost=1e-2) - - if mode=='pos': - self.task1 = mink.FrameTask( - frame_name='tip1', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task2 = mink.FrameTask( - frame_name='tip2', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task3 = mink.FrameTask( - frame_name='tip3', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task4 = mink.FrameTask( - frame_name='tip4', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - elif mode=='quat': #we control the orientation - self.task1 = mink.FrameTask( - frame_name='tip1', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task2 = mink.FrameTask( - frame_name='tip2', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task3 = mink.FrameTask( - frame_name='tip3', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task4 = mink.FrameTask( - frame_name='tip4', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - else: - print(f"Error, unknown mode: {mode}") - return -1 - # Regulate all equality constraints with the same cost. - eq_task = mink.EqualityConstraintTask(self.model, cost=1000.0) - - self.tasks = [ - eq_task, - self.posture_task, - self.task1, - self.task2, - self.task3, - self.task4, - ] - - - - self.model = self.configuration.model - self.data = self.configuration.data - self.solver = "quadprog" - - self.motor_pos=[] - self.metadata=[] - self.node = Node() - - def run(self): - """TODO: Add docstring.""" - with mujoco.viewer.launch_passive(self.model, self.data) as viewer: - - rate = RateLimiter(frequency=1000.0) - # dt = rate.dt - # t = 0 - self.configuration.update_from_keyframe("zero") - - # Initialize mocap bodies at their respective sites. - self.posture_task.set_target_from_configuration(self.configuration) - - mink.move_mocap_to_frame(self.model, self.data, "finger1_target", "tip1", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger2_target", "tip2", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger3_target", "tip3", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger4_target", "tip4", "site") - - - for event in self.node: - event_type = event["type"] - - if event_type == "INPUT": - event_id = event["id"] - - if event_id == "tick": - # self.node.send_output("tick", pa.array([]), event["metadata"]) - - if not viewer.is_running(): - break - - step_start = time.time() - - - - self.task1.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger1_target") - ) - self.task2.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger2_target") - ) - self.task3.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger3_target") - ) - self.task4.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger4_target") - ) - - - - - - # vel = mink.solve_ik(self.configuration, self.tasks, self.model.opt.timestep, self.solver, 1e-5) - # self.configuration.integrate_inplace(vel, self.model.opt.timestep) - vel = mink.solve_ik(self.configuration, self.tasks, rate.dt, self.solver, 1e-5) - self.configuration.integrate_inplace(vel, rate.dt) - - - - # Step the simulation forward - # mujoco.mj_step(self.m, self.data) - - # mujoco.mj_step(self.model, self.data) - - - #get the motors position and send - - - f1_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger1_motor1") - f1_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger1_motor2") - f2_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger2_motor1") - f2_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger2_motor2") - f3_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger3_motor1") - f3_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger3_motor2") - f4_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger4_motor1") - f4_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger4_motor2") - # print(f"motor1: {self.data.joint(f1_motor1).qpos} motor2: {self.data.joint(f1_motor2).qpos}") - self.metadata=event["metadata"] - self.metadata["l_finger1"]=[0,1] - self.metadata["l_finger2"]=[2,3] - self.metadata["l_finger3"]=[4,5] - self.metadata["l_finger4"]=[6,7] - - self.motor_pos=np.zeros(8); - self.motor_pos[self.metadata["l_finger1"]]=np.array([self.data.joint(f1_motor1).qpos[0],self.data.joint(f1_motor2).qpos[0]]) - self.motor_pos[self.metadata["l_finger2"]]=np.array([self.data.joint(f2_motor1).qpos[0],self.data.joint(f2_motor2).qpos[0]]) - self.motor_pos[self.metadata["l_finger3"]]=np.array([self.data.joint(f3_motor1).qpos[0],self.data.joint(f3_motor2).qpos[0]]) - self.motor_pos[self.metadata["l_finger4"]]=np.array([self.data.joint(f4_motor1).qpos[0],self.data.joint(f4_motor2).qpos[0]]) - - - - viewer.sync() - - # Rudimentary time keeping, will drift relative to wall clock. - time_until_next_step = self.model.opt.timestep - ( - time.time() - step_start - ) - if time_until_next_step > 0: - time.sleep(time_until_next_step) - - elif event_id == "pull_position": - self.pull_position(self.node, event["metadata"]) - - - elif event_id == "tick_ctrl": - if len(self.metadata)>0: - self.node.send_output("mj_l_joints_pos", pa.array(self.motor_pos), self.metadata) - # self.pull_position(self.node, event["metadata"]) - - elif event_id == "pull_velocity": - self.pull_velocity(self.node, event["metadata"]) - elif event_id == "pull_current": - self.pull_current(self.node, event["metadata"]) - elif event_id == "write_goal_position": - self.write_goal_position(event["value"]) - elif event_id == "l_hand_pos": - self.write_mocap_pos(event["value"]) - elif event_id == "l_hand_quat": - self.write_mocap_quat(event["value"]) - - elif event_id == "end": - break - - elif event_type == "ERROR": - raise ValueError( - "An error occurred in the dataflow: " + event["error"], - ) - - self.node.send_output("end", pa.array([])) - - def pull_position(self, node, metadata): - """TODO: Add docstring.""" - - def pull_velocity(self, node, metadata): - """TODO: Add docstring.""" - - def pull_current(self, node, metadata): - """TODO: Add docstring.""" - - def write_goal_position(self, goal_position_with_joints): - """TODO: Add docstring.""" - joints = goal_position_with_joints.field("joints") - goal_position = goal_position_with_joints.field("values") - - for i, joint in enumerate(joints): - self.data.joint(joint.as_py()).qpos[0] = goal_position[i].as_py() - - def write_mocap_pos(self, hand): - - #please, a method to access the mocap objects by name... - - if "l_tip1" in hand[0]: - [x,y,z]=hand[0]['l_tip1'].values - self.data.mocap_pos[0]=[x.as_py()*1.5+0.025,y.as_py()*1.5-0.022,z.as_py()*1.5+0.098] - if "l_tip2" in hand[0]: - [x,y,z]=hand[0]['l_tip2'].values - self.data.mocap_pos[1]=[x.as_py()*1.5+0.025,y.as_py()*1.5+0.009,z.as_py()*1.5+0.092] - if "l_tip3" in hand[0]: - [x,y,z]=hand[0]['l_tip3'].values - self.data.mocap_pos[2]=[x.as_py()*1.5+0.025,y.as_py()*1.5+0.040,z.as_py()*1.5+0.082] - if "l_tip4" in hand[0]: - [x,y,z]=hand[0]['l_tip4'].values - self.data.mocap_pos[3]=[x.as_py()*1.5+0.024,y.as_py()*1.5-0.019,z.as_py()*1.5+0.017] - - - def write_mocap_quat(self, hand): - #please, a method to access the mocap objects by name... - - if "l_tip1" in hand[0]: - [w,x,y,z]=hand[0]['l_tip1'].values - self.data.mocap_quat[0]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "l_tip2" in hand[0]: - [w,x,y,z]=hand[0]['l_tip2'].values - self.data.mocap_quat[1]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "l_tip3" in hand[0]: - [w,x,y,z]=hand[0]['l_tip3'].values - self.data.mocap_quat[2]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "l_tip4" in hand[0]: - [w,x,y,z]=hand[0]['l_tip4'].values - self.data.mocap_quat[3]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - - - - -def main(): - """Handle dynamic nodes, ask for the name of the node in the dataflow.""" - - parser = argparse.ArgumentParser() - parser.add_argument("-m", "--mode", type=str, choices=['pos','quat'], default='pos', - help="control mode: pos=position (we control the position of the tip) quat=quaternion (we control the orientation of the tip)") - args = parser.parse_args() - client = Client(args.mode) - client.run() +"""Mujoco client node for left-hand simulation.""" +from mj_mink_common import run_main if __name__ == "__main__": - main() + run_main("left") diff --git a/Demo/AHSimulation/AHSimulation/mj_mink_right.py b/Demo/AHSimulation/AHSimulation/mj_mink_right.py index 37eb58c..8a9e72b 100644 --- a/Demo/AHSimulation/AHSimulation/mj_mink_right.py +++ b/Demo/AHSimulation/AHSimulation/mj_mink_right.py @@ -1,322 +1,6 @@ -"""Mujoco Client: This node is used to represent simulated robot, it can be used to read virtual positions, or can be controlled.""" - -import argparse - -import os -import time - -import mujoco -import mujoco.viewer -import pyarrow as pa -from dora import Node - -import mink -from loop_rate_limiters import RateLimiter -from mink.contrib import TeleopMocap -from pathlib import Path -import numpy as np - -ROOT_PATH = Path(os.path.dirname(os.path.abspath(__file__))).parent - - -class Client: - """TODO: Add docstring.""" - - def __init__(self, mode='pos'): - """TODO: Add docstring.""" - - - self.model = mujoco.MjModel.from_xml_path( - f"{ROOT_PATH}/AHSimulation/AH_Right/mjcf/scene.xml" - ) - # self.data=mujoco.MjData(self.model) - - - self.configuration = mink.Configuration(self.model) - - self.posture_task = mink.PostureTask(self.model, cost=1e-2) - - if mode=='pos': - self.task1 = mink.FrameTask( - frame_name='tip1', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task2 = mink.FrameTask( - frame_name='tip2', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task3 = mink.FrameTask( - frame_name='tip3', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - - self.task4 = mink.FrameTask( - frame_name='tip4', - frame_type="site", - position_cost=1.0, - orientation_cost=0.0, - lm_damping=1.0, - ) - elif mode=='quat': #we control the orientation - self.task1 = mink.FrameTask( - frame_name='tip1', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task2 = mink.FrameTask( - frame_name='tip2', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task3 = mink.FrameTask( - frame_name='tip3', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - - self.task4 = mink.FrameTask( - frame_name='tip4', - frame_type="site", - position_cost=0.0, - orientation_cost=1.0, - lm_damping=1.0, - ) - else: - print(f"Error, unknown mode: {mode}") - return -1 - # Regulate all equality constraints with the same cost. - eq_task = mink.EqualityConstraintTask(self.model, cost=1000.0) - - self.tasks = [ - eq_task, - self.posture_task, - self.task1, - self.task2, - self.task3, - self.task4, - ] - - - - self.model = self.configuration.model - self.data = self.configuration.data - self.solver = "quadprog" - - self.motor_pos=[] - self.metadata=[] - self.node = Node() - - def run(self): - """TODO: Add docstring.""" - with mujoco.viewer.launch_passive(self.model, self.data) as viewer: - - rate = RateLimiter(frequency=1000.0) - # dt = rate.dt - # t = 0 - self.configuration.update_from_keyframe("zero") - - # Initialize mocap bodies at their respective sites. - self.posture_task.set_target_from_configuration(self.configuration) - - mink.move_mocap_to_frame(self.model, self.data, "finger1_target", "tip1", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger2_target", "tip2", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger3_target", "tip3", "site") - mink.move_mocap_to_frame(self.model, self.data, "finger4_target", "tip4", "site") - - - for event in self.node: - event_type = event["type"] - - if event_type == "INPUT": - event_id = event["id"] - - if event_id == "tick": - # self.node.send_output("tick", pa.array([]), event["metadata"]) - - if not viewer.is_running(): - break - - step_start = time.time() - - - - self.task1.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger1_target") - ) - self.task2.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger2_target") - ) - self.task3.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger3_target") - ) - self.task4.set_target( - mink.SE3.from_mocap_name(self.model, self.data, "finger4_target") - ) - - - - - - # vel = mink.solve_ik(self.configuration, self.tasks, self.model.opt.timestep, self.solver, 1e-5) - # self.configuration.integrate_inplace(vel, self.model.opt.timestep) - vel = mink.solve_ik(self.configuration, self.tasks, rate.dt, self.solver, 1e-5) - self.configuration.integrate_inplace(vel, rate.dt) - - - - # Step the simulation forward - # mujoco.mj_step(self.m, self.data) - - # mujoco.mj_step(self.model, self.data) - - - #get the motors position and send for real motor control - - f1_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger1_motor1") - f1_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger1_motor2") - f2_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger2_motor1") - f2_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger2_motor2") - f3_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger3_motor1") - f3_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger3_motor2") - f4_motor1=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger4_motor1") - f4_motor2=mujoco.mj_name2id(self.model,mujoco.mjtObj.mjOBJ_JOINT,"finger4_motor2") - # print(f"motor1: {self.data.joint(f1_motor1).qpos} motor2: {self.data.joint(f1_motor2).qpos}") - self.metadata=event["metadata"] - self.metadata["r_finger1"]=[0,1] - self.metadata["r_finger2"]=[2,3] - self.metadata["r_finger3"]=[4,5] - self.metadata["r_finger4"]=[6,7] - - self.motor_pos=np.zeros(8); - self.motor_pos[self.metadata["r_finger1"]]=np.array([self.data.joint(f1_motor1).qpos[0],self.data.joint(f1_motor2).qpos[0]]) - self.motor_pos[self.metadata["r_finger2"]]=np.array([self.data.joint(f2_motor1).qpos[0],self.data.joint(f2_motor2).qpos[0]]) - self.motor_pos[self.metadata["r_finger3"]]=np.array([self.data.joint(f3_motor1).qpos[0],self.data.joint(f3_motor2).qpos[0]]) - self.motor_pos[self.metadata["r_finger4"]]=np.array([self.data.joint(f4_motor1).qpos[0],self.data.joint(f4_motor2).qpos[0]]) - - - - viewer.sync() - - # Rudimentary time keeping, will drift relative to wall clock. - time_until_next_step = self.model.opt.timestep - ( - time.time() - step_start - ) - if time_until_next_step > 0: - time.sleep(time_until_next_step) - - elif event_id == "pull_position": - self.pull_position(self.node, event["metadata"]) - - - elif event_id == "tick_ctrl": - if len(self.metadata)>0: - self.node.send_output("mj_r_joints_pos", pa.array(self.motor_pos), self.metadata) - # self.pull_position(self.node, event["metadata"]) - - elif event_id == "pull_velocity": - self.pull_velocity(self.node, event["metadata"]) - elif event_id == "pull_current": - self.pull_current(self.node, event["metadata"]) - elif event_id == "write_goal_position": - self.write_goal_position(event["value"]) - elif event_id == "r_hand_pos": - self.write_mocap_pos(event["value"]) - elif event_id == "r_hand_quat": - self.write_mocap_quat(event["value"]) - - elif event_id == "end": - break - - elif event_type == "ERROR": - raise ValueError( - "An error occurred in the dataflow: " + event["error"], - ) - - self.node.send_output("end", pa.array([])) - - def pull_position(self, node, metadata): - """TODO: Add docstring.""" - - def pull_velocity(self, node, metadata): - """TODO: Add docstring.""" - - def pull_current(self, node, metadata): - """TODO: Add docstring.""" - - def write_goal_position(self, goal_position_with_joints): - """TODO: Add docstring.""" - joints = goal_position_with_joints.field("joints") - goal_position = goal_position_with_joints.field("values") - - for i, joint in enumerate(joints): - self.data.joint(joint.as_py()).qpos[0] = goal_position[i].as_py() - - def write_mocap_pos(self, hand): - - #please, a method to access the mocap objects by name... - - if "r_tip1" in hand[0]: - [x,y,z]=hand[0]['r_tip1'].values - self.data.mocap_pos[0]=[x.as_py()*1.5-0.025,y.as_py()*1.5+0.022,z.as_py()*1.5+0.098] - if "r_tip2" in hand[0]: - [x,y,z]=hand[0]['r_tip2'].values - self.data.mocap_pos[1]=[x.as_py()*1.5-0.025,y.as_py()*1.5-0.009,z.as_py()*1.5+0.092] - if "r_tip3" in hand[0]: - [x,y,z]=hand[0]['r_tip3'].values - self.data.mocap_pos[2]=[x.as_py()*1.5-0.025,y.as_py()*1.5-0.040,z.as_py()*1.5+0.082] - if "r_tip4" in hand[0]: - [x,y,z]=hand[0]['r_tip4'].values - self.data.mocap_pos[3]=[x.as_py()*1.5+0.024,y.as_py()*1.5+0.019,z.as_py()*1.5+0.017] - - - def write_mocap_quat(self, hand): - #please, a method to access the mocap objects by name... - - if "r_tip1" in hand[0]: - [w,x,y,z]=hand[0]['r_tip1'].values - self.data.mocap_quat[0]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "r_tip2" in hand[0]: - [w,x,y,z]=hand[0]['r_tip2'].values - self.data.mocap_quat[1]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "r_tip3" in hand[0]: - [w,x,y,z]=hand[0]['r_tip3'].values - self.data.mocap_quat[2]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - if "r_tip4" in hand[0]: - [w,x,y,z]=hand[0]['r_tip4'].values - self.data.mocap_quat[3]=[w.as_py(),x.as_py(),y.as_py(),z.as_py()] - - - - -def main(): - """Handle dynamic nodes, ask for the name of the node in the dataflow.""" - - parser = argparse.ArgumentParser() - parser.add_argument("-m", "--mode", type=str, choices=['pos','quat'], default='pos', - help="control mode: pos=position (we control the position of the tip) quat=quaternion (we control the orientation of the tip)") - args = parser.parse_args() - client = Client(args.mode) - client.run() +"""Mujoco client node for right-hand simulation.""" +from mj_mink_common import run_main if __name__ == "__main__": - main() + run_main("right") diff --git a/Demo/tests/test_mj_mink.py b/Demo/tests/test_mj_mink.py new file mode 100644 index 0000000..ad2b41c --- /dev/null +++ b/Demo/tests/test_mj_mink.py @@ -0,0 +1,147 @@ +# Copyright (C) 2026 Julia Jia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import unittest + +_DEMO_ROOT = Path(__file__).resolve().parent.parent +_AH_SIM = _DEMO_ROOT / "AHSimulation" / "AHSimulation" +if str(_AH_SIM) not in sys.path: + sys.path.insert(0, str(_AH_SIM)) + +try: + import mj_mink_common as common +except Exception: + common = None + + +def _scene_exists(side): + scene_dir = "AH_Right" if side == "right" else "AH_Left" + return (_AH_SIM / scene_dir / "mjcf" / "scene.xml").exists() + + +@unittest.skipIf(common is None, "mj_mink_common not importable (mujoco/mink/dora)") +class TestMocapOffsets(unittest.TestCase): + def test_has_right_and_left(self): + self.assertIn("right", common.MOCAP_POS_OFFSETS) + self.assertIn("left", common.MOCAP_POS_OFFSETS) + + def test_each_side_has_four_tips(self): + for side in ("right", "left"): + with self.subTest(side=side): + offsets = common.MOCAP_POS_OFFSETS[side] + self.assertEqual(len(offsets), 4, f"{side} should have 4 tip offsets") + + def test_each_offset_is_three_floats(self): + for side in ("right", "left"): + for i, t in enumerate(common.MOCAP_POS_OFFSETS[side]): + with self.subTest(side=side, tip=i): + self.assertEqual(len(t), 3) + for v in t: + self.assertIsInstance(v, (int, float)) + + +@unittest.skipIf(common is None, "mj_mink_common not importable") +class TestClientValidation(unittest.TestCase): + def test_invalid_side_raises(self): + with self.assertRaises(ValueError) as ctx: + common.Client(side="invalid", mode="pos") + self.assertIn("side must be 'left' or 'right'", str(ctx.exception)) + + def test_invalid_mode_raises_when_model_loaded(self): + if not _scene_exists("right"): + self.skipTest("AH_Right scene.xml not found") + with self.assertRaises(ValueError) as ctx: + common.Client(side="right", mode="bad") + self.assertIn("unknown mode", str(ctx.exception)) + + +@unittest.skipIf(common is None, "mj_mink_common not importable") +@unittest.skipIf(not _scene_exists("right"), "AH_Right scene.xml not found") +class TestClientRightAttrs(unittest.TestCase): + def setUp(self): + with patch.object(common, "Node", MagicMock()): + self.client = common.Client(side="right", mode="pos") + + def test_prefix(self): + self.assertEqual(self.client.prefix, "r_") + + def test_joints_output(self): + self.assertEqual(self.client.joints_output, "mj_r_joints_pos") + + def test_hand_pos_id(self): + self.assertEqual(self.client.hand_pos_id, "r_hand_pos") + + def test_hand_quat_id(self): + self.assertEqual(self.client.hand_quat_id, "r_hand_quat") + + def test_tip_names(self): + self.assertEqual( + self.client.tip_names, + ["r_tip1", "r_tip2", "r_tip3", "r_tip4"], + ) + + def test_finger_keys(self): + self.assertEqual( + self.client.finger_keys, + ["r_finger1", "r_finger2", "r_finger3", "r_finger4"], + ) + + def test_mocap_offsets_is_right(self): + self.assertEqual( + self.client.mocap_offsets, + common.MOCAP_POS_OFFSETS["right"], + ) + + +@unittest.skipIf(common is None, "mj_mink_common not importable") +@unittest.skipIf(not _scene_exists("left"), "AH_Left scene.xml not found") +class TestClientLeftAttrs(unittest.TestCase): + def setUp(self): + with patch.object(common, "Node", MagicMock()): + self.client = common.Client(side="left", mode="pos") + + def test_prefix(self): + self.assertEqual(self.client.prefix, "l_") + + def test_joints_output(self): + self.assertEqual(self.client.joints_output, "mj_l_joints_pos") + + def test_hand_pos_id(self): + self.assertEqual(self.client.hand_pos_id, "l_hand_pos") + + def test_tip_names(self): + self.assertEqual( + self.client.tip_names, + ["l_tip1", "l_tip2", "l_tip3", "l_tip4"], + ) + + def test_finger_keys(self): + self.assertEqual( + self.client.finger_keys, + ["l_finger1", "l_finger2", "l_finger3", "l_finger4"], + ) + + def test_mocap_offsets_is_left(self): + self.assertEqual( + self.client.mocap_offsets, + common.MOCAP_POS_OFFSETS["left"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/pixi.toml b/pixi.toml index 7d0d214..6af5bad 100644 --- a/pixi.toml +++ b/pixi.toml @@ -37,4 +37,5 @@ qpsolvers = { version = ">=4.7.1", extras = ["quadprog"] } # PYTHONPATH= avoids loading system/ROS pytest plugins when run locally test-python-example = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" test-demo = "cd Demo && python -m pytest tests/ -v" +test-demo-mj-mink = "python -m pytest Demo/tests/test_mj_mink.py -v" test-ahcontrol = "cargo test --manifest-path Demo/Cargo.toml -p AHControl" From a2f36972563b9f14d34dad5122fd18ea4409f829 Mon Sep 17 00:00:00 2001 From: juliaj Date: Fri, 20 Feb 2026 15:05:30 -0800 Subject: [PATCH 5/6] Fix CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b9c5c6..32b0326 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,9 @@ jobs: with: cache: ${{ !env.ACT }} + - name: Install system deps for AHControl (Rust) + run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config + - name: Run pre-commit run: pixi run pre-commit run --all-files From aebb365555f211969f25e7aa786c5505f5e3352c Mon Sep 17 00:00:00 2001 From: juliaj Date: Fri, 20 Feb 2026 15:22:40 -0800 Subject: [PATCH 6/6] Remove unused file --- Demo/AHControl/config/r_hand_team_julia.toml | 42 -------------------- 1 file changed, 42 deletions(-) delete mode 100644 Demo/AHControl/config/r_hand_team_julia.toml diff --git a/Demo/AHControl/config/r_hand_team_julia.toml b/Demo/AHControl/config/r_hand_team_julia.toml deleted file mode 100644 index 5f7ce2b..0000000 --- a/Demo/AHControl/config/r_hand_team_julia.toml +++ /dev/null @@ -1,42 +0,0 @@ -# Config for the right index finger1 -[Fingers] -[[motors]] -finger_name="r_finger1" -motor1.id = 1 -motor1.offset = 0.12217304763960307 -motor1.invert = false -motor1.model = "SCS0009" -motor2.id = 2 -motor2.offset = 0.08726646259971647 -motor2.invert = false -motor2.model = "SCS0009" -[[motors]] -finger_name="r_finger2" -motor1.id = 3 -motor1.offset = 0.0 -motor1.invert = false -motor1.model = "SCS0009" -motor2.id = 4 -motor2.offset = 0.12217304763960307 -motor2.invert = false -motor2.model = "SCS0009" -[[motors]] -finger_name="r_finger3" -motor1.id = 5 -motor1.offset = 0.08726646259971647 -motor1.invert = false -motor1.model = "SCS0009" -motor2.id = 6 -motor2.offset = 0.12217304763960307 -motor2.invert = false -motor2.model = "SCS0009" -[[motors]] -finger_name="r_finger4" -motor1.id = 7 -motor1.offset = 0.0 -motor1.invert = false -motor1.model = "SCS0009" -motor2.id = 8 -motor2.offset = 0.12217304763960307 -motor2.invert = false -motor2.model = "SCS0009"