Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
- name: Run pre-commit
run: pixi run pre-commit run --all-files

- name: SDK unit tests
run: pixi run test-sdk

- name: PythonExample unit tests
run: pixi run test-python-example

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ push.json

# Python package metadata (pip install -e)
*.egg-info/
dist/

# Rust build output
target/

# Dora / local output and logs
Demo/out/
out/
*.code-workspace
8 changes: 0 additions & 8 deletions AmazingHand.code-workspace

This file was deleted.

2 changes: 1 addition & 1 deletion Demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Running with pixi

Prerequisites: install [Pixi](https://pixi.prefix.dev/latest/installation/). Rust is needed for real hardware demos (AHControl). Before running real hardware demos, check the serial port in the dataflow YAML: the default is Linux (`/dev/ttyACM0`); on Windows use your COM port (e.g. `COM3`).
Prerequisites: install [Pixi](https://pixi.prefix.dev/latest/installation/). Rust is needed for real hardware demos (AHControl). Before running real hardware demos, check the serial port in the dataflow YAML: the default is Linux (`/dev/ttyACM0`); on Windows use your COM port (e.g. `COM3`). Run `pixi run check-devices` to list webcam indices and serial ports.

From the AmazingHand repository root (Git Bash on Windows, or a Unix shell on Linux/macOS):

Expand Down
28 changes: 28 additions & 0 deletions Demo/dataflow_tracking_real_team_krishan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# team_krishan (left hand), Windows: AHControl.exe, COM port.
# Dora spawns nodes with cwd = Demo/, path relative to Demo.
# See docs/canonical_hand_config_design.md.
nodes:
- id: hand_tracker
build: python -m pip install -e HandTracking
path: HandTracking/HandTracking/main.py
inputs:
tick: dora/timer/millis/50
outputs:
- l_hand_pos

- id: l_hand_simulation
build: python -m pip install -e AHSimulation
path: AHSimulation/AHSimulation/mj_mink_left.py
inputs:
l_hand_pos: hand_tracker/l_hand_pos
tick: dora/timer/millis/2
tick_ctrl: dora/timer/millis/10
outputs:
- mj_l_joints_pos

- id: hand_controller
build: cargo build -p AHControl
path: target/debug/AHControl.exe
args: --serialport COM3 --config ../config/calibration/l_hand_team_krishan.toml
inputs:
mj_l_joints_pos: l_hand_simulation/mj_l_joints_pos
126 changes: 126 additions & 0 deletions Demo/scripts/check_devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# 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.

"""Check webcam indices and serial ports. Run before demos to verify devices."""

import os
import sys

_MAX_CAMERA_PROBE = 3


def check_webcams():
"""Probe camera indices 0..N and report which open successfully."""
try:
import cv2
except ImportError:
print("Webcam: opencv-python not installed (pip install opencv-python)")
return
available = []
devnull = os.open(os.devnull, os.O_WRONLY)
stderr_fd = os.dup(2)
try:
os.dup2(devnull, 2)
for i in range(_MAX_CAMERA_PROBE):
cap = cv2.VideoCapture(i)
if cap.isOpened():
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
available.append((i, w, h))
cap.release()
finally:
os.dup2(stderr_fd, 2)
os.close(stderr_fd)
os.close(devnull)
if available:
print("Webcam indices:")
for idx, w, h in available:
print(f" {idx}: {w}x{h}")
print(" Use index 0 in HandTracking unless you have multiple cameras.")
if len(available) >= 2:
print(" Note: indices 0 and 1 may be the same camera (Linux exposes multiple /dev/video* nodes). Try 0 first; if wrong, try 1.")
else:
print(f"Webcam: no cameras found (indices 0..{_MAX_CAMERA_PROBE - 1})")


def _is_usb_port(device):
"""Prefer USB serial devices (hand controller) over built-in ttyS."""
d = device.upper()
return "ACM" in d or "USB" in d or d.startswith("COM")


def _port_status(device):
"""Try opening port. Return 'available', 'in_use', or 'no_permission'."""
try:
import serial
except ImportError:
return None
try:
ser = serial.Serial(device, timeout=0.1)
ser.close()
return "available"
except OSError as e:
if e.errno == 16: # EBUSY
return "in_use"
if e.errno in (13, 1): # EACCES, EPERM
return "no_permission"
return None
except Exception as e:
msg = str(e).lower()
if "busy" in msg or "in use" in msg or "resource" in msg:
return "in_use"
if "denied" in msg or "permission" in msg or "access" in msg:
return "no_permission"
return None


def check_serial_ports():
"""List serial ports. Linux: /dev/ttyACM*, /dev/ttyUSB*. Windows: COM*."""
try:
import serial.tools.list_ports
except ImportError:
print("Serial: pyserial not installed (pip install pyserial)")
return
ports = list(serial.tools.list_ports.comports())
usb = [p for p in ports if _is_usb_port(p.device)]
other = [p for p in ports if p not in usb]
shown = usb or other
if shown:
print("Serial ports:")
for p in sorted(shown, key=lambda x: (not _is_usb_port(x.device), x.device)):
desc = p.description or ""
note = " (USB, likely hand)" if _is_usb_port(p.device) else ""
status = _port_status(p.device)
if status == "in_use":
note += " [IN USE by another process]"
elif status == "no_permission":
note += " [no permission; Linux: add user to dialout group]"
print(f" {p.device}: {desc}{note}")
if usb:
print(" Use the USB device in dataflow YAML --serialport.")
else:
print("Serial: no serial ports found. Connect the hand via USB and ensure drivers are installed.")


def main():
print("=== AmazingHand device check ===\n")
check_webcams()
print()
check_serial_ports()
return 0


if __name__ == "__main__":
sys.exit(main())
22 changes: 21 additions & 1 deletion FORK.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Differences from Upstream

See the fork notice in [README.md](README.md). This file is for documenting how this repo differs from [pollen-robotics/AmazingHand](https://github.com/pollen-robotics/AmazingHand) (e.g. layout, tooling, refactors).
See the fork notice in [README.md](README.md). This file documents how this repo differs from [pollen-robotics/AmazingHand](https://github.com/pollen-robotics/AmazingHand).

## Cross-Platform Support (Linux, Windows)

Pixi-based setup; MSVC toolchain for Rust on Windows. See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).

## Canonical Configuration

Shared hand geometry, per-physical-hand calibration, named profiles. See [docs/canonical_hand_config_design.md](docs/canonical_hand_config_design.md).

## AmazingHand SDK

Python package for named poses and raw angles. See [README_PKG.md](README_PKG.md).

## CI

GitHub Actions for lint (pre-commit), SDK, PythonExample, Demo, and AHControl tests. See [.github/workflows/ci.yml](.github/workflows/ci.yml).

## Other Changes

Pixi for dependency management; unit tests for SDK, PythonExample, Demo, and AHControl; Dora/MuJoCo simulation demos; pre-commit hooks.
10 changes: 9 additions & 1 deletion PythonExample/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
import sys
from pathlib import Path
Expand All @@ -26,6 +27,7 @@
except ImportError:
tomllib = None

_log = logging.getLogger(__name__)
_REPO_ROOT = Path(__file__).resolve().parent.parent
_CANONICAL_CONFIG_ROOT = _REPO_ROOT / "config"
_PROFILE_ENV = "AMAZINGHAND_PROFILE"
Expand Down Expand Up @@ -155,10 +157,16 @@ def load_config_canonical(profile=None, config_root=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()
name = (profile or os.environ.get(_PROFILE_ENV) or "team_krishan").strip().lower()
source = "argument" if profile else ("env " + _PROFILE_ENV if os.environ.get(_PROFILE_ENV) else "default")
section = data.get("profile", {}).get(name, {})
if not section:
_log.warning(
"Profile %r not found in profiles.toml; using defaults. Set %s to a valid profile name.",
name, _PROFILE_ENV
)
return _DEFAULTS.copy()
print(f"Using profile {name!r} (from {source}). Set {_PROFILE_ENV} to override.")
out = {
"port": (section.get("port") or _DEFAULTS["port"]) or "",
"baudrate": section.get("baudrate", _DEFAULTS["baudrate"]),
Expand Down
44 changes: 44 additions & 0 deletions README_PKG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# amazinghand

Python SDK for Amazing Hand robotic hand (Pollen Robotics). Control the hand via named poses and raw angles.

## Install

```bash
pip install amazinghand
```

## Quick start

```python
from amazinghand import AmazingHand, list_poses

print("Available poses:", list_poses())
hand = AmazingHand(profile="default")
hand.apply_pose("rock")
hand.apply_pose("paper")
hand.apply_pose("scissors")
```

## Configuration

Set `AMAZINGHAND_CONFIG` to your config directory, or place config under `~/.config/amazinghand` (Linux) / `%LOCALAPPDATA%\amazinghand` (Windows).

Config resolution order:

1. `AMAZINGHAND_CONFIG` env
2. `config_root` argument to `AmazingHand()`
3. Repo config when running from source
4. User config dir
5. Bundled config (pip-installed)

Environment variables:

- `AMAZINGHAND_CONFIG`: path to directory with `profiles.toml` and `calibration/`
- `AMAZINGHAND_PROFILE`: profile name (default: `default`)

To add a calibration: copy `calibration/right_hand.toml` to your file, fill in servo IDs and `rest_deg`, add a profile in `profiles.toml`, and reference it via `right_hand_calibration` / `left_hand_calibration`.

## License

Apache 2.0
34 changes: 34 additions & 0 deletions docs/maintainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,38 @@ act push
act pull_request
```

## Publishing to PyPI

Build the package:

```bash
pixi run build-python
```

Publish to Test PyPI and PyPI:

```bash
pixi run publish-testpypi
pixi run publish-pypi
```

Configure credentials in `~/.pypirc` so twine does not prompt:

```ini
[distutils]
index-servers =
pypi
testpypi

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-xxxxxxxx

[pypi]
username = __token__
password = pypi-xxxxxxxx
```

Replace `pypi-xxxxxxxx` with your API token from PyPI account settings. Create separate tokens for Test PyPI and PyPI if needed.

Loading