feat(p5,p6): scaffold morse + nfc puzzles behind CONFIG flags, default off
CI / platformio (pull_request) Failing after 3m49s
CI / platformio (pull_request) Failing after 3m49s
This commit is contained in:
@@ -20,4 +20,6 @@ idf_component_register(
|
||||
PRIV_REQUIRES
|
||||
local_puzzles
|
||||
p7_coffre
|
||||
p5_morse
|
||||
p6_nfc
|
||||
)
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "puzzle_binding.h"
|
||||
#include "local_puzzles.h"
|
||||
#include "p7_coffre.h"
|
||||
#include "p5_morse.h"
|
||||
#include "p6_nfc.h"
|
||||
|
||||
static const char *TAG = "game_endpoint";
|
||||
|
||||
@@ -39,11 +41,40 @@ static const char *TAG = "game_endpoint";
|
||||
static puzzle_state_t *s_pstate = NULL;
|
||||
// Last step id armed via POST /game/step. Empty string = none.
|
||||
static char s_current_step_id[64] = {0};
|
||||
// Last armed puzzle type: "qr" | "sound" | "none" | "" (empty = nothing armed yet).
|
||||
static char s_current_armed[8] = {0};
|
||||
// Last armed puzzle type: "qr" | "sound" | "morse" | "nfc" | "none" | ""
|
||||
static char s_current_armed[12] = {0}; // "qr"|"sound"|"morse"|"nfc"|"none"
|
||||
// Display scene metadata of the current step (lenient parse; defaults safe).
|
||||
static scene_binding_t s_current_scene = {0};
|
||||
|
||||
// ─── P5 / P6 solved-callback context ────────────────────────────────────────
|
||||
// Mirrors the on_qr_solved / on_sound_solved pattern in local_puzzles.c:
|
||||
// context is copied at arm time, callback is called on the puzzle task.
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
uint8_t frag[PB_MAX_FRAG];
|
||||
uint8_t frag_len;
|
||||
} ep_puzzle_ctx_t;
|
||||
|
||||
static ep_puzzle_ctx_t s_morse_ctx = {0};
|
||||
static ep_puzzle_ctx_t s_nfc_ctx = {0};
|
||||
|
||||
static void on_morse_solved(void)
|
||||
{
|
||||
if (s_pstate) {
|
||||
puzzle_state_report(s_pstate, s_morse_ctx.id,
|
||||
s_morse_ctx.frag, s_morse_ctx.frag_len);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_nfc_solved(void)
|
||||
{
|
||||
if (s_pstate) {
|
||||
puzzle_state_report(s_pstate, s_nfc_ctx.id,
|
||||
s_nfc_ctx.frag, s_nfc_ctx.frag_len);
|
||||
}
|
||||
}
|
||||
|
||||
// Whitelist mirrored in the 4xx error message so the operator can
|
||||
// recover without grepping the source. Keep aligned with
|
||||
// hints_client_set_group_profile() validation.
|
||||
@@ -333,6 +364,8 @@ static esp_err_t scenario_apply_buffer(const char *body, size_t len,
|
||||
|
||||
// New scenario supersedes any live puzzle — disarm and clear remembered step.
|
||||
local_puzzles_disarm();
|
||||
p5_morse_disarm(); // no-op when CONFIG_ZACUS_P5_MORSE_ENABLE=n
|
||||
p6_nfc_disarm(); // no-op when CONFIG_ZACUS_P6_NFC_ENABLE=n
|
||||
s_current_step_id[0] = '\0';
|
||||
memset(&s_current_scene, 0, sizeof(s_current_scene));
|
||||
// Re-arm the coffre for the new game session.
|
||||
@@ -727,7 +760,7 @@ static esp_err_t handle_espnow_cmd_post(httpd_req_t *req) {
|
||||
|
||||
// Core step-change logic, shared by the HTTP handler and internal callers
|
||||
// (e.g. shell app launch on the local display). Error contract:
|
||||
// ESP_OK armed (armed_out = "qr"|"sound"|"none")
|
||||
// ESP_OK armed (armed_out = "qr"|"sound"|"morse"|"nfc"|"none")
|
||||
// ESP_ERR_INVALID_STATE not ready (no puzzle_state wired)
|
||||
// ESP_ERR_NOT_SUPPORTED no scenario stored / storage unavailable
|
||||
// ESP_ERR_NOT_FOUND unknown step id
|
||||
@@ -758,6 +791,9 @@ esp_err_t game_endpoint_apply_step(const char *step_id,
|
||||
|
||||
// Disarm current puzzle before re-arming.
|
||||
local_puzzles_disarm();
|
||||
// Disarm P5/P6 stubs (no-ops when their CONFIG flags are off).
|
||||
p5_morse_disarm();
|
||||
p6_nfc_disarm();
|
||||
|
||||
// Parse binding for this step (+ display scene metadata, lenient).
|
||||
puzzle_binding_t binding;
|
||||
@@ -804,6 +840,26 @@ esp_err_t game_endpoint_apply_step(const char *step_id,
|
||||
if (aerr == ESP_ERR_INVALID_STATE) return ESP_ERR_TIMEOUT;
|
||||
if (aerr != ESP_OK) return aerr;
|
||||
armed = "sound";
|
||||
} else if (binding.type == PB_MORSE) {
|
||||
// P5 morse: copy context then arm.
|
||||
// p5_morse_arm() is a no-op stub when CONFIG_ZACUS_P5_MORSE_ENABLE=n.
|
||||
s_morse_ctx.id = binding.id;
|
||||
s_morse_ctx.frag_len = binding.fragment_len;
|
||||
if (binding.fragment_len > 0) {
|
||||
memcpy(s_morse_ctx.frag, binding.fragment, binding.fragment_len);
|
||||
}
|
||||
p5_morse_arm(binding.morse_expected, on_morse_solved);
|
||||
armed = "morse";
|
||||
} else if (binding.type == PB_NFC) {
|
||||
// P6 nfc: copy context then arm.
|
||||
// p6_nfc_arm() is a no-op stub when CONFIG_ZACUS_P6_NFC_ENABLE=n.
|
||||
s_nfc_ctx.id = binding.id;
|
||||
s_nfc_ctx.frag_len = binding.fragment_len;
|
||||
if (binding.fragment_len > 0) {
|
||||
memcpy(s_nfc_ctx.frag, binding.fragment, binding.fragment_len);
|
||||
}
|
||||
p6_nfc_arm(binding.nfc_uid, on_nfc_solved);
|
||||
armed = "nfc";
|
||||
}
|
||||
|
||||
strncpy(s_current_armed, armed, sizeof(s_current_armed) - 1);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#define PB_MAX_FRAG 4 // = PUZZLE_MAX_FRAG
|
||||
#define PB_MAX_PUZZLE_ID 8 // = PUZZLE_MAX_ID
|
||||
|
||||
typedef enum { PB_NONE = 0, PB_QR, PB_SOUND } puzzle_binding_type_t;
|
||||
typedef enum { PB_NONE = 0, PB_QR, PB_SOUND, PB_MORSE, PB_NFC } puzzle_binding_type_t;
|
||||
|
||||
typedef struct {
|
||||
puzzle_binding_type_t type; // PB_NONE if the step has no puzzle
|
||||
@@ -23,6 +23,10 @@ typedef struct {
|
||||
int melody[PB_MAX_NOTES];
|
||||
size_t note_count;
|
||||
int tolerance; // default 1
|
||||
// morse (P5): expected word in uppercase, e.g. "ZACUS"
|
||||
char morse_expected[32];
|
||||
// nfc (P6): expected tag UID as colon-hex string, e.g. "A1:B2:C3:D4"
|
||||
char nfc_uid[20];
|
||||
// common
|
||||
uint8_t fragment[PB_MAX_FRAG];
|
||||
uint8_t fragment_len;
|
||||
|
||||
@@ -60,6 +60,10 @@ esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
ptype = PB_QR;
|
||||
} else if (strcmp(jtype->valuestring, "sound") == 0) {
|
||||
ptype = PB_SOUND;
|
||||
} else if (strcmp(jtype->valuestring, "morse") == 0) {
|
||||
ptype = PB_MORSE;
|
||||
} else if (strcmp(jtype->valuestring, "nfc") == 0) {
|
||||
ptype = PB_NFC;
|
||||
} else {
|
||||
cJSON_Delete(root);
|
||||
memset(out, 0, sizeof(*out));
|
||||
@@ -120,7 +124,7 @@ esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
out->codes[i][PB_MAX_LABEL - 1] = '\0';
|
||||
}
|
||||
out->code_count = (size_t)n;
|
||||
} else { // PB_SOUND
|
||||
} else if (ptype == PB_SOUND) {
|
||||
const cJSON *jmelody = cJSON_GetObjectItemCaseSensitive(puzzle, "melody");
|
||||
if (!cJSON_IsArray(jmelody)) {
|
||||
cJSON_Delete(root);
|
||||
@@ -156,6 +160,37 @@ esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
|
||||
} else {
|
||||
out->tolerance = 1;
|
||||
}
|
||||
} else if (ptype == PB_MORSE) {
|
||||
// puzzle.expected: required string, uppercase word, e.g. "ZACUS"
|
||||
const cJSON *jexp = cJSON_GetObjectItemCaseSensitive(puzzle, "expected");
|
||||
if (!cJSON_IsString(jexp) || !jexp->valuestring || !jexp->valuestring[0]) {
|
||||
cJSON_Delete(root);
|
||||
memset(out, 0, sizeof(*out));
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (strlen(jexp->valuestring) >= sizeof(out->morse_expected)) {
|
||||
cJSON_Delete(root);
|
||||
memset(out, 0, sizeof(*out));
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
strncpy(out->morse_expected, jexp->valuestring,
|
||||
sizeof(out->morse_expected) - 1);
|
||||
out->morse_expected[sizeof(out->morse_expected) - 1] = '\0';
|
||||
} else if (ptype == PB_NFC) {
|
||||
// puzzle.uid: required string, colon-hex UID, e.g. "A1:B2:C3:D4"
|
||||
const cJSON *juid = cJSON_GetObjectItemCaseSensitive(puzzle, "uid");
|
||||
if (!cJSON_IsString(juid) || !juid->valuestring || !juid->valuestring[0]) {
|
||||
cJSON_Delete(root);
|
||||
memset(out, 0, sizeof(*out));
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (strlen(juid->valuestring) >= sizeof(out->nfc_uid)) {
|
||||
cJSON_Delete(root);
|
||||
memset(out, 0, sizeof(*out));
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
strncpy(out->nfc_uid, juid->valuestring, sizeof(out->nfc_uid) - 1);
|
||||
out->nfc_uid[sizeof(out->nfc_uid) - 1] = '\0';
|
||||
}
|
||||
|
||||
// id/type written last — all error exits above leave *out zeroed
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"p5_morse.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_driver_gpio
|
||||
log
|
||||
freertos
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
// p5_morse.h — Morse / telegraph-key puzzle driver for Zacus master (P5).
|
||||
//
|
||||
// When CONFIG_ZACUS_P5_MORSE_ENABLE=n (default) all functions are empty stubs
|
||||
// (init returns ESP_OK, arm/disarm are no-ops) so callers need no #ifdef guards.
|
||||
//
|
||||
// When enabled:
|
||||
// 1. Call p5_morse_init() once at boot (after gpio driver is available).
|
||||
// 2. Call p5_morse_arm(expected, puzzle_id, fragment, frag_len) when the
|
||||
// gateway POSTs /game/step with a "morse" puzzle.
|
||||
// 3. A FreeRTOS task decodes dot/dash timing on CONFIG_ZACUS_P5_GPIO and
|
||||
// calls the solved_cb supplied to p5_morse_arm() when the decoded word
|
||||
// matches `expected`.
|
||||
// 4. Call p5_morse_disarm() on step change or game reset.
|
||||
//
|
||||
// Timing constants are set via Kconfig (DOT_MS, DASH_MS, GAP_MS).
|
||||
// The key is active-LOW by default (CONFIG_ZACUS_P5_ACTIVE_LOW=y).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Callback type invoked on the p5_morse task when the sequence is solved.
|
||||
// Must be ISR-safe in the sense that it runs inside the morse task context;
|
||||
// do NOT call p5_morse_arm/disarm from it — defer via a task notification.
|
||||
typedef void (*p5_morse_solved_cb_t)(void);
|
||||
|
||||
/**
|
||||
* @brief Initialise the P5 Morse puzzle driver.
|
||||
*
|
||||
* Configures CONFIG_ZACUS_P5_GPIO as input with pull-up and spawns the
|
||||
* morse decoding task (blocked until p5_morse_arm() is called).
|
||||
* Idempotent — safe to call more than once.
|
||||
*
|
||||
* When CONFIG_ZACUS_P5_MORSE_ENABLE=n returns ESP_OK immediately.
|
||||
*
|
||||
* @return ESP_OK on success, or a driver error code.
|
||||
*/
|
||||
esp_err_t p5_morse_init(void);
|
||||
|
||||
/**
|
||||
* @brief Arm the morse puzzle for a specific expected sequence.
|
||||
*
|
||||
* @param expected NUL-terminated Morse code string to match,
|
||||
* e.g. "...-" (upper-case letters via kMorseTable)
|
||||
* OR a plain word like "ZACUS" (decoded internally).
|
||||
* The string is copied — caller buffer may be transient.
|
||||
* @param solved_cb Callback invoked exactly once when the sequence matches.
|
||||
* Pass NULL to skip the notification (e.g. during tests).
|
||||
*
|
||||
* When CONFIG_ZACUS_P5_MORSE_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p5_morse_arm(const char *expected, p5_morse_solved_cb_t solved_cb);
|
||||
|
||||
/**
|
||||
* @brief Disarm the puzzle (stop decoding, reset state).
|
||||
*
|
||||
* When CONFIG_ZACUS_P5_MORSE_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p5_morse_disarm(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,265 @@
|
||||
// p5_morse.c — P5 Morse / telegraph-key puzzle driver.
|
||||
//
|
||||
// All real implementation is inside #if CONFIG_ZACUS_P5_MORSE_ENABLE so the
|
||||
// object compiles to pure stubs when the flag is off (default). No #ifdef
|
||||
// leaks into callers — they include p5_morse.h and call unconditionally.
|
||||
//
|
||||
// Logic adapted from puzzles/p5_morse/main/main.c (standalone ESP-NOW
|
||||
// firmware), repackaged as a library with an arm/disarm API and a solved
|
||||
// callback instead of the espnow_slave_notify_solved path.
|
||||
|
||||
#include "p5_morse.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
static const char *TAG = "p5_morse";
|
||||
|
||||
#if CONFIG_ZACUS_P5_MORSE_ENABLE
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// ─── Morse table A-Z ────────────────────────────────────────────────────────
|
||||
|
||||
static const char *kMorseTable[26] = {
|
||||
".-", "-...", "-.-.", "-..", ".", "..-.", "--.",
|
||||
"....", "..", ".---", "-.-", ".-..", "--", "-.",
|
||||
"---", ".--.", "--.-", ".-.", "...", "-", "..-",
|
||||
"...-", ".--", "-..-", "-.--", "--.."
|
||||
};
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Expected word (decoded as letters, uppercase, e.g. "ZACUS").
|
||||
static char s_expected[32] = {0};
|
||||
// Accumulator for letters decoded so far.
|
||||
static char s_received[32] = {0};
|
||||
static uint8_t s_recv_pos = 0;
|
||||
// Current symbol dot/dash buffer.
|
||||
static char s_symbol[16] = {0};
|
||||
static uint8_t s_sym_pos = 0;
|
||||
|
||||
static volatile bool s_armed = false;
|
||||
static volatile bool s_solved = false;
|
||||
|
||||
static p5_morse_solved_cb_t s_solved_cb = NULL;
|
||||
|
||||
static bool s_initialised = false;
|
||||
|
||||
static TaskHandle_t s_task_handle = NULL;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
static uint32_t now_ms(void)
|
||||
{
|
||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static bool key_pressed(void)
|
||||
{
|
||||
#if CONFIG_ZACUS_P5_ACTIVE_LOW
|
||||
return (gpio_get_level(CONFIG_ZACUS_P5_GPIO) == 0);
|
||||
#else
|
||||
return (gpio_get_level(CONFIG_ZACUS_P5_GPIO) != 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Decode accumulated symbol (dot/dash string) to a letter and append it.
|
||||
static void decode_symbol(void)
|
||||
{
|
||||
if (s_sym_pos == 0) return;
|
||||
s_symbol[s_sym_pos] = '\0';
|
||||
|
||||
for (int i = 0; i < 26; i++) {
|
||||
if (strcmp(kMorseTable[i], s_symbol) == 0) {
|
||||
char letter = (char)('A' + i);
|
||||
if (s_recv_pos < (uint8_t)(sizeof(s_received) - 1)) {
|
||||
s_received[s_recv_pos++] = letter;
|
||||
s_received[s_recv_pos] = '\0';
|
||||
}
|
||||
ESP_LOGD(TAG, "decoded: %s → %c (so far: %s)",
|
||||
s_symbol, letter, s_received);
|
||||
break;
|
||||
}
|
||||
}
|
||||
s_sym_pos = 0;
|
||||
memset(s_symbol, 0, sizeof(s_symbol));
|
||||
}
|
||||
|
||||
// Check whether the received word matches; invoke callback on match.
|
||||
static void check_word(void)
|
||||
{
|
||||
if (strcmp(s_received, s_expected) == 0) {
|
||||
s_solved = true;
|
||||
ESP_LOGI(TAG, "SOLVED: \"%s\" decoded correctly", s_expected);
|
||||
if (s_solved_cb) {
|
||||
s_solved_cb();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "wrong word: got \"%s\", expected \"%s\" — resetting",
|
||||
s_received, s_expected);
|
||||
s_recv_pos = 0;
|
||||
memset(s_received, 0, sizeof(s_received));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Morse decoding task ─────────────────────────────────────────────────────
|
||||
|
||||
static void morse_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
uint32_t press_start = 0;
|
||||
uint32_t last_release = 0;
|
||||
bool key_down = false;
|
||||
|
||||
for (;;) {
|
||||
if (!s_armed || s_solved) {
|
||||
// Idle: yield and wait for the next arm cycle.
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t t = now_ms();
|
||||
bool pressed = key_pressed();
|
||||
|
||||
if (pressed && !key_down) {
|
||||
// Key press start.
|
||||
key_down = true;
|
||||
press_start = t;
|
||||
|
||||
} else if (!pressed && key_down) {
|
||||
// Key release — classify as dot or dash.
|
||||
key_down = false;
|
||||
uint32_t duration = t - press_start;
|
||||
last_release = t;
|
||||
|
||||
if (duration <= (uint32_t)CONFIG_ZACUS_P5_DOT_MS) {
|
||||
if (s_sym_pos < (uint8_t)(sizeof(s_symbol) - 1))
|
||||
s_symbol[s_sym_pos++] = '.';
|
||||
ESP_LOGD(TAG, "dot (%lu ms)", (unsigned long)duration);
|
||||
} else if (duration >= (uint32_t)CONFIG_ZACUS_P5_DASH_MS) {
|
||||
if (s_sym_pos < (uint8_t)(sizeof(s_symbol) - 1))
|
||||
s_symbol[s_sym_pos++] = '-';
|
||||
ESP_LOGD(TAG, "dash (%lu ms)", (unsigned long)duration);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "ignored press %lu ms (between dot and dash thresholds)",
|
||||
(unsigned long)duration);
|
||||
}
|
||||
|
||||
} else if (!key_down && last_release > 0) {
|
||||
uint32_t silence = t - last_release;
|
||||
|
||||
// Letter gap: decode accumulated symbol.
|
||||
if (silence > (uint32_t)CONFIG_ZACUS_P5_GAP_MS && s_sym_pos > 0) {
|
||||
decode_symbol();
|
||||
last_release = t;
|
||||
}
|
||||
// Word gap (2x letter gap): check assembled word.
|
||||
if (silence > (uint32_t)(CONFIG_ZACUS_P5_GAP_MS * 2u) && s_recv_pos > 0) {
|
||||
check_word();
|
||||
last_release = 0;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); // 100 Hz sampling
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
esp_err_t p5_morse_init(void)
|
||||
{
|
||||
if (s_initialised) return ESP_OK;
|
||||
|
||||
gpio_config_t cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_ZACUS_P5_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
esp_err_t err = gpio_config(&cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "gpio_config(GPIO %d): %s",
|
||||
CONFIG_ZACUS_P5_GPIO, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
BaseType_t rc = xTaskCreate(morse_task, "p5_morse", 3072, NULL, 5,
|
||||
&s_task_handle);
|
||||
if (rc != pdPASS) {
|
||||
ESP_LOGE(TAG, "xTaskCreate(p5_morse) failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_initialised = true;
|
||||
ESP_LOGI(TAG, "init OK (GPIO %d, dot<=%d ms, dash>=%d ms, gap>%d ms)",
|
||||
CONFIG_ZACUS_P5_GPIO,
|
||||
CONFIG_ZACUS_P5_DOT_MS,
|
||||
CONFIG_ZACUS_P5_DASH_MS,
|
||||
CONFIG_ZACUS_P5_GAP_MS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p5_morse_arm(const char *expected, p5_morse_solved_cb_t solved_cb)
|
||||
{
|
||||
if (!s_initialised) {
|
||||
ESP_LOGW(TAG, "arm called before init — ignored");
|
||||
return;
|
||||
}
|
||||
// Reset state.
|
||||
s_armed = false; // pause task briefly while we reset
|
||||
s_solved = false;
|
||||
s_recv_pos = 0;
|
||||
s_sym_pos = 0;
|
||||
memset(s_received, 0, sizeof(s_received));
|
||||
memset(s_symbol, 0, sizeof(s_symbol));
|
||||
|
||||
if (expected && expected[0]) {
|
||||
strncpy(s_expected, expected, sizeof(s_expected) - 1);
|
||||
s_expected[sizeof(s_expected) - 1] = '\0';
|
||||
} else {
|
||||
s_expected[0] = '\0';
|
||||
}
|
||||
s_solved_cb = solved_cb;
|
||||
s_armed = true;
|
||||
ESP_LOGI(TAG, "armed, expected \"%s\"", s_expected);
|
||||
}
|
||||
|
||||
void p5_morse_disarm(void)
|
||||
{
|
||||
s_armed = false;
|
||||
s_solved = false;
|
||||
s_recv_pos = 0;
|
||||
s_sym_pos = 0;
|
||||
memset(s_received, 0, sizeof(s_received));
|
||||
memset(s_symbol, 0, sizeof(s_symbol));
|
||||
ESP_LOGI(TAG, "disarmed");
|
||||
}
|
||||
|
||||
#else // CONFIG_ZACUS_P5_MORSE_ENABLE not set — emit stubs only
|
||||
|
||||
esp_err_t p5_morse_init(void)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p5_morse_arm(const char *expected, p5_morse_solved_cb_t solved_cb)
|
||||
{
|
||||
(void)expected;
|
||||
(void)solved_cb;
|
||||
/* stub — CONFIG_ZACUS_P5_MORSE_ENABLE=n */
|
||||
}
|
||||
|
||||
void p5_morse_disarm(void)
|
||||
{
|
||||
/* stub — CONFIG_ZACUS_P5_MORSE_ENABLE=n */
|
||||
}
|
||||
|
||||
#endif // CONFIG_ZACUS_P5_MORSE_ENABLE
|
||||
@@ -0,0 +1,12 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"p6_nfc.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_driver_gpio
|
||||
esp_driver_i2c
|
||||
esp_driver_spi
|
||||
log
|
||||
freertos
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
// p6_nfc.h — NFC / alchemical-symbols puzzle driver for Zacus master (P6).
|
||||
//
|
||||
// When CONFIG_ZACUS_P6_NFC_ENABLE=n (default) all functions are empty stubs
|
||||
// (init returns ESP_OK, arm/disarm are no-ops) so callers need no #ifdef guards.
|
||||
//
|
||||
// When enabled:
|
||||
// 1. Call p6_nfc_init() once at boot.
|
||||
// 2. Call p6_nfc_arm(expected_uid, solved_cb) when the gateway POSTs
|
||||
// /game/step with a "nfc" puzzle. `expected_uid` is a hex UID string
|
||||
// like "A1:B2:C3:D4" (4-byte NTAG213) or a space-separated sequence
|
||||
// of UIDs for ordered multi-tag placement.
|
||||
// 3. A FreeRTOS task polls the NFC reader at ~5 Hz and calls solved_cb
|
||||
// when the presented tag UID matches (or the full sequence is placed).
|
||||
// 4. Call p6_nfc_disarm() on step change or game reset.
|
||||
//
|
||||
// Hardware backend is selected via Kconfig:
|
||||
// PN532 over I2C or SPI → CONFIG_ZACUS_P6_READER_PN532
|
||||
// PN7150 over I2C → CONFIG_ZACUS_P6_READER_PN7150
|
||||
//
|
||||
// The hardware HAL is STUBBED in this scaffold revision: the read function
|
||||
// compiles, links, and logs "NFC read (stub)" but returns no UIDs until the
|
||||
// real HAL is wired in. This is the same pattern as media_manager stubs.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Callback type invoked on the p6_nfc task when the tag / sequence is solved.
|
||||
// Must not call p6_nfc_arm/disarm from within — defer via task notification.
|
||||
typedef void (*p6_nfc_solved_cb_t)(void);
|
||||
|
||||
/**
|
||||
* @brief Initialise the P6 NFC puzzle driver.
|
||||
*
|
||||
* Initialises the configured bus (I2C or SPI) and the NFC reader IC.
|
||||
* Spawns the NFC polling task (blocked until p6_nfc_arm() is called).
|
||||
* Idempotent — safe to call more than once.
|
||||
*
|
||||
* When CONFIG_ZACUS_P6_NFC_ENABLE=n returns ESP_OK immediately.
|
||||
*
|
||||
* @return ESP_OK on success, or a driver error code.
|
||||
*/
|
||||
esp_err_t p6_nfc_init(void);
|
||||
|
||||
/**
|
||||
* @brief Arm the NFC puzzle for a specific expected tag UID or sequence.
|
||||
*
|
||||
* @param expected_uid NUL-terminated UID string to match. Format:
|
||||
* single tag → "A1:B2:C3:D4"
|
||||
* The string is copied — caller buffer may be transient.
|
||||
* @param solved_cb Callback invoked exactly once on a match.
|
||||
* Pass NULL to skip (e.g. during tests).
|
||||
*
|
||||
* When CONFIG_ZACUS_P6_NFC_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p6_nfc_arm(const char *expected_uid, p6_nfc_solved_cb_t solved_cb);
|
||||
|
||||
/**
|
||||
* @brief Disarm the puzzle (stop polling, reset state).
|
||||
*
|
||||
* When CONFIG_ZACUS_P6_NFC_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p6_nfc_disarm(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,290 @@
|
||||
// p6_nfc.c — P6 NFC / alchemical-symbols puzzle driver.
|
||||
//
|
||||
// All real implementation is inside #if CONFIG_ZACUS_P6_NFC_ENABLE so the
|
||||
// object compiles to pure stubs when the flag is off (default). No #ifdef
|
||||
// leaks into callers.
|
||||
//
|
||||
// Hardware HAL for the NFC reader is STUBBED: nfc_hal_read_uid() returns
|
||||
// false (no card present) and logs once per arm cycle. The real HAL will be
|
||||
// implemented when the PN532/PN7150 module is wired (replace the stub block
|
||||
// marked with "STUB" below).
|
||||
//
|
||||
// Logic adapted from puzzles/p6_symboles_nfc/main/main.c, repackaged as a
|
||||
// library with arm/disarm API and a solved callback.
|
||||
|
||||
#include "p6_nfc.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
static const char *TAG = "p6_nfc";
|
||||
|
||||
#if CONFIG_ZACUS_P6_NFC_ENABLE
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// ─── I2C / SPI bus init (HAL layer) ─────────────────────────────────────────
|
||||
//
|
||||
// The bus init and NFC read are separated into nfc_hal_init() /
|
||||
// nfc_hal_read_uid() so the stub is easy to swap for real silicon code.
|
||||
|
||||
#if CONFIG_ZACUS_P6_BUS_I2C
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
static i2c_master_bus_handle_t s_i2c_bus = NULL;
|
||||
static i2c_master_dev_handle_t s_i2c_dev = NULL;
|
||||
|
||||
static esp_err_t nfc_hal_init(void)
|
||||
{
|
||||
i2c_master_bus_config_t bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = CONFIG_ZACUS_P6_I2C_SDA,
|
||||
.scl_io_num = CONFIG_ZACUS_P6_I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
esp_err_t err = i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_new_master_bus: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = (uint16_t)CONFIG_ZACUS_P6_I2C_ADDR,
|
||||
.scl_speed_hz = 100000,
|
||||
};
|
||||
err = i2c_master_bus_add_device(s_i2c_bus, &dev_cfg, &s_i2c_dev);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "i2c_master_bus_add_device(addr=0x%02X): %s",
|
||||
CONFIG_ZACUS_P6_I2C_ADDR, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "I2C bus init OK (SDA=%d SCL=%d addr=0x%02X)",
|
||||
CONFIG_ZACUS_P6_I2C_SDA, CONFIG_ZACUS_P6_I2C_SCL,
|
||||
CONFIG_ZACUS_P6_I2C_ADDR);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// STUB: real I2C read of PN532/PN7150 comes here.
|
||||
// Returns true and fills uid_out[4] when a card is present.
|
||||
static bool nfc_hal_read_uid(uint8_t uid_out[4])
|
||||
{
|
||||
(void)uid_out;
|
||||
// HAL stub — hardware not yet wired. Replace with PN532/PN7150 I2C
|
||||
// read sequence once the module is connected.
|
||||
ESP_LOGD(TAG, "NFC read (stub, I2C 0x%02X)", CONFIG_ZACUS_P6_I2C_ADDR);
|
||||
return false;
|
||||
}
|
||||
|
||||
#elif CONFIG_ZACUS_P6_BUS_SPI
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
static spi_device_handle_t s_spi = NULL;
|
||||
|
||||
static esp_err_t nfc_hal_init(void)
|
||||
{
|
||||
spi_bus_config_t buscfg = {
|
||||
.miso_io_num = 13, // default SPI2 pins — override via sdkconfig if needed
|
||||
.mosi_io_num = 11,
|
||||
.sclk_io_num = 12,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 64,
|
||||
};
|
||||
// Use SPI3_HOST to avoid conflict with possible SD card on SPI2.
|
||||
esp_err_t err = spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "spi_bus_initialize(SPI3): %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = 1000000, // 1 MHz (PN532 max SPI is 5 MHz)
|
||||
.mode = 0,
|
||||
.spics_io_num = 10, // CS pin — adjust via Kconfig when available
|
||||
.queue_size = 4,
|
||||
};
|
||||
err = spi_bus_add_device(SPI3_HOST, &devcfg, &s_spi);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "spi_bus_add_device: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "SPI bus init OK (SPI3, CS=10)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// STUB: real SPI read of PN532 comes here.
|
||||
static bool nfc_hal_read_uid(uint8_t uid_out[4])
|
||||
{
|
||||
(void)uid_out;
|
||||
ESP_LOGD(TAG, "NFC read (stub, SPI)");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif // bus type
|
||||
|
||||
// ─── UID comparison helper ───────────────────────────────────────────────────
|
||||
|
||||
// Parse a colon-separated hex UID string "A1:B2:C3:D4" into uid[4].
|
||||
// Returns true on success.
|
||||
static bool parse_uid(const char *str, uint8_t uid[4])
|
||||
{
|
||||
unsigned int b[4];
|
||||
if (sscanf(str, "%x:%x:%x:%x", &b[0], &b[1], &b[2], &b[3]) != 4)
|
||||
return false;
|
||||
for (int i = 0; i < 4; i++) uid[i] = (uint8_t)b[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static char s_expected_uid[20] = {0}; // "A1:B2:C3:D4\0"
|
||||
static uint8_t s_expected_raw[4] = {0};
|
||||
static bool s_expected_valid = false;
|
||||
|
||||
static volatile bool s_armed = false;
|
||||
static volatile bool s_solved = false;
|
||||
|
||||
static p6_nfc_solved_cb_t s_solved_cb = NULL;
|
||||
|
||||
static bool s_initialised = false;
|
||||
|
||||
// ─── NFC polling task ────────────────────────────────────────────────────────
|
||||
|
||||
static void nfc_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
uint8_t uid[4] = {0};
|
||||
uint8_t last_uid[4] = {0};
|
||||
uint32_t last_read_ms = 0;
|
||||
|
||||
for (;;) {
|
||||
if (!s_armed || s_solved) {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t t = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
|
||||
if (nfc_hal_read_uid(uid)) {
|
||||
// Debounce: ignore same tag within 1 s.
|
||||
bool same = (memcmp(uid, last_uid, 4) == 0);
|
||||
if (!same || (t - last_read_ms) > 1000) {
|
||||
memcpy(last_uid, uid, 4);
|
||||
last_read_ms = t;
|
||||
|
||||
ESP_LOGI(TAG, "tag read: %02X:%02X:%02X:%02X",
|
||||
uid[0], uid[1], uid[2], uid[3]);
|
||||
|
||||
if (s_expected_valid &&
|
||||
memcmp(uid, s_expected_raw, 4) == 0) {
|
||||
s_solved = true;
|
||||
ESP_LOGI(TAG, "SOLVED: tag %s matched", s_expected_uid);
|
||||
if (s_solved_cb) {
|
||||
s_solved_cb();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "tag %02X:%02X:%02X:%02X not the expected %s",
|
||||
uid[0], uid[1], uid[2], uid[3], s_expected_uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(200)); // 5 Hz polling
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
esp_err_t p6_nfc_init(void)
|
||||
{
|
||||
if (s_initialised) return ESP_OK;
|
||||
|
||||
esp_err_t err = nfc_hal_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "nfc_hal_init failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
BaseType_t rc = xTaskCreate(nfc_task, "p6_nfc", 3072, NULL, 5, NULL);
|
||||
if (rc != pdPASS) {
|
||||
ESP_LOGE(TAG, "xTaskCreate(p6_nfc) failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_initialised = true;
|
||||
#if CONFIG_ZACUS_P6_READER_PN532 && CONFIG_ZACUS_P6_BUS_I2C
|
||||
ESP_LOGI(TAG, "init OK (PN532, I2C, stub HAL)");
|
||||
#elif CONFIG_ZACUS_P6_READER_PN532
|
||||
ESP_LOGI(TAG, "init OK (PN532, SPI, stub HAL)");
|
||||
#else
|
||||
ESP_LOGI(TAG, "init OK (PN7150, I2C, stub HAL)");
|
||||
#endif
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p6_nfc_arm(const char *expected_uid, p6_nfc_solved_cb_t solved_cb)
|
||||
{
|
||||
if (!s_initialised) {
|
||||
ESP_LOGW(TAG, "arm called before init — ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
s_armed = false;
|
||||
s_solved = false;
|
||||
s_expected_valid = false;
|
||||
memset(s_expected_uid, 0, sizeof(s_expected_uid));
|
||||
memset(s_expected_raw, 0, sizeof(s_expected_raw));
|
||||
|
||||
if (expected_uid && expected_uid[0]) {
|
||||
strncpy(s_expected_uid, expected_uid, sizeof(s_expected_uid) - 1);
|
||||
s_expected_uid[sizeof(s_expected_uid) - 1] = '\0';
|
||||
s_expected_valid = parse_uid(s_expected_uid, s_expected_raw);
|
||||
if (!s_expected_valid) {
|
||||
ESP_LOGW(TAG, "arm: could not parse UID \"%s\" — tag match disabled",
|
||||
expected_uid);
|
||||
}
|
||||
}
|
||||
|
||||
s_solved_cb = solved_cb;
|
||||
s_armed = true;
|
||||
ESP_LOGI(TAG, "armed, expected UID \"%s\"", s_expected_uid);
|
||||
}
|
||||
|
||||
void p6_nfc_disarm(void)
|
||||
{
|
||||
s_armed = false;
|
||||
s_solved = false;
|
||||
ESP_LOGI(TAG, "disarmed");
|
||||
}
|
||||
|
||||
#else // CONFIG_ZACUS_P6_NFC_ENABLE not set — emit stubs only
|
||||
|
||||
esp_err_t p6_nfc_init(void)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p6_nfc_arm(const char *expected_uid, p6_nfc_solved_cb_t solved_cb)
|
||||
{
|
||||
(void)expected_uid;
|
||||
(void)solved_cb;
|
||||
/* stub — CONFIG_ZACUS_P6_NFC_ENABLE=n */
|
||||
}
|
||||
|
||||
void p6_nfc_disarm(void)
|
||||
{
|
||||
/* stub — CONFIG_ZACUS_P6_NFC_ENABLE=n */
|
||||
}
|
||||
|
||||
#endif // CONFIG_ZACUS_P6_NFC_ENABLE
|
||||
@@ -25,4 +25,6 @@ idf_component_register(
|
||||
display_ui
|
||||
puzzle_state
|
||||
p7_coffre
|
||||
p5_morse
|
||||
p6_nfc
|
||||
)
|
||||
|
||||
@@ -72,4 +72,141 @@ menu "Zacus Game Hardware"
|
||||
|
||||
endif # ZACUS_P7_COFFRE_ENABLE
|
||||
|
||||
# ─── P5 Morse / telegraph key ───────────────────────────────────────────────
|
||||
|
||||
config ZACUS_P5_MORSE_ENABLE
|
||||
bool "Enable P5 Morse puzzle (telegraph key input)"
|
||||
default n
|
||||
help
|
||||
Drives the P5 code-Morse puzzle: player taps a telegraph key and
|
||||
the firmware decodes dot/dash sequences. When disabled all
|
||||
p5_morse_*() functions are empty stubs so callers compile without
|
||||
#ifdef guards.
|
||||
Enable once the telegraph key (or push button) is wired up.
|
||||
|
||||
if ZACUS_P5_MORSE_ENABLE
|
||||
|
||||
config ZACUS_P5_GPIO
|
||||
int "GPIO pin for the telegraph key / button"
|
||||
default 14
|
||||
range 0 48
|
||||
help
|
||||
Input GPIO. The key connects this pin to GND when pressed.
|
||||
An internal pull-up is enabled by the driver.
|
||||
|
||||
config ZACUS_P5_ACTIVE_LOW
|
||||
bool "Key active-LOW (pressed = GPIO reads 0)"
|
||||
default y
|
||||
help
|
||||
Set y when using a standard normally-open button to GND
|
||||
(internal pull-up enabled). Set n for active-high sensors.
|
||||
|
||||
config ZACUS_P5_DOT_MS
|
||||
int "Maximum dot press duration (ms)"
|
||||
default 200
|
||||
range 10 2000
|
||||
help
|
||||
A key press shorter than or equal to this value is decoded
|
||||
as a dot. Presses above this threshold become dashes.
|
||||
|
||||
config ZACUS_P5_DASH_MS
|
||||
int "Minimum dash press duration (ms)"
|
||||
default 600
|
||||
range 50 5000
|
||||
help
|
||||
A key press must last at least this long to be decoded as a
|
||||
dash. Presses between DOT_MS+1 and DASH_MS-1 are discarded
|
||||
as noise or accidental touches.
|
||||
|
||||
config ZACUS_P5_GAP_MS
|
||||
int "Letter-gap silence duration (ms)"
|
||||
default 800
|
||||
range 100 5000
|
||||
help
|
||||
Silence between key presses longer than this value triggers
|
||||
decoding of the accumulated symbol (letter boundary).
|
||||
|
||||
endif # ZACUS_P5_MORSE_ENABLE
|
||||
|
||||
# ─── P6 NFC / alchemical symbols ────────────────────────────────────────────
|
||||
|
||||
config ZACUS_P6_NFC_ENABLE
|
||||
bool "Enable P6 NFC puzzle (alchemical symbol tags)"
|
||||
default n
|
||||
help
|
||||
Drives the P6 alchemical-symbols puzzle: the player places NFC
|
||||
tags (NTAG213) on a wooden tablet; the firmware reads the UIDs
|
||||
and checks the placement order. When disabled all p6_nfc_*()
|
||||
functions are empty stubs so callers compile without #ifdef guards.
|
||||
Enable once the NFC reader module is wired up.
|
||||
|
||||
if ZACUS_P6_NFC_ENABLE
|
||||
|
||||
choice ZACUS_P6_READER_TYPE
|
||||
prompt "NFC reader IC"
|
||||
default ZACUS_P6_READER_PN532
|
||||
help
|
||||
Select the NFC reader module on the board.
|
||||
PN532 – NXP PN532 (common breakout, SPI or I2C).
|
||||
PN7150 – NXP PN7150 (NCI-based, I2C only).
|
||||
|
||||
config ZACUS_P6_READER_PN532
|
||||
bool "PN532 (NXP)"
|
||||
config ZACUS_P6_READER_PN7150
|
||||
bool "PN7150 (NXP NCI)"
|
||||
endchoice
|
||||
|
||||
choice ZACUS_P6_BUS_TYPE
|
||||
prompt "Communication bus"
|
||||
default ZACUS_P6_BUS_I2C
|
||||
help
|
||||
Select the bus connecting the ESP32 to the NFC reader.
|
||||
I2C – two-wire, uses SDA/SCL. PN7150 only supports I2C.
|
||||
SPI – four-wire, faster. PN532 only.
|
||||
|
||||
config ZACUS_P6_BUS_I2C
|
||||
bool "I2C"
|
||||
config ZACUS_P6_BUS_SPI
|
||||
bool "SPI (PN532 only)"
|
||||
endchoice
|
||||
|
||||
config ZACUS_P6_I2C_SDA
|
||||
int "I2C SDA GPIO"
|
||||
default 8
|
||||
range 0 48
|
||||
depends on ZACUS_P6_BUS_I2C
|
||||
help
|
||||
GPIO number for the I2C data line (SDA).
|
||||
Default 8 is free on the Freenove Media Kit board.
|
||||
|
||||
config ZACUS_P6_I2C_SCL
|
||||
int "I2C SCL GPIO"
|
||||
default 9
|
||||
range 0 48
|
||||
depends on ZACUS_P6_BUS_I2C
|
||||
help
|
||||
GPIO number for the I2C clock line (SCL).
|
||||
Default 9 is free on the Freenove Media Kit board.
|
||||
|
||||
config ZACUS_P6_I2C_ADDR
|
||||
hex "NFC reader I2C address"
|
||||
default 0x24
|
||||
range 0x08 0x77
|
||||
depends on ZACUS_P6_BUS_I2C
|
||||
help
|
||||
7-bit I2C address of the NFC reader.
|
||||
PN532 default: 0x24. PN7150 default: 0x28.
|
||||
|
||||
config ZACUS_P6_IRQ_GPIO
|
||||
int "IRQ / data-ready GPIO (-1 = polling mode)"
|
||||
default -1
|
||||
range -1 48
|
||||
help
|
||||
Optional interrupt / data-ready pin from the NFC reader.
|
||||
Set -1 to use polling mode (5 Hz by default).
|
||||
When wired, the driver waits for a falling edge before
|
||||
reading, which reduces SPI/I2C bus traffic.
|
||||
|
||||
endif # ZACUS_P6_NFC_ENABLE
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
#include "board_pins_mediakit.h"
|
||||
#include "display_ui.h"
|
||||
#include "p7_coffre.h"
|
||||
#include "p5_morse.h"
|
||||
#include "p6_nfc.h"
|
||||
|
||||
// Hints engine endpoint (slice 5). Hardcoded for now — slice 7 will move
|
||||
// this to NVS so the field operator can repoint the firmware without a flash.
|
||||
@@ -597,6 +599,20 @@ void app_main(void) {
|
||||
ESP_LOGW(TAG, "p7_coffre_init: %s", esp_err_to_name(coffre_err));
|
||||
}
|
||||
|
||||
// P5 Morse puzzle: configure telegraph-key GPIO and start the
|
||||
// decoding task. No-op stub when CONFIG_ZACUS_P5_MORSE_ENABLE=n.
|
||||
esp_err_t morse_err = p5_morse_init();
|
||||
if (morse_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "p5_morse_init: %s", esp_err_to_name(morse_err));
|
||||
}
|
||||
|
||||
// P6 NFC puzzle: initialise bus and NFC polling task.
|
||||
// No-op stub when CONFIG_ZACUS_P6_NFC_ENABLE=n.
|
||||
esp_err_t nfc_err = p6_nfc_init();
|
||||
if (nfc_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "p6_nfc_init: %s", esp_err_to_name(nfc_err));
|
||||
}
|
||||
|
||||
// Task 7: mic_broker takes ownership of the Media Kit I2S IN
|
||||
// pins (3/14/46 per board_pins_mediakit.h) BEFORE
|
||||
// voice_pipeline_init — the pipeline's own init call then
|
||||
|
||||
Reference in New Issue
Block a user