b2267f2261
CI / platformio (pull_request) Failing after 6m18s
- scenario_mesh: ESP-NOW frame protocol component (master + box3_voice): chunking, per-source reassembly, deferred apply off the Wi-Fi callback. - game_endpoint: POST /game/scenario/relay (master) + shared scenario_apply. - box3_voice: scenario receiver wiring (scenario_mesh_init). - espnow_slave (shared by all puzzles): demux scenario frames inside the single recv callback so a misrouted relay can't corrupt the MSG_* stream; reassemble per source MAC; optional consumer hook (logs+drops by default, puzzles have no scenario engine). Add missing esp_mac.h include for MACSTR/MAC2STR. - CMakeLists (scenario_mesh + 4 puzzle mains): drop invalid `esp_now` REQUIRES; the ESP-NOW API lives in esp_wifi. Fixes "Failed to resolve component esp_now". - docs: scenario-mesh receiver patch note (puzzles done via defensive demux; PLIP out of scope — Wi-Fi/HTTP-only, no ESP-NOW stack). Builds green under ESP-IDF 5.4.4: idf_zacus, box3_voice, p7_coffre. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
4.1 KiB
C
87 lines
4.1 KiB
C
// scenario_mesh — ESP-NOW transport for Runtime 3 IR hot-load (Phase 2).
|
|
//
|
|
// Implements tasks 4 & 5 of docs/specs/2026-05-24-firmware-scenario-hotload.md:
|
|
//
|
|
// * Frame protocol: the IR JSON blob is chunked into ESP-NOW frames of
|
|
// <= SCENARIO_MESH_FRAME_MAX (240) bytes. Each frame carries a 4-byte
|
|
// header { seq:u16, total:u16 } (little-endian on the wire) followed by
|
|
// up to SCENARIO_MESH_PAYLOAD_MAX payload bytes.
|
|
// * Sender (master): scenario_mesh_send() resolves an alias to a MAC,
|
|
// registers the peer, and transmits every frame sequentially, awaiting
|
|
// the per-frame esp_now send-callback ack before advancing.
|
|
// * Receiver (peer board): the registered esp_now recv callback accumulates
|
|
// frames keyed by (sender MAC + total) until `total` frames have arrived,
|
|
// concatenates them, and hands the reassembled buffer to the apply
|
|
// callback supplied at init — the same internal `_scenario_apply()` path
|
|
// the HTTP POST /game/scenario handler uses.
|
|
//
|
|
// NOTE ON THE "EXISTING PEER REGISTRY":
|
|
// The spec references an existing ESP-NOW peer registry + `espnow_recv_cb`
|
|
// to extend. In the IDF tree (idf_zacus) no such registry exists yet — the
|
|
// only ESP-NOW code is the legacy Arduino lib/espnow_common (broadcast-only,
|
|
// puzzle-id keyed, not an IDF component). So this component carries its own
|
|
// minimal alias->MAC table (scenario_mesh_register_peer / _mac_for_alias).
|
|
// When a real shared registry lands, point mac_for_alias() at it.
|
|
|
|
#pragma once
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#include "esp_err.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
// ESP-NOW hard limit on a single payload is 250 bytes. We cap the whole
|
|
// frame (header + payload) at 240 to stay clear of vendor headers and keep a
|
|
// safety margin, matching the spec's "<= 240-byte ESP-NOW frames".
|
|
#define SCENARIO_MESH_FRAME_MAX 240
|
|
#define SCENARIO_MESH_HEADER_BYTES 4
|
|
#define SCENARIO_MESH_PAYLOAD_MAX (SCENARIO_MESH_FRAME_MAX - SCENARIO_MESH_HEADER_BYTES) // 236
|
|
|
|
// Largest IR blob we will reassemble on the receive side. Mirrors
|
|
// GAME_ENDPOINT_MAX_SCENARIO_BYTES (64 KiB). 64 KiB / 236 ≈ 285 frames, well
|
|
// under the u16 sequence space.
|
|
#define SCENARIO_MESH_MAX_BLOB (64 * 1024)
|
|
|
|
// Per-frame ack timeout. ESP-NOW send-callbacks normally fire within a few ms;
|
|
// a generous window absorbs RF retries without stalling the relay loop.
|
|
#define SCENARIO_MESH_ACK_TIMEOUT_MS 300
|
|
|
|
// Callback invoked on the receive side once a full blob has been reassembled.
|
|
// `data` is a NUL-terminated buffer of `len` bytes (the IR JSON). The callback
|
|
// must NOT take ownership — the buffer is freed by scenario_mesh after return.
|
|
// Return ESP_OK if the scenario was applied; any other value is logged.
|
|
typedef esp_err_t (*scenario_mesh_apply_cb_t)(const char *data, size_t len);
|
|
|
|
// Initialize ESP-NOW (idempotent — tolerates an already-initialized stack),
|
|
// register the send + recv callbacks, and register the broadcast peer.
|
|
//
|
|
// `apply_cb` may be NULL on a pure sender (master) that never receives
|
|
// scenarios; pass the board's _scenario_apply wrapper on receiver boards.
|
|
esp_err_t scenario_mesh_init(scenario_mesh_apply_cb_t apply_cb);
|
|
|
|
// Register / update an alias -> MAC mapping in the local peer table and add the
|
|
// MAC as an unencrypted ESP-NOW peer. Safe to call repeatedly with the same
|
|
// alias (updates the MAC). Returns ESP_ERR_NO_MEM if the table is full.
|
|
esp_err_t scenario_mesh_register_peer(const char *alias, const uint8_t mac[6]);
|
|
|
|
// Resolve an alias to its MAC. Returns ESP_OK and fills `mac_out` on hit,
|
|
// ESP_ERR_NOT_FOUND otherwise.
|
|
esp_err_t scenario_mesh_mac_for_alias(const char *alias, uint8_t mac_out[6]);
|
|
|
|
// Chunk `data` (len bytes) into frames and send them all sequentially to
|
|
// `dest_mac`, awaiting the per-frame ack. Returns ESP_OK only if every frame
|
|
// was acked; ESP_ERR_TIMEOUT if any frame ack timed out, or the underlying
|
|
// esp_now_send error. The caller (relay handler) treats a non-OK return as a
|
|
// skipped peer and continues with the others.
|
|
esp_err_t scenario_mesh_send(const uint8_t dest_mac[6],
|
|
const char *data, size_t len);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|