March 18
ESP32 Power Monitor Bridge & Dynamic ROS 2 Publishers
Today, I spent some time figuring out the architecture for my power monitoring setup. I evaluated two different paths for wiring up my INA3221 power monitors:
- Path 1: Connect the INA3221 directly to the Jetson’s GPIO pins and use the Jetson for direct I2C communication.
- Path 2: Use an ESP32 as a bridge. Connect the power monitors to the ESP32 via I2C, then connect the ESP32 to the Jetson via USB. The Jetson would then read the INA3221 data as serial output from the ESP32.
The Decision: I decided to go with Path 2 (ESP32 Bridge). This lets me leverage the existing Arduino libraries to easily interface with the INA3221. Plus, by connecting the ESP32 to the Jetson via USB, the Jetson can internally measure the power draw coming from the ESP32 via its own USB ports.
Serial Communication & Custom Formatting
Currently, the INA3221 is only reading 1 channel (as only one is connected to my test resistor). The ESP32 outputs the serial data in a CSV-style format. To handle multiple channels cleanly, I built a comma/semicolon-mixed data format that loops through all 3 channels:
#Loop through all 3 channels
Serial.print(SENSOR_ID);
Serial.print(",");
Serial.print(channel_idx + 1);
Serial.print(",");
Serial.print(busVoltage, 4);
Serial.print(",");
Serial.print(current, 4);
Serial.print(",");
Serial.print(power, 4);
The resulting serial output string looks like this:
0,1,12.1000,0.4500,5.4450;0,2,0.0000,0.0000,0.0000;0,3,11.9000,0.1200,1.4280
(Note: Sensor ID is always 0 right now since I only have one sensor active).
ROS 2 Integration & Dynamic Publishers
Next, I built a ROS 2 node using pyserial to read this incoming data. To keep things scalable, I set up a dictionary where the key is a tuple of (sensor_id, channel_id) and the value is a dedicated ROS publisher. This allows me to dynamically spin up publishers to different topics on the fly:
topic_name = f'/power/sensor_{sensor_id}/channel_{channel_id}'
self.channel_publishers[key] = self.create_publisher(PowerConsumption, topic_name, 10)
This successfully generated the following topics:
/power/sensor_0/channel_1
/power/sensor_0/channel_2
/power/sensor_0/channel_3
I also updated my custom ROS message (PowerConsumption.msg) to include identifiers for both the sensor and the channel:
uint8 sensor_id
uint8 channel_id
float64 voltage
float64 current
float64 power
Testing it out with ros2 topic echo /power/sensor_0/channel_1, everything works perfectly:
sensor_id: 0
channel_id: 1
voltage: 3.256
current: 0.0148
power: 0.0482
This architecture makes it super safe to scale to more channels per sensor, or even add entirely new INA3221 boards if I run out of channels.
Scaling Up: Adding the INA260
To test that scalability, I added an INA260 to the mix and connected it to the ESP32 using a very similar approach.
One hardware hurdle: The INA3221 was already occupying I2C address 0x40. To fix the conflict, I bridged the A1 soldering pads on the INA260, changing its address to 0x41.
The Final Polish
To make the ROS ecosystem more user-friendly, I created a config.json file to map these raw, dynamically generated topics into human-readable names. After some cleaning, refactoring, and storing the wiring constants, my final ROS topic list looks like this:
/power/by_name/ina260_aux/single_channel
/power/by_name/ina3221_main/accessory_rail
/power/by_name/ina3221_main/compute_rail
/power/by_name/ina3221_main/drive_motor
It’s feeling much more robust now. Let me know what you guys think of this approach!
Wiring
The current test board for the power modules are really messy XD.
But if it works, don’t touch it!