# cactus_fullmode Integration Notes

For the integrator (Opus / next-session Sonnet) merging this library into the
live `t1-sensor-firmware` build.  All changes needed in existing files are
listed here; **no existing source files were modified** by this PR.

---

## 1. platformio.ini — one line needed

Append to `[env:heltec_wifi_lora_v4]` (and the uav_bridge variant if desired):

```ini
lib_extra_dirs = lib
```

Verify it is not already set — the current platformio.ini (as of 2026-04-24)
does not include `lib_extra_dirs`.  PlatformIO will then discover
`lib/cactus_fullmode/library.json` automatically.

---

## 2. config.h — one constant to add

Add after the existing `MODEL_PATH_PROTOCOL_ANOMALY` line:

```c
// Cactus Full Mode threat classifier (.cactus format, 128-in 7-out)
#define MODEL_PATH_THREAT_CACTUS  "/models/threat_classifier.cactus"
```

---

## 3. main.cpp — two additions (do not modify any existing code)

### 3a. In `setup()`, after `model_manager_init()` and before `correlator_init()`:

```cpp
#include "fullmode_t1.h"
// ...
// ---- Initialize Full Mode .cactus models ----
Serial.printf("[%s] Initializing cactus_fullmode...\n", TAG);
fullmode_t1_init();
```

The `#include` goes with the other includes at the top of main.cpp.

### 3b. In `loop()`, immediately after the existing `model_manager_infer_all_resident`
block (after the closing `}` of `if (s_multimodel_active)`):

```cpp
// ---- Cactus Full Mode inference ----
{
    static ModelInferenceResult fm_results[FULLMODE_T1_MAX_SLOTS];
    int fm_count = fullmode_t1_infer_all_resident(
        s_spectrum, SPECTRUM_BINS,
        fm_results, FULLMODE_T1_MAX_SLOTS);
    for (int i = 0; i < fm_count; i++) {
        if (fm_results[i].valid) correlator_feed(fm_results[i]);
    }
}
```

Results use `slot_id = MODEL_BACKEND_CACTUS | cactus_local_idx` (e.g. 0xC0)
so they are distinguishable from TFLite slots (0..7) in the correlator buffer.
The correlator processes them identically — `correlator_feed` accepts any
`ModelInferenceResult` regardless of backend.

---

## 4. SPIFFS data upload

The artifact `data/models/threat_classifier.cactus` (6.0 KB) must be uploaded
to SPIFFS alongside the existing `.tflite` files:

```bash
pio run --target uploadfs -e heltec_wifi_lora_v4
```

---

## 5. PSRAM budget impact

| Component            | PSRAM per slot | Max slots | Max total |
|----------------------|---------------|-----------|-----------|
| TFLite model_manager | ~256 KB arena + model | 8 | ~4 MB (MODEL_PSRAM_BUDGET) |
| cactus_fullmode      | 256 KB (CACTUS_FM_PSRAM_PER_SLOT) | 4 | **1 MB** |

The existing `MODEL_PSRAM_BUDGET` is 4 MB.  cactus_fullmode sits **outside**
that budget (separate heap_caps_malloc calls).  Actual usage for the shipped
`threat_classifier.cactus` (128→32→32→7, int8) is:

- Weights: 128×32 + 32×32 + 32×7 = 5,376 int8 bytes (~5 KB)
- Biases (fp32): (32+32+7)×4 = 284 bytes
- Scratch ping-pong: 2×128×4 = 1 KB (largest intermediate = 128 inputs)
- Pool alignment overhead: ~64 bytes
- **Total per slot: ~7 KB actual** (pool ceiling: 256 KB; safe for 4 models)

If you later load 4 large fp32 models you could use up to 4×256 KB = 1 MB.
The ESP32-S3 has 8 MB PSRAM; total combined peak (TFLite 4 MB + cactus 1 MB)
leaves ~3 MB free — well within bounds.

To tighten the per-slot ceiling for small models reduce:
```c
// In platformio.ini build_flags or config.h:
-DCACTUS_FM_PSRAM_PER_SLOT=(64*1024)
```

---

## 6. On-device test

The hardware smoke test is at `test/test_fullmode/test_fullmode_ondevice.cpp`.

**This test cannot run in the native environment** because it depends on
SPIFFS, `heap_caps_malloc`, and the Arduino Serial API — none of which are
available in the `[env:native]` stub environment.  Run it on hardware:

```bash
pio test -e heltec_wifi_lora_v4 -f test_fullmode
```

The test writes a hand-crafted minimal `.cactus` blob to SPIFFS, loads it,
infers on a known input, and verifies the softmax output sums to 1.0 and the
correct class wins.

---

## 7. Re-training / replacing the .cactus artifact

```bash
cd C:\Users\markl\cactus_engine
python scripts/export_t1_threat.py
# Then re-upload SPIFFS:
pio run --target uploadfs -e heltec_wifi_lora_v4
```

Replace synthetic training data in `export_t1_threat.py` with real spectra
from a Castleton range capture before production deployment.

---

## 8. MODEL_BACKEND_CACTUS sentinel

`fullmode_t1.h` defines:
```c
#define MODEL_BACKEND_CACTUS  0xC0u
```

`fullmode_t1_infer_all_resident()` sets `result.slot_id = MODEL_BACKEND_CACTUS | local_idx`
(e.g. `0xC0`, `0xC1`, …).  TFLite slots are `0x00`–`0x07`.  The correlator
does not inspect `slot_id` for routing; it uses the class labels and
confidences only.  If a future version of `model_manager` adds backend
dispatch, match on `(slot_id & 0xC0) == MODEL_BACKEND_CACTUS`.
