·
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.26 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.11 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

Sketches inclosos en tots els board packages — disponibles a File → Examples → IoTVertebrae.

Exemples generals (tots els packages)
BasicDigital

Demostra totes les operacions d'E/S digitals. Configura una vèrtebra, llegeix entrades síncronament i escriu sortides.

  • dsetup() — configura els costats com DIN / DOUT
  • din() — lectura síncrona
  • dout() — escriu byte complet
  • dversion() / getdsetup() — informació
BasicAnalog

Demostra totes les operacions d'E/S analògiques, síncrones i no bloquejants via memòria interna.

  • aoutv() — escriu DAC en volts (0–10 V)
  • ainv() — lectura ADC síncrona
  • setAinFreq() — activa enviament periòdic async
  • iainv() — lectura de memòria no bloquejant
AnalogFourChannels

Llegeix i escriu simultàniament els quatre canals ADC/DAC d'una vèrtebra analògica. Útil per a proves de totes les entrades i sortides analògiques a la vegada.

  • ain() / aout() — tots 4 canals
  • ain2v() / v2aout() — conversió de tensió
FullDemo

Demostració completa de tota la API. Ambdues vèrtebres a la mateixa adreça. Inclou callback async de canvis digitals.

  • Totes les funcions digitals i analògiques
  • onDinChange() callback async (flag volatile segur)
  • idin() / idinbit() — lectures de memòria
  • Ona triangular amb aoutv()
Exemples de cap (Head)
Head02 (ESP32-S3 · QEMU)

Firmware complet per al cap02 (ESP32-S3) amb connectivitat MQTT. Inclou blocs de compilació condicional #ifdef IOTV_QEMU_BUILD per adaptar-se automàticament al simulador QEMU o al maquinari real.

  • WiFi / Ethernet QEMU (OpenEth) automàtic
  • Bus CAN real ↔ MQTT shim al simulador
  • #ifdef IOTV_QEMU_BUILD — codi específic de simulació
Head02Min (ESP32-S3 · QEMU)

Versió mínima del firmware Head02. Arrenca el bus CAN / MQTT i alimenta les vèrtebres i costelles. És la base recomanada per a projectes nous que comencen des del cap.

  • Inici amb begin() i gestió d'alimentació
  • Compatible amb simulador i maquinari real
Head01Legacy (ESP32)

Firmware complet per al cap01 (ESP32 legacy) amb connectivitat MQTT via bus I2C. Equivalent a Head02 però per al maquinari de la primera generació.

  • Comunicació I2C amb vèrtebres
  • Compatible amb Head01Legacy board package
Head01LegacyMin (ESP32)

Versió mínima del firmware Head01. Alimenta les vèrtebres i costelles i cedeix el control a la Raspberry Pi via I2C o CAN. Firmware recomanat per a instal·lacions head01 en producció.

  • Inici mínim — alimentació i espera
  • La RPi pren el control via I2C
ℹ #ifdef IOTV_QEMU_BUILD: Els exemples Head02 i Head02Min detecten automàticament si compilen per al simulador (macro IOTV_QEMU_BUILD definida pel board package QEMU). Dins d'aquest bloc, el WiFi es redirigeix a Ethernet emulat i el bus CAN es substitueix per un shim MQTT sobre TCP. El mateix sketch compila i funciona tant al maquinari real com al simulador sense modificacions.
Nota — seqüència de power-on: Des de la v0.2, begin() gestiona automàticament el power-on. No cal cap pinMode / delay manual a setup(). Feu servir begin(tx, rx, bitrate, false) per ometre el power-on si el bus ja està alimentat.
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 din(uint8_t addr, IotvSide side, bool* ok) sync blocking

Overload of din() that also reports whether a valid reply arrived. Sets *ok = true if the vertebra responded within 500 ms, *ok = false on timeout (returned value is 0 and meaningless). Pass nullptr to behave identically to the two-argument version.

example
bool ok;
uint8_t v = iotv.din(addr, SIDE_A, &ok);
if (!ok) Serial.println("timeout");
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.

bool ainv4(uint8_t addr, IotvSide side, float out[4]) sync blocking

Reads all four analog channels of one side in a single CAN request (one RTR + one reply). Faster than calling ainv() four times. Fills out[0..3] with channels 1–4 in volts. Also updates internal memory so subsequent iainv() calls reflect the values. Returns true on success; false on timeout (out is left untouched).

example
float ch[4];
if (iotv.ainv4(addr, SIDE_A, ch)) {
  Serial.printf("ch1=%.2f ch2=%.2f\n", ch[0], ch[1]);
}
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);
bool aMemValid(uint8_t addr, IotvSide side) non-blocking

Returns true if at least one analog reading has been received for the given address and side (via ainv(), ainv4(), or the async dispatcher). Allows distinguishing "no data yet" from a genuine 0.0 V reading, avoiding spurious publications at startup.

example
if (iotv.aMemValid(addr, SIDE_A)) {
  float v = iotv.iainv(addr, SIDE_A, 1);
  // guaranteed to be a real reading
}
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.10 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