|
| 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