Arm Kinematics and Code Generation
The arm system is intentionally split across two environments:
- the toolchain does the heavy math on the development machine
- the runtime library executes pre-solved servo angles on the robot
That design is why the arm feature is practical on the Wombat. The robot does not need numpy, scipy, or ikpy to move to named arm positions during a match.
The Real Pipeline
graph LR
A["raccoon.project.yml
definitions.arm: ArmChain"] --> B["raccoon codegen"]
B --> C["ArmChainGenerator"]
C --> D["ikpy chain + IK solve"]
D --> E["joint angles
mathematical space"]
E --> F["joint_to_servo_deg()
servo command space"]
F --> G["generated defs.py
ArmPreset(...)"]
G --> H["runtime mission code
Defs.arm.home()"]
H --> I["parallel servo steps"]
For named positions, inverse kinematics runs during code generation. The generated defs.py contains literal servo angles. At runtime, ArmPreset only looks up those angles and issues servo steps.
Why It Works This Way
This split gives you:
- deterministic startup and match-time behavior
- no runtime IK dependency on the robot
- earlier failure for unreachable or unsafe positions
- generated Python stubs that expose named arm positions as methods
The engineering decision here is not “runtime IK is too hard.” It is that competition robots benefit more from compile-time validation than from solving the same arm geometry repeatedly during a run.
ArmChain YAML Shape
The arm lives in definitions: as type: ArmChain.
definitions:
shoulder_servo:
type: Servo
port: 0
elbow_servo:
type: Servo
port: 1
arm:
type: ArmChain
joints:
- servo: shoulder_servo
length_cm: 12.5
joint_range_deg: [0, 90]
servo_range_deg: [10, 130]
axis: [0, 1, 0]
mount_rpy_deg: [0, 0, 0]
- servo: elbow_servo
length_cm: 10
joint_range_deg: [-45, 45]
servo_range_deg: [150, 30]
axis: [0, 1, 0]
offset_cm: [0, 0, 0]
tip_offset_cm: [2, 0, 0]
workspace:
z_min_cm: 2
z_max_cm: 35
reach_max_cm: 25
forbidden_zones:
- name: hits_chassis
condition: "shoulder_servo_deg > 75 and elbow_servo_deg < -30"
positions:
home: {x: 10, y: 0, z: 15}
grab: {x: 18, y: 0, z: 5}
Joint Model
Each joint contributes:
servo: reference to an existing servo definitionlength_cm: structural link length along the joint’s local+Xjoint_range_deg: valid mathematical joint angle rangeservo_range_deg: physical servo command rangeaxis: rotation axis for IKmount_rpy_deg: fixed mount orientation of the jointoffset_cm: bracket offset from the previous link end to this joint pivot
Chain order is base to tip. That order must match reality.
Geometry Composition
The chain builder uses a simple but important rule:
length_cmdescribes the rigid segment that extends along the joint’s local+Xoffset_cmis an additional bracket translation applied on top of the previous segment end
So if joint i has length_cm = L, then joint i+1 defaults to sitting at [L, 0, 0] in joint i’s output frame unless offset_cm adds more translation.
The same composition rule applies at the tip:
- without
tip_offset_cm, the end effector sits at the end of the last segment - with
tip_offset_cm, the final tip position is the last segment end plus that extra offset
Joint Space vs Servo Space
Inverse kinematics solves for mathematical joint angles. Those are not always the angles you can send directly to hardware.
The toolchain therefore applies a per-joint linear mapping:
t = (joint_deg - joint_lo) / (joint_hi - joint_lo)
servo_deg = servo_lo + t * (servo_hi - servo_lo)
This handles both normal and inverted servos:
- normal mapping:
joint_range_deg: [0, 90],servo_range_deg: [10, 130] - inverted mapping:
joint_range_deg: [0, 90],servo_range_deg: [130, 10]
If servo_range_deg runs backward, the servo is inverted relative to the mathematical joint.
Workspace Guards
ArmChainGenerator checks named positions against workspace before accepting them.
Supported guards:
z_min_cmz_max_cmreach_max_cm
reach_max_cm is computed as:
sqrt(x^2 + y^2 + z^2)
These checks happen during code generation, not at runtime.
Forbidden Zones
Forbidden zones let you reject mechanically dangerous joint combinations even if IK technically converges.
Each zone contains:
namecondition
The condition is evaluated against a restricted context containing:
joint_0_deg,joint_1_deg, …{servo_name}_degfor each referenced servo
Example:
forbidden_zones:
- name: cable_tension
condition: "joint_0_deg < 10 and elbow_servo_deg > 40"
If a named position solves into a forbidden zone, code generation fails with a descriptive error.
What raccoon codegen Actually Emits
The generated result is an ArmPreset expression in defs.py:
arm = ArmPreset(
joints=[shoulder_servo.device, elbow_servo.device],
positions={
"home": [92.5, 35.0],
"grab": [61.2, 148.4],
},
)
That means:
- the YAML
positions:are Cartesian targets - the generated
positions=dict is already in servo command degrees - runtime arm moves are just multi-servo presets
Runtime Behavior
At runtime:
Defs.arm.home()returns a step that moves all arm servos in parallelDefs.arm.home(speed=60)uses eased servo motion at60 deg/s- one-joint arms return a single servo step
- multi-joint arms return a
parallel(...)step
This is why named positions are cheap and reliable.
Current State of arm.to(x, y, z)
The runtime API exposes arm.to(x, y, z), but that path is not implemented yet.
Current behavior:
- if
ikpyis missing, it raises an import error with installation guidance - if
ikpyis present, it still raisesNotImplementedError
So today, named positions are the real production path. The Web IDE and toolchain can solve FK/IK, but match-time dynamic runtime IK is not the supported control path yet.
Web IDE Relationship
The Web IDE arm editor uses the same toolchain-side kinematics helpers as code generation:
- chain construction
- forward kinematics
- inverse kinematics
- joint-axis visualization
- link-segment visualization
That is why editing an arm in the Web IDE and generating code from the CLI can stay consistent: they are sharing the same math code, not two separate implementations.
Common Failure Modes
missing 'length_cm': a joint is structurally underspecifiedservo '...' not found in definitions: the arm references a nonexistent servoIK failed to converge: target is unreachable or blocked by joint limits- workspace violation: target is outside allowed
zor reach limits - forbidden-zone violation: target solves, but the resulting joint combination is mechanically unsafe
Practical Authoring Rules
- Keep joints in true kinematic order from base to tip.
- Measure lengths from pivot to pivot, not from plastic edge to plastic edge.
- Use
joint_range_degfor the mathematical mechanism limit, not just “what the servo seems okay with.” - Use
servo_range_degto encode physical servo orientation and inversion. - Prefer named positions for competition code.
- Use workspace guards and forbidden zones early. They are cheap mechanical safety.