/* * 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 #include #include #include #include #include #include #include #include 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 volatile 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(8, 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"); }