282 lines
9.2 KiB
C
282 lines
9.2 KiB
C
/*
|
||
* keypitecc – door controller
|
||
*
|
||
* Hardware:
|
||
* GPIO 9 – Boot button (active-low, press = GND)
|
||
* GPIO 15 – User LED (active-high, 1 = on)
|
||
*
|
||
* Button state machine (5-second window):
|
||
* IDLE
|
||
* └─ press ──► PENDING_OPEN (slow blink, will run SSH open command)
|
||
* └─ press ──► PENDING_LOCK (fast blink, will run SSH lock command)
|
||
* └─ press ──► IDLE (cancelled)
|
||
* Any pending state expires after 5 s → command is executed → IDLE
|
||
*
|
||
* LED idle behaviour:
|
||
* WiFi connected → LED ON
|
||
* WiFi disconnected → LED OFF
|
||
*/
|
||
|
||
#include "atecc608a.h"
|
||
#include "ssh_client.h"
|
||
#include "wifi.h"
|
||
|
||
#include "sdkconfig.h"
|
||
#include <driver/gpio.h>
|
||
#include <esp_log.h>
|
||
#include <freertos/FreeRTOS.h>
|
||
#include <freertos/queue.h>
|
||
#include <freertos/task.h>
|
||
#include <freertos/timers.h>
|
||
#include <nvs_flash.h>
|
||
#include <stdbool.h>
|
||
#include <stdint.h>
|
||
|
||
static const char *TAG = "main";
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* GPIO defines
|
||
* ---------------------------------------------------------------------- */
|
||
#define GPIO_BTN 9 /* Boot button, active-low */
|
||
#define GPIO_LED 15 /* User LED, active-high */
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* Button / LED types
|
||
* ---------------------------------------------------------------------- */
|
||
typedef enum {
|
||
BTN_STATE_IDLE,
|
||
BTN_STATE_PENDING_OPEN,
|
||
BTN_STATE_PENDING_LOCK,
|
||
} btn_state_t;
|
||
|
||
typedef enum {
|
||
LED_MODE_WIFI_STATUS, /* ON = connected, OFF = disconnected */
|
||
LED_MODE_SLOW_BLINK, /* 500 ms on / 500 ms off (open pending) */
|
||
LED_MODE_FAST_BLINK, /* 100 ms on / 100 ms off (lock pending) */
|
||
} led_mode_t;
|
||
|
||
typedef enum {
|
||
EVT_BUTTON_PRESS,
|
||
EVT_TIMER_EXPIRED,
|
||
} event_type_t;
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* Shared state
|
||
* ---------------------------------------------------------------------- */
|
||
static volatile led_mode_t s_led_mode = LED_MODE_WIFI_STATUS;
|
||
static btn_state_t s_btn_state = BTN_STATE_IDLE;
|
||
|
||
static QueueHandle_t s_event_queue;
|
||
static TimerHandle_t s_action_timer;
|
||
|
||
/* Debounce: ignore button edges within 200 ms of the previous one */
|
||
static volatile TickType_t s_last_btn_tick = 0;
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* SSH task – spawned on demand so the button task stays responsive
|
||
* ---------------------------------------------------------------------- */
|
||
static void ssh_task(void *arg)
|
||
{
|
||
const char *cmd = (const char *)arg;
|
||
ESP_LOGI(TAG, "Executing SSH command: %s", cmd);
|
||
bool ok = ssh_execute_command(cmd);
|
||
ESP_LOGI(TAG, "SSH command %s: %s", cmd, ok ? "succeeded" : "FAILED");
|
||
vTaskDelete(NULL);
|
||
}
|
||
|
||
static void trigger_ssh(const char *cmd)
|
||
{
|
||
/* Stack size 16 kB – SSH needs heap + TLS, keep priority low */
|
||
if (xTaskCreate(ssh_task, "ssh", 16384, (void *)cmd, 3, NULL) != pdPASS) {
|
||
ESP_LOGE(TAG, "Failed to create SSH task");
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* FreeRTOS software timer callback – runs in timer daemon task context
|
||
* ---------------------------------------------------------------------- */
|
||
static void action_timer_cb(TimerHandle_t xTimer)
|
||
{
|
||
(void)xTimer;
|
||
event_type_t evt = EVT_TIMER_EXPIRED;
|
||
xQueueSend(s_event_queue, &evt, 0);
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* GPIO ISR
|
||
* ---------------------------------------------------------------------- */
|
||
static void IRAM_ATTR button_isr(void *arg)
|
||
{
|
||
(void)arg;
|
||
TickType_t now = xTaskGetTickCountFromISR();
|
||
if ((now - s_last_btn_tick) < pdMS_TO_TICKS(200)) {
|
||
return; /* debounce */
|
||
}
|
||
s_last_btn_tick = now;
|
||
|
||
event_type_t evt = EVT_BUTTON_PRESS;
|
||
xQueueSendFromISR(s_event_queue, &evt, NULL);
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* Button task – owns the state machine
|
||
* ---------------------------------------------------------------------- */
|
||
static void button_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
event_type_t evt;
|
||
|
||
for (;;) {
|
||
if (xQueueReceive(s_event_queue, &evt, portMAX_DELAY) != pdTRUE) {
|
||
continue;
|
||
}
|
||
|
||
switch (s_btn_state) {
|
||
|
||
case BTN_STATE_IDLE:
|
||
if (evt == EVT_BUTTON_PRESS) {
|
||
ESP_LOGI(TAG, "Button: PENDING_OPEN in 5s (press again for LOCK, third press cancels)");
|
||
s_btn_state = BTN_STATE_PENDING_OPEN;
|
||
s_led_mode = LED_MODE_SLOW_BLINK;
|
||
xTimerStart(s_action_timer, 0);
|
||
}
|
||
break;
|
||
|
||
case BTN_STATE_PENDING_OPEN:
|
||
if (evt == EVT_BUTTON_PRESS) {
|
||
ESP_LOGI(TAG, "Button: switching to PENDING_LOCK");
|
||
s_btn_state = BTN_STATE_PENDING_LOCK;
|
||
s_led_mode = LED_MODE_FAST_BLINK;
|
||
xTimerReset(s_action_timer, 0); /* restart 5s from now */
|
||
} else if (evt == EVT_TIMER_EXPIRED) {
|
||
ESP_LOGI(TAG, "Timer expired: executing OPEN command");
|
||
s_btn_state = BTN_STATE_IDLE;
|
||
s_led_mode = LED_MODE_WIFI_STATUS;
|
||
trigger_ssh(CONFIG_SSH_OPEN_COMMAND);
|
||
}
|
||
break;
|
||
|
||
case BTN_STATE_PENDING_LOCK:
|
||
if (evt == EVT_BUTTON_PRESS) {
|
||
ESP_LOGI(TAG, "Button: action CANCELLED");
|
||
xTimerStop(s_action_timer, 0);
|
||
s_btn_state = BTN_STATE_IDLE;
|
||
s_led_mode = LED_MODE_WIFI_STATUS;
|
||
} else if (evt == EVT_TIMER_EXPIRED) {
|
||
ESP_LOGI(TAG, "Timer expired: executing LOCK command");
|
||
s_btn_state = BTN_STATE_IDLE;
|
||
s_led_mode = LED_MODE_WIFI_STATUS;
|
||
trigger_ssh(CONFIG_SSH_LOCK_COMMAND);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* LED task – drives GPIO 15 according to the current led_mode
|
||
* ---------------------------------------------------------------------- */
|
||
static void led_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
bool led_state = false;
|
||
|
||
for (;;) {
|
||
led_mode_t mode = s_led_mode; /* atomic read (single-byte enum) */
|
||
|
||
switch (mode) {
|
||
case LED_MODE_WIFI_STATUS:
|
||
led_state = wifi_is_connected();
|
||
gpio_set_level(GPIO_LED, led_state ? 0 : 1);
|
||
vTaskDelay(pdMS_TO_TICKS(250));
|
||
break;
|
||
|
||
case LED_MODE_SLOW_BLINK:
|
||
gpio_set_level(GPIO_LED, 1);
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
gpio_set_level(GPIO_LED, 0);
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
break;
|
||
|
||
case LED_MODE_FAST_BLINK:
|
||
gpio_set_level(GPIO_LED, 1);
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
gpio_set_level(GPIO_LED, 0);
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* GPIO initialisation
|
||
* ---------------------------------------------------------------------- */
|
||
static void gpio_init(void)
|
||
{
|
||
/* LED output */
|
||
gpio_config_t led_cfg = {
|
||
.pin_bit_mask = (1ULL << GPIO_LED),
|
||
.mode = GPIO_MODE_OUTPUT,
|
||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||
.intr_type = GPIO_INTR_DISABLE,
|
||
};
|
||
gpio_config(&led_cfg);
|
||
gpio_set_level(GPIO_LED, 1); /* off (active-high) */
|
||
|
||
/* Button input with interrupt on falling edge (active-low) */
|
||
gpio_config_t btn_cfg = {
|
||
.pin_bit_mask = (1ULL << GPIO_BTN),
|
||
.mode = GPIO_MODE_INPUT,
|
||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||
.intr_type = GPIO_INTR_NEGEDGE,
|
||
};
|
||
gpio_config(&btn_cfg);
|
||
|
||
gpio_install_isr_service(0);
|
||
gpio_isr_handler_add(GPIO_BTN, button_isr, NULL);
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------
|
||
* app_main
|
||
* ---------------------------------------------------------------------- */
|
||
void app_main(void)
|
||
{
|
||
/* NVS (required by WiFi) */
|
||
esp_err_t ret = nvs_flash_init();
|
||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||
ret = nvs_flash_init();
|
||
}
|
||
ESP_ERROR_CHECK(ret);
|
||
|
||
/* ATECC608B */
|
||
if (!atecc608B_init()) {
|
||
ESP_LOGW(TAG, "ATECC608B init failed – SSH authentication will not work");
|
||
} else {
|
||
atecc608B_print_config();
|
||
ssh_print_public_key(); /* print key for authorized_keys setup */
|
||
}
|
||
|
||
/* WiFi */
|
||
wifi_init();
|
||
|
||
/* GPIO and event infrastructure */
|
||
gpio_init();
|
||
|
||
s_event_queue = xQueueCreate(4, sizeof(event_type_t));
|
||
configASSERT(s_event_queue);
|
||
|
||
/* 5-second one-shot timer (auto-reload disabled) */
|
||
s_action_timer = xTimerCreate("action", pdMS_TO_TICKS(5000),
|
||
pdFALSE, NULL, action_timer_cb);
|
||
configASSERT(s_action_timer);
|
||
|
||
/* Tasks */
|
||
xTaskCreate(button_task, "button", 4096, NULL, 5, NULL);
|
||
xTaskCreate(led_task, "led", 2048, NULL, 4, NULL);
|
||
|
||
ESP_LOGI(TAG, "keypitecc ready – press the boot button to operate the door");
|
||
}
|