Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b3e1f8b
Merge pull request #17 from AD-SDL/grab_offset
Dozgulbas Dec 16, 2025
e99e54e
Bug fix for rotations with pick plate
Dozgulbas Feb 4, 2026
eee8739
Merge pull request #23 from AD-SDL/rotation_fix
RyanTheRobothead Feb 5, 2026
a9131ed
Local changes
Dozgulbas Feb 4, 2026
0aecc12
Initial migration to madsci v0.7 for testing in the RPL
Dozgulbas Mar 2, 2026
e11d200
Automatic configuration documentation
Dozgulbas Mar 2, 2026
5bf957b
Update to v0.7, add move_to_location and move_neutral actions
Dozgulbas Mar 9, 2026
0ad964a
Update pre-commit workflow to use pdm + uv
Dozgulbas Mar 9, 2026
c3f71d1
Update pdm lockfile
Dozgulbas Mar 9, 2026
6d06b41
Initialize pdm
Dozgulbas Mar 9, 2026
a7f3a8b
Merge pull request #25 from AD-SDL/madsci_0_7
RyanTheRobothead Mar 9, 2026
3a1bfdb
Removed old kinematics
Dozgulbas May 13, 2026
deb066d
Using new internal Kinematics
Dozgulbas May 13, 2026
65053ad
Custom TCS server methods
Dozgulbas May 13, 2026
01dfed3
TCS instructions
Dozgulbas May 13, 2026
528a906
Added new implementations to you Force Compliance & Height Detect
Dozgulbas May 19, 2026
856c374
Updated TCS Custom.gpl to add Force Compliance
Dozgulbas May 19, 2026
f77f7f2
Updated docs
Dozgulbas May 19, 2026
2dd75e7
Interface tests
Dozgulbas May 19, 2026
ab6dfeb
Merge branch 'interface_updates' of https://github.com/AD-SDL/pf400_m…
Dozgulbas May 19, 2026
77676fa
Using hardcoded compliance values
Dozgulbas May 19, 2026
1851139
More tests
Dozgulbas May 19, 2026
5850f5d
Use horizontal compliance with pick & place
Dozgulbas May 27, 2026
929eeee
Notebooks to work on the TCP settings
Dozgulbas May 27, 2026
7c1cae3
Interface tests
Dozgulbas May 27, 2026
986288d
Merge branch 'update_action_locations' into interface_updates
Dozgulbas May 28, 2026
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
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### PF400NodeConfig

# NODE_STATUS_UPDATE_INTERVAL=2.0
# NODE_STATE_UPDATE_INTERVAL=2.0
# NODE_NODE_NAME=null
# NODE_NODE_ID=null
# NODE_NODE_TYPE=null
# NODE_MODULE_NAME=null
# NODE_MODULE_VERSION=null
# NODE_URL="http://127.0.0.1:2000/"
# NODE_UVICORN_KWARGS={"limit_concurrency":10}
# NODE_ENABLE_RATE_LIMITING=true
# NODE_RATE_LIMIT_REQUESTS=100
# NODE_RATE_LIMIT_WINDOW=60
# NODE_RATE_LIMIT_SHORT_REQUESTS=50
# NODE_RATE_LIMIT_SHORT_WINDOW=1
# NODE_RATE_LIMIT_CLEANUP_INTERVAL=300
# NODE_PF400_IP=null
# NODE_PF400_PORT=10100
# NODE_PF400_STATUS_PORT=10000
9 changes: 9 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

# Automatically sets up your devbox environment whenever you cd into this
# directory via our direnv integration:

eval "$(devbox generate direnv --print-envrc)"

# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
# for more details
14 changes: 10 additions & 4 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
name: Checkout code
- uses: actions/setup-python@v4
name: Setup Python
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.9
python-version: "3.10"
- name: Install dependencies
env:
PDM_USE_UV: "True"
run: pdm install
- uses: pre-commit/action@v3.0.1
name: Run Pre-Commit Checks
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ cython_debug/
#.idea/

.vscode
.madsci/

### macOS ###
# General
Expand Down
11 changes: 9 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/kynan/nbstripout
rev: 0.8.1
rev: 0.9.1
hooks:
- id: nbstripout
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.15.4
hooks:
# Run the linter.
- id: ruff
args: [--fix]
exclude: '\.ipynb$' # Exclude Jupyter Notebooks from linting.
# Run the formatter.
- id: ruff-format
- repo: https://github.com/jag-k/pydantic-settings-export
rev: v1.0.3
hooks:
- id: pydantic-settings-export
entry: pdm run pydantic-settings-export
language: system
files: ^src/pf400_rest_node\.py$
exclude: '\.ipynb$' # Exclude Jupyter Notebooks from all hooks.
106 changes: 106 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# AGENTS.md/CLAUDE.md

This file provides guidance to AI Agents. Note that CLAUDE.md and AGENTS.md are symlinked together.

## Overview

MADSci node module for the Brooks Automation PreciseFlex 400 (PF400) robot arm. Exposes a FastAPI REST server that translates MADSci actions into telnet commands sent to the physical robot.

## Installation and Running

```bash
# Install
pdm install

# Configure (see Configuration section below), then run
pdm run python -m pf400_rest_node

# Run via Docker
docker compose up
```

Configuration is loaded automatically via MADSci's walk-up settings discovery. The Docker `compose.yaml` sets `PF400_IP` via environment variable.

## Linting

```bash
# Lint
ruff check src/

# Format
ruff format src/

# Fix auto-fixable issues
ruff check --fix src/
```

No pytest test suite — tests are Jupyter notebooks in `tests/` (`test_pf400_interface.ipynb`, `test_pf400_node.ipynb`) and require a live robot connection.

## Code Architecture

Three layers, each building on the previous:

### 1. `src/pf400_rest_node.py` — MADSci REST Node
`PF400Node(RestNode)` implements the MADSci node interface:
- `startup_handler`: initializes MADSci resource templates (gripper slot, lid slot, plate lid asset) and instantiates `PF400`
- `state_handler`: polls `movement_state` (0=power off, 1=ready, >1=busy)
- Actions decorated with `@action`: `transfer`, `pick_plate`, `place_plate`, `remove_lid`, `replace_lid`
- Config via `PF400NodeConfig(RestNodeConfig)` — adds `pf400_ip`, `pf400_port` (10100), `pf400_status_port` (10000)

### 2. `src/pf400_interface/pf400.py` — Robot Driver
`PF400(KINEMATICS)` communicates with the robot via **two telnet connections**:
- Command connection: port 10100 — sends GPL (robot programming language) commands
- Status connection: port 10000 — reads robot state

Key driver concepts:
- Locations are represented as 6-element joint-angle arrays: `[z_mm, shoulder_deg, elbow_deg, wrist_deg, gripper_mm, rail_mm]`
- Motion profiles (from `pf400_constants.py`): profile 1 (slow), profile 2 (fast, 120 speed), profile 3 (straight-line)
- `grip_wide=True` → gripper opens to 130mm / closes to 127mm; `False` → 90mm / 85mm
- Plate rotation: `""` or `"narrow"` → 0° wrist offset; `"wide"` → 90° wrist offset
- `default_approach_height = 15.0` steps above/below the target position for approach moves

Key methods:
- `transfer(source, target, ...)` — full pick+place with optional approach waypoints and plate rotation
- `pick_plate(source, ...)` / `place_plate(target, ...)` — individual pick/place
- `remove_lid(source, target, lid_height=7.0, ...)` / `replace_lid(...)` — lid handling
- `check_incorrect_plate_orientation(representation, rotation)` — corrects joint angles when orientation mismatches

### 3. `src/pf400_interface/pf400_kinematics.py` — Kinematics
`KINEMATICS` base class with forward/inverse kinematics:
- Arm segments: shoulder=302mm, elbow=289mm, end-effector=162mm
- Joint coordinate order: `[z, shoulder, elbow, wrist, gripper, rail]`
- Cartesian coordinate order: `[X, Y, Z, yaw, pitch=90, roll=180]`
- Rail (joint[5]) is the horizontal position and must be subtracted from X before calling `inverse_kinematics`

### Supporting files
- `pf400_constants.py` — `ERROR_CODES`, `MOTION_PROFILES`, `OUTPUT_CODES` dicts
- `pf400_errors.py` — `Pf400ConnectionError`, `Pf400CommandError`, `Pf400ResponseError`
- `src/keyboard_control.py` — interactive keyboard teleoperation utility (arrow keys = XY, W/S = Z, N = neutral)

## Configuration

Configuration uses MADSci's Pydantic Settings system. Settings are loaded via walk-up file discovery (searching from CWD up to the `.madsci/` sentinel directory), with this priority order: CLI args → environment variables → `node.settings.yaml` → `settings.yaml`.

Example `node.settings.yaml`:

```yaml
node_name: pf400
node_url: http://0.0.0.0:2000
pf400_ip: 192.168.1.100
pf400_port: 10100
pf400_status_port: 10000
```

All settings can also be set via environment variables (e.g. `PF400_IP`, `NODE_URL`).

The node's stable identity (ID) is persisted in the `.madsci/registry.json` file found by the same walk-up discovery. The node registers itself under its `node_name` on first startup and reuses the same ULID on subsequent restarts.

`definitions/pf400.info.yaml` is auto-generated from the node's declared actions and capabilities — do not edit it manually.

| Parameter | Default | Description |
|---|---|---|
| `pf400_ip` | None (required) | Robot IP address |
| `pf400_port` | 10100 | Command telnet port |
| `pf400_status_port` | 10000 | Status telnet port |
| `node_url` | `http://127.0.0.1:2000/` | REST API base URL |
| `status_update_interval` | 2.0s | State polling interval |
1 change: 1 addition & 0 deletions CLAUDE.md
14 changes: 8 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ LABEL org.opencontainers.image.licenses=MIT
# Module specific logic goes below here #
#########################################

RUN mkdir -p pf400_module
ARG USER_ID=9999
ARG GROUP_ID=9999

COPY ./src pf400_module/src
COPY ./README.md pf400_module/README.md
COPY ./pyproject.toml pf400_module/pyproject.toml
COPY ./src /home/madsci/pf400_module/src
COPY ./README.md /home/madsci/pf400_module/README.md
COPY ./pyproject.toml /home/madsci/pf400_module/pyproject.toml

RUN --mount=type=cache,target=/root/.cache \
pip install -e ./pf400_module
uv pip install --python ${MADSCI_VENV}/bin/python -e /home/madsci/pf400_module && \
chown -R ${USER_ID}:${GROUP_ID} /home/madsci/pf400_module

CMD ["python", "pf400_module/scripts/pf400_rest_node.py"]
CMD ["python", "-m", "pf400_rest_node"]

#########################################
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,32 @@

Implementation of a MADSci Node Module for integrating a Brooks Automation PreciseFlex 400 (PF400).

See `definitions/pf400.node.yaml` for an example node definition file, and `definitions/pf400.node.info.yaml` for a description of the capabilities of the node.

## Installation and Usage

### Python

This project uses [PDM](https://pdm-project.org/en/latest/#installation) for dependency management.

```bash
# Create a virtual environment named .venv
python -m venv .venv
# Activate the virtual environment on Linux or macOS
source .venv/bin/activate
# Alternatively, activate the virtual environment on Windows
# .venv\Scripts\activate
# Install the module and dependencies in the venv
pip install .
# Start the node
python -m pf400_rest_node --host=<HOSTNAME> --port <PORT> --pf400_ip <IP> --pf400_port <PORT>
# Install dependencies
pdm install
# Create a settings file (see Configuration below), then start the node
pdm run python -m pf400_rest_node
```

### Configuration

Settings are loaded automatically via MADSci's walk-up file discovery. Create a `node.settings.yaml` in your working directory (or any parent up to the `.madsci/` sentinel):

```yaml
node_name: pf400
node_url: http://0.0.0.0:2000
pf400_ip: 192.168.1.100
pf400_port: 10100
pf400_status_port: 10000
```

You can use `0.0.0.0` as the hostname to connect from any device on the local network, or `127.0.0.1` to limit it only to local connections.
All settings can also be provided as environment variables — see [`.env.example`](.env.example) for the full list. For detailed descriptions of every option, see [`docs/Configuration.md`](docs/Configuration.md). The node's stable ID is stored in `.madsci/registry.json` and reused across restarts.

### Docker

Expand Down
9 changes: 6 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ services:
environment:
- USER_ID=${USER_ID:-1000}
- GROUP_ID=${GROUP_ID:-1000}
- PF400_IP=146.137.240.35
env_file:
- path: .env
required: false
build:
context: .
dockerfile: Dockerfile
tags:
- ghcr.io/ad-sdl/pf400_module:latest
- ghcr.io/ad-sdl/pf400_module:dev
command: python -m pf400_rest_node --node_definition definitions/pf400.node.yaml
command: python -m pf400_rest_node
volumes:
- ./definitions:/home/madsci/definitions/
- ./settings.yaml:/home/madsci/settings.yaml
- ./.madsci:/home/madsci/.madsci
Loading
Loading