commit 84e552cf8bd4f9af774c78504dcbfe845c8885a3
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-13T09:55:12Z |
| subject | LD2410S: Wake up esp32! |
commit 84e552cf8bd4f9af774c78504dcbfe845c8885a3
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-13T09:55:12Z
LD2410S: Wake up esp32!
---
README.md | 88 ++++++++++++++++++++++++++++++++++++++++-
esp32/main/CMakeLists.txt | 2 +-
esp32/main/Kconfig.projbuild | 37 +++++++++++++++++
esp32/main/main.c | 15 +++++++
esp32/main/pn532.c | 49 +++++++++++++++++++++++
esp32/main/presence.c | 74 ++++++++++++++++++++++++++++++++++
esp32/main/presence.h | 10 +++++
esp32/main/reader.h | 3 +-
tests/include/driver/gpio.h | 18 +++++++++
tests/include/esp_idf_version.h | 4 ++
tests/include/esp_sleep.h | 7 ++++
tests/include/sdkconfig.h | 9 +++++
tests/pn532_test.c | 60 ++++++++++++++++++++++++++++
tests/presence_test.c | 66 +++++++++++++++++++++++++++++++
tests/run.sh | 6 ++-
15 files changed, 444 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 05c1402..a145861 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,93 @@ UID advertisements. See [the current relay protocol](esp32/PROTOCOL.md).
## Workflow
-Presence -> HLK-LD2410S -> ESP32-C3 -> PN532
+
+LD2410S OT2 HIGH -> ESP32-C3 wakes/boots -> PN532 UART wake -> BLE reconnect
+and Home group configuration -> ECP + NFC polling.
+
+OT2 LOW -> absence confirmation -> finish any active NFC session -> PN532
+RF off + PowerDown -> stop BLE -> ESP32-C3 deep sleep until OT2 HIGH.
+
+### Presence wiring
+
+| Module pin | ESP32-C3 / supply |
+| --- | --- |
+| LD2410S 3V3 | Always-on regulated 3.3 V |
+| LD2410S GND | Common GND |
+| LD2410S OT2 | **GPIO5** (HIGH = present) |
+| LD2410S OT1/TX and RX | Disconnected in relay firmware |
+| PN532 RX | **GPIO3** |
+| PN532 TX | **GPIO4** |
+| PN532 supply / GND | Existing regulated supply / common GND |
+
+Remove the calibration firmware's radar UART wiring from GPIO3/4 before
+reconnecting PN532. The relay uses OT2 only and does not change the radar's
+saved calibration/settings. Keep LD2410S powered during ESP sleep. PN532 also
+stays supplied: this is its software PowerDown mode, not a switched supply.
+Its UART TX drive from the ESP is held idle HIGH across sleep to avoid false
+wakes. Never connect battery voltage directly to 3.3 V power or GPIO pins.
+
+### Sleep behavior and settings
+
+`idf.py menuconfig` -> **Home Key presence / sleep**:
+
+- Presence sleep is enabled by default on ESP32-C3. Disable it for bench
+ operation without LD2410S; otherwise an unconnected input reads absent.
+- OT2 defaults to GPIO5. Only GPIO0-5 support this C3 deep-sleep wake path;
+ GPIO3/4 conflict with PN532 and are rejected at compile time. Avoid GPIO2
+ because it is a boot strapping pin.
+- Default additional absence confirmation: **1 second** after observing LOW.
+ The radar's own configured absence delay happens first.
+- Default cold/reset boot grace: **10 seconds** awake for sensor startup and
+ USB access. It does not delay NFC startup. GPIO wakes skip this grace.
+
+Deep sleep **disconnects BLE** and wake starts a new boot. The configured reader
+address remains the same, but `homekey-go` must reconnect and resend its Home
+group before polling resumes. Approach-to-ready time therefore includes boot
+and BLE reconnect, not just PN532 wake. No ECP/credential data is cached across
+sleep, and there are no periodic timer wakes.
+
+An active NFC callback finishes (or hits its existing session timeout) before
+the owner task considers sleep. Absence may therefore take longer during an
+authentication. PN532 PowerDown must return a complete success response; a
+failure keeps ESP awake, wakes/reinitializes PN532, and retries. A returning
+HIGH during shutdown triggers an immediate GPIO wake after sleep entry.
+
+The implementation selects the renamed wake API on ESP-IDF 6.x and the older
+API on 5.x. It needs no automatic light-sleep or Bluetooth sleep configuration.
+The standalone pin/PN532 test builds do not use presence control.
+
+### Build and verify on hardware
+
+```sh
+cd esp32
+idf.py menuconfig
+idf.py build
+idf.py -p /dev/cu.usbmodem101 flash monitor
+```
+
+1. Keep someone in range. Expect the OT2 GPIO log, PN532 firmware log, BLE
+ connection, Home Key ECP configuration, and normal phone authentication.
+2. Leave the area. After OT2 goes LOW and the confirmation interval passes,
+ expect `Absent: PN532 asleep, BLE stopped; ESP32 deep sleep`.
+3. Approach again. Expect a new boot and BLE/ECP setup, then test Home Key.
+4. Repeat several cycles; also test presence returning during shutdown and
+ leaving during authentication. Compare supply current awake/asleep.
+
+Native USB can disappear in deep sleep. Approach the sensor to wake it, or
+hold BOOT and tap RESET to enter the ROM downloader if flashing cannot connect.
+Development-board LEDs/regulators and PN532 board circuitry can dominate sleep
+current; chip sleep does not guarantee a particular battery life.
+
+Host tests cover absence timing, cold/GPIO wake policy, disabled presence,
+PN532 wake delay, confirmed/rejected/truncated/missing PowerDown responses,
+the polling-to-sleep transition, and existing APDU/ECP behavior. This change
+has not been compiled with a full ESP-IDF toolchain or tested on physical
+hardware in the development environment.
+
+Protocol references: [LD2410S OT2 definition](https://h.hlktech.com/download/HLK-LD2410S-24G/1/HLK-LD2410S-%E4%B8%B2%E5%8F%A3%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE-V1.00%20.pdf),
+[PN532 user manual, section 7.2.11](https://www.nxp.com/docs/en/user-guide/141520.pdf),
+[ESP32-C3 sleep modes](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/system/sleep_modes.html).
## Legacy Home Assistant UI setup
diff --git a/esp32/main/CMakeLists.txt b/esp32/main/CMakeLists.txt
index e869fb3..3343ad9 100644
--- a/esp32/main/CMakeLists.txt
+++ b/esp32/main/CMakeLists.txt
@@ -16,7 +16,7 @@ elseif(PNC532_TEST)
)
else()
idf_component_register(
- SRCS "main.c" "pn532.c" "relay.c"
+ SRCS "main.c" "pn532.c" "relay.c" "presence.c"
INCLUDE_DIRS "."
PRIV_REQUIRES bt nvs_flash driver esp_timer
esp_hw_support esp_driver_uart
diff --git a/esp32/main/Kconfig.projbuild b/esp32/main/Kconfig.projbuild
new file mode 100644
index 0000000..e9465be
--- /dev/null
+++ b/esp32/main/Kconfig.projbuild
@@ -0,0 +1,37 @@
+menu "Home Key presence / sleep"
+ config HOMEKEY_PRESENCE_ENABLED
+ bool "Use LD2410S OT2 to control deep sleep"
+ depends on IDF_TARGET_ESP32C3
+ default y
+ help
+ Keep LD2410S powered. OT2 HIGH means present, LOW means absent.
+ Deep sleep disconnects BLE; Go must reconnect on every wake.
+ Disable for bench testing without the sensor.
+
+ config HOMEKEY_PRESENCE_GPIO
+ int "LD2410S OT2 GPIO"
+ depends on HOMEKEY_PRESENCE_ENABLED
+ range 0 5
+ default 5
+ help
+ C3 deep-sleep wake requires GPIO0-5. GPIO3/4 are reserved for
+ PN532 UART and rejected at compile time. Avoid strapping GPIO2.
+
+ config HOMEKEY_PRESENCE_ABSENCE_MS
+ int "Additional absence confirmation (ms)"
+ depends on HOMEKEY_PRESENCE_ENABLED
+ range 100 10000
+ default 1000
+ help
+ Added after the sensor's own absence delay. An active NFC session
+ finishes before sleep is considered.
+
+ config HOMEKEY_PRESENCE_BOOT_GRACE_MS
+ int "Minimum awake time after reset / power-on (ms)"
+ depends on HOMEKEY_PRESENCE_ENABLED
+ range 0 60000
+ default 10000
+ help
+ Gives the sensor time to start and USB time to connect. Skipped
+ on GPIO wake from deep sleep. Does not delay NFC startup.
+endmenu
diff --git a/esp32/main/main.c b/esp32/main/main.c
index 9e896a6..c91e419 100644
--- a/esp32/main/main.c
+++ b/esp32/main/main.c
@@ -1,4 +1,5 @@
#include <assert.h>
+#include <stdatomic.h>
#include <string.h>
#include "esp_err.h"
#include "esp_log.h"
@@ -13,6 +14,7 @@
#include "services/gatt/ble_svc_gatt.h"
#include "reader.h"
#include "relay.h"
+#include "presence.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -68,6 +70,7 @@ static int apdu_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt
}
static uint8_t own_addr_type;
static uint16_t connection = BLE_HS_CONN_HANDLE_NONE;
+static atomic_bool shutting_down;
/* Accessed only by the NimBLE host task. */
static uint8_t reply[8];
static bool have_reply;
@@ -157,6 +160,8 @@ static int gap_event(struct ble_gap_event *event, void *arg)
}
static void advertise(void)
{
+ if (atomic_load(&shutting_down))
+ return;
struct ble_hs_adv_fields fields = {0};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.uuids128 = (ble_uuid128_t *)&service_uuid;
@@ -223,11 +228,21 @@ static void nfc_task(void *arg)
{
(void)arg;
start_polling(report_card);
+ /* PN532 owner has finished the last session and confirmed PowerDown. */
+ atomic_store(&shutting_down, true);
+ int rc;
+ while ((rc = nimble_port_stop()) != 0) {
+ ESP_LOGE(TAG, "NimBLE stop failed: %d; retrying", rc);
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+ ESP_ERROR_CHECK(nimble_port_deinit());
+ presence_enter_sleep();
vTaskDelete(NULL);
}
void app_main(void)
{
+ presence_init();
relay_init();
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
diff --git a/esp32/main/pn532.c b/esp32/main/pn532.c
index 11f063d..fa4a239 100644
--- a/esp32/main/pn532.c
+++ b/esp32/main/pn532.c
@@ -1,6 +1,8 @@
#include <string.h>
#include "driver/uart.h"
+#include "driver/gpio.h"
+#include "sdkconfig.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
@@ -8,6 +10,7 @@
#include "freertos/task.h"
#include "reader.h"
#include "relay.h"
+#include "presence.h"
#define PN532_UART UART_NUM_1
#define PN532_TX_GPIO 3
@@ -16,6 +19,12 @@
#define COMMAND_TIMEOUT_MS 1000
#define POLL_INTERVAL_MS 300
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+_Static_assert(CONFIG_HOMEKEY_PRESENCE_GPIO != PN532_TX_GPIO &&
+ CONFIG_HOMEKEY_PRESENCE_GPIO != PN532_RX_GPIO,
+ "LD2410S OT2 conflicts with PN532 UART");
+#endif
+
static const char *TAG = "pn532";
static const uint8_t ack[] = {0x00, 0x00, 0xff, 0x00, 0xff, 0x00};
@@ -33,6 +42,9 @@ static void pn532_uart_init(void)
ESP_ERROR_CHECK(uart_param_config(PN532_UART, &config));
ESP_ERROR_CHECK(uart_set_pin(PN532_UART, PN532_TX_GPIO, PN532_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
+ /* Restore UART routing before releasing the idle-high sleep hold. */
+ ESP_ERROR_CHECK(gpio_hold_dis(PN532_TX_GPIO));
+ gpio_deep_sleep_hold_dis();
ESP_ERROR_CHECK(uart_flush_input(PN532_UART));
ESP_LOGI(TAG, "UART ready: TX=GPIO%d RX=GPIO%d baud=%d",
PN532_TX_GPIO, PN532_RX_GPIO, PN532_BAUD);
@@ -292,6 +304,12 @@ static bool initialize_reader(void)
/* Discard stale input before starting this attempt. */
ESP_ERROR_CHECK(uart_flush_input(PN532_UART));
+ /* PowerDown wake requires oscillator startup time after serial activity.
+ * Keep the proven startup packet too, but do not rely on its short prefix. */
+ static const uint8_t wake[] = {0x55, 0x55};
+ if (!write_bytes(wake, sizeof(wake)))
+ return false;
+ vTaskDelay(pdMS_TO_TICKS(10));
int n = command(firmware, sizeof(firmware), response);
if (n != 6 || response[2] != 0x32) {
@@ -317,6 +335,25 @@ static bool initialize_reader(void)
return true;
}
+static bool power_down_reader(void)
+{
+ static const uint8_t power_down[] = {0x16, 0x10}; /* HSU wake only */
+ uint8_t response[128];
+ release_rf();
+ int n = command(power_down, sizeof(power_down), response);
+ if (n != 3 || response[2] != 0) {
+ ESP_LOGE(TAG, "PN532 PowerDown not confirmed; staying awake to retry");
+ return false;
+ }
+ /* PN532 needs ~1ms after its response to enter PowerDown. */
+ vTaskDelay(pdMS_TO_TICKS(10));
+ ESP_ERROR_CHECK(uart_wait_tx_done(PN532_UART, pdMS_TO_TICKS(100)));
+ /* Preserve idle HIGH so the floating TX pin cannot wake the PN532. */
+ ESP_ERROR_CHECK(gpio_hold_en(PN532_TX_GPIO));
+ gpio_deep_sleep_hold_en();
+ return true;
+}
+
/* ECP v2 Home Key, with software CRC-A. These bytes identify a Home;
* they are public routing metadata, never authentication credentials. */
static void build_ecp(uint8_t frame[18], const uint8_t group[8])
@@ -398,6 +435,18 @@ void start_polling(pn532_card_callback_t callback)
nfc_presence_t presence = {0};
for (;;) {
+ /* Only this task touches PN532. The synchronous card callback has
+ * returned, so an in-flight authentication is never cut off here. */
+ if (presence_sleep_requested()) {
+ if (power_down_reader())
+ return;
+ /* A lost response leaves power state uncertain. Wake and restore
+ * the reader before retrying; never blindly poll a sleeping chip. */
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ while (!initialize_reader())
+ vTaskDelay(pdMS_TO_TICKS(2000));
+ continue;
+ }
nfc_card_t card = {0};
uint8_t group[8];
bool ecp = relay_ecp_group(group);
diff --git a/esp32/main/presence.c b/esp32/main/presence.c
new file mode 100644
index 0000000..a3f1ba6
--- /dev/null
+++ b/esp32/main/presence.c
@@ -0,0 +1,74 @@
+#include "presence.h"
+#include "sdkconfig.h"
+#include "driver/gpio.h"
+#include "esp_err.h"
+#include "esp_idf_version.h"
+#include "esp_log.h"
+#include "esp_sleep.h"
+#include "esp_timer.h"
+
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+static int64_t boot_deadline;
+static int64_t absent_since = -1;
+#endif
+
+void presence_init(void)
+{
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+ const gpio_config_t input = {
+ .pin_bit_mask = 1ULL << CONFIG_HOMEKEY_PRESENCE_GPIO,
+ .mode = GPIO_MODE_INPUT,
+ .pull_down_en = GPIO_PULLDOWN_ENABLE,
+ .pull_up_en = GPIO_PULLUP_DISABLE,
+ .intr_type = GPIO_INTR_DISABLE,
+ };
+ ESP_ERROR_CHECK(gpio_config(&input));
+ /* Arm before any shutdown work. Level wake also covers presence returning
+ * between the final check and entry into deep sleep. */
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
+ ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(
+ input.pin_bit_mask, ESP_GPIO_WAKEUP_GPIO_HIGH));
+#else
+ ESP_ERROR_CHECK(esp_deep_sleep_enable_gpio_wakeup(
+ input.pin_bit_mask, ESP_GPIO_WAKEUP_GPIO_HIGH));
+#endif
+ boot_deadline = esp_timer_get_time();
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
+ bool gpio_wakeup = (esp_sleep_get_wakeup_causes() &
+ (1U << ESP_SLEEP_WAKEUP_GPIO)) != 0;
+#else
+ bool gpio_wakeup = esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO;
+#endif
+ if (!gpio_wakeup)
+ boot_deadline += (int64_t)CONFIG_HOMEKEY_PRESENCE_BOOT_GRACE_MS * 1000;
+ ESP_LOGI("presence", "LD2410S OT2=GPIO%d (HIGH=present); deep sleep enabled",
+ CONFIG_HOMEKEY_PRESENCE_GPIO);
+#endif
+}
+
+bool presence_sleep_requested(void)
+{
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+ int64_t now = esp_timer_get_time();
+ int level = gpio_get_level(CONFIG_HOMEKEY_PRESENCE_GPIO);
+ // ESP_LOGI("presence", "GPIO%d reads %d", CONFIG_HOMEKEY_PRESENCE_GPIO, level);
+ if (level) {
+ absent_since = -1;
+ return false;
+ }
+ if (absent_since < 0)
+ absent_since = now;
+ return now >= boot_deadline &&
+ now - absent_since >= (int64_t)CONFIG_HOMEKEY_PRESENCE_ABSENCE_MS * 1000;
+#else
+ return false;
+#endif
+}
+
+void presence_enter_sleep(void)
+{
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+ ESP_LOGI("presence", "Absent: PN532 asleep, BLE stopped; ESP32 deep sleep");
+ esp_deep_sleep_start();
+#endif
+}
diff --git a/esp32/main/presence.h b/esp32/main/presence.h
new file mode 100644
index 0000000..859f892
--- /dev/null
+++ b/esp32/main/presence.h
@@ -0,0 +1,10 @@
+#ifndef HOMEKEY_PRESENCE_H
+#define HOMEKEY_PRESENCE_H
+#include <stdbool.h>
+
+void presence_init(void);
+/* NFC owner only: checks absence between complete NFC sessions. */
+bool presence_sleep_requested(void);
+/* Called after PN532 PowerDown succeeded and Bluetooth has stopped. */
+void presence_enter_sleep(void);
+#endif
diff --git a/esp32/main/reader.h b/esp32/main/reader.h
index f82c253..baa0b42 100644
--- a/esp32/main/reader.h
+++ b/esp32/main/reader.h
@@ -17,7 +17,8 @@ typedef struct {
typedef bool (*pn532_card_callback_t)(const nfc_card_t *card);
-/* Blocks in the caller's task. Callback returns false if its queue is full. */
+/* Blocks in the caller's task. Returns only after absence and successful
+ * PN532 PowerDown; caller must stop BLE and enter deep sleep. */
void start_polling(pn532_card_callback_t callback);
/* Only called by the NFC owner task while its target is active. */
int pn532_exchange(uint8_t target, const uint8_t *apdu, size_t length,
diff --git a/tests/include/driver/gpio.h b/tests/include/driver/gpio.h
new file mode 100644
index 0000000..0991e04
--- /dev/null
+++ b/tests/include/driver/gpio.h
@@ -0,0 +1,18 @@
+#ifndef MOCK_GPIO_H
+#define MOCK_GPIO_H
+#include "mock.h"
+#define GPIO_MODE_INPUT 1
+#define GPIO_PULLDOWN_ENABLE 1
+#define GPIO_PULLUP_DISABLE 0
+#define GPIO_INTR_DISABLE 0
+typedef struct {
+ uint64_t pin_bit_mask;
+ int mode, pull_down_en, pull_up_en, intr_type;
+} gpio_config_t;
+int gpio_config(const gpio_config_t *config);
+int gpio_get_level(int pin);
+int gpio_hold_en(int pin);
+int gpio_hold_dis(int pin);
+void gpio_deep_sleep_hold_en(void);
+void gpio_deep_sleep_hold_dis(void);
+#endif
diff --git a/tests/include/esp_idf_version.h b/tests/include/esp_idf_version.h
new file mode 100644
index 0000000..278cbc0
--- /dev/null
+++ b/tests/include/esp_idf_version.h
@@ -0,0 +1,4 @@
+#define ESP_IDF_VERSION_VAL(a,b,c) (((a)<<16)|((b)<<8)|(c))
+#ifndef ESP_IDF_VERSION
+#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(6,0,0)
+#endif
diff --git a/tests/include/esp_sleep.h b/tests/include/esp_sleep.h
new file mode 100644
index 0000000..f9fd944
--- /dev/null
+++ b/tests/include/esp_sleep.h
@@ -0,0 +1,7 @@
+#include "mock.h"
+#define ESP_GPIO_WAKEUP_GPIO_HIGH 1
+#define ESP_SLEEP_WAKEUP_GPIO 7
+int esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(uint64_t mask, int mode);
+int esp_deep_sleep_enable_gpio_wakeup(uint64_t mask, int mode);
+int esp_sleep_get_wakeup_cause(void);
+void esp_deep_sleep_start(void);
diff --git a/tests/include/sdkconfig.h b/tests/include/sdkconfig.h
new file mode 100644
index 0000000..b2b16bc
--- /dev/null
+++ b/tests/include/sdkconfig.h
@@ -0,0 +1,9 @@
+#ifndef TEST_SDKCONFIG_H
+#define TEST_SDKCONFIG_H
+#ifndef TEST_PRESENCE_DISABLED
+#define CONFIG_HOMEKEY_PRESENCE_ENABLED 1
+#define CONFIG_HOMEKEY_PRESENCE_GPIO 1
+#define CONFIG_HOMEKEY_PRESENCE_ABSENCE_MS 1000
+#define CONFIG_HOMEKEY_PRESENCE_BOOT_GRACE_MS 10000
+#endif
+#endif
diff --git a/tests/pn532_test.c b/tests/pn532_test.c
index e02b95b..67f3dc8 100644
--- a/tests/pn532_test.c
+++ b/tests/pn532_test.c
@@ -7,6 +7,14 @@ static unsigned writes;
static uint8_t last_tx[300];
static size_t last_tx_len;
static uint8_t tx_history[16][300];
+static bool held, deep_hold, sleep_requested;
+static bool auto_reply;
+static void response(const uint8_t *d, uint8_t n);
+bool presence_sleep_requested(void) { return sleep_requested; }
+int gpio_hold_en(int pin) { assert(pin == PN532_TX_GPIO); held = true; return ESP_OK; }
+int gpio_hold_dis(int pin) { assert(pin == PN532_TX_GPIO); held = false; return ESP_OK; }
+void gpio_deep_sleep_hold_en(void) { deep_hold = true; }
+void gpio_deep_sleep_hold_dis(void) { deep_hold = false; }
bool relay_ecp_group(uint8_t out[8]) { (void)out; return false; }
int64_t esp_timer_get_time(void) { return now; }
void vTaskDelay(TickType_t t) { now += t * 1000; }
@@ -36,6 +44,20 @@ int uart_write_bytes(int u, const void *d, size_t n)
assert(writes < 16);
memcpy(tx_history[writes], d, n);
++writes;
+ if (auto_reply && n > 2) {
+ const uint8_t *frame = d;
+ if (frame[0] == 0x55)
+ frame += 5;
+ uint8_t cmd = frame[6];
+ uint8_t reply[] = {0xd5, (uint8_t)(cmd + 1), 0, 0, 0, 0};
+ if (cmd == 0x02) {
+ assert(now >= 10000); /* Explicit oscillator delay preceded firmware. */
+ reply[2] = 0x32;
+ response(reply, 6);
+ } else {
+ response(reply, cmd == 0x16 ? 3 : 2);
+ }
+ }
return (int)n;
}
int uart_wait_tx_done(int u, TickType_t t)
@@ -80,6 +102,8 @@ static void reset(void)
total = pos = 0;
now = 0;
writes = 0;
+ held = deep_hold = false;
+ auto_reply = sleep_requested = false;
}
static void append(const uint8_t *d, size_t n)
{
@@ -202,6 +226,42 @@ int main(void)
response(rf, 2);
assert(!broadcast_ecp(gid));
assert(writes == 5);
+ /* Deep sleep is allowed only after a complete success response. */
+ const uint8_t sleeping[] = {0xd5, 0x17, 0};
+ reset();
+ response(rf, 2);
+ response(sleeping, 3);
+ assert(power_down_reader());
+ assert(held && deep_hold && writes == 2);
+ assert(tx_history[0][6] == 0x32 && tx_history[0][8] == 0);
+ assert(tx_history[1][6] == 0x16 && tx_history[1][7] == 0x10);
+ assert(now >= 10000);
+ reset();
+ response(rf, 2);
+ uint8_t refused[] = {0xd5, 0x17, 0x27};
+ response(refused, 3);
+ assert(!power_down_reader());
+ assert(!held && !deep_hold);
+ reset();
+ response(rf, 2);
+ response(sleeping, 2); /* Truncated status must not authorize sleep. */
+ assert(!power_down_reader());
+ assert(!held && !deep_hold);
+ reset();
+ response(rf, 2); /* PowerDown response lost. */
+ assert(!power_down_reader());
+ assert(!held && !deep_hold);
+ pn532_uart_init();
+ assert(!held && !deep_hold);
+ reset();
+ auto_reply = true;
+ assert(initialize_reader());
+ assert(writes == 4 && tx_history[0][0] == 0x55 && tx_history[0][1] == 0x55);
+ reset();
+ auto_reply = sleep_requested = true;
+ start_polling(NULL); /* Absence path returns before any card callback. */
+ assert(held && deep_hold && writes == 6);
+ assert(tx_history[5][6] == 0x16);
puts("PN532: ECP frame/CRC-A, raw transmit and expected timeout; startup prefix preserved; APDU >64 bytes; status/chaining rejection; fragmented "
"frames, checksums, timeout and UID parsing passed");
}
diff --git a/tests/presence_test.c b/tests/presence_test.c
new file mode 100644
index 0000000..8ace3d9
--- /dev/null
+++ b/tests/presence_test.c
@@ -0,0 +1,66 @@
+#include <stdio.h>
+#include "../esp32/main/presence.c"
+static int64_t now;
+static int level, cause;
+static unsigned armed, sleeps;
+int64_t esp_timer_get_time(void) { return now; }
+int gpio_config(const gpio_config_t *c)
+{
+ assert(c->pin_bit_mask == (1ULL << 5));
+ assert(c->mode == GPIO_MODE_INPUT && c->pull_down_en);
+ return ESP_OK;
+}
+int gpio_get_level(int pin) { assert(pin == 5); return level; }
+int esp_sleep_get_wakeup_cause(void) { return cause; }
+int esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(uint64_t mask, int mode)
+{
+ assert(mask == (1ULL << 5) && mode == ESP_GPIO_WAKEUP_GPIO_HIGH);
+ ++armed;
+ return ESP_OK;
+}
+int esp_deep_sleep_enable_gpio_wakeup(uint64_t mask, int mode)
+{
+ return esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(mask, mode);
+}
+void esp_deep_sleep_start(void) { assert(armed); ++sleeps; }
+int main(void)
+{
+ presence_init();
+#ifdef CONFIG_HOMEKEY_PRESENCE_ENABLED
+ assert(armed == 1);
+ assert(!presence_sleep_requested());
+ now = 9999999;
+ assert(!presence_sleep_requested());
+ now = 10000000;
+ assert(presence_sleep_requested());
+ level = 1;
+ assert(!presence_sleep_requested());
+ level = 0;
+ assert(!presence_sleep_requested());
+ now += 999999;
+ assert(!presence_sleep_requested());
+ level = 1; /* An observed presence cancels the previous absence. */
+ assert(!presence_sleep_requested());
+ level = 0;
+ assert(!presence_sleep_requested());
+ now += 1000000;
+ assert(presence_sleep_requested());
+ /* New GPIO-wake boot skips cold-start grace. */
+ now = 0;
+ absent_since = -1;
+ cause = ESP_SLEEP_WAKEUP_GPIO;
+ presence_init();
+ assert(!presence_sleep_requested());
+ now = 1000000;
+ assert(presence_sleep_requested());
+ level = 1; /* Wake source was armed before a late return of presence. */
+ presence_enter_sleep();
+ assert(sleeps == 1);
+#else
+ now = 100000000;
+ assert(!presence_sleep_requested());
+ presence_enter_sleep();
+ assert(armed == 0 && sleeps == 0);
+#endif
+ puts("Presence: boot grace, absence confirmation, return of presence, GPIO wake and disabled mode passed");
+}
diff --git a/tests/run.sh b/tests/run.sh
index 8cc7a98..74d19ac 100755
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -3,7 +3,11 @@ set -eu
cd "$(dirname "$0")"
build_dir=$(mktemp -d)
trap 'rm -rf "$build_dir"' EXIT
-for name in pn532 relay; do
+for name in pn532 relay presence; do
cc -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -g -Iinclude "${name}_test.c" -o "$build_dir/$name"
"$build_dir/$name"
done
+for config in '-DESP_IDF_VERSION=0x050401' '-DTEST_PRESENCE_DISABLED'; do
+ cc -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -g -Iinclude "$config" presence_test.c -o "$build_dir/presence-config"
+ "$build_dir/presence-config"
+done