Files
2026-10-23 15:52:51 +02:00

231 lines
12 KiB
Markdown

# ecmc six-DOF robot arm — simulation prototype
![Six-joint arm simulation](docs/arm.png)
A generic six-revolute-joint industrial-style arm with C++ forward and inverse
kinematics, twelve ecmc axes, and an offline/read-only EPICS 3D viewer.
This is an example geometry, not a calibrated model of a commercial robot.
**All twelve axes are virtual in this simulation.** `virt` and `phys` are group
names: `virt` contains the six Cartesian tool coordinates; `phys` contains the
six simulated rotary joints. No physical drives or EtherCAT hardware are used.
| Axis IDs | Group | Meaning | Trajectory | Encoder feedback |
|---|---|---|---|---|
| 1–6 | `virt` | X, Y, Z, Rx, Ry, Rz offsets from home tool pose | Internal | FK from joint encoders |
| 7–12 | `phys` | J1–J6 angular offsets from home joints | External IK, or internal for direct joint moves | Ideal copy of selected joint trajectory |
## Run the offline demo
```sh
make test
python3 -m venv .venv
.venv/bin/pip install -r python/requirements.txt
.venv/bin/python python/viewer.py
# Direct joint controls instead of Cartesian controls:
.venv/bin/python python/viewer.py --joints
# Headless screenshot:
.venv/bin/python python/viewer.py --snapshot build/arm.png
```
The six sliders set Cartesian home offsets, then **Move (3 seconds)** starts a
shared quintic interpolation. Blue shows joint feedback and orange the IK
command. An unsuccessful IK solve retains the last valid joints and displays an
error. Retargeting starts a new profile without velocity continuity. Joint mode
interpolates joint offsets instead. The offline plant is ideal; it has no lag,
load, drive dynamics, collision checks or physical safety system.
Both Python and the IOC share `src/kinematics.hpp`. The viewer calls a small
C ABI in `src/c_api.cpp`; `make viewer-lib` rebuilds `build/libarm.so` (`.dylib`
on macOS). No EPICS libraries or Python GUI dependencies are needed by `make test`;
the Makefiles use the PSI `/ioc/tools` build infrastructure.
## Arm geometry and conventions
Lengths are in mm; joint and Cartesian Euler angles are in degrees. The base
frame is right handed, with +Z upward. Revolute axes act in each successive
local frame. With transforms multiplied in the order written:
```text
T = Rz(q1) Tz(H) Ry(q2) Tx(A) Ry(q3) Tx(B)
Rx(q4) Ry(q5) Rx(q6) Tx(Tool)
```
The last three axes intersect at the wrist centre. The eight plotted points
include coincident wrist joints; the drawing is a link skeleton, not solid CAD.
`cfg/geometry.json` contains `[H, A, B, Tool]`, default `[300, 300, 250, 80]`.
The IOC uses macros `BASE_HEIGHT`, `UPPER_ARM`, `FOREARM`, `TOOL` in the
`MACROS` argument in `cfg/load.cmd`. They have the same defaults. Keep JSON and
IOC macros identical; neither side automatically reads the other's geometry.
| Joint | Local rotation | Home angle | Absolute model limits | Axis offset limits |
|---|---|---:|---|---|
| J1 | Z | 0° | −170…170° | −170…170° |
| J2 | Y | −35° | −120…120° | −85…155° |
| J3 | Y | 70° | −140…140° | −210…70° |
| J4 | X | 0° | −180…180° | −180…180° |
| J5 | Y | 35° | −120…120° | −155…85° |
| J6 | X | 0° | −180…180° | −180…180° |
The chosen home avoids a straight elbow/wrist and gives a useful local workspace.
All IOC axes start at **zero offsets**, representing these nominal joint angles.
This is mathematical initialization, not a homing or calibration procedure.
Cartesian XYZ are base-frame displacements from the home tool position. For
orientation, `R = Rz(Rz) Ry(Ry) Rx(Rx) R_home`. Thus zero Cartesian angles mean
the home tool orientation, not alignment of the tool with the base. The Euler
representation has a singularity at Ry = ±90°. FK computes absolute tool
transforms directly, then converts them into these Cartesian offsets for EPICS.
To model a particular robot, replace the transform chain, joint limits and home
configuration consistently in C++, YAML and viewer. Arbitrary URDF loading,
manufacturer-specific wrist offsets and calibration are not implemented.
## Inverse kinematics
IK is a bounded, local damped least-squares solve, seeded from the previous
accepted joint command (nominal home initially). It minimizes a six-component
residual: three translation errors and the rotation logarithm of
`R_target R_actual^T`, weighted by 200 mm/radian. Orientation residuals use
rotation matrices rather than subtracting Euler angles.
A central-difference Jacobian uses 1e-5-radian perturbations. Each solve allows
at most 100 iterations, clamps trial angles to joint limits, limits steps to
0.2 radians, and adapts damping according to whether the error decreases.
Convergence requires weighted residual norm below 1e-5. All cyclic arrays have
fixed size; the solver performs no file I/O or heap allocation.
The seed encourages a nearby continuous solution. It does not enumerate all
elbow/wrist configurations or guarantee the nearest branch. Unreachable targets,
joint limits, singularities and poor seeds can prevent convergence. No explicit
Jacobian-condition monitor or global path planner is provided. A reachable
endpoint alone does not validate the path to it.
## ecmc IOC
Build on the Linux IOC development host with the source headers matching the
installed ecmc version:
```sh
make
make pvs CPP_LOGIC_ROOT=/path/to/ecmccfg/examples/PSI/plugins/cpp_logic
make test-logic ECMC_ROOT=/path/to/ecmc
```
The Makefiles follow the hexapod project's PSI pattern: the top-level
Makefile includes `/ioc/tools/sls_ioc.makefile` and delegates `make build` to
`src/Makefile`, which includes `/ioc/tools/driver.makefile`. The module is `main`,
uses C++17, targets `deb10%` / `deb12%`, and requires `ecmc v11.2.0_RC1`.
Use the normal PSI build/install workflow to stage `bin/libmain.so`, together
with `cfg/` and the startup script. `make logic` is an alias for the IOC build.
`build` is phony so the local `build/` directory cannot suppress IOC compilation.
Custom PV substitutions are checked in as `cfg/libmain.so_cpp_logic.subs`.
`make pvs` regenerates them from `src/logic.cpp`; set `CPP_LOGIC_ROOT` to the
source tree's `examples/PSI/plugins/cpp_logic` directory. `make ui` uses the same
path to generate `qt/MTEST04-MTN-ARM_cpp_logic.ui`. The viewer's local C API
library remains a separate `make viewer-lib` target.
`MTEST04-MTN-ARM_startup.script` follows the ecmccfg `cpp_logic` examples and
requires `ecmccfg v11.2.0_RC1` / `ecmc v11.2.0_RC1`, no EtherCAT master,
and `EC_RATE=1000`. Start with your normal PSI IOC launcher from this project
directory; the launcher supplies `iocInit`, and ecmccfg finalizes at that point.
The parameters YAML is a deployment example: adjust host, port and version for
your installation. Axis IDs 1–12 and the default state-machine ID 0 must be free.
The startup loads the shared Cartesian YAML six times and the six numbered
joint YAML files (each with its own numeric limits), then C++ logic, then
`Arm_SM` (`virt` master group, `phys` slave group). Each axis has motor records,
source changes allowed while enabled, 1 s auto-enable timeout, 2 s auto-disable
delay, and at-target monitoring with a 0.01-unit band for 100 cycles (100 ms
at 1 kHz). Default velocity is 1 mm/s for XYZ and 1°/s for rotation/joints.
Joint soft limits are configured in home-relative degrees. Cartesian soft limits
are disabled; IK checks joint limits on each accepted command.
C++ binds to asyn `axN.setpos` and `axN.actpos`, not PLC object paths.
IK feeds external joint setpoints. The ideal joint plant copies the selected
joint trajectory into the external encoder, so direct joint trajectories also
produce FK feedback. Axis/C++ ordering introduces cyclic pipeline latency.
A disabled simulated joint still follows this mathematical ideal plant.
**Use Cartesian `virt` axes for normal tool motion.** Independent `phys` joint
moves can cross singularities, change assembly branches or cause collisions.
Direct joint moves are for controlled simulation/commissioning checks, with the
state machine selecting internal joint trajectories. Before returning to
Cartesian motion, synchronize Cartesian commands to actual pose and verify the
IK seed/branch; automatic bumpless mode transfer is not implemented.
Fault codes are latched: 0 OK; 1 invalid input or joint bounds; 2 IK failure;
3 host output write failure; 4 axis error. Fix the cause and restart/reload.
A fault inhibits new IK trajectory commands, but valid simulated joint encoder
and FK feedback continue following the selected joint trajectories. This is not
a coherent group stop, and a failed write can leave partial updates. C++ faults are not wired into a safety-rated stop system.
The built-in master/slave state machine does not replace that integration.
Independent Cartesian motor moves are not a coordinated six-axis Cartesian path.
No joint-space speed/acceleration limiting of IK output, collision avoidance,
singularity avoidance, torque control, real drives or physical safety integration
is implemented. This is simulation software, not a hardware motion controller
ready for commissioning. Hexapod timing measurements do not apply to this arm.
Measure the complete IOC cycle and overruns before assuming 1 kHz operation.
## Read-only EPICS viewer and caQtDM
![Running ecmc arm IOC with caQtDM axis panels and the read-only 3D viewer](ecmc_arm_ioc_running.png)
```sh
.venv/bin/pip install -r python/requirements-ioc.txt
make viewer-lib
.venv/bin/python python/viewer.py --prefix MTEST04-MTN-ARM:
caqtdm -macro "IOC=MTEST04-MTN-ARM" ecmcCppLogicOverview.ui
caqtdm -macro "IOC=MTEST04-MTN-ARM,CPP_ID=0" ecmcCppLogic.ui
```
The standard caQtDM panels come from ecmccfg. There is no custom arm panel yet.
Live Python mode disables motion controls and subscribes to:
| PV suffix | Value |
|---|---|
| `arm-pose-set` / `arm-pose-actual` | Six home-relative tool coordinates, mm/degrees |
| `arm-joint-set` / `arm-joint-actual` | Six home-relative joint offsets, degrees |
| `arm-fault` | Latched adapter fault |
| `arm-fault-axis` / `arm-fault-axis-error` | First faulting axis ID and its error code, captured for fault 4 |
| `arm-joint-trajectory` | Selected trajectory of each joint axis, home-relative degrees |
| `arm-ik-residual` | Weighted IK residual norm |
| `arm-ik-iterations` | IK correction attempts |
| `arm-simulation` | Always 1 |
The viewer accepts a prefix with or without the trailing colon. Its geometry
must match the IOC. pyepics requires libca and CA connectivity. Callbacks update
a locked cache; GUI refresh does not issue blocking PV reads. Disconnected,
invalid and pending samples are reported. Separate PV records are not an atomic
IOC snapshot, and unchanged values do not constitute a heartbeat check.
## Validation
`make test` covers independent straight-arm/base-rotation checks, rotation error
at 180°, 500 near-home IK/FK round trips, 1,000 continuous joint-path samples,
invalid/unreachable inputs, singular pose holding, Python/C ABI round trips,
and mocked CA cache connection/value checks. `make test-logic` checks bindings,
IK/plant/FK timing, direct joint feedback, joint limits and latched faults.
A headless viewer snapshot checks rendering separately. A live robot-arm IOC and
physical robot have not been tested; the full workspace is not validated.
### Tracing simulated feedback
During a small move, compare `arm-joint-set` (IK command),
`arm-joint-trajectory` (each joint's selected trajectory), and `arm-joint-actual`
(encoder feedback). In this ideal simulation the latter two should agree after
cyclic pipeline latency. A changing IK command with stationary selected
trajectories points to the axis/source/enable/state-machine path. A changing
selected trajectory with stationary encoder feedback points to the simulation
plant, encoder-source selection, or a stopped C++ logic instance. The joint
`Jn-PosSet` and `Jn-PosAct` records expose those same axis values.
`arm-fault-axis` and `arm-fault-axis-error` capture the first axis error seen by
the adapter, even if that axis error later clears. Feedback continues through
latched IK/axis faults when joint samples are finite and within model limits;
new IK commands remain inhibited until reload/restart. This allows actual
feedback to follow an axis stop trajectory instead of freezing on the fault.