# IoT-Vertebrae Arduino IDE — API Reference

> **Context file for AI code generation.**
> Use this document when asking an AI to write Arduino C++ sketches for IoT-Vertebrae,
> or to translate Python (RPi / simulator) code to Arduino C++.

---

## Overview

**Library:** `IoTVertebrae` — include with `#include <IoTVertebrae.h>`
**Global instance:** `iotv` (pre-declared by the library)
**Platform:** ESP32-S3 (head02) or ESP32 Legacy (head01)
**Bus:** CAN / TWAI at 100 kbps
**Board FQBNs:**
- ESP32-S3: `IoTVertebrae:esp32:iot_vertebrae` (IDF 5.1, CAN TX: GPIO17 / RX: GPIO18)
- ESP32 Legacy: `IoTVertebrae-legacy:esp32:iot_vertebrae_legacy` (IDF 4.4, CAN TX: GPIO27 / RX: GPIO26)
- QEMU Simulator: `IoTVertebrae-qemu:esp32:iot_vertebrae_qemu`

---

## Types & Constants

### `IotvSide`
```cpp
SIDE_A = 0   // rib connector A
SIDE_B = 1   // rib connector B
```

### `IotvDigMode`
```cpp
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`)
```cpp
IOTV_DELAY_3V3_MS  1000   // wait after 3.3V power-on
IOTV_DELAY_24V_MS  1000   // wait after 24V power-on
IOTV_DELAY_BUS_MS   200   // CAN bus stabilization
```

---

## 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.

```cpp
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).

| param | type | description |
|-------|------|-------------|
| txPin | int | CAN TX GPIO number |
| rxPin | int | CAN RX GPIO number |
| bitrate | uint32_t | 100000 / 250000 / 500000 bps |
| doPowerOn | bool | `true` = perform power-on sequence |

```cpp
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).

```cpp
uint8_t a = iotv.addr("0101"); // → 5
```

> **Note (power-on sequence):** From v0.2 onwards, `begin()` manages power-on automatically.
> No manual `pinMode` / `delay` is needed in `setup()`.
> Use `begin(tx, rx, bitrate, false)` to skip power-on if the bus is already powered.

---

## 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.

| param | type | values |
|-------|------|--------|
| addr | uint8_t | 0–15 |
| modeA / modeB | IotvDigMode | `DIN`, `DOUT`, `PWM`, `TOUCH`, `NONE` |

```cpp
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 internal memory so a subsequent `idin()` reflects this read.

```cpp
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.
Pass `nullptr` to behave identically to the two-argument version.

```cpp
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).

```cpp
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.

```cpp
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.

```cpp
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.

> **Thread safety:** Do not call `Serial` or other non-thread-safe functions inside the callback.
> Use a `volatile` flag and process it in `loop()`.

```cpp
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 0.00 V even if the real signal is different. This is not an error —
> it is the library's default before any reply has been received.
>
> **⚠ Do NOT pass the result of `ainv()` to `ain2v()`.**
> `ainv()` already returns volts. `ain2v()` expects a raw value (0–26624).
> Applying both produces a completely wrong result.

```cpp
float v = iotv.ainv(addr, SIDE_A, 1); // channel 1, already in volts
```

### `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
(`out[0]` = channel 1, `out[1]` = channel 2, etc.).
Also updates internal memory so subsequent `iainv()` and `aMemValid()` calls reflect the values.
Returns `true` on success; `false` on timeout (`out` is left untouched).

> **Preferred over `ainv()` in QEMU:** In the QEMU simulator, `ainv4()` is the only
> synchronous call that reliably updates the internal memory flag (`aMemValid()`).
> Use `ainv4()` as the default analog read method in QEMU-compatible sketches.

```cpp
float ch[4];
if (iotv.ainv4(addr, SIDE_A, ch)) {
    // ch[0] = channel 1, ch[1] = channel 2, ...
    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.

```cpp
// 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.

> **⚠ QEMU simulator behaviour (verified):** In the QEMU simulator, `ainv()` alone does
> NOT update the internal memory flag that `aMemValid()` checks — it always returns `false`
> even after a successful `ainv()` call. Only `ainv4()` reliably sets the flag in QEMU.
> **Pattern for QEMU-compatible code:**
> 1. Call `ainv4()` once in `setup()` to prime the flag.
> 2. Use `ainv4()` (not `ainv()`) in `loop()` for all analog reads.
> 3. Guard with `aMemValid()` or check the `bool ok` returned by `ainv4()` directly.

```cpp
// setup(): prime the internal memory flag
float ch[4];
iotv.ainv4(ADDR, SIDE_B, ch);

// loop(): robust analog read pattern
float ch[4];
bool ok = iotv.ainv4(ADDR, SIDE_B, ch);
if (!ok) {
    // timeout — vertebra did not reply
    return;
}
float volts = ch[0]; // channel 1 → index 0
```

### `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.

```cpp
iotv.aoutv(addr, SIDE_B, 1, 5.0f); // 5V on channel 1
```

### `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.

> **EEPROM note (Python/RPi API only):** On the RPi CAN/I2C API, adding 4096 to the raw
> value writes to the DAC's EEPROM (persists after reset). This feature does NOT apply to
> the Arduino library's `aout()` — the library clamps to 0–4095 precisely to prevent
> accidental EEPROM writes. Limit EEPROM writes to fewer than 20,000 lifetime operations.

---

## 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`.

| raw | volts |
|-----|-------|
| 0 | −10.0 V |
| 13312 | 0.0 V |
| 26624 | +10.0 V |

### `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.

| volts | raw |
|-------|-----|
| 0.0 | 0 |
| 5.0 | 2048 |
| 10.0 | 4095 |

---

## Channel Numbering — Arduino vs Python

| Context | Digital address | Analog channel |
|---------|----------------|----------------|
| **Arduino IDE (this library)** | `uint8_t` 0–15 | `uint8_t` 1–4 |
| **RPi CAN/I2C Python** | `str` `'0000'`–`'1111'` | `int` 1–4 |
| **Web Simulator Python** | `str` `'0000'`–`'1111'` | `int` 0–3 ⚠ |

> ⚠ The web simulator uses channels 0–3; the Arduino library and RPi API use channels 1–4.

**Address conversion:**
```cpp
// Arduino: integer address
uint8_t addr = 0;                     // vertebra at address 0
uint8_t addr = iotv.addr("0101");    // from binary string → 5

// Python RPi/Simulator: binary string
addr = '0000'   # → address 0
addr = '0101'   # → address 5
```

---

## Analog Voltage Ranges

| Direction | Range | Notes |
|-----------|-------|-------|
| ADC input (`ainv`) | −10.0 to +10.0 V | Signed; raw 0–26624 |
| DAC output (`aoutv`) | 0.0 to +10.0 V | Unsigned; raw 0–4095 |

---

## Sketch Structure

### Digital vertebra (minimal)

```cpp
#include <IoTVertebrae.h>

const uint8_t ADDR = 0;

void setup() {
    Serial.begin(115200);
    if (!iotv.begin()) {
        Serial.println("CAN init failed");
        while (true);
    }
    iotv.dsetup(ADDR, DIN, DOUT);
}

void loop() {
    uint8_t inputs = iotv.din(ADDR, SIDE_A);
    iotv.dout(ADDR, SIDE_B, inputs);  // mirror inputs to outputs
    delay(100);
}
```

### Analog vertebra — QEMU-compatible pattern

Use `ainv4()` instead of `ainv()` in QEMU. Prime `aMemValid()` with one `ainv4()` call in `setup()`.

```cpp
#include <IoTVertebrae.h>

const uint8_t ADDR    = 0;
const uint8_t CHANNEL = 1;  // channel 1 → index 0 in ainv4() array

void setup() {
    Serial.begin(115200);
    if (!iotv.begin()) {
        Serial.println("CAN init failed");
        while (true);
    }
    // Prime internal memory so aMemValid() returns true in loop()
    float ch[4];
    iotv.ainv4(ADDR, SIDE_B, ch);
}

void loop() {
    float ch[4];
    bool ok = iotv.ainv4(ADDR, SIDE_B, ch);
    if (!ok) {
        Serial.println("timeout");
        delay(100);
        return;
    }
    float volts = ch[CHANNEL - 1];  // channel 1 → index 0
    Serial.printf("ch%d: %.2f V\n", CHANNEL, volts);
    delay(1000);
}
```

---

## QEMU Simulator Notes

When compiling for the QEMU simulator board package (`IoTVertebrae-qemu`),
the macro `IOTV_QEMU_BUILD` is defined automatically. Use it for conditional compilation:

```cpp
#ifdef IOTV_QEMU_BUILD
    // Simulator-specific code (WiFi → Ethernet, CAN → MQTT)
#else
    // Real hardware code
#endif
```

| Feature | Real hardware | QEMU simulator |
|---------|--------------|----------------|
| WiFi | Normal ESP32 WiFi | Redirected to emulated Ethernet (OpenEth) |
| CAN / TWAI | Physical bus | Redirected to MQTT (broker: 192.168.4.1) |
| Power pins | 3V3 / 24V control | Not available |
| Vertebrae | Physical hardware | Interactive SVG digital twins in browser |

To use: compile with `Sketch → Export Compiled Binary (Ctrl+Alt+S)`,
then upload the generated ZIP from the `build/` folder to `iotvSim.binefa.cat`.

---

## Python → Arduino Translation Reference

When translating Python (RPi or simulator) code to Arduino C++:

| Python (RPi CAN / Simulator) | Arduino C++ |
|------------------------------|-------------|
| `iotv.can_on()` / `iotv.init()` | `iotv.begin()` |
| `iotv.can_off()` / `iotv.reset()` | `iotv.end()` |
| `iotv.dsetup('0000', 'dout', 'din')` | `iotv.dsetup(0, DOUT, DIN)` |
| `iotv.din('0000', 'B')` → `str` | `iotv.din(0, SIDE_B)` → `uint8_t` |
| `iotv.dinbit('0000', 'B', 1)` (sim only) | `iotv.idinbit(0, SIDE_B, 1)` |
| `iotv.dout('0000', 'A', 0xFF)` | `iotv.dout(0, SIDE_A, 0xFF)` |
| `iotv.doutbit('0000', 'A', 5, 1)` | `iotv.doutbit(0, SIDE_A, 5, 1)` |
| `iotv.ain('0000', 'B', 1)` (RPi, ch 1–4) | `iotv.ain(0, SIDE_B, 1)` |
| `iotv.ain('0000', 'B', 0)` (sim, ch 0–3) | `iotv.ain(0, SIDE_B, 1)` ⚠ +1 |
| `iotv.ainv('0000', 'B', 1)` | `iotv.ainv(0, SIDE_B, 1)` — prefer `ainv4` in QEMU |
| `iotv.ainBatch('0000', 'B')` → list[4] | `iotv.ainv4(0, SIDE_B, ch)` → `float ch[4]` |
| `iotv.aout('0000', 'B', 1, val)` | `iotv.aout(0, SIDE_B, 1, val)` |
| `iotv.aoutv(...)` (not in Python) | `iotv.aoutv(0, SIDE_B, 1, 5.0f)` |
| `iotv.ain2v(raw)` | `iotv.ain2v(raw)` (static, same) |
| `iotv.v2aout(volts)` | `iotv.v2aout(volts)` (static, same) |
| `time.sleep(0.5)` | `delay(500)` |
| `on_din_change(addr, cb)` | `iotv.onDinChange(cb)` |
| Parse bit from `str`: `int(val[6])` | Read bit: `(byte >> 6) & 1` |

> ⚠ Simulator Python uses analog channels 0–3; Arduino and RPi use 1–4. Add 1 when translating from simulator.
>
> ⚠ **Do NOT chain `ain2v(ainv(...))`** — `ainv()` already returns volts; `ain2v()` expects a raw value (0–26624). Only use `ain2v()` with the raw result of `ain()`.

---

## Included Examples

Available via `File → Examples → IoTVertebrae` after installing the board package.

| Sketch | Demonstrates |
|--------|-------------|
| `BasicDigital` | `dsetup()`, `din()`, `dout()`, `dversion()`, `getdsetup()` |
| `BasicAnalog` | `aoutv()`, `ainv()`, `setAinFreq()`, `iainv()` |
| `AnalogFourChannels` | All 4 ADC/DAC channels, `ain2v()`, `v2aout()` |
| `FullDemo` | All digital + analog functions, `onDinChange()` callback, triangular wave with `aoutv()` |
| `Head02` | Full ESP32-S3 firmware with MQTT, `#ifdef IOTV_QEMU_BUILD` |
| `Head02Min` | Minimal ESP32-S3 firmware base (recommended starting point) |
| `Head01Legacy` | Full ESP32 legacy firmware with I2C MQTT |
| `Head01LegacyMin` | Minimal ESP32 legacy firmware |
