Skip to content

Commit 697ab31

Browse files
awesomebytesclaude
andcommitted
Merge control_kit: Python ros2_control controller inside the real controller_manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents 90b1fea + 60fffc5 commit 697ab31

9 files changed

Lines changed: 1745 additions & 1 deletion

File tree

docs/control_kit/CONTROL_KIT.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# CONTROL_KIT.md — API cheat sheet
2+
3+
`rclcppyy.kits.control_kit` — write a ros2_control controller in Python and run it inside a
4+
real `controller_manager::ControllerManager` in-process. Requires the pixi `control` env
5+
(`pixi run -e control ...`) and rclcpp initialized. See [REPORT.md](REPORT.md) for the
6+
mechanics/verdict and [WHY.md](WHY.md) for the stock-ros2_control contrast.
7+
8+
## Bring-up
9+
10+
```python
11+
from rclcppyy.bringup_rclcpp import bringup_rclcpp
12+
from rclcppyy.kits import control_kit as ck
13+
14+
bringup_rclcpp().init() # rclcpp must be up
15+
ns = ck.bringup_control() # JIT-include CM/controller headers, load .so, cppdef glue
16+
# returns cppyy.gbl.controller_interface; idempotent
17+
ck.load_message_support() # OPTIONAL: std_msgs typesupport for a command topic
18+
ck.warmup() # OPTIONAL: front-load the CM-construction JIT
19+
```
20+
21+
## The mock hardware rig
22+
23+
```python
24+
urdf = ck.mock_system_urdf(["joint1", "joint2"]) # mock_components/GenericSystem, 2 joints
25+
# mock_system_urdf(joints, command_interfaces=("position",),
26+
# state_interfaces=("position","velocity"),
27+
# robot_name="mock_bot", initial_value=0.0) -> URDF str
28+
rig = ck.make_controller_manager(urdf, update_rate=100) # real ControllerManager -> ControlRig
29+
# make_controller_manager(urdf, update_rate=100, node_name="controller_manager",
30+
# parameters=None) -> ControlRig
31+
```
32+
33+
`rig.cm` is the **real** `controller_manager::ControllerManager` — call any of its methods
34+
directly (`is_resource_manager_initialized()`, `get_update_rate()`, `get_loaded_controllers()`,
35+
`read`/`update`/`write`, ...).
36+
37+
## ControlRig methods
38+
39+
| method | does |
40+
|---|---|
41+
| `rig.load_controller(name, type, parameters=None)` | load a **stock C++** controller by pluginlib type; `parameters` (dict) set on its node. **Before the loop.** |
42+
| `rig.add_python_controller(controller, name, type_label="python_controller")` | inject a **Python** controller instance (a `ControllerInterface` subclass). **Before the loop.** |
43+
| `rig.set_controller_parameters(ctrl, {...})` | set params on a loaded controller's node |
44+
| `rig.configure(name) -> bool` | configure (INACTIVE); True on OK |
45+
| `rig.activate(names, timeout_s=5.0) -> bool` | activate (ACTIVE); off-thread switch + pumped `update()` |
46+
| `rig.deactivate(names, timeout_s=5.0) -> bool` | deactivate (INACTIVE) |
47+
| `rig.update(period=None)` | one `read``update``write` cycle |
48+
| `rig.spin()` | `executor.spin_some()` — service controller-node topics (not during a switch) |
49+
| `rig.run(seconds, rate_hz=None, spin=False, on_cycle=None) -> int` | hold the loop at `rate_hz` for `seconds` |
50+
| `rig.add_node(node)` | add an rclcpp node (e.g. a command publisher) to the CM executor |
51+
52+
**Ordering rule:** load/configure/add every controller **before** the first
53+
`update()`/`activate()`/`run()`. After the loop has run, `load_*`/`add_*` raise (the CM's
54+
real-time-safe list swap would deadlock a synchronous load) — mirrors `ros2_control_node`.
55+
56+
## Writing a Python controller
57+
58+
Derive `ck.ControllerInterface` and override the real C++ virtuals by name:
59+
60+
```python
61+
class MyController(ck.ControllerInterface):
62+
def __init__(self):
63+
super().__init__() # REQUIRED
64+
def on_init(self): return ck.CallbackReturn.SUCCESS
65+
def command_interface_configuration(self):
66+
return ck.interface_config(["joint1/position"]) # what to claim
67+
def state_interface_configuration(self):
68+
return ck.interface_config(["joint1/position"])
69+
def on_configure(self, prev_state): return ck.CallbackReturn.SUCCESS
70+
def on_activate(self, prev_state): return ck.CallbackReturn.SUCCESS
71+
def on_deactivate(self, prev_state): return ck.CallbackReturn.SUCCESS
72+
def update(self, time, period): # called every cycle
73+
n = ck.n_command_interfaces(self)
74+
for i in range(n):
75+
x = ck.read_state(self, i) # read hardware state
76+
ck.write_command(self, i, ...) # write hardware command
77+
return ck.return_type.OK
78+
```
79+
80+
### Enums and helpers
81+
82+
| name | note |
83+
|---|---|
84+
| `ck.CallbackReturn` | `.SUCCESS` / `.FAILURE` / `.ERROR` — return from `on_init`/`on_*` |
85+
| `ck.return_type` | `.OK` / `.ERROR` — return from `update` |
86+
| `ck.ok(value) -> bool` | True if a returned `return_type` is OK (handles the uint8→1-char-str crossing) |
87+
| `ck.interface_config(names, config_type="individual")` | build an `InterfaceConfiguration` (`"individual"`/`"all"`/`"none"`) |
88+
| `ck.interface_configuration_type` | the `INDIVIDUAL`/`ALL`/`NONE` enum |
89+
90+
### Interface accessors (call inside `update`, after activation)
91+
92+
| function | returns / does |
93+
|---|---|
94+
| `ck.n_state_interfaces(self)` / `ck.n_command_interfaces(self)` | count of claimed interfaces |
95+
| `ck.read_state(self, i)` | value of the i-th claimed **state** interface (float) |
96+
| `ck.read_command(self, i)` | value in the i-th claimed **command** interface |
97+
| `ck.write_command(self, i, value)` | write the i-th claimed **command** interface |
98+
99+
Interface order matches the `*_interface_configuration` names.
100+
101+
## Full example (self-commanding PD)
102+
103+
```python
104+
bringup_rclcpp().init(); ck.bringup_control()
105+
106+
class PD(ck.ControllerInterface):
107+
def __init__(self): super().__init__(); self.target=[0.5,-0.3]; self.kp=0.4
108+
def on_init(self): return ck.CallbackReturn.SUCCESS
109+
def command_interface_configuration(self): return ck.interface_config(["j1/position","j2/position"])
110+
def state_interface_configuration(self): return ck.interface_config(["j1/position","j2/position"])
111+
def on_configure(self,s): return ck.CallbackReturn.SUCCESS
112+
def on_activate(self,s): return ck.CallbackReturn.SUCCESS
113+
def on_deactivate(self,s):return ck.CallbackReturn.SUCCESS
114+
def update(self,t,p):
115+
for i in range(ck.n_command_interfaces(self)):
116+
c = ck.read_state(self,i); ck.write_command(self,i, c+self.kp*(self.target[i]-c))
117+
return ck.return_type.OK
118+
119+
rig = ck.make_controller_manager(ck.mock_system_urdf(["j1","j2"]))
120+
rig.add_python_controller(PD(), "pd"); rig.configure("pd"); rig.activate(["pd"])
121+
rig.run(seconds=2.0, rate_hz=100)
122+
# teardown is automatic (registered atexit): deactivate + drop the CM before rclcpp shutdown
123+
```
124+
125+
## Tasks
126+
127+
```
128+
pixi run -e control demo-control-rig # d01: CM + mock HW + stock C++ controller (Stages 1-2)
129+
pixi run -e control demo-control-python # d02: the Python PD controller showcase (Stage 3)
130+
pixi run -e control bench-control # bench: Python vs C++ loop numbers (Stage 4)
131+
pixi run -e control test-control # the test suite (auto-skips without ros2_control)
132+
```
133+
134+
## Gotchas (see REPORT §2)
135+
136+
- **Load before the loop** (above). **`super().__init__()` is required** in your controller.
137+
- Derive `ck.ControllerInterface` **directly** (not a further Python/JIT base) or cppyy's
138+
override dispatcher fails.
139+
- A returned `return_type` is a 1-char `str` — use `ck.ok(...)`, not `int(...)`.
140+
- Clean exit is automatic via the teardown registry; don't `unload_controller` a Python
141+
controller yourself.

0 commit comments

Comments
 (0)