Arduino library for
IoT-Vertebrae

IoT-Vertebrae is an Arduino library for ESP32 platforms that provides a clean API to communicate with modular digital and analog I/O nodes over a CAN bus.

ESP32-S3 (head02) ESP32 Legacy (head01) CAN / TWAI 100 kbps
BasicDigital.ino
// One-call init: power-on + CAN bus
if (!iotv.begin()) { … }

// Configure vertebra: A=input, B=output
iotv.dsetup(addr, DIN, DOUT);

// Read digital inputs
uint8_t val = iotv.din(addr, SIDE_A);

// Read analog (non-blocking, async)
float v = iotv.iainv(addr, SIDE_A, 1);
Installation

Add the board URLs to Arduino IDE, then install the packages.

ESP32-S3 — head02

For boards based on ESP32-S3-WROOM (IDF 5.1). CAN TX: GPIO17 / RX: GPIO18.

https://iotv.binefa.cat/arduino/iot-vertebrae/package_iotv_index.json copy
v1.0.23 FQBN: IoTVertebrae:esp32:iot_vertebrae
ESP32 Legacy — head01

For boards based on ESP32-WROOM-32D (IDF 4.4). CAN TX: GPIO27 / RX: GPIO26.

https://iotv.binefa.cat/arduino/iot-vertebrae-legacy/package_iotv_legacy_index.json copy
v1.0.6 FQBN: IoTVertebrae-legacy:esp32:iot_vertebrae_legacy
Steps — Arduino IDE 2
1
Open File → Preferences. Add all three URLs to Additional boards manager URLs (one per line):
https://raw.githubusercontent.com/vishalsoniindia/Multi_ESP32_Package/refs/heads/main/package_multi_esp32_index.json copy
https://iotv.binefa.cat/arduino/iot-vertebrae/package_iotv_index.json copy
https://iotv.binefa.cat/arduino/iot-vertebrae-legacy/package_iotv_legacy_index.json copy
2
Open Tools → Board → Boards Manager. Install in this order:
  1. esp32_board_0 version 2.0.17 (required for Legacy)
  2. esp32_board_1 version 3.0.7 (required for ESP32-S3)
  3. IoTVertebrae (ESP32-S3) and/or IoTVertebrae-legacy (ESP32)
3
Select the board: Tools → Board → IoT-Vertebrae Boards → IoT-Vertebrae (ESP32-S3) or the Legacy variant.
4
Open an example: File → Examples → IoTVertebrae → FullDemo. Compile and upload.
arduino-cli
# Add all index URLs (once)
arduino-cli config add board_manager.additional_urls \
  https://raw.githubusercontent.com/vishalsoniindia/Multi_ESP32_Package/refs/heads/main/package_multi_esp32_index.json
arduino-cli config add board_manager.additional_urls \
  https://iotv.binefa.cat/arduino/iot-vertebrae/package_iotv_index.json
arduino-cli config add board_manager.additional_urls \
  https://iotv.binefa.cat/arduino/iot-vertebrae-legacy/package_iotv_legacy_index.json

# Update index and install base packages first
arduino-cli core update-index
arduino-cli core install esp32_board_0:esp32@2.0.17  # required for Legacy
arduino-cli core install esp32_board_1:esp32@3.0.7   # required for S3
arduino-cli core install IoTVertebrae:esp32           # S3
arduino-cli core install IoTVertebrae-legacy:esp32    # Legacy

# Compile
arduino-cli compile --fqbn IoTVertebrae:esp32:iot_vertebrae MySketch.ino
Steps — Arduino IDE 2
⚠ Windows prerequisite: The Espressif base board packages must be installed first. Without this step, compilation fails due to missing SDK headers.
1
Open File → Preferences. Add all three URLs to Additional boards manager URLs (one per line):
https://raw.githubusercontent.com/vishalsoniindia/Multi_ESP32_Package/refs/heads/main/package_multi_esp32_index.json copy
https://iotv.binefa.cat/arduino/iot-vertebrae/package_iotv_index.json copy
https://iotv.binefa.cat/arduino/iot-vertebrae-legacy/package_iotv_legacy_index.json copy
2
Open Boards Manager. Install in this order:
  1. esp32_board_0 version 2.0.17 (required for Legacy)
  2. esp32_board_1 version 3.0.7 (required for ESP32-S3)
  3. IoTVertebrae (ESP32-S3) and/or IoTVertebrae-legacy (ESP32)
3
Select the board and COM port. Open an example and compile.
4
If the legacy compilation fails with "filename or extension is too long", use the external CLI with a short path. Download the automated install script and run it from PowerShell:
⇓ install-iotv-windows.ps1
# Run from the folder where the script was downloaded:
powershell -ExecutionPolicy Bypass -File .\install-iotv-windows.ps1

The script installs arduino-cli to C:\Ar with a short path to avoid the Windows filename length limit, and sets up both IoTVertebrae board packages.

arduino-cli (PowerShell)
# Once installed by the script, compile with:
$CLI = "C:\Users\$env:USERNAME\AppData\Local\arduino-cli\arduino-cli.exe"
$CFG = "C:\Ar\arduino-cli.yaml"

# ESP32-S3
& $CLI --config-file $CFG compile `
  --fqbn "IoTVertebrae:esp32:iot_vertebrae" sketch.ino

# ESP32 Legacy
& $CLI --config-file $CFG compile `
  --fqbn "IoTVertebrae-legacy:esp32:iot_vertebrae_legacy" sketch.ino

# Upload (replace COM3 with the correct port)
& $CLI --config-file $CFG upload `
  --fqbn "IoTVertebrae-legacy:esp32:iot_vertebrae_legacy" `
  --port COM3 sketch.ino
Examples

Three ready-to-upload sketches included in both packages.

BasicDigital

Demonstrates all digital I/O operations. Configure a vertebra, read inputs synchronously, and write outputs.

  • dsetup() — configure sides as DIN / DOUT
  • din() — synchronous read
  • dout() — write full byte
  • dversion() / getdsetup() — info
BasicAnalog

Demonstrates all analog I/O operations, both synchronous and non-blocking via internal memory.

  • aoutv() — write DAC in volts (0–10 V)
  • ainv() — synchronous ADC read
  • setAinFreq() — enable periodic async push
  • iainv() — non-blocking memory read
FullDemo

Complete demonstration of the entire API. Both digital and analog vertebrae at the same address. Includes async digital change callback.

  • All digital and analog functions
  • onDinChange() async callback (safe volatile flag)
  • idin() / idinbit() — memory reads
  • Triangular wave on aoutv()
Note — power-on sequence: Since v0.2, begin() automatically handles the power-on sequence (GPIO raise + stabilization delays). No manual pinMode / delay needed in setup(). Use begin(tx, rx, bitrate, false) to skip power-on if the bus is already powered.
API Reference

Global instance: iotv — include with #include <IoTVertebrae.h>

Initialization
bool begin() blocking

Initializes the CAN bus using the default pins and bitrate defined in the board package. Automatically performs the power-on sequence (raises 3.3V rail, then 24V rail, waits for stabilization). Returns true on success.

example
if (!iotv.begin()) {
  Serial.println("CAN error");
  while (true);
}
bool begin(int txPin, int rxPin, uint32_t bitrate, bool doPowerOn) blocking

Full overload. Specify custom CAN pins and bitrate. Set doPowerOn = false to skip the power-on sequence (useful when the bus is already powered or in lab bench setups).

paramtypedescription
txPinintCAN TX GPIO number
rxPinintCAN RX GPIO number
bitrateuint32_t100000 / 250000 / 500000 bps
doPowerOnbooltrue = perform power-on sequence
example
iotv.begin(17, 18, 100000, false); // skip power-on
void end()

Stops the TWAI driver, deletes FreeRTOS queues and dispatcher task, then powers off (24V first, then 3.3V).

static uint8_t addr(const char* binStr) static

Converts a 4-bit binary string to an address byte (0–15).

example
uint8_t a = iotv.addr("0101"); // → 5
Digital vertebra — configuration
void dsetup(uint8_t addr, IotvDigMode modeA, IotvDigMode modeB)

Configures the operating mode of both sides of a digital vertebra. Sends one or two CAN frames as required by the protocol.

paramtypevalues
addruint8_t0–15
modeA / modeBIotvDigModeDIN, DOUT, PWM, TOUCH, NONE
example
iotv.dsetup(addr, DIN, DOUT); // A=input, B=output
iotv.dsetup(addr, PWM, DIN);  // A=PWM output, B=input
String dversion(uint8_t addr) blocking

Reads the firmware version string from a digital vertebra (e.g. "1.5"). Returns "" on timeout.

String getdsetup(uint8_t addr) blocking

Reads the current configuration from the vertebra and returns a human-readable string (e.g. "A:din, B:dout").

Digital vertebra — read / write
uint8_t din(uint8_t addr, IotvSide side) sync blocking

Sends an RTR request and waits up to 500 ms for the vertebra to reply with the 8-bit input state (active-high corrected). Returns 0 on timeout. Also updates the internal memory so a subsequent idin() reflects this read.

example
uint8_t v = iotv.din(addr, SIDE_A);
uint8_t idin(uint8_t addr, IotvSide side) non-blocking

Returns the last digital input value stored in internal memory by the dispatcher. Does not send any CAN frame — safe to call every loop iteration. Returns 0 if no data has been received yet for this address.

uint8_t idinbit(uint8_t addr, IotvSide side, uint8_t bit) non-blocking

Returns a single bit (0 or 1) from the internal digital memory. bit is 0–7 (LSB first).

example
uint8_t b = iotv.idinbit(addr, SIDE_A, 3); // bit 3
void dout(uint8_t addr, IotvSide side, uint8_t value)

Writes a full byte to a digital output side. All 8 outputs are updated in a single CAN frame.

example
iotv.dout(addr, SIDE_B, 0b00001111); // bits 0-3 ON
void doutbit(uint8_t addr, IotvSide side, uint8_t bit, uint8_t val)

Writes a single output bit without affecting the others. bit is 0–7, val is 0 or 1.

example
iotv.doutbit(addr, SIDE_B, 2, 1); // set bit 2
void doutpwm(uint8_t addr, IotvSide side, uint8_t bit, uint8_t val)

Writes a PWM duty cycle (0–255) to a single output bit on a side configured as PWM. The side must have been set to PWM mode via dsetup().

void onDinChange(void (*cb)(uint8_t addr, uint8_t valA, uint8_t valB)) async callback

Registers a callback invoked by the internal dispatcher (core 0) whenever a spontaneous digital change notification is received. Do not call Serial or other non-thread-safe functions inside the callback. Use a volatile flag and process it in loop().

example
volatile bool changed = false;
volatile uint8_t lastA, lastB;

void onChange(uint8_t addr, uint8_t a, uint8_t b) {
  lastA = a; lastB = b; changed = true; // no Serial here!
}
iotv.onDinChange(onChange);
Analog vertebra — read / write
float ainv(uint8_t addr, IotvSide side, uint8_t channel) sync blocking

Sends an RTR request and returns the ADC reading in volts (range −10.0 to +10.0 V, 2 decimal places). Waits up to 500 ms. Note: ADC stabilizes ~330 ms after a DAC write; the first read after power-on may return −10.00 V.

example
float v = iotv.ainv(addr, SIDE_A, 1); // channel 1
uint16_t ain(uint8_t addr, IotvSide side, uint8_t channel) sync blocking

Same as ainv() but returns the raw 16-bit ADC value (0–26624). Use ain2v() to convert.

float iainv(uint8_t addr, IotvSide side, uint8_t channel) non-blocking

Returns the last analog value (in volts) stored in internal memory by the async dispatcher. Requires setAinFreq() to be called first. Returns 0.0 if no data yet. Safe to call every loop iteration.

example
// In setup:
iotv.setAinFreq(addr, 200); // 200 ms period
// In loop:
float v = iotv.iainv(addr, SIDE_A, 1);
void setAinFreq(uint8_t addr, uint32_t periodMs)

Commands the analog vertebra to start pushing ADC readings autonomously every periodMs milliseconds. The dispatcher (running on core 0) receives these frames and updates internal memory. Pass periodMs = 0 to disable. Recommended: 50–500 ms.

void aoutv(uint8_t addr, IotvSide side, uint8_t channel, float volts)

Writes a voltage (0.0–10.0 V) to a DAC channel. The library converts to the 12-bit raw value (0–4095) and clamps to avoid accidental EEPROM writes on the vertebra firmware. ADC stabilization takes ~330 ms after a write.

example
iotv.aoutv(addr, SIDE_B, 1, 5.0f); // 5V on ch1
void aout(uint8_t addr, IotvSide side, uint8_t channel, uint16_t val)

Raw DAC write. val is 0–4095 (0 V to 10 V). Use v2aout() to convert from volts.

Analog vertebra — information
String aversion(uint8_t addr) blocking

Returns the analog vertebra firmware version string (e.g. "1.5").

String getasetup(uint8_t addr) blocking

Returns the analog vertebra side configuration as a string (e.g. "A:ain, B:aout").

Utilities
static float ain2v(uint16_t raw) static

Converts raw ADC (0–26624) to voltage (−10.0 to +10.0 V, 2 decimal places). Formula: ((20×raw)/26624)−10.

static uint16_t v2aout(float volts) static

Converts voltage (0.0–10.0 V) to raw DAC value (0–4095), clamped to avoid out-of-range values.

Types & constants
IotvSide
SIDE_A = 0   // rib connector A
SIDE_B = 1   // rib connector B
IotvDigMode
DIN   = 0   // digital input
DOUT  = 1   // digital output
PWM   = 2   // PWM output (one side only)
TOUCH = 3   // touch input (side B only)
NONE  = 4   // no rib connected
Timing constants (overridable with #define before #include)
IOTV_DELAY_3V3_MS  1000  // wait after 3.3V on
IOTV_DELAY_24V_MS  1000  // wait after 24V on
IOTV_DELAY_BUS_MS   200  // CAN bus stabilization
🖥️ QEMU Simulator

Compile and run IoT-Vertebrae sketches in a web simulator — no hardware needed.

IoT-Vertebrae QEMU (Simulator)

Board package for the web simulator iotvSim.binefa.cat. Firmware runs inside QEMU and communicates with SVG digital twins via MQTT.

https://iotvSim.binefa.cat/arduino/package_iotv_qemu_index.json copy
v1.0.0 WiFi→Ethernet · CAN→MQTT
How it works

The student compiles with the QEMU board, uploads the binary ZIP to the simulator, and QEMU runs the firmware. Physical vertebrae are replaced by interactive SVG twins in the browser.

ESP32 QEMU Digital twins MQTT bridge
⚠ Prerequisite: esp32 by Espressif Systems version 3.0.7 must be installed first (same as for ESP32-S3 boards).
Steps
1
Add the simulator URL to Additional boards manager URLs:
https://iotvSim.binefa.cat/arduino/package_iotv_qemu_index.json copy
2
Open Boards Manager, search IoT-Vertebrae QEMU and install it.
3
Select Tools → Board → IoT-Vertebrae QEMU (Simulador).
4
Write your sketch normally with #include <IoTVertebrae.h>. Compile with Sketch → Export Compiled Binary (Ctrl+Alt+S).
5
Upload the generated ZIP (from the build/ folder) to iotvSim.binefa.cat. Click ▶ Start QEMU and watch the digital twins in action.
Differences from real hardware
WiFi Automatically redirected to emulated Ethernet (QEMU OpenEth) CAN / TWAI Automatically redirected to MQTT (broker: 192.168.4.1) Power pins Not available (3V3 / 24V disabled) Physical vertebrae Replaced by interactive SVG digital twins