feat(p7): scaffold coffre actuator behind CONFIG_ZACUS_P7_COFFRE_ENABLE (default off) #15
@@ -19,4 +19,5 @@ idf_component_register(
|
||||
sd_storage
|
||||
PRIV_REQUIRES
|
||||
local_puzzles
|
||||
p7_coffre
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "scenario_mesh.h"
|
||||
#include "puzzle_binding.h"
|
||||
#include "local_puzzles.h"
|
||||
#include "p7_coffre.h"
|
||||
|
||||
static const char *TAG = "game_endpoint";
|
||||
|
||||
@@ -334,6 +335,9 @@ static esp_err_t scenario_apply_buffer(const char *body, size_t len,
|
||||
local_puzzles_disarm();
|
||||
s_current_step_id[0] = '\0';
|
||||
memset(&s_current_scene, 0, sizeof(s_current_scene));
|
||||
// Re-arm the coffre for the new game session.
|
||||
// p7_coffre_lock() is a no-op stub when CONFIG_ZACUS_P7_COFFRE_ENABLE=n.
|
||||
p7_coffre_lock();
|
||||
|
||||
// Hot-reload-via-reboot until scenario_engine_reload() lands (Phase 3).
|
||||
schedule_restart();
|
||||
@@ -767,6 +771,12 @@ esp_err_t game_endpoint_apply_step(const char *step_id,
|
||||
strncpy(s_current_step_id, step_id, sizeof(s_current_step_id) - 1);
|
||||
s_current_step_id[sizeof(s_current_step_id) - 1] = '\0';
|
||||
|
||||
// P7 coffre: fire the actuator when the final win step is reached.
|
||||
// p7_coffre_unlock() is a no-op stub when CONFIG_ZACUS_P7_COFFRE_ENABLE=n.
|
||||
if (strcmp(step_id, "STEP_FINAL_WIN") == 0) {
|
||||
p7_coffre_unlock();
|
||||
}
|
||||
|
||||
const char *armed = "none";
|
||||
if (binding.type == PB_QR) {
|
||||
const char *ptrs[PB_MAX_CODES];
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"p7_coffre.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
esp_driver_ledc
|
||||
esp_driver_gpio
|
||||
log
|
||||
freertos
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
// p7_coffre — unlocking actuator for the Zacus final chest (P7).
|
||||
//
|
||||
// When CONFIG_ZACUS_P7_COFFRE_ENABLE=n (default) all three functions are
|
||||
// empty stubs (init returns ESP_OK, unlock/lock are no-ops) so callers do
|
||||
// not need #ifdef guards.
|
||||
//
|
||||
// When enabled, call p7_coffre_init() once at boot, then p7_coffre_unlock()
|
||||
// when STEP_FINAL_WIN is reached. p7_coffre_lock() re-arms between games.
|
||||
//
|
||||
// Actuator selection and GPIO are configured via Kconfig (menuconfig or
|
||||
// sdkconfig):
|
||||
// SERVO – LEDC TIMER_2 / CHANNEL_2, 50 Hz, 13-bit duty cycle.
|
||||
// RELAY – plain GPIO output with optional one-shot pulse.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialise the P7 coffre actuator.
|
||||
*
|
||||
* Servo: configures LEDC timer + channel and moves to the locked position.
|
||||
* Relay: configures the GPIO as output and asserts the rest (inactive) state.
|
||||
* Idempotent — safe to call more than once.
|
||||
*
|
||||
* When CONFIG_ZACUS_P7_COFFRE_ENABLE=n this is a stub that returns ESP_OK.
|
||||
*
|
||||
* @return ESP_OK on success, or a driver error code.
|
||||
*/
|
||||
esp_err_t p7_coffre_init(void);
|
||||
|
||||
/**
|
||||
* @brief Unlock the coffre (STEP_FINAL_WIN reached).
|
||||
*
|
||||
* Servo: moves to CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG.
|
||||
* Relay: energises the coil; if CONFIG_ZACUS_P7_RELAY_PULSE_MS > 0 the coil
|
||||
* is de-energised after that many milliseconds (blocking call on the
|
||||
* caller's task — keep short; 800 ms default).
|
||||
*
|
||||
* When CONFIG_ZACUS_P7_COFFRE_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p7_coffre_unlock(void);
|
||||
|
||||
/**
|
||||
* @brief Re-arm the coffre to the locked position (between games).
|
||||
*
|
||||
* Servo: moves back to CONFIG_ZACUS_P7_SERVO_LOCK_DEG.
|
||||
* Relay: de-energises the coil (if not already de-energised by the pulse).
|
||||
*
|
||||
* When CONFIG_ZACUS_P7_COFFRE_ENABLE=n this is a no-op.
|
||||
*/
|
||||
void p7_coffre_lock(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,232 @@
|
||||
// p7_coffre — P7 coffre unlocking actuator driver.
|
||||
//
|
||||
// All real implementation is inside #if CONFIG_ZACUS_P7_COFFRE_ENABLE so the
|
||||
// object compiles to pure stubs when the flag is off (default). No #ifdef
|
||||
// leaks into callers — they include p7_coffre.h and call the three functions
|
||||
// unconditionally.
|
||||
|
||||
#include "p7_coffre.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
static const char *TAG = "p7_coffre";
|
||||
|
||||
// ─── Servo duty helper ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Maps an angle in degrees [0, 180] to a LEDC duty value.
|
||||
//
|
||||
// Timer parameters (compile-time constants):
|
||||
// Period : 20 ms (50 Hz)
|
||||
// Pulse range : 500 µs (0°) … 2500 µs (180°) — standard SG90 / MG90S
|
||||
// Resolution : 13 bits → 8192 counts per 20 ms
|
||||
//
|
||||
// counts_per_us = 8192 / 20000 = 0.4096
|
||||
// duty(0°) = 500 * 0.4096 = 205
|
||||
// duty(90°) = 1500 * 0.4096 = 614
|
||||
// duty(180°) = 2500 * 0.4096 = 1024
|
||||
|
||||
#if CONFIG_ZACUS_P7_COFFRE_ENABLE
|
||||
|
||||
#include "driver/ledc.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
// LEDC resources — chosen to avoid conflicts with existing assignments:
|
||||
// TIMER_0 / CHANNEL_0 = camera XCLK (qr_puzzle)
|
||||
// TIMER_1 / CHANNEL_1 = display backlight (display_ui)
|
||||
// TIMER_2 / CHANNEL_2 = P7 servo (this file)
|
||||
#define P7_LEDC_TIMER LEDC_TIMER_2
|
||||
#define P7_LEDC_CHANNEL LEDC_CHANNEL_2
|
||||
#define P7_LEDC_MODE LEDC_LOW_SPEED_MODE
|
||||
#define P7_LEDC_RES LEDC_TIMER_13_BIT // 8192 counts per period
|
||||
#define P7_LEDC_FREQ_HZ 50u // 20 ms period
|
||||
|
||||
// counts = pulse_us * (2^13 / 20000)
|
||||
// Use integer arithmetic: counts = pulse_us * 8192 / 20000
|
||||
#define P7_SERVO_COUNTS(pulse_us) ((uint32_t)(pulse_us) * 8192u / 20000u)
|
||||
|
||||
// Pulse width at each extreme (SG90-compatible).
|
||||
#define P7_SERVO_PULSE_MIN_US 500u // 0°
|
||||
#define P7_SERVO_PULSE_MAX_US 2500u // 180°
|
||||
|
||||
// Convert angle [0, 180] to duty count.
|
||||
static uint32_t angle_to_duty(int deg) {
|
||||
if (deg < 0) deg = 0;
|
||||
if (deg > 180) deg = 180;
|
||||
uint32_t pulse_us = P7_SERVO_PULSE_MIN_US +
|
||||
(uint32_t) deg *
|
||||
(P7_SERVO_PULSE_MAX_US - P7_SERVO_PULSE_MIN_US) / 180u;
|
||||
return P7_SERVO_COUNTS(pulse_us);
|
||||
}
|
||||
|
||||
static bool s_initialised = false;
|
||||
|
||||
// ─── Servo path ─────────────────────────────────────────────────────────────
|
||||
|
||||
#if CONFIG_ZACUS_P7_ACTUATOR_SERVO
|
||||
|
||||
static esp_err_t servo_set_angle(int deg) {
|
||||
uint32_t duty = angle_to_duty(deg);
|
||||
esp_err_t err = ledc_set_duty(P7_LEDC_MODE, P7_LEDC_CHANNEL, duty);
|
||||
if (err != ESP_OK) return err;
|
||||
return ledc_update_duty(P7_LEDC_MODE, P7_LEDC_CHANNEL);
|
||||
}
|
||||
|
||||
esp_err_t p7_coffre_init(void) {
|
||||
if (s_initialised) return ESP_OK;
|
||||
|
||||
// Configure the 50 Hz timer.
|
||||
ledc_timer_config_t timer_cfg = {
|
||||
.speed_mode = P7_LEDC_MODE,
|
||||
.duty_resolution = P7_LEDC_RES,
|
||||
.timer_num = P7_LEDC_TIMER,
|
||||
.freq_hz = P7_LEDC_FREQ_HZ,
|
||||
.clk_cfg = LEDC_AUTO_CLK,
|
||||
};
|
||||
esp_err_t err = ledc_timer_config(&timer_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ledc_timer_config: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Bind the channel to the GPIO at the lock angle.
|
||||
uint32_t lock_duty = angle_to_duty(CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
|
||||
ledc_channel_config_t ch_cfg = {
|
||||
.gpio_num = CONFIG_ZACUS_P7_GPIO,
|
||||
.speed_mode = P7_LEDC_MODE,
|
||||
.channel = P7_LEDC_CHANNEL,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.timer_sel = P7_LEDC_TIMER,
|
||||
.duty = lock_duty,
|
||||
.hpoint = 0,
|
||||
};
|
||||
err = ledc_channel_config(&ch_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ledc_channel_config: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_initialised = true;
|
||||
ESP_LOGI(TAG, "servo init OK (GPIO %d, lock=%d°, unlock=%d°, "
|
||||
"LEDC TIMER_%d/CH_%d 50 Hz 13-bit)",
|
||||
CONFIG_ZACUS_P7_GPIO,
|
||||
CONFIG_ZACUS_P7_SERVO_LOCK_DEG,
|
||||
CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG,
|
||||
(int) P7_LEDC_TIMER, (int) P7_LEDC_CHANNEL);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p7_coffre_unlock(void) {
|
||||
if (!s_initialised) {
|
||||
ESP_LOGW(TAG, "unlock called before init — ignored");
|
||||
return;
|
||||
}
|
||||
esp_err_t err = servo_set_angle(CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "servo_set_angle(%d): %s",
|
||||
CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG, esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "coffre UNLOCKED (servo -> %d deg)",
|
||||
CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG);
|
||||
}
|
||||
}
|
||||
|
||||
void p7_coffre_lock(void) {
|
||||
if (!s_initialised) return;
|
||||
esp_err_t err = servo_set_angle(CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "servo_set_angle(%d): %s",
|
||||
CONFIG_ZACUS_P7_SERVO_LOCK_DEG, esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "coffre re-LOCKED (servo -> %d deg)",
|
||||
CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CONFIG_ZACUS_P7_ACTUATOR_SERVO
|
||||
|
||||
// ─── Relay path ─────────────────────────────────────────────────────────────
|
||||
|
||||
#if CONFIG_ZACUS_P7_ACTUATOR_RELAY
|
||||
|
||||
// Helper: set the relay coil to active (energised) or rest.
|
||||
// Handles active-high vs active-low inversion.
|
||||
static void relay_set(bool active) {
|
||||
int level;
|
||||
#if CONFIG_ZACUS_P7_RELAY_ACTIVE_HIGH
|
||||
level = active ? 1 : 0;
|
||||
#else
|
||||
level = active ? 0 : 1;
|
||||
#endif
|
||||
gpio_set_level(CONFIG_ZACUS_P7_GPIO, level);
|
||||
}
|
||||
|
||||
esp_err_t p7_coffre_init(void) {
|
||||
if (s_initialised) return ESP_OK;
|
||||
|
||||
gpio_config_t cfg = {
|
||||
.pin_bit_mask = (1ULL << CONFIG_ZACUS_P7_GPIO),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.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_P7_GPIO, esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Start in rest (de-energised) state.
|
||||
relay_set(false);
|
||||
s_initialised = true;
|
||||
ESP_LOGI(TAG, "relay init OK (GPIO %d, active-%s, pulse=%d ms)",
|
||||
CONFIG_ZACUS_P7_GPIO,
|
||||
CONFIG_ZACUS_P7_RELAY_ACTIVE_HIGH ? "HIGH" : "LOW",
|
||||
CONFIG_ZACUS_P7_RELAY_PULSE_MS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p7_coffre_unlock(void) {
|
||||
if (!s_initialised) {
|
||||
ESP_LOGW(TAG, "unlock called before init — ignored");
|
||||
return;
|
||||
}
|
||||
relay_set(true);
|
||||
ESP_LOGI(TAG, "coffre UNLOCKED (relay energised)");
|
||||
|
||||
#if CONFIG_ZACUS_P7_RELAY_PULSE_MS > 0
|
||||
vTaskDelay(pdMS_TO_TICKS(CONFIG_ZACUS_P7_RELAY_PULSE_MS));
|
||||
relay_set(false);
|
||||
ESP_LOGI(TAG, "coffre relay de-energised after %d ms pulse",
|
||||
CONFIG_ZACUS_P7_RELAY_PULSE_MS);
|
||||
#endif
|
||||
}
|
||||
|
||||
void p7_coffre_lock(void) {
|
||||
if (!s_initialised) return;
|
||||
relay_set(false);
|
||||
ESP_LOGI(TAG, "coffre relay de-energised (lock/re-arm)");
|
||||
}
|
||||
|
||||
#endif // CONFIG_ZACUS_P7_ACTUATOR_RELAY
|
||||
|
||||
#else // CONFIG_ZACUS_P7_COFFRE_ENABLE not set — emit stubs only
|
||||
|
||||
esp_err_t p7_coffre_init(void) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void p7_coffre_unlock(void) {
|
||||
/* stub — CONFIG_ZACUS_P7_COFFRE_ENABLE=n */
|
||||
}
|
||||
|
||||
void p7_coffre_lock(void) {
|
||||
/* stub — CONFIG_ZACUS_P7_COFFRE_ENABLE=n */
|
||||
}
|
||||
|
||||
#endif // CONFIG_ZACUS_P7_COFFRE_ENABLE
|
||||
@@ -24,4 +24,5 @@ idf_component_register(
|
||||
espressif__mdns
|
||||
display_ui
|
||||
puzzle_state
|
||||
p7_coffre
|
||||
)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
menu "Zacus Game Hardware"
|
||||
|
||||
config ZACUS_P7_COFFRE_ENABLE
|
||||
bool "Enable P7 coffre actuator (servo or relay)"
|
||||
default n
|
||||
help
|
||||
Drives the unlocking actuator for the final chest (P7) when
|
||||
STEP_FINAL_WIN is reached. When disabled all p7_coffre_*()
|
||||
functions are empty stubs so callers compile without #ifdef.
|
||||
Enable once the actuator (9g servo or 5V relay) is wired up.
|
||||
|
||||
if ZACUS_P7_COFFRE_ENABLE
|
||||
|
||||
choice ZACUS_P7_ACTUATOR_TYPE
|
||||
prompt "Actuator type"
|
||||
default ZACUS_P7_ACTUATOR_SERVO
|
||||
help
|
||||
Select the hardware wired to CONFIG_ZACUS_P7_GPIO.
|
||||
SERVO – 50 Hz PWM via LEDC (TIMER_2 / CHANNEL_2).
|
||||
RELAY – logic-level GPIO output (active-high or active-low).
|
||||
|
||||
config ZACUS_P7_ACTUATOR_SERVO
|
||||
bool "Servo PWM (9g, 50 Hz LEDC)"
|
||||
config ZACUS_P7_ACTUATOR_RELAY
|
||||
bool "Relay / solid-state switch (GPIO)"
|
||||
endchoice
|
||||
|
||||
config ZACUS_P7_GPIO
|
||||
int "GPIO pin for the actuator"
|
||||
default 13
|
||||
range 0 48
|
||||
help
|
||||
GPIO number for the servo signal wire or the relay control
|
||||
input. Default 13 is free on the Freenove Media Kit board.
|
||||
|
||||
if ZACUS_P7_ACTUATOR_SERVO
|
||||
config ZACUS_P7_SERVO_LOCK_DEG
|
||||
int "Servo lock angle (degrees, 0-180)"
|
||||
default 0
|
||||
range 0 180
|
||||
help
|
||||
Angle at which the servo holds the coffre locked.
|
||||
Corresponds to a 500-2500 µs pulse on a 20 ms period.
|
||||
|
||||
config ZACUS_P7_SERVO_UNLOCK_DEG
|
||||
int "Servo unlock angle (degrees, 0-180)"
|
||||
default 90
|
||||
range 0 180
|
||||
help
|
||||
Angle driven on STEP_FINAL_WIN to open the coffre.
|
||||
endif
|
||||
|
||||
if ZACUS_P7_ACTUATOR_RELAY
|
||||
config ZACUS_P7_RELAY_ACTIVE_HIGH
|
||||
bool "Relay active-HIGH (coil energised when GPIO=1)"
|
||||
default y
|
||||
help
|
||||
Set y for most 5V relay modules with active-high inputs.
|
||||
Set n for active-low modules (e.g. opto-coupled boards
|
||||
where the relay fires when the pin is pulled LOW).
|
||||
|
||||
config ZACUS_P7_RELAY_PULSE_MS
|
||||
int "Relay pulse duration (ms, 0 = hold permanently)"
|
||||
default 800
|
||||
range 0 10000
|
||||
help
|
||||
If > 0 the relay is energised for this many milliseconds
|
||||
then returned to rest (one-shot electronic latch).
|
||||
Set 0 to keep the relay energised until p7_coffre_lock()
|
||||
is called (e.g. when a magnetic solenoid holds the bolt).
|
||||
endif
|
||||
|
||||
endif # ZACUS_P7_COFFRE_ENABLE
|
||||
|
||||
endmenu
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "mic_broker.h"
|
||||
#include "board_pins_mediakit.h"
|
||||
#include "display_ui.h"
|
||||
#include "p7_coffre.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.
|
||||
@@ -587,6 +588,15 @@ void app_main(void) {
|
||||
local_puzzles_init(&s_pstate);
|
||||
ESP_LOGI(TAG, "puzzle_state + local_puzzles initialised");
|
||||
|
||||
// P7 coffre actuator: configure GPIO/LEDC at boot so the
|
||||
// first call to p7_coffre_unlock() (on STEP_FINAL_WIN) has
|
||||
// zero warm-up latency. A no-op stub when
|
||||
// CONFIG_ZACUS_P7_COFFRE_ENABLE=n (default).
|
||||
esp_err_t coffre_err = p7_coffre_init();
|
||||
if (coffre_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "p7_coffre_init: %s", esp_err_to_name(coffre_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