API & reference
The ESA API, the ExoCore executables, the messages they exchange, troubleshooting and the known limitations of v1.0.
ESA API
Public interface of the SoftExoWalk class in ESA.h.
| Function | Arguments | Purpose |
|---|---|---|
setupCommunication | ("wifi", ssid, pass, agent_ip, port) | Selects the micro-ROS transport: Wi-Fi (UDP) or USB (serial). |
initializeSystem | () → bool | Configures every deviceList[] row: analog pins, digital input modes, I²C drivers; sizes the message. |
setupNode | (name = "microROS_Node") | Names the board's ROS 2 node. |
setupPublisher | (topic = "", pub_rate = 0) | Sensing hub: topic and publication rate in Hz (also the filter design rate). |
setupActuation | (topic, ActuatorType, out, x1 = -1, x2 = -1) | Actuation: one command topic per actuator, e.g. ACT_FESTO_VEAB_L26; up to 5 per board. |
bootSystem | () | Creates the publisher, timer, subscriptions and executor; initializes the actuators. Always last. |
readSensorsData | () | Reads every device once into the message (call in loop()). |
publish | (reconnect = false) | Spins the executor; with true, rebuilds the link after an agent restart. |
actuate | (topic = "") | Spins the executor and drives all actuators, or only the one on topic. |
emergencyShutdown | () | Stops the board's ROS entities. |
get_actCount / get_actCMD(id) / get_pubRate / get_debugFlag | () | Number of actuators, latest command of actuator id, publication rate, debug flag. |
ExoCore executables
The six executables of the exo_stack package. Launch files start one process per YAML entry.
| Executable | Configured by | Started by | Role |
|---|---|---|---|
sync | n/a | ros2 run exo_stack sync | synchronizes the hubs into /synced_sensors |
gait_event_detector | gait_events.yaml | gait_event_detector_launch.py | one detector per entry |
mplanner | mplanner.yaml | mplanner_launch.py | one motion planner per entry |
actuation_profile | profile.yaml | profile_launch.py | one profile node per entry (lookup table or HIL candidate) |
cost | profile.yaml (HIL) | cost_launch.py | per-gait-cycle cost |
hil | profile.yaml (HIL) | hil_launch.py | HIL optimizer (start it last) |
Troubleshooting
| Symptom | Likely cause | Remedy |
|---|---|---|
| Status LED on the board blinks rapidly | a ROS entity could not be created | start the agent; check IP, port and topic names |
| Board restarts every few seconds | watchdog: loop() blocked | remove delays and blocking calls from the sketch and drivers |
| USB link drops or never connects | the sketch writes to Serial | remove all Serial calls when using the USB transport |
| Analog channel stuck at 0 or full scale with Wi-Fi | pin on ADC2 | move it to GPIO 32–39 |
| New I²C device not found | duplicate bus–address pair, registry full, or header included elsewhere | see the rules in §2.5 |
| Duplicate-definition errors when compiling | two copies of the ESA library | keep one folder in ~/Arduino/libraries |
/synced_sensors silent | a configured hub is not publishing, or a topic name differs | every hub in sensors.yaml must be running |
| Warning Expected N intensities, got M | channel list and board disagree | align sensors.yaml with the device list (§2.2-C) |
| FileNotFoundError … exo_stack_resources | folder not in the home directory | move it to ~/exo_stack_resources |
| module has no attribute … | class name differs from file name | rename one of them (§2.7) |
| Planner warns of a profile-type mismatch | suffixes of builder, planner and profile_type differ | use List everywhere or Function everywhere |
| HIL stops after candidate #1 | optimizer started before profile and cost stages; or cost_topic differs | start HIL last (§2.3) |
| Profile number mismatch … Ignoring | a late cost from the previous candidate | normal once after each change |
| PASE cannot start the agent | agent workspace not sourced | add it to ~/.bashrc (§2.1-E) |
ROS 2 and micro-ROS interfaces
Table S3 lists the topic of every ESA hub and ExoCore node and what each element of its message means. Each node and every message layout is detailed, with examples, under ROS 2 interfaces.
| Topic | Type | From → to | Content of the message |
|---|---|---|---|
| ESA sensing hub: micro-ROS node, one per board (×N) | |||
/<hub topic>setupPublisher(); sensors.yaml | sensor_msgs/ LaserScan | hub → synchronizer | intensities[i]: channel i, in device-list order: AI in volts (0–3.3), DI 0 or 1, I²C in the driver's units; range_min: number of devices; range_max: number of channels; header.stamp: hub time since the last /sync_trigger. Other fields unused. Sent at the setupPublisher() rate. |
/sync_triggerfixed name | std_msgs/ Int32 | synchronizer → every hub | data = 0, sent at 5 Hz in trigger mode; on reception a hub stores millis() as its time offset. |
| ESA actuation hub: micro-ROS node (×M) | |||
/<command topic>setupActuation(); output_topic in mplanner.yaml | std_msgs/ Float32 | planner → actuation hub | data: set-point in the driver's unit (kPa for ACT_FESTO_VEAB_L26, degrees for ACT_SG90_SERVO). Kept as the actuator's latest command; 0 until the first message. |
| Synchronizer: sync (×1) | |||
/synced_sensorsoutput_topic in sensors.yaml | std_msgs/ Float32MultiArray | synchronizer → detectors, cost evaluators | data: the channels of every hub, concatenated in sensors.yaml order (hub by hub, channel by channel), for each set of hub messages whose stamps agree within slop_time. No time stamp. |
| Gait-event detector: gait_event_detector (×E) | |||
/<event topic>output_topic in gait_events.yaml | std_msgs/ Int32 | detector → planner | data = 1 at each detected event; nothing is sent between events. |
| External estimators and sensors: your own nodes, optional | |||
/<speed_topic>profile.yaml | std_msgs/ Float32 | estimator → profile node | walking speed, in the unit of the lookup-table keys. |
/<intent_topic>profile.yaml | std_msgs/ Float32 | estimator → profile node | locomotion intent; 0 (standing) raises the profile's e-stop flag. |
/<gait_phase_topic>mplanner.yaml | std_msgs/ Float32 | estimator → planner | gait phase, used as the time argument t by gait_phase_based planners. |
/<external_sensors_topic>cost block of profile.yaml | std_msgs/ Float32MultiArray | sensor → cost evaluator | any vector, e.g. metabolic rate or EMG, handed to the cost plug-in with the synchronized channels. |
/<additional_gait_marker_topic>cost block of profile.yaml | std_msgs/ Int32 | your node → cost evaluator | marker value appended to the next cost sample, e.g. a flexion–extension split. |
| Profile node: actuation_profile (×P) | |||
/<profile_topic>profile_topic of the planner entry | std_msgs/ Float32MultiArray | profile node → planner | [k, type, start, body…, end, stop]. k: candidate number, 0 in Deployment; type: 1 = list, 2 = coefficients; start, end: profile window, in units of t; body: (t, value) pairs or coefficients; stop: 1 = emergency stop. Sent every 1/rate in Deployment, once per candidate in HIL. |
| Motion planner: mplanner (×P) | |||
/<output_topic>mplanner.yaml | std_msgs/ Float32 | planner → actuation hub or any ROS 2 driver | set-point at rate: next_point(t) inside the profile window; default_output outside it or on e-stop; the manual command in manual mode. |
/<planner>/profile_statusplanner name | std_msgs/ Float32MultiArray | planner → cost evaluator | [k, cycle] at each gait event: the candidate being played and its gait-cycle count (0 in Deployment). |
/<planner>/manual_modeplanner name | std_msgs/ Int32 | PASE or you → planner | 1 = manual override on, 0 = back to profiles. |
/<planner>/manual_cmdplanner name | std_msgs/ Float32 | PASE or you → planner | set-point sent while manual mode is on. |
| Cost evaluator: cost (×C, one per profile with a cost) | |||
/<cost_topic>profile.yaml | std_msgs/ Float32MultiArray | cost evaluator → HIL optimizer | [k, cost]: mean of the per-cycle costs over gait_cycles_to_consider cycles, after skipping gait_cycles_to_skip; k = 0 in Deployment. |
| HIL optimizer: hil (×H, one per profile in HIL mode) | |||
/<hil_output_topic>profile.yaml | std_msgs/ Float32MultiArray | optimizer → profile node | [k, p1 … pn]: candidate number and one parameter per optimized dimension; a new candidate follows each cost it accepts. On stop, every round and the best candidate are written to ~/exo_stack_resources/hil_results/. |
All ExoCore topics use best-effort reliability, and the hubs publish and subscribe best-effort over micro-ROS.
k ties the HIL messages together: the cost evaluator counts only cycles of the candidate in profile_status, and the optimizer accepts only a cost carrying the k it proposed.
ExoStack uses standard message types only, so no custom message package has to be built for micro-ROS.
Known limitations of v1.0
- Device-list values
dev_misc_dataanddev_misc_data2reach the I²C drivers that read them (the BNO085 channel count); the NAU7802 calibration and the AS5600 direction pin come from the drivers' registration lines. - Function profiles (
Builder1_FunctionwithPolynomialFunctionorCubicPolynomialFunction) andPCHIPInterpolatedListdo not run in v1.0; use a_Listbuilder withLinearInterpolatedList. - PASE writes the approach
gait_phase, which the planner does not recognise, and addsinit_pointsandgp_paramsto Bayesian profiles, whichOPT_BAYESIANdoes not accept: configure phase-based planners and Bayesian profiles in the YAML files. - The I²C registry holds 8 entries, all used by the shipped drivers (§2.5).