ExoStack / Extend

ESA drivers

Add I²C sensors and actuators to ESA: each new device is a header–source pair written from a template.

How to read the templatesIn every template, MYSENS1 marks a value you change; the number points to its explanation below the template.

I²C sensor driver

A new I²C sensor is one header and one source file in src/sensing/I2C/. The header ends with a registration line; at program start the driver enters itself in the I²C registry (Table S2) under its bus–address pair, and any device-list row with that pair uses it. No other file of the library is edited.

ItemConventionExample (shipped)
LocationESA/src/sensing/I2C/n/a
Files<DEV>.h and <DEV>.cppNAU7802.h, NAU7802.cpp
Functions<DEV>__INIT_ and <DEV>__READ_: two underscores before, one afterNAU7802__INIT_, NAU7802__READ_
Registrationone REGISTER_I2C_SENSOR line per address and bus, in the headerREGISTER_I2C_SENSOR(0x2A, MAIN, …)
Vendor libraryinstalled in ~/Arduino/libraries/SparkFun_Qwiic_Scale_NAU7802…

Template: header

src/sensing/I2C/MYSENS.h
#pragma once
#include "I2C_Manager.h"
#include "MySensorLib.h"1                   // the chip's Arduino library

bool MYSENS2__INIT_(I2C_Sens_Ctx* sens);
bool MYSENS2__READ_(I2C_Sens_Ctx* sens);

//                 address     bus        init function     read function     info1, info2 (optional)
REGISTER_I2C_SENSOR(0x483,  MAIN4,  MYSENS__INIT_,  MYSENS__READ_,  1.0, 0.05);
REGISTER_I2C_SENSOR(0x48,  AUX1,  MYSENS__INIT_,  MYSENS__READ_,  1.0, 0.0);    // same chip on the second bus
1header of the vendor library for the chip
2device name <DEV>; use the same name for the file, both functions and in the .cpp
3the chip's 7-bit I²C address; add one line per address the chip can take
4MAIN (SDA 21 / SCL 22) or AUX1 (SDA 19 / SCL 18); add a line per bus you wire it to
5values of info1, info2 handed to the driver for this bus and address (e.g. calibration constants)

Template: source

src/sensing/I2C/MYSENS.cpp
#include "MYSENS1.h"

static constexpr uint8_t N_CH = 22;                  // channels this device publishes

bool MYSENS__INIT_(I2C_Sens_Ctx* sens) {
    if (!sens) return false;
    TwoWire& wire = (sens->line == MAIN) ? Wire : Wire1;  // bus chosen in the device list

    MySensor* dev = new MySensor();3                   // one driver object per instance
    for (int i = 0; i <= _MAX_SENSOR_INIT_RETRIES; i++) {
        if (dev->begin(sens->addr, wire)4) {
            dev->setRate(100);5                         // optional chip configuration
            deviceList[sens->idx].data_per_device = N_CH;  // declare the channel count
            sensorIndices += N_CH;                         // reserve the slots in the message
            sens->sens_instance = (void*)dev;              // keep the object for READ_
            return true;
        }
        delay(5);
    }
    return false;                                          // device reported as not ready
}

bool MYSENS__READ_(I2C_Sens_Ctx* sens) {
    if (!sens || !sens->sens_instance) { sensorIndices += N_CH; return false; }
    MySensor* dev = (MySensor*)sens->sens_instance;

    if (!dev->dataReady()6) {                           // no new sample this cycle:
        sensorIndices += N_CH;                             // skip the slots, keep the previous values
        return false;
    }
    msg.intensities.data[sensorIndices++] = dev->readX()7 * sens->info1 + sens->info2;
    msg.intensities.data[sensorIndices++] = dev->readY()7;
    return true;                                           // exactly N_CH values, in a fixed order
}
1include your own header
2number of values the device publishes every cycle, the length of its slot range
3the vendor driver class (or your own register-level code)
4the call that starts the chip at sens->addr on wire; must return true on success
5any one-time chip set-up (range, rate, gain); delete if not needed
6the call that reports a new sample; if the chip always has one, use true
7the reads; scale with info1/info2 where calibration applies; one line per channel

Use it

  1. Copy both files into ~/Arduino/libraries/ESA/src/sensing/I2C/ and install the vendor library.
  2. Add a row to the sketch: { I2C, 0x48, "MYS", MAIN },.
  3. Add N_CH names to the hub in sensors.yaml, at the position of the new row.
  4. Compile and upload; ros2 topic echo <hub topic> --once shows the new range_max.
DRIVER RULES
  • Include the header only from its own .cpp; including it elsewhere registers the driver twice.
  • A bus–address pair belongs to one driver. The registry has a fixed capacity, MAX_I2C_SENS in I2C_Manager.h (8 in v1.0, all taken by the shipped drivers): delete registration lines for addresses you do not wire, or raise the constant.
  • READ_ runs every cycle: no delay(), no blocking waits, no Serial output.
  • Always advance sensorIndices by N_CH, even when you write nothing, so that every later channel stays in its slot.

Actuator driver

A new actuator is one header and one source file in src/actuation/. The header registers the driver under an identifier, added once to the actuator list in actuation.h; the sketch selects the driver by that identifier in setupActuation().

ItemConventionExample (shipped)
LocationESA/src/actuation/n/a
FilesACT_<DEV>.h and ACT_<DEV>.cppACT_FESTO_VEAB_L26.h/.cpp
Functions<DEV>__INIT_ and <DEV>__ACTUATE_FESTO_VEAB_L26__INIT_, FESTO_VEAB_L26__ACTUATE_
IdentifierACT_<DEV>: added to the ActuatorType list in actuation.h, used in setupActuation()ACT_FESTO_VEAB_L26

Template: identifier

src/actuation/actuation.h (excerpt)
// UPDATE BELOW ENUM FOR NEW ACTUATOR
typedef enum {
    NO_ACT = 0,
    ACT_FESTO_VEAB_L26,
    ACT_SG90_SERVO,
    LED_TEST,
    ACT_MYACT1,                     // new identifier, always before ACTUATOR_COUNT
    ACTUATOR_COUNT
} ActuatorType;
1identifier of the new actuator; the same word is used in its registration line and in the sketch

Template: header

src/actuation/ACT_MYACT.h
#pragma once
#include "actuation.h"

void MYACT1__INIT_(ActInfo* sub);
void MYACT1__ACTUATE_(ActInfo* sub);

REGISTER_ACTUATOR(ACT_MYACT2, MYACT__INIT_, MYACT__ACTUATE_)
1driver name <DEV>, used for both functions
2the identifier added to actuation.h; use it in setupActuation()

Template: source

src/actuation/ACT_MYACT.cpp
#include "ACT_MYACT1.h"

void MYACT__INIT_(ActInfo* sub) {                 // once, during bootSystem()
    if (!sub) return;
    pinMode(sub->out_gpio_i2caddr, OUTPUT);2       // 'out' argument of setupActuation()
    if (sub->extra_info1 > 0) pinMode(sub->extra_info1, OUTPUT);3   // 'x1', optional
}

void MYACT__ACTUATE_(ActInfo* sub) {              // every loop, from actuate()
    if (!sub) return;
    float cmd = sub->val;                             // latest command (0 before the first message)
    int duty = constrain((int)(cmd * 2.55f), 0, 255);4   // convert the physical set-point
    analogWrite(sub->out_gpio_i2caddr, duty);5          // drive the hardware
}
1include your own header
2one-time set-up of the output (pin mode, I²C driver object, PWM frequency …)
3use extra_info1 / extra_info2 (the x1, x2 arguments) for any extra pins or settings; omitted arguments arrive as 0
4the conversion from the commanded physical quantity (kPa, degrees, %) to the hardware unit
5the hardware call (analogWrite, digitalWrite, an I²C driver call …)
ActInfo fieldTypeComes from
valfloatlatest Float32 on the command topic
out_gpio_i2caddrint8out argument of setupActuation()
extra_info1, extra_info2int8x1, x2 arguments (−128…127; 0 if omitted)
topicNameconst char*command topic

Optional: smooth an analog feedback signal

A driver can filter a feedback input with the library's software filters, as the pneumatic valve driver does with its pressure sensor. Register the pin in INIT_, read it in ACTUATE_:

feedback filter
// in MYACT__INIT_ :
act_fb_filter_config[_act_fb_idx].filter_type = 11;      // 1 = moving average (filter number = extra_info1 - 8)
act_fb_filter_config[_act_fb_idx].param1      = 102;     // window
act_fb_filter_config[_act_fb_idx].gpio        = sub->extra_info2;
_act_fb_idx++;

// in MYACT__ACTUATE_ :
float fb_volts = _act_fb_filt.analog_filter(sub->extra_info2);
1filter: 1 moving average, 2 median, 3 exponential, 4–7 second-order Butterworth LPF/HPF/BPF/notch
2first filter parameter (window, α or cut-off Hz); param2 holds Q for Butterworth

Use it

  1. Add ACT_MYACT, to the ActuatorType list in ~/Arduino/libraries/ESA/src/actuation/actuation.h, before ACTUATOR_COUNT.
  2. Copy both files into ~/Arduino/libraries/ESA/src/actuation/.
  3. In the actuation sketch: exo.setupActuation("/my/cmd", ACT_MYACT, out, x1, x2);
  4. Set the same topic as output_topic of a planner in mplanner.yaml.
  5. Compile and upload; test with ros2 topic pub --once /my/cmd std_msgs/msg/Float32 "{data: 50.0}".
DRIVER RULES
  • Include the header only from its own .cpp.
  • ACTUATE_ runs every loop for every actuator: keep it short and non-blocking.
  • Variables declared at file scope are shared by all instances of the driver. If a board may run several, keep per-instance state in a small table keyed by sub->out_gpio_i2caddr.
  • Choose the zero command to be a safe state of the hardware: the driver receives 0 until the first message arrives.