# Batch-B Integration Notes — mesh_timesync / enrollment / ota_signed

Three operational-essential modules for the T1 sensor. No existing files
are modified. All changes described here belong in **config.h** and
**main.cpp** only.

---

## 1. config.h — add the three new MSG_TYPE constants

Append after `MSG_TYPE_BEHAVIORAL_NLL 0x13`:

```cpp
#define MSG_TYPE_ENROLLMENT           0x17  // T1 -> T2: first-boot registration
#define MSG_TYPE_ENROLLMENT_RESPONSE  0x18  // T2 -> T1: assigned ID + position
#define MSG_TYPE_TIME_SYNC            0x19  // T2 -> T1: UTC epoch broadcast
```

---

## 2. main.cpp — include order

Add near the top of main.cpp (after existing includes):

```cpp
#include "mesh_timesync.h"
#include "enrollment.h"
#include "ota_signed.h"
```

---

## 3. main.cpp — setup() changes

### 3a. enrollment_init() must be called FIRST (before every other subsystem)

Replace the line that assigns `s_sensor_id`:

```cpp
// OLD:
static uint32_t s_sensor_id = DEFAULT_SENSOR_ID;

// NEW — at top of setup(), before any other subsystem init:
if (!enrollment_init()) {
    // Not yet enrolled — loop re-broadcasting every 5 s until T2 responds.
    // Subsystems that depend on sensor_id must wait; early-return loop:
    while (!enrollment_is_complete()) {
        enrollment_tick();
        delay(100);
    }
}
uint32_t s_sensor_id = enrollment_get_sensor_id();
```

If the sensor has already enrolled (NVS has `pdl_enroll/done=1`),
`enrollment_init()` returns true immediately and the while-loop is skipped.

### 3b. timesync_init() — call alongside other subsystem inits

```cpp
timesync_init();
```

Place after `enrollment_init()` and before `mesh_start_receive()`.

### 3c. Load OTA public key from NVS at boot

```cpp
{
    uint8_t pk[32];
    if (ota_signed_get_pubkey(pk)) {
        ota_signed_set_pubkey(pk);  // cache in module; NVS already set
        Serial.printf("[T1] OTA signing key loaded from NVS\n");
    } else {
        Serial.printf("[T1] WARNING: No OTA signing key in NVS — "
                      "all firmware chunks will be rejected until key is provisioned\n");
    }
}
```

---

## 4. main.cpp — loop() RX dispatch: add three new cases

Inside the `switch (msg_type)` block:

```cpp
case MSG_TYPE_TIME_SYNC:             // 0x19
    timesync_handle_packet(rx_buf, rx_len);
    break;

case MSG_TYPE_ENROLLMENT_RESPONSE:   // 0x18
    enrollment_handle_response(rx_buf, rx_len);
    if (enrollment_is_complete()) {
        s_sensor_id = enrollment_get_sensor_id();
        ffps_init(s_sensor_id);       // re-init FFPS with real sensor ID
        health_init(s_sensor_id);     // re-init health reporter
    }
    break;

case MSG_TYPE_ENROLLMENT:            // 0x17 — from another T1; relay only
    // T1 sensors do not process peer enrollment broadcasts — drop silently.
    break;
```

---

## 5. main.cpp — timestamp upgrade (replace millis() fallback)

Locate the detection event build in loop():

```cpp
// OLD:
evt.timestamp_utc = (uint32_t)(millis() / 1000);  // TODO: real UTC

// NEW:
evt.timestamp_utc = timesync_is_synced()
                  ? (uint32_t)(timesync_now_utc_ms() / 1000)
                  : (uint32_t)(millis() / 1000);
```

---

## 6. main.cpp — OTA chunk gate (signature check before apply)

In the `case MSG_TYPE_OTA_CHUNK:` handler, wrap the existing call:

```cpp
case MSG_TYPE_OTA_CHUNK: {
    // Signature is the last 64 bytes of the full OTA packet.
    // If packet is too short to carry a sig, reject.
    if ((size_t)rx_len >= 64) {
        const uint8_t* sig = rx_buf + rx_len - 64;
        if (!ota_signed_verify_chunk(rx_buf, rx_len - 64, sig)) {
            Serial.printf("[T1] OTA chunk rejected: bad Ed25519 signature\n");
            break;
        }
    } else {
        Serial.printf("[T1] OTA chunk rejected: packet too short for sig\n");
        break;
    }
    ota_handle_chunk(rx_buf, rx_len);
    break;
}
```

> The T2 gateway must append the 64-byte Ed25519 signature of the chunk
> payload (excluding the sig itself) as the final 64 bytes of each
> MSG_TYPE_OTA_CHUNK packet.

---

## 7. enrollment_tick() — call every loop iteration until enrolled

Add near the top of loop(), before the RF scan:

```cpp
if (!enrollment_is_complete()) {
    enrollment_tick();
}
```

---

## 8. platformio.ini — add test_batch_b to native test_filter

```ini
[env:native]
...
test_filter =
    test_command_handler
    test_spoof_pec
    test_markov_scorer
    test_wfx_features
    test_batch_b
```

---

## 9. NVS namespace summary

| Namespace   | Key          | Type   | Written by         | Read by          |
|-------------|--------------|--------|--------------------|------------------|
| pdl_enroll  | done         | u8     | enrollment.cpp     | enrollment.cpp   |
| pdl_enroll  | sensor_id    | u32    | enrollment.cpp     | enrollment.cpp   |
| pdl_enroll  | lat          | i32    | enrollment.cpp     | (app use)        |
| pdl_enroll  | lon          | i32    | enrollment.cpp     | (app use)        |
| pdl_pki     | ed25519_pk   | blob32 | ota_signed.cpp     | ota_signed.cpp   |

---

## 10. Key provisioning flow (T2 side, for reference)

1. Factory: T2 generates an Ed25519 keypair; distributes `pk[32]` to sensors
   via MSG_TYPE_ENROLLMENT_RESPONSE bytes [16..27] (first 16) plus a follow-up
   MSG_TYPE_OTA_CHUNK carrying the remaining 16 bytes.
2. Sensor stores the full 32-byte key in NVS `pdl_pki/ed25519_pk` via
   `ota_signed_set_pubkey()`.
3. All subsequent OTA deliveries are signed by the T2 with the corresponding
   private key and verified by the T1 before application.
