# INTEROP_INTEGRATION_NOTES

Build of multi-protocol coexistence for the T1 sensor:

* `radio_scheduler.{h,cpp}` — single-radio time-slice scheduler
* `meshtastic_emit.{h,cpp}` — Meshtastic LongFast US915 RX/TX
* `rnode_bridge.{h,cpp}`    — Reticulum/RNode KISS-over-LoRa RX/TX
* `mqtt_client.{h,cpp}`     — WiFi STA + MQTT publish/subscribe
* `interop_router.{h,cpp}`  — glue between Meshtastic/RNode and PDL mesh

The integration below is intentionally a hint sheet — `main.cpp` is NOT modified by this batch. The next person (or me) wires it up explicitly.

All new code is feature-flag gated by `-DPDL_INTEROP_ENABLED`. Sensors that don't need multi-protocol use the existing `env:heltec_wifi_lora_v4`; interop sensors flash `env:heltec_wifi_lora_v4_interop`.

---

## 1. New `#include`s at the top of `main.cpp`

```cpp
#ifdef PDL_INTEROP_ENABLED
#include "radio_scheduler.h"
#include "meshtastic_emit.h"
#include "rnode_bridge.h"
#include "mqtt_client.h"
#include "interop_router.h"
#endif
```

## 2. `setup()` — call order

Append to the very end of the existing `setup()`, AFTER `mesh_init()` (the scheduler uses `mesh_get_radio()`) and AFTER `enrollment_init()` (Meshtastic NodeID is derived from the assigned sensor ID):

```cpp
#ifdef PDL_INTEROP_ENABLED
    radio_sched_init();      // must come BEFORE protocol inits
    meshtastic_init();
    rnode_init();
    mqtt_init();
    interop_router_init();
#endif
```

## 3. `loop()` — call order

The scheduler tick MUST run at the top of the loop, before any radio TX/RX. Place it as the FIRST statement inside `loop()`:

```cpp
void loop() {
#ifdef PDL_INTEROP_ENABLED
    radio_sched_tick();
    interop_router_tick();
#endif

    // ... existing main-loop body unchanged ...

#ifdef PDL_INTEROP_ENABLED
    // Towards the end of the loop, opportunistic NodeInfo emit. Internal
    // rate-limiter ensures it only fires once per minute and only during
    // a Meshtastic time-slice.
    if (meshtastic_is_enabled()) meshtastic_emit_nodeinfo();
#endif
}
```

## 4. RX dispatch — extend `mesh_receive()` consumer

After the existing block that calls `handleIncomingCommand()` on receipt of `MSG_TYPE_COMMAND`, add:

```cpp
#ifdef PDL_INTEROP_ENABLED
    // If we just received a packet WHILE the scheduler is in MESHTASTIC or
    // RNODE mode, route it through the corresponding parser (which may
    // re-enqueue it as MSG_TYPE_INTEROP_RX for forwarding to PDL).
    RadioMode m = radio_sched_current_mode();
    if (m == RADIO_MODE_MESHTASTIC) {
        meshtastic_handle_packet(rx_buf, rx_len);
    } else if (m == RADIO_MODE_RNODE) {
        rnode_handle_packet(rx_buf, rx_len);
    }

    // PDL-mode receive of an interop-shaped packet (T2 told us to forward
    // a chunk OUT through MT/RN, or another sensor relayed an inbound
    // packet from a different protocol).
    if (rx_len > 0) {
        if (rx_buf[0] == MSG_TYPE_INTEROP_TX) {
            // Strip leading type byte and dispatch to the chosen protocol
            interop_router_dispatch_tx(rx_buf + 1, rx_len - 1);
        } else if (rx_buf[0] == MSG_TYPE_RADIO_MODE) {
            // Body: u8 pdl_pct, u8 mesh_pct, u8 rnode_pct (already persisted
            // by the command handler before this hits the wire — re-read NVS).
            radio_sched_reload_weights();
        }
    }
#endif
```

## 5. New `SensorCmdType` verbs

Add to the enum in `command_handler.h` and to the dispatcher switch:

| Value | Name | Param format |
|------:|---|---|
| 22 | `INTEROP_SEND` | `[u8 proto][u8 plen][bytes payload]` — `proto = 1` for Meshtastic (payload = ASCII text), `proto = 2` for RNode (payload = `[32 hex-char dest][bytes lxmf]`) |
| 23 | `RADIO_MODE_SET` | `[u8 pdl_pct][u8 mesh_pct][u8 rnode_pct]` — must sum to 100 |
| 24 | `MQTT_PROVISION` | `[u8 field_id][u8 vlen][bytes value]` — `field_id` indexes `MQTT_NVS_*` keys |
| 25 | `MESHTASTIC_PROVISION` | `[u8 enabled][char name[12]]` |
| 26 | `RNODE_PROVISION` | `[u8 enabled]` |

The dispatcher cases (sketch — full impl is a follow-up):

```cpp
case SensorCmdType::INTEROP_SEND: {
    if (pkt.param_len < 2) { ack.status = (uint8_t)CmdAckStatus::INVALID; break; }
    bool ok = interop_router_dispatch_tx(pkt.params, pkt.param_len);
    ack.status = ok ? (uint8_t)CmdAckStatus::OK : (uint8_t)CmdAckStatus::FAILED;
    break;
}
case SensorCmdType::RADIO_MODE_SET: {
    if (pkt.param_len != 3) { ack.status = (uint8_t)CmdAckStatus::INVALID; break; }
    nvs_handle_t h;
    if (nvs_open(RADIO_SCHED_NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
        nvs_set_u8(h, RADIO_SCHED_NVS_PDL,  pkt.params[0]);
        nvs_set_u8(h, RADIO_SCHED_NVS_MESH, pkt.params[1]);
        nvs_set_u8(h, RADIO_SCHED_NVS_RN,   pkt.params[2]);
        nvs_commit(h);
        nvs_close(h);
    }
    radio_sched_reload_weights();
    ack.status = (uint8_t)CmdAckStatus::OK;
    break;
}
// MQTT_PROVISION / MESHTASTIC_PROVISION / RNODE_PROVISION: write the
// matching NVS namespace key. After MQTT_PROVISION the sensor must reboot
// to re-init WiFi+MQTT (the existing REBOOT verb is fine for that).
```

## 6. NVS key reference

| Namespace | Key | Type | Default | Owner |
|---|---|---|---|---|
| `pdl_radio` | `pdl_pct`     | u8   | 80 | radio_scheduler.cpp |
| `pdl_radio` | `mesh_pct`    | u8   | 15 | radio_scheduler.cpp |
| `pdl_radio` | `rnode_pct`   | u8   |  5 | radio_scheduler.cpp |
| `pdl_mesht` | `enabled`     | u8   |  0 | meshtastic_emit.cpp |
| `pdl_mesht` | `name`        | str  | "PDL-Sensor" | meshtastic_emit.cpp |
| `pdl_rnode` | `enabled`     | u8   |  0 | rnode_bridge.cpp |
| `pdl_mqtt`  | `ssid`        | str  | "" | mqtt_client.cpp |
| `pdl_mqtt`  | `psk`         | str  | "" | mqtt_client.cpp |
| `pdl_mqtt`  | `broker_host` | str  | "" | mqtt_client.cpp |
| `pdl_mqtt`  | `broker_port` | u16  | 1883 (8883 if tls=1) | mqtt_client.cpp |
| `pdl_mqtt`  | `client_id`   | str  | "pdl-<SENSOR_NAME>" | mqtt_client.cpp |
| `pdl_mqtt`  | `tls`         | u8   | 0 | mqtt_client.cpp |
| `pdl_mqtt`  | `user`        | str  | "" | mqtt_client.cpp |
| `pdl_mqtt`  | `pass`        | str  | "" | mqtt_client.cpp |
| `pdl_mqtt`  | `ca_cert`     | blob | (none) | mqtt_client.cpp |

## 7. Provisioning a sensor end-to-end

From a T2 gateway (over the existing LoRa command channel), or a USB serial provisioning tool:

```text
# Enable Meshtastic emit + set node short-name
MESHTASTIC_PROVISION enabled=1 name="PDL-LBG01"

# Enable RNode bridge
RNODE_PROVISION enabled=1

# Configure MQTT (one field per command — keep packets ≤ 32 bytes)
MQTT_PROVISION field=ssid          value="OperatorWiFi"
MQTT_PROVISION field=psk           value="hunter2_xxxxx"
MQTT_PROVISION field=broker_host   value="t2-gateway.lan"
MQTT_PROVISION field=broker_port   value=1883
MQTT_PROVISION field=tls           value=0

# Optionally rebalance time slices (defaults 80/15/5)
RADIO_MODE_SET pdl_pct=70 mesh_pct=20 rnode_pct=10

REBOOT
```

For TLS: set `tls=1` and (optionally) `ca_cert` blob. v1 uses `WiFiClientSecure::setInsecure()` if no CA is provisioned — for production, write the CA PEM to NVS and update `mqtt_client.cpp` to call `setCACert(ca_pem)` (TODO marker in code).

## 8. Image-size impact

* `env:heltec_wifi_lora_v4`         — unchanged (no interop code linked)
* `env:heltec_wifi_lora_v4_interop` — adds ~70-90 KB flash:
  * PubSubClient ~8 KB
  * WiFi/STA + WiFiClientSecure ~30 KB
  * meshtastic_emit + rnode_bridge + mqtt_client + interop_router + radio_scheduler ~25 KB
  * MQTT packet buffer (`MQTT_MAX_PACKET_SIZE=512`) — 512 B RAM

## 9. Coordination notes for T2 mirror agent

The new packet types `MSG_TYPE_INTEROP_RX (0x21)`, `MSG_TYPE_INTEROP_TX (0x22)`, `MSG_TYPE_RADIO_MODE (0x23)` need to be plumbed through:

* `t2-gateway-runtime/gateway/packet_format.py` — add the three constants and a parser for the wire payload above.
* `t2-gateway-runtime/gateway/lora_receiver.py` — when the LoRa receiver gets `0x21`, dispatch to `interop_bridge.ingest_pdl_interop_rx()`.
* `t2-gateway-runtime/gateway/sensor_commander.py` — add the new SensorCmdType verbs (22-26) so a CLI / API can compose `INTEROP_SEND` and `RADIO_MODE_SET` packets.
* `t2-gateway-runtime/gateway/local_api.py` — expose `POST /api/operator/interop/send` and `POST /api/operator/interop/radio_mode` (see `gateway/HINTS_FOR_T2_MIRROR_AGENT.md`).

The new gateway-side bridge module is at `t2-gateway-runtime/gateway/interop_bridge.py`.
