commit 5be8ff6a1040884ca1ee504a28968f79fa9760ba
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-05T02:28:58Z |
| subject | Added service for key exchange |
commit 5be8ff6a1040884ca1ee504a28968f79fa9760ba
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-05T02:28:58Z
Added service for key exchange
---
README.md | 40 +-
custom_components/homekey_reader/__init__.py | 10 +-
custom_components/homekey_reader/config_flow.py | 164 ++---
custom_components/homekey_reader/manifest.json | 8 +-
custom_components/homekey_reader/sensor.py | 220 +++----
custom_components/homekey_reader/strings.json | 34 +-
.../homekey_reader/translations/en.json | 34 +-
esp32/PROTOCOL.md | 52 ++
esp32/main/CMakeLists.txt | 34 +-
esp32/main/main.c | 127 +++-
esp32/main/pin_test.c | 102 +--
esp32/main/pn532.c | 455 +++++++-------
esp32/main/pn532_test.c | 690 ++++++++++-----------
esp32/main/reader.h | 85 +--
esp32/main/relay.c | 211 +++++++
esp32/main/relay.h | 15 +
tests/include/driver/uart.h | 1 +
tests/include/esp_err.h | 1 +
tests/include/esp_log.h | 1 +
tests/include/esp_random.h | 1 +
tests/include/esp_timer.h | 1 +
tests/include/freertos/FreeRTOS.h | 3 +
tests/include/freertos/queue.h | 2 +
tests/include/freertos/semphr.h | 5 +
tests/include/freertos/task.h | 1 +
tests/include/mock.h | 47 ++
tests/pn532_test.c | 175 ++++++
tests/relay_test.c | 123 ++++
tests/run.sh | 9 +
29 files changed, 1705 insertions(+), 946 deletions(-)
diff --git a/README.md b/README.md
index e9cd928..57e51a3 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,12 @@
In development. NOT FOR USE!
-## Home Assistant UI setup
+Current firmware connects directly to `homekey-go` over BLE GATT and relays
+NFC APDUs. The HACS component and service-data examples below describe the
+earlier advertisement-based firmware; current firmware does not emit those
+UID advertisements. See [the current relay protocol](esp32/PROTOCOL.md).
+
+## Legacy Home Assistant UI setup
HACS
@@ -36,21 +41,9 @@ idf.py set-target esp32c3
idf.py -p /dev/cu.usbmodem101 flash monitor
```
-## Mock Flow
+## Current flow
```text
-ESP32-C3
- mock PN532 UID (DE:AD:BE:EF)
- |
- v
-BLE service-data advertisement
- |
- v
-Home Assistant Bluetooth
- |
- +--> HomeKey Reader config entry
- +--> sensor: Last NFC UID
- +--> event: homekey_reader_packet
```
Service UUID:
@@ -62,8 +55,8 @@ Service UUID:
Advertisement payload after the UUID:
```text
-byte 0 protocol version = 1
-byte 1 message type = 1 (mock PN532 tag)
+byte 0 protocol version = 1
+byte 1 message type = 1 (mock PN532 tag)
byte 2..3 sequence, uint16 little-endian
byte 4..7 mock UID = DE AD BE EF
```
@@ -80,7 +73,18 @@ Test pins
idf.py -B build-pin-test -D PIN_TEST=ON build flash monitor
```
-Test pnc532
+Test pn532
```
-idf.py -B build-pnc532-test -D PNC532_TEST=ON build flash monitor
+idf.py -B build-pn532-test -D PNC532_TEST=ON build flash monitor
```
+
+## NFC APDU relay
+
+The firmware now exposes session status and an APDU mailbox for `homekey-go`
+0.0.3. Flash the matching firmware before updating the Go service. NFC runs in
+one UART owner task; the RF field stays on throughout authentication.
+
+The first test uses an existing Home Key opened manually in Wallet. Express-mode
+ECP wakeup, FAST authentication and unknown-device attestation are separate
+follow-up work. `homekey-go` verifies the key; UID detection alone never unlocks.
+See `esp32/PROTOCOL.md` for the mailbox format and `tests/run.sh` for host tests.
diff --git a/custom_components/homekey_reader/__init__.py b/custom_components/homekey_reader/__init__.py
index a15f2be..df6a7d7 100644
--- a/custom_components/homekey_reader/__init__.py
+++ b/custom_components/homekey_reader/__init__.py
@@ -10,11 +10,11 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Set up HomeKey Reader from a config entry."""
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
- return True
+ """Set up HomeKey Reader from a config entry."""
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
- """Unload a HomeKey Reader config entry."""
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+ """Unload a HomeKey Reader config entry."""
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
diff --git a/custom_components/homekey_reader/config_flow.py b/custom_components/homekey_reader/config_flow.py
index a28b511..2191e5a 100644
--- a/custom_components/homekey_reader/config_flow.py
+++ b/custom_components/homekey_reader/config_flow.py
@@ -15,85 +15,85 @@ from .const import DOMAIN, SERVICE_UUID
class HomeKeyReaderConfigFlow(ConfigFlow, domain=DOMAIN):
- """Handle a config flow for HomeKey Reader."""
-
- VERSION = 1
-
- def __init__(self) -> None:
- self._discovery_info: BluetoothServiceInfoBleak | None = None
- self._discovered: dict[str, BluetoothServiceInfoBleak] = {}
-
- async def async_step_bluetooth(
- self, discovery_info: BluetoothServiceInfoBleak
- ) -> ConfigFlowResult:
- """Handle Bluetooth discovery."""
- await self.async_set_unique_id(discovery_info.address)
- self._abort_if_unique_id_configured()
-
- self._discovery_info = discovery_info
- name = discovery_info.name or discovery_info.address
- self.context["title_placeholders"] = {"name": name}
- return await self.async_step_bluetooth_confirm()
-
- async def async_step_bluetooth_confirm(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Confirm a Bluetooth-discovered reader."""
- assert self._discovery_info is not None
-
- if user_input is not None:
- return self.async_create_entry(
- title=self._discovery_info.name or "HomeKey Reader",
- data={CONF_ADDRESS: self._discovery_info.address},
- )
-
- self._set_confirm_only()
- return self.async_show_form(
- step_id="bluetooth_confirm",
- description_placeholders={
- "name": self._discovery_info.name or self._discovery_info.address,
- "address": self._discovery_info.address,
- },
- )
-
- async def async_step_user(
- self, user_input: dict[str, Any] | None = None
- ) -> ConfigFlowResult:
- """Let the user choose a currently visible reader."""
- if user_input is not None:
- address = user_input[CONF_ADDRESS]
- info = self._discovered[address]
-
- await self.async_set_unique_id(address, raise_on_progress=False)
- self._abort_if_unique_id_configured()
-
- return self.async_create_entry(
- title=info.name or "HomeKey Reader",
- data={CONF_ADDRESS: address},
- )
-
- configured = self._async_current_ids(include_ignore=False)
- wanted_uuid = SERVICE_UUID.lower()
-
- for info in bluetooth.async_discovered_service_info(
- self.hass, connectable=False
- ):
- service_data_uuids = {uuid.lower() for uuid in info.service_data}
- if wanted_uuid not in service_data_uuids:
- continue
- if info.address in configured:
- continue
- self._discovered[info.address] = info
-
- if not self._discovered:
- return self.async_abort(reason="no_devices_found")
-
- choices = {
- address: f"{info.name or 'HomeKey Reader'} ({address})"
- for address, info in self._discovered.items()
- }
-
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(choices)}),
- )
+ """Handle a config flow for HomeKey Reader."""
+
+ VERSION = 1
+
+ def __init__(self) -> None:
+ self._discovery_info: BluetoothServiceInfoBleak | None = None
+ self._discovered: dict[str, BluetoothServiceInfoBleak] = {}
+
+ async def async_step_bluetooth(
+ self, discovery_info: BluetoothServiceInfoBleak
+ ) -> ConfigFlowResult:
+ """Handle Bluetooth discovery."""
+ await self.async_set_unique_id(discovery_info.address)
+ self._abort_if_unique_id_configured()
+
+ self._discovery_info = discovery_info
+ name = discovery_info.name or discovery_info.address
+ self.context["title_placeholders"] = {"name": name}
+ return await self.async_step_bluetooth_confirm()
+
+ async def async_step_bluetooth_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Confirm a Bluetooth-discovered reader."""
+ assert self._discovery_info is not None
+
+ if user_input is not None:
+ return self.async_create_entry(
+ title=self._discovery_info.name or "HomeKey Reader",
+ data={CONF_ADDRESS: self._discovery_info.address},
+ )
+
+ self._set_confirm_only()
+ return self.async_show_form(
+ step_id="bluetooth_confirm",
+ description_placeholders={
+ "name": self._discovery_info.name or self._discovery_info.address,
+ "address": self._discovery_info.address,
+ },
+ )
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> ConfigFlowResult:
+ """Let the user choose a currently visible reader."""
+ if user_input is not None:
+ address = user_input[CONF_ADDRESS]
+ info = self._discovered[address]
+
+ await self.async_set_unique_id(address, raise_on_progress=False)
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=info.name or "HomeKey Reader",
+ data={CONF_ADDRESS: address},
+ )
+
+ configured = self._async_current_ids(include_ignore=False)
+ wanted_uuid = SERVICE_UUID.lower()
+
+ for info in bluetooth.async_discovered_service_info(
+ self.hass, connectable=False
+ ):
+ service_data_uuids = {uuid.lower() for uuid in info.service_data}
+ if wanted_uuid not in service_data_uuids:
+ continue
+ if info.address in configured:
+ continue
+ self._discovered[info.address] = info
+
+ if not self._discovered:
+ return self.async_abort(reason="no_devices_found")
+
+ choices = {
+ address: f"{info.name or 'HomeKey Reader'} ({address})"
+ for address, info in self._discovered.items()
+ }
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(choices)}),
+ )
diff --git a/custom_components/homekey_reader/manifest.json b/custom_components/homekey_reader/manifest.json
index bc56041..176b699 100644
--- a/custom_components/homekey_reader/manifest.json
+++ b/custom_components/homekey_reader/manifest.json
@@ -5,10 +5,10 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"bluetooth": [
- {
- "service_data_uuid": "7b1e0001-7b1e-4a11-9abc-0123456789ab",
- "connectable": false
- }
+ {
+ "service_data_uuid": "7b1e0001-7b1e-4a11-9abc-0123456789ab",
+ "connectable": false
+ }
],
"codeowners": [],
"integration_type": "device",
diff --git a/custom_components/homekey_reader/sensor.py b/custom_components/homekey_reader/sensor.py
index 221caaa..885c08e 100644
--- a/custom_components/homekey_reader/sensor.py
+++ b/custom_components/homekey_reader/sensor.py
@@ -18,117 +18,117 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
- hass: HomeAssistant,
- entry: ConfigEntry,
- async_add_entities: AddEntitiesCallback,
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
) -> None:
- """Set up the HomeKey Reader sensor."""
- async_add_entities([HomeKeyReaderUidSensor(hass, entry)])
+ """Set up the HomeKey Reader sensor."""
+ async_add_entities([HomeKeyReaderUidSensor(hass, entry)])
class HomeKeyReaderUidSensor(SensorEntity):
- """Last NFC UID received from one HomeKey Reader."""
-
- _attr_has_entity_name = True
- _attr_name = "Last NFC UID"
- _attr_icon = "mdi:nfc"
-
- def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
- self.hass = hass
- self._entry = entry
- self._address = entry.data[CONF_ADDRESS]
- self._attr_unique_id = f"{self._address}_last_uid"
- self._attr_native_value = None
- self._attr_available = False
- self._attrs: dict[str, object] = {}
-
- self._attr_device_info = DeviceInfo(
- identifiers={(DOMAIN, self._address)},
- name=entry.title,
- manufacturer="HomeKey Reader",
- model="ESP32-C3 + PN532",
- )
-
- @property
- def extra_state_attributes(self) -> dict[str, object]:
- return self._attrs
-
- async def async_added_to_hass(self) -> None:
- """Subscribe to Bluetooth advertisements."""
-
- @callback
- def _packet_seen(
- service_info: bluetooth.BluetoothServiceInfoBleak,
- change: bluetooth.BluetoothChange,
- ) -> None:
- payload = service_info.service_data.get(SERVICE_UUID)
- if payload is None:
- return
-
- if len(payload) not in (7, 10, 13):
- _LOGGER.warning(
- "Invalid HomeKey Reader packet from %s: %s",
- service_info.address,
- payload.hex(),
- )
- return
-
- version = payload[0]
- seq = int.from_bytes(payload[1:3], "little")
- data = payload[3:]
-
- if version != 1:
- _LOGGER.warning(
- "Unsupported HomeKey Reader version from %s: %s",
- service_info.address,
- payload.hex(),
- )
- return
-
- uid = ":".join(f"{byte:02X}" for byte in data)
-
- self._attr_native_value = uid
- self._attr_available = True
- self._attrs = {
- "address": service_info.address,
- "rssi": service_info.rssi,
- "sequence": seq,
- "protocol_version": version,
- "raw": payload.hex(),
- }
- self.async_write_ha_state()
-
- self.hass.bus.async_fire(
- EVENT_PACKET,
- {
- "uid": uid,
- **self._attrs,
- },
- )
-
- @callback
- def _unavailable(_service_info: bluetooth.BluetoothServiceInfoBleak) -> None:
- self._attr_available = False
- self.async_write_ha_state()
-
- self.async_on_remove(
- bluetooth.async_register_callback(
- self.hass,
- _packet_seen,
- {
- "address": self._address,
- "service_data_uuid": SERVICE_UUID,
- "connectable": False,
- },
- bluetooth.BluetoothScanningMode.ACTIVE,
- replay=bluetooth.BluetoothCallbackReplay.NEWEST_FIRST,
- )
- )
- self.async_on_remove(
- bluetooth.async_track_unavailable(
- self.hass,
- _unavailable,
- self._address,
- connectable=False,
- )
- )
+ """Last NFC UID received from one HomeKey Reader."""
+
+ _attr_has_entity_name = True
+ _attr_name = "Last NFC UID"
+ _attr_icon = "mdi:nfc"
+
+ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
+ self.hass = hass
+ self._entry = entry
+ self._address = entry.data[CONF_ADDRESS]
+ self._attr_unique_id = f"{self._address}_last_uid"
+ self._attr_native_value = None
+ self._attr_available = False
+ self._attrs: dict[str, object] = {}
+
+ self._attr_device_info = DeviceInfo(
+ identifiers={(DOMAIN, self._address)},
+ name=entry.title,
+ manufacturer="HomeKey Reader",
+ model="ESP32-C3 + PN532",
+ )
+
+ @property
+ def extra_state_attributes(self) -> dict[str, object]:
+ return self._attrs
+
+ async def async_added_to_hass(self) -> None:
+ """Subscribe to Bluetooth advertisements."""
+
+ @callback
+ def _packet_seen(
+ service_info: bluetooth.BluetoothServiceInfoBleak,
+ change: bluetooth.BluetoothChange,
+ ) -> None:
+ payload = service_info.service_data.get(SERVICE_UUID)
+ if payload is None:
+ return
+
+ if len(payload) not in (7, 10, 13):
+ _LOGGER.warning(
+ "Invalid HomeKey Reader packet from %s: %s",
+ service_info.address,
+ payload.hex(),
+ )
+ return
+
+ version = payload[0]
+ seq = int.from_bytes(payload[1:3], "little")
+ data = payload[3:]
+
+ if version != 1:
+ _LOGGER.warning(
+ "Unsupported HomeKey Reader version from %s: %s",
+ service_info.address,
+ payload.hex(),
+ )
+ return
+
+ uid = ":".join(f"{byte:02X}" for byte in data)
+
+ self._attr_native_value = uid
+ self._attr_available = True
+ self._attrs = {
+ "address": service_info.address,
+ "rssi": service_info.rssi,
+ "sequence": seq,
+ "protocol_version": version,
+ "raw": payload.hex(),
+ }
+ self.async_write_ha_state()
+
+ self.hass.bus.async_fire(
+ EVENT_PACKET,
+ {
+ "uid": uid,
+ **self._attrs,
+ },
+ )
+
+ @callback
+ def _unavailable(_service_info: bluetooth.BluetoothServiceInfoBleak) -> None:
+ self._attr_available = False
+ self.async_write_ha_state()
+
+ self.async_on_remove(
+ bluetooth.async_register_callback(
+ self.hass,
+ _packet_seen,
+ {
+ "address": self._address,
+ "service_data_uuid": SERVICE_UUID,
+ "connectable": False,
+ },
+ bluetooth.BluetoothScanningMode.ACTIVE,
+ replay=bluetooth.BluetoothCallbackReplay.NEWEST_FIRST,
+ )
+ )
+ self.async_on_remove(
+ bluetooth.async_track_unavailable(
+ self.hass,
+ _unavailable,
+ self._address,
+ connectable=False,
+ )
+ )
diff --git a/custom_components/homekey_reader/strings.json b/custom_components/homekey_reader/strings.json
index b3f286e..a0c17a9 100644
--- a/custom_components/homekey_reader/strings.json
+++ b/custom_components/homekey_reader/strings.json
@@ -1,22 +1,22 @@
{
"title": "HomeKey Reader",
"config": {
- "step": {
- "user": {
- "title": "Select a HomeKey Reader",
- "description": "Choose a currently visible reader.",
- "data": {
- "address": "Reader"
- }
- },
- "bluetooth_confirm": {
- "title": "Set up {name}",
- "description": "Set up HomeKey Reader {name} at {address}?"
- }
- },
- "abort": {
- "already_configured": "This reader is already configured.",
- "no_devices_found": "No HomeKey Reader is currently visible over Bluetooth."
- }
+ "step": {
+ "user": {
+ "title": "Select a HomeKey Reader",
+ "description": "Choose a currently visible reader.",
+ "data": {
+ "address": "Reader"
+ }
+ },
+ "bluetooth_confirm": {
+ "title": "Set up {name}",
+ "description": "Set up HomeKey Reader {name} at {address}?"
+ }
+ },
+ "abort": {
+ "already_configured": "This reader is already configured.",
+ "no_devices_found": "No HomeKey Reader is currently visible over Bluetooth."
+ }
}
}
diff --git a/custom_components/homekey_reader/translations/en.json b/custom_components/homekey_reader/translations/en.json
index b3f286e..a0c17a9 100644
--- a/custom_components/homekey_reader/translations/en.json
+++ b/custom_components/homekey_reader/translations/en.json
@@ -1,22 +1,22 @@
{
"title": "HomeKey Reader",
"config": {
- "step": {
- "user": {
- "title": "Select a HomeKey Reader",
- "description": "Choose a currently visible reader.",
- "data": {
- "address": "Reader"
- }
- },
- "bluetooth_confirm": {
- "title": "Set up {name}",
- "description": "Set up HomeKey Reader {name} at {address}?"
- }
- },
- "abort": {
- "already_configured": "This reader is already configured.",
- "no_devices_found": "No HomeKey Reader is currently visible over Bluetooth."
- }
+ "step": {
+ "user": {
+ "title": "Select a HomeKey Reader",
+ "description": "Choose a currently visible reader.",
+ "data": {
+ "address": "Reader"
+ }
+ },
+ "bluetooth_confirm": {
+ "title": "Set up {name}",
+ "description": "Set up HomeKey Reader {name} at {address}?"
+ }
+ },
+ "abort": {
+ "already_configured": "This reader is already configured.",
+ "no_devices_found": "No HomeKey Reader is currently visible over Bluetooth."
+ }
}
}
diff --git a/esp32/PROTOCOL.md b/esp32/PROTOCOL.md
new file mode 100644
index 0000000..70c7f42
--- /dev/null
+++ b/esp32/PROTOCOL.md
@@ -0,0 +1,52 @@
+# BLE NFC relay v1
+
+Service: `7a6b0001-5b21-4f36-8e5d-9c3a26d74210`.
+
+| Characteristic suffix | Access | Value |
+| --- | --- | --- |
+| `0002` | Read/write | Existing 8-byte PING/PONG probe |
+| `0003` | Read | 18-byte NFC session status |
+| `0004` | Read/write | Fragmented APDU request / stable response mailbox |
+
+All multi-byte integers below are big-endian. The UUID suffix means the last
+four digits of the UUID's first component, with the remainder unchanged.
+
+Status: `version:u8=1, active:u8, uid_length:u8, sak:u8, session:u32, uid[10]`.
+UID bytes are metadata, not credentials. Active means an APDU session is open;
+it does not guarantee that the phone remains physically present. Session IDs
+start from a random boot seed and increase for each offered target. Reconnect
+invalidates all old requests and responses.
+
+APDU packet header (12 bytes):
+`version:u8=1, kind:u8, session:u32, sequence:u16, offset:u16, total:u16`.
+
+| Kind | Direction | Meaning |
+| --- | --- | --- |
+| `01` | Go → ESP32 | Command fragment; payload follows the header |
+| `02` | Go → ESP32 | Release session; zero offset/total, no payload |
+| `10` | ESP32 → Go | Pending response; zero offset/total, no payload |
+| `11` | ESP32 → Go | Ready response; zero offset, total = payload size |
+| `12` | ESP32 → Go | Transport error; zero offset, total = 1, error byte |
+
+Each APDU sequence starts at 1 and strictly increases within its session. Only
+one APDU is admitted at a time. Fragments must be contiguous with matching
+session/sequence/total, within 4..240 bytes. A duplicate, gap, stale session or
+busy write is rejected. No APDU is executed until its complete payload is
+queued to the NFC owner task.
+
+Go uses ATT write requests and fragments to MTU-3 when BlueZ reports MTU,
+otherwise to 20 bytes. Responses are read using BlueZ's long-read support.
+The response remains stable across repeated reads until the next complete
+request or a connection/session reset. A GATT callback never touches UART.
+
+Error bytes: 1 = invalid APDU/target, 2 = UART/frame/timeout failure,
+3 = PN532 error or unsupported chaining flags, 4 = response too large.
+Normal `11` responses include the APDU's SW1/SW2 bytes. PN532 errors cannot
+be mistaken for an APDU status word.
+
+The owner holds the selected target while Go sends SELECT/AUTH0/AUTH1 and
+CONTROL FLOW. It turns RF off after release, transport failure, disconnect or
+15-second expiry, then resumes presence polling. Held cards are suppressed by
+the existing UID presence state until removal; disconnected detections are
+retried after reconnection. GPIO3 TX / GPIO4 RX and startup wake packets are
+preserved.
diff --git a/esp32/main/CMakeLists.txt b/esp32/main/CMakeLists.txt
index 5ba8686..e869fb3 100644
--- a/esp32/main/CMakeLists.txt
+++ b/esp32/main/CMakeLists.txt
@@ -2,23 +2,23 @@ option(PIN_TEST "Build UART loopback test" OFF)
option(PNC532_TEST "Build PNC532 test" OFF)
if(PIN_TEST)
- idf_component_register(
- SRCS "pin_test.c"
- INCLUDE_DIRS "."
- PRIV_REQUIRES driver esp_driver_uart
- )
+ idf_component_register(
+ SRCS "pin_test.c"
+ INCLUDE_DIRS "."
+ PRIV_REQUIRES driver esp_driver_uart
+ )
elseif(PNC532_TEST)
- idf_component_register(
- SRCS "pn532_test.c"
- INCLUDE_DIRS "."
- PRIV_REQUIRES nvs_flash driver esp_timer
- esp_hw_support esp_driver_uart
- )
+ idf_component_register(
+ SRCS "pn532_test.c"
+ INCLUDE_DIRS "."
+ PRIV_REQUIRES nvs_flash driver esp_timer
+ esp_hw_support esp_driver_uart
+ )
else()
- idf_component_register(
- SRCS "main.c" "pn532.c"
- INCLUDE_DIRS "."
- PRIV_REQUIRES bt nvs_flash driver esp_timer
- esp_hw_support esp_driver_uart
- )
+ idf_component_register(
+ SRCS "main.c" "pn532.c" "relay.c"
+ INCLUDE_DIRS "."
+ PRIV_REQUIRES bt nvs_flash driver esp_timer
+ esp_hw_support esp_driver_uart
+ )
endif()
diff --git a/esp32/main/main.c b/esp32/main/main.c
index da97b8c..ccb3caf 100644
--- a/esp32/main/main.c
+++ b/esp32/main/main.c
@@ -12,6 +12,9 @@
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "reader.h"
+#include "relay.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
#define DEVICE_NAME "homekey-reader"
static const char *TAG = "homekey-reader";
@@ -20,19 +23,55 @@ static const char *TAG = "homekey-reader";
* Characteristic: 7a6b0002-5b21-4f36-8e5d-9c3a26d74210
*/
static const ble_uuid128_t service_uuid = BLE_UUID128_INIT(
- 0x10,0x42,0xd7,0x26,0x3a,0x9c,0x5d,0x8e,0x36,0x4f,0x21,0x5b,0x01,0x00,0x6b,0x7a);
+ 0x10, 0x42, 0xd7, 0x26, 0x3a, 0x9c, 0x5d, 0x8e, 0x36, 0x4f, 0x21, 0x5b, 0x01, 0x00, 0x6b, 0x7a);
static const ble_uuid128_t ping_uuid = BLE_UUID128_INIT(
- 0x10,0x42,0xd7,0x26,0x3a,0x9c,0x5d,0x8e,0x36,0x4f,0x21,0x5b,0x02,0x00,0x6b,0x7a);
+ 0x10, 0x42, 0xd7, 0x26, 0x3a, 0x9c, 0x5d, 0x8e, 0x36, 0x4f, 0x21, 0x5b, 0x02, 0x00, 0x6b, 0x7a);
+static const ble_uuid128_t status_uuid = BLE_UUID128_INIT(
+ 0x10, 0x42, 0xd7, 0x26, 0x3a, 0x9c, 0x5d, 0x8e, 0x36, 0x4f, 0x21, 0x5b, 0x03, 0x00, 0x6b, 0x7a);
+static const ble_uuid128_t apdu_uuid = BLE_UUID128_INIT(
+ 0x10, 0x42, 0xd7, 0x26, 0x3a, 0x9c, 0x5d, 0x8e, 0x36, 0x4f, 0x21, 0x5b, 0x04, 0x00, 0x6b, 0x7a);
+static int status_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn;
+ (void)attr;
+ (void)arg;
+ if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR)
+ return BLE_ATT_ERR_UNLIKELY;
+ uint8_t data[18];
+ size_t n = relay_status(data);
+ return os_mbuf_append(ctxt->om, data, n) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
+}
+static int apdu_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn;
+ (void)attr;
+ (void)arg;
+ uint8_t data[RELAY_VALUE_MAX];
+ if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
+ size_t n = relay_response(data);
+ return os_mbuf_append(ctxt->om, data, n) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
+ }
+ if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
+ size_t n = OS_MBUF_PKTLEN(ctxt->om);
+ if (n > sizeof(data) || n < RELAY_HEADER)
+ return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+ if (os_mbuf_copydata(ctxt->om, 0, n, data) != 0)
+ return BLE_ATT_ERR_UNLIKELY;
+ return relay_write(data, n) ? 0 : BLE_ATT_ERR_VALUE_NOT_ALLOWED;
+ }
+ return BLE_ATT_ERR_UNLIKELY;
+}
static uint8_t own_addr_type;
static uint16_t connection = BLE_HS_CONN_HANDLE_NONE;
/* Accessed only by the NimBLE host task. */
static uint8_t reply[8];
static bool have_reply;
-static int ping_access(uint16_t conn, uint16_t attr,
- struct ble_gatt_access_ctxt *ctxt, void *arg)
+static int ping_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
- (void)conn; (void)attr; (void)arg;
+ (void)conn;
+ (void)attr;
+ (void)arg;
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
uint8_t request[8];
if (OS_MBUF_PKTLEN(ctxt->om) != sizeof(request))
@@ -48,21 +87,27 @@ static int ping_access(uint16_t conn, uint16_t attr,
return 0;
}
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
- if (!have_reply) return 0; /* Empty until the first PING. */
- return os_mbuf_append(ctxt->om, reply, sizeof(reply)) == 0
- ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
+ if (!have_reply)
+ return 0; /* Empty until the first PING. */
+ return os_mbuf_append(ctxt->om, reply, sizeof(reply)) == 0 ? 0
+ : BLE_ATT_ERR_INSUFFICIENT_RES;
}
return BLE_ATT_ERR_UNLIKELY;
}
static const struct ble_gatt_svc_def services[] = {
- { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &service_uuid.u,
- .characteristics = (struct ble_gatt_chr_def[]) {
- { .uuid = &ping_uuid.u, .access_cb = ping_access,
- .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE },
- {0}
- } },
- {0}
-};
+ {.type = BLE_GATT_SVC_TYPE_PRIMARY,
+ .uuid = &service_uuid.u,
+ .characteristics =
+ (struct ble_gatt_chr_def[]){
+ {.uuid = &ping_uuid.u,
+ .access_cb = ping_access,
+ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE},
+ {.uuid = &status_uuid.u, .access_cb = status_access, .flags = BLE_GATT_CHR_F_READ},
+ {.uuid = &apdu_uuid.u,
+ .access_cb = apdu_access,
+ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE},
+ {0}}},
+ {0}};
static void advertise(void);
static int gap_event(struct ble_gap_event *event, void *arg)
{
@@ -71,20 +116,25 @@ static int gap_event(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
connection = event->connect.conn_handle;
+ relay_connected(true);
have_reply = false;
ESP_LOGI(TAG, "BLE gateway connected");
- } else advertise();
+ } else
+ advertise();
break;
case BLE_GAP_EVENT_DISCONNECT:
+ relay_connected(false);
connection = BLE_HS_CONN_HANDLE_NONE;
have_reply = false;
ESP_LOGI(TAG, "BLE gateway disconnected: %d", event->disconnect.reason);
advertise();
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
- if (connection == BLE_HS_CONN_HANDLE_NONE) advertise();
+ if (connection == BLE_HS_CONN_HANDLE_NONE)
+ advertise();
+ break;
+ default:
break;
- default: break;
}
return 0;
}
@@ -96,33 +146,39 @@ static void advertise(void)
fields.num_uuids128 = 1;
fields.uuids128_is_complete = 1;
int rc = ble_gap_adv_set_fields(&fields);
- if (rc != 0) { ESP_LOGE(TAG, "advertisement fields: %d", rc); return; }
+ if (rc != 0) {
+ ESP_LOGE(TAG, "advertisement fields: %d", rc);
+ return;
+ }
struct ble_hs_adv_fields scan = {0};
scan.name = (uint8_t *)DEVICE_NAME;
scan.name_len = strlen(DEVICE_NAME);
scan.name_is_complete = 1;
rc = ble_gap_adv_rsp_set_fields(&scan);
- if (rc != 0) { ESP_LOGE(TAG, "scan response: %d", rc); return; }
+ if (rc != 0) {
+ ESP_LOGE(TAG, "scan response: %d", rc);
+ return;
+ }
struct ble_gap_adv_params params = {0};
params.conn_mode = BLE_GAP_CONN_MODE_UND;
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
params.itvl_min = BLE_GAP_ADV_ITVL_MS(100);
params.itvl_max = BLE_GAP_ADV_ITVL_MS(150);
- rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
- ¶ms, gap_event, NULL);
- if (rc != 0) ESP_LOGE(TAG, "advertisement start: %d", rc);
+ rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, ¶ms, gap_event, NULL);
+ if (rc != 0)
+ ESP_LOGE(TAG, "advertisement start: %d", rc);
}
-/* NFC task stays independent. Card events are logged for now;
- * the old non-connectable UID advertisements are replaced by GATT.
- */
+/* This callback runs in the NFC owner task; it holds the target active
+ * until Go finishes authentication or the relay session expires. */
static bool report_card(const nfc_card_t *card)
{
ESP_LOGI(TAG, "NFC card detected; uid_len=%u", card->uid_len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, card->uid, card->uid_len, ESP_LOG_INFO);
- return true;
+ return relay_card(card);
}
static void on_reset(int reason)
{
+ relay_connected(false);
connection = BLE_HS_CONN_HANDLE_NONE;
have_reply = false;
ESP_LOGE(TAG, "NimBLE reset: %d", reason);
@@ -136,8 +192,8 @@ static void on_sync(void)
assert(rc == 0);
rc = ble_hs_id_copy_addr(own_addr_type, addr, NULL);
assert(rc == 0);
- ESP_LOGI(TAG, "BLE address %02X:%02X:%02X:%02X:%02X:%02X",
- addr[5],addr[4],addr[3],addr[2],addr[1],addr[0]);
+ ESP_LOGI(TAG, "BLE address %02X:%02X:%02X:%02X:%02X:%02X", addr[5], addr[4], addr[3], addr[2],
+ addr[1], addr[0]);
advertise();
}
static void host_task(void *arg)
@@ -146,8 +202,16 @@ static void host_task(void *arg)
nimble_port_run();
nimble_port_freertos_deinit();
}
+static void nfc_task(void *arg)
+{
+ (void)arg;
+ start_polling(report_card);
+ vTaskDelete(NULL);
+}
+
void app_main(void)
{
+ relay_init();
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
@@ -166,5 +230,6 @@ void app_main(void)
rc = ble_gatts_add_svcs(services);
assert(rc == 0);
nimble_port_freertos_init(host_task);
- start_polling(report_card);
+ BaseType_t started = xTaskCreate(nfc_task, "pn532", 8192, NULL, 4, NULL);
+ assert(started == pdPASS);
}
diff --git a/esp32/main/pin_test.c b/esp32/main/pin_test.c
index 74fe746..db1fb9f 100644
--- a/esp32/main/pin_test.c
+++ b/esp32/main/pin_test.c
@@ -14,65 +14,65 @@ static const char *TAG = "uart-loopback";
void app_main(void)
{
- const uart_config_t config = {
- .baud_rate = 115200,
- .data_bits = UART_DATA_8_BITS,
- .parity = UART_PARITY_DISABLE,
- .stop_bits = UART_STOP_BITS_1,
- .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
- .source_clk = UART_SCLK_DEFAULT,
- };
+ const uart_config_t config = {
+ .baud_rate = 115200,
+ .data_bits = UART_DATA_8_BITS,
+ .parity = UART_PARITY_DISABLE,
+ .stop_bits = UART_STOP_BITS_1,
+ .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+ .source_clk = UART_SCLK_DEFAULT,
+ };
- ESP_ERROR_CHECK(uart_param_config(TEST_UART, &config));
- ESP_ERROR_CHECK(uart_set_pin(
- TEST_UART, TX_PIN, RX_PIN,
- UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
- ESP_ERROR_CHECK(uart_driver_install(
- TEST_UART, 256, 0, 0, NULL, 0));
+ ESP_ERROR_CHECK(uart_param_config(TEST_UART, &config));
+ ESP_ERROR_CHECK(uart_set_pin(
+ TEST_UART, TX_PIN, RX_PIN,
+ UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
+ ESP_ERROR_CHECK(uart_driver_install(
+ TEST_UART, 256, 0, 0, NULL, 0));
- const uint8_t tx[] = {
- 0x55, 0xaa, 0x00, 0xff, 0x02, 0xfe, 0xd4, 0x2a
- };
+ const uint8_t tx[] = {
+ 0x55, 0xaa, 0x00, 0xff, 0x02, 0xfe, 0xd4, 0x2a
+ };
- for (;;) {
- uint8_t rx[sizeof(tx)] = {0};
- size_t received = 0;
+ for (;;) {
+ uint8_t rx[sizeof(tx)] = {0};
+ size_t received = 0;
- ESP_ERROR_CHECK(uart_flush_input(TEST_UART));
+ ESP_ERROR_CHECK(uart_flush_input(TEST_UART));
- int written = uart_write_bytes(TEST_UART, tx, sizeof(tx));
- ESP_LOGI(TAG, "TX: %d bytes", written);
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, tx, sizeof(tx), ESP_LOG_INFO);
+ int written = uart_write_bytes(TEST_UART, tx, sizeof(tx));
+ ESP_LOGI(TAG, "TX: %d bytes", written);
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, tx, sizeof(tx), ESP_LOG_INFO);
- /* Accumulate reads: UART may return fewer bytes than requested. */
- while (received < sizeof(rx)) {
- int n = uart_read_bytes(
- TEST_UART,
- rx + received,
- sizeof(rx) - received,
- pdMS_TO_TICKS(500));
+ /* Accumulate reads: UART may return fewer bytes than requested. */
+ while (received < sizeof(rx)) {
+ int n = uart_read_bytes(
+ TEST_UART,
+ rx + received,
+ sizeof(rx) - received,
+ pdMS_TO_TICKS(500));
- if (n <= 0) {
- ESP_LOGW(TAG, "read stopped: %d", n);
- break;
- }
- received += (size_t)n;
- }
+ if (n <= 0) {
+ ESP_LOGW(TAG, "read stopped: %d", n);
+ break;
+ }
+ received += (size_t)n;
+ }
- ESP_LOGI(TAG, "RX: %u bytes", (unsigned)received);
- if (received > 0) {
- ESP_LOG_BUFFER_HEX_LEVEL(
- TAG, rx, received, ESP_LOG_INFO);
- }
+ ESP_LOGI(TAG, "RX: %u bytes", (unsigned)received);
+ if (received > 0) {
+ ESP_LOG_BUFFER_HEX_LEVEL(
+ TAG, rx, received, ESP_LOG_INFO);
+ }
- if (written == sizeof(tx) &&
- received == sizeof(tx) &&
- memcmp(tx, rx, sizeof(tx)) == 0) {
- ESP_LOGI(TAG, "PASS: RX = TX");
- } else {
- ESP_LOGE(TAG, "FAIL: RX != TX");
- }
+ if (written == sizeof(tx) &&
+ received == sizeof(tx) &&
+ memcmp(tx, rx, sizeof(tx)) == 0) {
+ ESP_LOGI(TAG, "PASS: RX = TX");
+ } else {
+ ESP_LOGE(TAG, "FAIL: RX != TX");
+ }
- vTaskDelay(pdMS_TO_TICKS(2000));
- }
+ vTaskDelay(pdMS_TO_TICKS(2000));
+ }
}
\ No newline at end of file
diff --git a/esp32/main/pn532.c b/esp32/main/pn532.c
index 82f9c2c..62f0068 100644
--- a/esp32/main/pn532.c
+++ b/esp32/main/pn532.c
@@ -7,6 +7,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "reader.h"
+#include "relay.h"
#define PN532_UART UART_NUM_1
#define PN532_TX_GPIO 3
@@ -20,163 +21,164 @@ static const uint8_t ack[] = {0x00, 0x00, 0xff, 0x00, 0xff, 0x00};
static void pn532_uart_init(void)
{
- const uart_config_t config = {
- .baud_rate = PN532_BAUD,
- .data_bits = UART_DATA_8_BITS,
- .parity = UART_PARITY_DISABLE,
- .stop_bits = UART_STOP_BITS_1,
- .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
- .source_clk = UART_SCLK_DEFAULT,
- };
- ESP_ERROR_CHECK(uart_driver_install(PN532_UART, 1024, 0, 0, NULL, 0));
- 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));
- 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);
+ const uart_config_t config = {
+ .baud_rate = PN532_BAUD,
+ .data_bits = UART_DATA_8_BITS,
+ .parity = UART_PARITY_DISABLE,
+ .stop_bits = UART_STOP_BITS_1,
+ .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+ .source_clk = UART_SCLK_DEFAULT,
+ };
+ ESP_ERROR_CHECK(uart_driver_install(PN532_UART, 1024, 0, 0, NULL, 0));
+ 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));
+ 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);
}
static bool write_bytes(const uint8_t *data, size_t len)
{
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, data, len, ESP_LOG_DEBUG);
- return uart_write_bytes(PN532_UART, data, len) == (int)len &&
- uart_wait_tx_done(PN532_UART, pdMS_TO_TICKS(100)) == ESP_OK;
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, data, len, ESP_LOG_DEBUG);
+ return uart_write_bytes(PN532_UART, data, len) == (int)len &&
+ uart_wait_tx_done(PN532_UART, pdMS_TO_TICKS(100)) == ESP_OK;
}
/* One deadline for the entire command, including ACK and fragmented response. */
static bool read_exact(uint8_t *data, size_t len, int64_t deadline)
{
- size_t received = 0;
- while (received < len) {
- int64_t remaining = deadline - esp_timer_get_time();
- if (remaining <= 0) {
- return false;
- }
- TickType_t ticks = pdMS_TO_TICKS((remaining + 999) / 1000);
- if (ticks == 0) {
- ticks = 1;
- }
- int n = uart_read_bytes(PN532_UART, data + received, len - received, ticks);
- if (n < 0) {
- return false;
- }
- received += (size_t)n;
- }
- return true;
+ size_t received = 0;
+ while (received < len) {
+ int64_t remaining = deadline - esp_timer_get_time();
+ if (remaining <= 0) {
+ return false;
+ }
+ TickType_t ticks = pdMS_TO_TICKS((remaining + 999) / 1000);
+ if (ticks == 0) {
+ ticks = 1;
+ }
+ int n = uart_read_bytes(PN532_UART, data + received, len - received, ticks);
+ if (n < 0) {
+ return false;
+ }
+ received += (size_t)n;
+ }
+ return true;
}
/* Returns 0 for ACK, positive for a normal frame, -1 for malformed/timeout.
* Extended frames are unnecessary for these small commands and are rejected.
*/
static bool read_logged(uint8_t *buffer, size_t len,
- int64_t deadline, const char *stage)
+ int64_t deadline, const char *stage)
{
- if (!read_exact(buffer, len, deadline)) {
- ESP_LOGW(TAG, "RX failed at %s: wanted %u bytes",
- stage, (unsigned)len);
- return false;
- }
+ if (!read_exact(buffer, len, deadline)) {
+ ESP_LOGW(TAG, "RX failed at %s: wanted %u bytes",
+ stage, (unsigned)len);
+ return false;
+ }
#ifdef FRAME_DEBUG
- ESP_LOGI(TAG, "RX %s (%u bytes)", stage, (unsigned)len);
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, len, ESP_LOG_INFO);
+ ESP_LOGI(TAG, "RX %s (%u bytes)", stage, (unsigned)len);
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, len, ESP_LOG_INFO);
#endif
- return true;
+ return true;
}
static int read_frame(uint8_t *payload, size_t capacity, int64_t deadline)
{
- uint8_t prefix[3] = {0};
- do {
- prefix[0] = prefix[1];
- prefix[1] = prefix[2];
- if (!read_logged(&prefix[2], 1, deadline, "preamble byte")) {
- return -1;
- }
- } while (prefix[0] != 0 ||
- prefix[1] != 0 ||
- prefix[2] != 0xff);
-
- uint8_t lengths[2];
- if (!read_logged(lengths, sizeof(lengths), deadline, "LEN/LCS")) {
- return -1;
- }
-
- if (lengths[0] == 0 && lengths[1] == 0xff) {
- uint8_t postamble;
- if (!read_logged(&postamble, 1, deadline, "ACK postamble")) {
- return -1;
- }
- if (postamble != 0) {
- ESP_LOGW(TAG, "invalid ACK postamble");
- return -1;
- }
- return 0;
- }
-
- size_t len = lengths[0];
- if (len == 0 ||
- (uint8_t)(lengths[0] + lengths[1]) != 0 ||
- len > capacity) {
- ESP_LOGW(TAG, "invalid length: LEN=%u LCS=0x%02X capacity=%u",
- (unsigned)len, lengths[1], (unsigned)capacity);
- return -1;
- }
-
- if (!read_logged(payload, len, deadline, "payload")) {
- return -1;
- }
-
- uint8_t tail[2];
- if (!read_logged(tail, sizeof(tail), deadline, "DCS/postamble")) {
- return -1;
- }
- if (tail[1] != 0) {
- ESP_LOGW(TAG, "invalid postamble: 0x%02X", tail[1]);
- return -1;
- }
-
- uint8_t checksum = tail[0];
- for (size_t i = 0; i < len; ++i) {
- checksum += payload[i];
- }
- if (checksum != 0) {
- ESP_LOGW(TAG, "invalid checksum: sum=0x%02X", checksum);
- return -1;
- }
-
- return (int)len;
+ uint8_t prefix[3] = {0};
+ do {
+ prefix[0] = prefix[1];
+ prefix[1] = prefix[2];
+ if (!read_logged(&prefix[2], 1, deadline, "preamble byte")) {
+ return -1;
+ }
+ } while (prefix[0] != 0 ||
+ prefix[1] != 0 ||
+ prefix[2] != 0xff);
+
+ uint8_t lengths[2];
+ if (!read_logged(lengths, sizeof(lengths), deadline, "LEN/LCS")) {
+ return -1;
+ }
+
+ if (lengths[0] == 0 && lengths[1] == 0xff) {
+ uint8_t postamble;
+ if (!read_logged(&postamble, 1, deadline, "ACK postamble")) {
+ return -1;
+ }
+ if (postamble != 0) {
+ ESP_LOGW(TAG, "invalid ACK postamble");
+ return -1;
+ }
+ return 0;
+ }
+
+ size_t len = lengths[0];
+ if (len == 0 ||
+ (uint8_t)(lengths[0] + lengths[1]) != 0 ||
+ len > capacity) {
+ ESP_LOGW(TAG, "invalid length: LEN=%u LCS=0x%02X capacity=%u",
+ (unsigned)len, lengths[1], (unsigned)capacity);
+ return -1;
+ }
+
+ if (!read_logged(payload, len, deadline, "payload")) {
+ return -1;
+ }
+
+ uint8_t tail[2];
+ if (!read_logged(tail, sizeof(tail), deadline, "DCS/postamble")) {
+ return -1;
+ }
+ if (tail[1] != 0) {
+ ESP_LOGW(TAG, "invalid postamble: 0x%02X", tail[1]);
+ return -1;
+ }
+
+ uint8_t checksum = tail[0];
+ for (size_t i = 0; i < len; ++i) {
+ checksum += payload[i];
+ }
+ if (checksum != 0) {
+ ESP_LOGW(TAG, "invalid checksum: sum=0x%02X", checksum);
+ return -1;
+ }
+
+ return (int)len;
}
/* Normal frame builder. cmd includes command code, excludes host TFI (D4). */
-static int command(const uint8_t *cmd, size_t cmd_len, uint8_t response[128])
+static int command_full(const uint8_t *cmd, size_t cmd_len, uint8_t *response,
+ size_t capacity, unsigned timeout_ms)
{
- uint8_t frame[64];
- if (cmd_len == 0 || cmd_len > sizeof(frame) - 8) {
- return -1;
- }
-
- size_t len = cmd_len + 1;
- frame[0] = 0;
- frame[1] = 0;
- frame[2] = 0xff;
- frame[3] = (uint8_t)len;
- frame[4] = (uint8_t)(0 - len);
- frame[5] = 0xd4;
- memcpy(frame + 6, cmd, cmd_len);
-
- uint8_t checksum = 0xd4;
- for (size_t i = 0; i < cmd_len; ++i) {
- checksum += cmd[i];
- }
- frame[6 + cmd_len] = (uint8_t)(0 - checksum);
- frame[7 + cmd_len] = 0;
+ uint8_t frame[256];
+ if (cmd_len == 0 || cmd_len > sizeof(frame) - 8 || cmd_len + 1 >= 255) {
+ return -1;
+ }
+
+ size_t len = cmd_len + 1;
+ frame[0] = 0;
+ frame[1] = 0;
+ frame[2] = 0xff;
+ frame[3] = (uint8_t)len;
+ frame[4] = (uint8_t)(0 - len);
+ frame[5] = 0xd4;
+ memcpy(frame + 6, cmd, cmd_len);
+
+ uint8_t checksum = 0xd4;
+ for (size_t i = 0; i < cmd_len; ++i) {
+ checksum += cmd[i];
+ }
+ frame[6 + cmd_len] = (uint8_t)(0 - checksum);
+ frame[7 + cmd_len] = 0;
#ifdef FRAME_DEBUG
- ESP_LOGI(TAG, "TX command 0x%02X (%u bytes)",
- cmd[0], (unsigned)(cmd_len + 8));
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, frame, cmd_len + 8, ESP_LOG_INFO);
+ ESP_LOGI(TAG, "TX command 0x%02X (%u bytes)",
+ cmd[0], (unsigned)(cmd_len + 8));
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, frame, cmd_len + 8, ESP_LOG_INFO);
#endif
bool startup = cmd[0] == 0x02 || cmd[0] == 0x14;
@@ -202,55 +204,93 @@ static int command(const uint8_t *cmd, size_t cmd_len, uint8_t response[128])
}
}
- int64_t deadline =
- esp_timer_get_time() + COMMAND_TIMEOUT_MS * 1000;
- int n;
+ int64_t deadline =
+ esp_timer_get_time() + (int64_t)timeout_ms * 1000;
+ int n;
- do {
- n = read_frame(response, 128, deadline);
+ do {
+ n = read_frame(response, capacity, deadline);
#ifdef FRAME_DEBUG
- if (n > 0) {
- ESP_LOGI(TAG, "RX response (%d bytes)", n);
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, response, n, ESP_LOG_INFO);
- } else if (n == 0) {
- ESP_LOGI(TAG, "RX ACK");
- } else {
- ESP_LOGW(TAG, "RX failed or timed out: %d", n);
- }
+ if (n > 0) {
+ ESP_LOGI(TAG, "RX response (%d bytes)", n);
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, response, n, ESP_LOG_INFO);
+ } else if (n == 0) {
+ ESP_LOGI(TAG, "RX ACK");
+ } else {
+ ESP_LOGW(TAG, "RX failed or timed out: %d", n);
+ }
#else
- if (n < 0) {
- ESP_LOGW(TAG, "RX failed or timed out: %d", n);
- }
+ if (n < 0) {
+ ESP_LOGW(TAG, "RX failed or timed out: %d", n);
+ }
#endif
- } while (n == 0); /* ACK is not the command response. */
+ } while (n == 0); /* ACK is not the command response. */
- if (n >= 2 &&
- response[0] == 0xd5 &&
- response[1] == (uint8_t)(cmd[0] + 1)) {
- return n;
- }
+ if (n >= 2 &&
+ response[0] == 0xd5 &&
+ response[1] == (uint8_t)(cmd[0] + 1)) {
+ return n;
+ }
failed:
- /* Host ACK aborts a pending PN532 command. Drain before the next command. */
- ESP_LOGW(TAG, "command 0x%02X failed or timed out", cmd[0]);
+ /* Host ACK aborts a pending PN532 command. Drain before the next command. */
+ ESP_LOGW(TAG, "command 0x%02X failed or timed out", cmd[0]);
+
+ ESP_LOGI(TAG, "TX abort ACK (%u bytes)", (unsigned)sizeof(ack));
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, ack, sizeof(ack), ESP_LOG_INFO);
+ (void)write_bytes(ack, sizeof(ack));
+
+ vTaskDelay(pdMS_TO_TICKS(20));
+ (void)uart_flush_input(PN532_UART);
+ return -1;
+}
+
+static int command(const uint8_t *cmd, size_t length, uint8_t response[128])
+{
+ return command_full(cmd, length, response, 128, COMMAND_TIMEOUT_MS);
+}
- ESP_LOGI(TAG, "TX abort ACK (%u bytes)", (unsigned)sizeof(ack));
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, ack, sizeof(ack), ESP_LOG_INFO);
- (void)write_bytes(ack, sizeof(ack));
+int pn532_exchange(uint8_t target, const uint8_t *apdu, size_t length, uint8_t *out,
+ size_t capacity, size_t *out_length)
+{
+ *out_length = 0;
+ if (target == 0 || length < 4 || length > RELAY_APDU_MAX)
+ return 1;
+ uint8_t cmd[RELAY_APDU_MAX + 2] = {0x40, target};
+ uint8_t response[256];
+ memcpy(cmd + 2, apdu, length);
+ int n = command_full(cmd, length + 2, response, sizeof(response), 3000);
+ if (n < 3)
+ return 2;
+ /* Error code + MI/chaining flags. Chained PN532 responses are outside
+ * this STANDARD-auth transport's bounded APDU contract. */
+ if (response[2] != 0) {
+ ESP_LOGW(TAG, "InDataExchange status=0x%02x", response[2]);
+ return 3;
+ }
+ if ((size_t)(n - 3) > capacity)
+ return 4;
+ memcpy(out, response + 3, n - 3);
+ *out_length = n - 3;
+ return 0;
+}
- vTaskDelay(pdMS_TO_TICKS(20));
- (void)uart_flush_input(PN532_UART);
- return -1;
+static void release_rf(void)
+{
+ static const uint8_t rf_off[] = {0x32, 0x01, 0x00};
+ uint8_t response[128];
+ if (command(rf_off, sizeof(rf_off), response) != 2)
+ ESP_LOGW(TAG, "RF-off failed");
}
static bool initialize_reader(void)
{
- static const uint8_t firmware[] = {0x02};
- static const uint8_t sam[] = {0x14, 0x01, 0x14, 0x01};
- static const uint8_t retries[] = {0x32, 0x05, 0xff, 0x01, 0x00};
- uint8_t response[128];
+ static const uint8_t firmware[] = {0x02};
+ static const uint8_t sam[] = {0x14, 0x01, 0x14, 0x01};
+ static const uint8_t retries[] = {0x32, 0x05, 0xff, 0x01, 0x00};
+ uint8_t response[128];
- /* Discard stale input before starting this attempt. */
+ /* Discard stale input before starting this attempt. */
ESP_ERROR_CHECK(uart_flush_input(PN532_UART));
int n = command(firmware, sizeof(firmware), response);
@@ -280,54 +320,51 @@ static bool initialize_reader(void)
/* 1 = card, 0 = confirmed no target, -1 = transport/protocol failure. */
static int poll_card(nfc_card_t *card)
{
- static const uint8_t poll[] = {0x4a, 0x01, 0x00};
- static const uint8_t rf_off[] = {0x32, 0x01, 0x00};
- uint8_t response[128];
- int n = command(poll, sizeof(poll), response);
- int result = -1;
- if (n == 3 && response[2] == 0) {
- result = 0;
- } else if (n >= 8 && response[2] == 1 &&
- nfc_uid_valid(response[7]) && n >= 8 + response[7]) {
- card->uid_len = response[7];
- memcpy(card->uid, response + 8, card->uid_len);
- result = 1;
- } else if (n >= 0) {
- ESP_LOGW(TAG, "invalid target response");
- }
- /* InListPassiveTarget turns RF on again for the next scan.
- * Turning it off here also lets a held card be reactivated next time.
- */
- if (command(rf_off, sizeof(rf_off), response) != 2) {
- return -1;
- }
- return result;
+ static const uint8_t poll[] = {0x4a, 0x01, 0x00};
+ uint8_t response[128];
+ int n = command(poll, sizeof(poll), response);
+ int result = -1;
+ if (n == 3 && response[2] == 0) {
+ result = 0;
+ } else if (n >= 8 && response[2] == 1 &&
+ nfc_uid_valid(response[7]) && n >= 8 + response[7]) {
+ card->target = response[3];
+ card->sak = response[6];
+ card->uid_len = response[7];
+ memcpy(card->uid, response + 8, card->uid_len);
+ result = 1;
+ } else if (n >= 0) {
+ ESP_LOGW(TAG, "invalid target response");
+ }
+ /* RF stays active through the callback's complete APDU session. */
+ return result;
}
void start_polling(pn532_card_callback_t callback)
{
- pn532_uart_init();
- vTaskDelay(pdMS_TO_TICKS(1000));
- while (!initialize_reader()) {
- ESP_LOGE(TAG, "PN532 initialization failed; retrying in 2 seconds");
- vTaskDelay(pdMS_TO_TICKS(2000));
- }
-
- nfc_presence_t presence = {0};
- for (;;) {
- nfc_card_t card = {0};
- int result = poll_card(&card);
- bool was_present = presence.present;
- if (nfc_presence_update(&presence, result, &card)) {
- ESP_LOGI(TAG, "card detected, UID length=%u", card.uid_len);
- ESP_LOG_BUFFER_HEX_LEVEL(TAG, card.uid, card.uid_len, ESP_LOG_INFO);
- if (callback(&card)) {
- nfc_presence_accept(&presence, &card);
- }
- }
- if (was_present && !presence.present) {
- ESP_LOGI(TAG, "card removed");
- }
- vTaskDelay(pdMS_TO_TICKS(POLL_INTERVAL_MS));
- }
+ pn532_uart_init();
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ while (!initialize_reader()) {
+ ESP_LOGE(TAG, "PN532 initialization failed; retrying in 2 seconds");
+ vTaskDelay(pdMS_TO_TICKS(2000));
+ }
+
+ nfc_presence_t presence = {0};
+ for (;;) {
+ nfc_card_t card = {0};
+ int result = poll_card(&card);
+ bool was_present = presence.present;
+ if (nfc_presence_update(&presence, result, &card)) {
+ ESP_LOGI(TAG, "card detected, UID length=%u", card.uid_len);
+ ESP_LOG_BUFFER_HEX_LEVEL(TAG, card.uid, card.uid_len, ESP_LOG_INFO);
+ if (callback(&card)) {
+ nfc_presence_accept(&presence, &card);
+ }
+ }
+ if (was_present && !presence.present) {
+ ESP_LOGI(TAG, "card removed");
+ }
+ release_rf();
+ vTaskDelay(pdMS_TO_TICKS(POLL_INTERVAL_MS));
+ }
}
diff --git a/esp32/main/pn532_test.c b/esp32/main/pn532_test.c
index d561dde..0e14416 100644
--- a/esp32/main/pn532_test.c
+++ b/esp32/main/pn532_test.c
@@ -7,412 +7,412 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
-#define PN532_UART UART_NUM_1
+#define PN532_UART UART_NUM_1
#define PN532_TX_GPIO 3
#define PN532_RX_GPIO 4
-#define PN532_BAUD 115200
+#define PN532_BAUD 115200
static const char *TAG = "pn532";
static void dump_hex(const char *prefix, const uint8_t *buf, size_t len)
{
- printf("%s", prefix);
+ printf("%s", prefix);
- for (size_t i = 0; i < len; i++) {
- printf("%02X ", buf[i]);
- }
+ for (size_t i = 0; i < len; i++) {
+ printf("%02X ", buf[i]);
+ }
- printf("\n");
+ printf("\n");
}
static void pn532_uart_init(void)
{
- const uart_config_t config = {
- .baud_rate = PN532_BAUD,
- .data_bits = UART_DATA_8_BITS,
- .parity = UART_PARITY_DISABLE,
- .stop_bits = UART_STOP_BITS_1,
- .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
- .source_clk = UART_SCLK_DEFAULT,
- };
-
- ESP_ERROR_CHECK(uart_driver_install(
- PN532_UART,
- 1024,
- 0,
- 0,
- NULL,
- 0
- ));
-
- 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
- ));
-
- 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
- );
+ const uart_config_t config = {
+ .baud_rate = PN532_BAUD,
+ .data_bits = UART_DATA_8_BITS,
+ .parity = UART_PARITY_DISABLE,
+ .stop_bits = UART_STOP_BITS_1,
+ .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+ .source_clk = UART_SCLK_DEFAULT,
+ };
+
+ ESP_ERROR_CHECK(uart_driver_install(
+ PN532_UART,
+ 1024,
+ 0,
+ 0,
+ NULL,
+ 0
+ ));
+
+ 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
+ ));
+
+ 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
+ );
}
static int pn532_read(uint8_t *buf, size_t size, int timeout_ms)
{
- return uart_read_bytes(
- PN532_UART,
- buf,
- size,
- pdMS_TO_TICKS(timeout_ms)
- );
+ return uart_read_bytes(
+ PN532_UART,
+ buf,
+ size,
+ pdMS_TO_TICKS(timeout_ms)
+ );
}
static void pn532_write(const uint8_t *buf, size_t len)
{
- dump_hex("TX: ", buf, len);
-
- uart_write_bytes(
- PN532_UART,
- buf,
- len
- );
-
- uart_wait_tx_done(
- PN532_UART,
- pdMS_TO_TICKS(100)
- );
+ dump_hex("TX: ", buf, len);
+
+ uart_write_bytes(
+ PN532_UART,
+ buf,
+ len
+ );
+
+ uart_wait_tx_done(
+ PN532_UART,
+ pdMS_TO_TICKS(100)
+ );
}
static void pn532_wakeup(void)
{
- /*
- * PN532 HSU wake-up preamble.
- *
- * The PN532 manual specifies 55 55 followed by the
- * normal synchronization sequence for waking from
- * LowVbat mode.
- */
- static const uint8_t wakeup[] = {
- 0x55, 0x55,
- 0x00, 0x00, 0x00, 0x00, 0x00,
- 0xFF
- };
-
- ESP_LOGI(TAG, "sending wakeup");
-
- pn532_write(wakeup, sizeof(wakeup));
-
- vTaskDelay(pdMS_TO_TICKS(10));
-
- uart_flush_input(PN532_UART);
+ /*
+ * PN532 HSU wake-up preamble.
+ *
+ * The PN532 manual specifies 55 55 followed by the
+ * normal synchronization sequence for waking from
+ * LowVbat mode.
+ */
+ static const uint8_t wakeup[] = {
+ 0x55, 0x55,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xFF
+ };
+
+ ESP_LOGI(TAG, "sending wakeup");
+
+ pn532_write(wakeup, sizeof(wakeup));
+
+ vTaskDelay(pdMS_TO_TICKS(10));
+
+ uart_flush_input(PN532_UART);
}
static void pn532_get_firmware_version(void)
{
- static const uint8_t cmd[] = {
- /* HSU wakeup */
- 0x55, 0x55,
- 0x00, 0x00, 0x00, 0x00, 0x00,
+ static const uint8_t cmd[] = {
+ /* HSU wakeup */
+ 0x55, 0x55,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
- /* PN532 frame */
- 0xFF,
- 0x02, 0xFE,
- 0xD4, 0x02,
- 0x2A,
- 0x00
- };
+ /* PN532 frame */
+ 0xFF,
+ 0x02, 0xFE,
+ 0xD4, 0x02,
+ 0x2A,
+ 0x00
+ };
- uint8_t buf[64];
+ uint8_t buf[64];
- uart_flush_input(PN532_UART);
+ uart_flush_input(PN532_UART);
- ESP_LOGI(TAG, "GetFirmwareVersion");
- pn532_write(cmd, sizeof(cmd));
+ ESP_LOGI(TAG, "GetFirmwareVersion");
+ pn532_write(cmd, sizeof(cmd));
- int len = pn532_read(buf, sizeof(buf), 1000);
+ int len = pn532_read(buf, sizeof(buf), 1000);
- if (len <= 0) {
- ESP_LOGE(TAG, "no response from PN532");
- return;
- }
+ if (len <= 0) {
+ ESP_LOGE(TAG, "no response from PN532");
+ return;
+ }
- dump_hex("RX: ", buf, len);
+ dump_hex("RX: ", buf, len);
- len = pn532_read(buf, sizeof(buf), 500);
+ len = pn532_read(buf, sizeof(buf), 500);
- if (len > 0) {
- dump_hex("RX: ", buf, len);
- }
+ if (len > 0) {
+ dump_hex("RX: ", buf, len);
+ }
}
static int pn532_read_response(uint8_t *buf, size_t size, int timeout_ms)
{
- int len = pn532_read(buf, size, timeout_ms);
+ int len = pn532_read(buf, size, timeout_ms);
- if (len <= 0) {
- return len;
- }
+ if (len <= 0) {
+ return len;
+ }
- dump_hex("RX: ", buf, len);
- return len;
+ dump_hex("RX: ", buf, len);
+ return len;
}
static void pn532_poll_card(void)
{
- /*
- * InListPassiveTarget
- *
- * D4 4A 01 00
- * │ │ └── BrTy = 0x00: ISO14443A @ 106 kbps
- * │ └───── MaxTg = 1
- * └──────── command
- *
- * Complete PN532 frame:
- *
- * 00 00 FF 04 FC D4 4A 01 00 E1 00
- */
- static const uint8_t cmd[] = {
- 0x00, 0x00, 0xFF,
- 0x04, 0xFC,
- 0xD4, 0x4A, 0x01, 0x00,
- 0xE1,
- 0x00
- };
-
- uint8_t buf[128];
-
- uart_flush_input(PN532_UART);
-
- pn532_write(cmd, sizeof(cmd));
-
- int len = pn532_read_response(
- buf,
- sizeof(buf),
- 1000
- );
-
- if (len <= 0) {
- /*
- * No card is normal.
- */
- return;
- }
-
- /*
- * ACK:
- *
- * 00 00 FF 00 FF 00
- *
- * Often ACK + response arrive in one UART read.
- *
- * Find the response frame:
- *
- * 00 00 FF LEN LCS D5 4B ...
- */
- int frame = -1;
-
- for (int i = 0; i + 6 < len; i++) {
- if (
- buf[i + 0] == 0x00 &&
- buf[i + 1] == 0x00 &&
- buf[i + 2] == 0xFF &&
- buf[i + 5] == 0xD5 &&
- buf[i + 6] == 0x4B
- ) {
- frame = i;
- break;
- }
- }
-
- /*
- * Response may have arrived separately from ACK.
- */
- if (frame < 0) {
- len = pn532_read_response(
- buf,
- sizeof(buf),
- 1000
- );
-
- if (len <= 0) {
- return;
- }
-
- for (int i = 0; i + 6 < len; i++) {
- if (
- buf[i + 0] == 0x00 &&
- buf[i + 1] == 0x00 &&
- buf[i + 2] == 0xFF &&
- buf[i + 5] == 0xD5 &&
- buf[i + 6] == 0x4B
- ) {
- frame = i;
- break;
- }
- }
- }
-
- if (frame < 0) {
- ESP_LOGW(TAG, "no InListPassiveTarget response found");
- return;
- }
-
- /*
- * Payload after D5 4B:
- *
- * NbTg
- *
- * If NbTg > 0:
- *
- * Tg
- * SENS_RES[2]
- * SEL_RES
- * NFCIDLength
- * NFCID[]
- */
- int p = frame + 7;
-
- if (p >= len) {
- return;
- }
-
- uint8_t targets = buf[p++];
-
- if (targets == 0) {
- return;
- }
-
- if (p + 5 > len) {
- ESP_LOGW(TAG, "short target response");
- return;
- }
-
- uint8_t tg = buf[p++];
-
- uint8_t sens_res_0 = buf[p++];
- uint8_t sens_res_1 = buf[p++];
-
- uint8_t sel_res = buf[p++];
-
- uint8_t uid_len = buf[p++];
-
- if (uid_len == 0 || uid_len > 10 || p + uid_len > len) {
- ESP_LOGW(TAG, "invalid UID length: %u", uid_len);
- return;
- }
-
- ESP_LOGI(
- TAG,
- "card: Tg=%u ATQA=%02X%02X SAK=%02X UID len=%u",
- tg,
- sens_res_0,
- sens_res_1,
- sel_res,
- uid_len
- );
-
- printf("UID: ");
-
- for (int i = 0; i < uid_len; i++) {
- if (i) {
- printf(":");
- }
-
- printf("%02X", buf[p + i]);
- }
-
- printf("\n");
+ /*
+ * InListPassiveTarget
+ *
+ * D4 4A 01 00
+ * │ │ └── BrTy = 0x00: ISO14443A @ 106 kbps
+ * │ └───── MaxTg = 1
+ * └──────── command
+ *
+ * Complete PN532 frame:
+ *
+ * 00 00 FF 04 FC D4 4A 01 00 E1 00
+ */
+ static const uint8_t cmd[] = {
+ 0x00, 0x00, 0xFF,
+ 0x04, 0xFC,
+ 0xD4, 0x4A, 0x01, 0x00,
+ 0xE1,
+ 0x00
+ };
+
+ uint8_t buf[128];
+
+ uart_flush_input(PN532_UART);
+
+ pn532_write(cmd, sizeof(cmd));
+
+ int len = pn532_read_response(
+ buf,
+ sizeof(buf),
+ 1000
+ );
+
+ if (len <= 0) {
+ /*
+ * No card is normal.
+ */
+ return;
+ }
+
+ /*
+ * ACK:
+ *
+ * 00 00 FF 00 FF 00
+ *
+ * Often ACK + response arrive in one UART read.
+ *
+ * Find the response frame:
+ *
+ * 00 00 FF LEN LCS D5 4B ...
+ */
+ int frame = -1;
+
+ for (int i = 0; i + 6 < len; i++) {
+ if (
+ buf[i + 0] == 0x00 &&
+ buf[i + 1] == 0x00 &&
+ buf[i + 2] == 0xFF &&
+ buf[i + 5] == 0xD5 &&
+ buf[i + 6] == 0x4B
+ ) {
+ frame = i;
+ break;
+ }
+ }
+
+ /*
+ * Response may have arrived separately from ACK.
+ */
+ if (frame < 0) {
+ len = pn532_read_response(
+ buf,
+ sizeof(buf),
+ 1000
+ );
+
+ if (len <= 0) {
+ return;
+ }
+
+ for (int i = 0; i + 6 < len; i++) {
+ if (
+ buf[i + 0] == 0x00 &&
+ buf[i + 1] == 0x00 &&
+ buf[i + 2] == 0xFF &&
+ buf[i + 5] == 0xD5 &&
+ buf[i + 6] == 0x4B
+ ) {
+ frame = i;
+ break;
+ }
+ }
+ }
+
+ if (frame < 0) {
+ ESP_LOGW(TAG, "no InListPassiveTarget response found");
+ return;
+ }
+
+ /*
+ * Payload after D5 4B:
+ *
+ * NbTg
+ *
+ * If NbTg > 0:
+ *
+ * Tg
+ * SENS_RES[2]
+ * SEL_RES
+ * NFCIDLength
+ * NFCID[]
+ */
+ int p = frame + 7;
+
+ if (p >= len) {
+ return;
+ }
+
+ uint8_t targets = buf[p++];
+
+ if (targets == 0) {
+ return;
+ }
+
+ if (p + 5 > len) {
+ ESP_LOGW(TAG, "short target response");
+ return;
+ }
+
+ uint8_t tg = buf[p++];
+
+ uint8_t sens_res_0 = buf[p++];
+ uint8_t sens_res_1 = buf[p++];
+
+ uint8_t sel_res = buf[p++];
+
+ uint8_t uid_len = buf[p++];
+
+ if (uid_len == 0 || uid_len > 10 || p + uid_len > len) {
+ ESP_LOGW(TAG, "invalid UID length: %u", uid_len);
+ return;
+ }
+
+ ESP_LOGI(
+ TAG,
+ "card: Tg=%u ATQA=%02X%02X SAK=%02X UID len=%u",
+ tg,
+ sens_res_0,
+ sens_res_1,
+ sel_res,
+ uid_len
+ );
+
+ printf("UID: ");
+
+ for (int i = 0; i < uid_len; i++) {
+ if (i) {
+ printf(":");
+ }
+
+ printf("%02X", buf[p + i]);
+ }
+
+ printf("\n");
}
static bool pn532_sam_config(void)
{
- /*
- * HSU wake-up + SAMConfiguration
- *
- * D4 14 01 14 01
- * │ │ └── IRQ enabled
- * │ └───── timeout
- * └──────── normal mode
- *
- * DCS:
- * D4 + 14 + 01 + 14 + 01 = FE
- * checksum = 02
- */
- static const uint8_t cmd[] = {
- /* HSU wake-up */
- 0x55, 0x55,
- 0x00, 0x00, 0x00, 0x00, 0x00,
-
- /* PN532 frame */
- 0xFF,
- 0x05, 0xFB,
- 0xD4, 0x14, 0x01, 0x14, 0x01,
- 0x02,
- 0x00
- };
-
- uint8_t buf[64];
-
- uart_flush_input(PN532_UART);
-
- ESP_LOGI(TAG, "SAMConfiguration");
- pn532_write(cmd, sizeof(cmd));
-
- int len = pn532_read(buf, sizeof(buf), 1000);
-
- if (len <= 0) {
- ESP_LOGE(TAG, "SAMConfiguration: no response");
- return false;
- }
-
- dump_hex("RX: ", buf, len);
-
- /*
- * Expected somewhere in the response:
- *
- * ACK:
- * 00 00 FF 00 FF 00
- *
- * response:
- * 00 00 FF 02 FE D5 15 16 00
- */
-
- return true;
+ /*
+ * HSU wake-up + SAMConfiguration
+ *
+ * D4 14 01 14 01
+ * │ │ └── IRQ enabled
+ * │ └───── timeout
+ * └──────── normal mode
+ *
+ * DCS:
+ * D4 + 14 + 01 + 14 + 01 = FE
+ * checksum = 02
+ */
+ static const uint8_t cmd[] = {
+ /* HSU wake-up */
+ 0x55, 0x55,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+
+ /* PN532 frame */
+ 0xFF,
+ 0x05, 0xFB,
+ 0xD4, 0x14, 0x01, 0x14, 0x01,
+ 0x02,
+ 0x00
+ };
+
+ uint8_t buf[64];
+
+ uart_flush_input(PN532_UART);
+
+ ESP_LOGI(TAG, "SAMConfiguration");
+ pn532_write(cmd, sizeof(cmd));
+
+ int len = pn532_read(buf, sizeof(buf), 1000);
+
+ if (len <= 0) {
+ ESP_LOGE(TAG, "SAMConfiguration: no response");
+ return false;
+ }
+
+ dump_hex("RX: ", buf, len);
+
+ /*
+ * Expected somewhere in the response:
+ *
+ * ACK:
+ * 00 00 FF 00 FF 00
+ *
+ * response:
+ * 00 00 FF 02 FE D5 15 16 00
+ */
+
+ return true;
}
void app_main(void)
{
- ESP_LOGI(TAG, "PN532 NFC reader");
+ ESP_LOGI(TAG, "PN532 NFC reader");
- pn532_uart_init();
+ pn532_uart_init();
- vTaskDelay(pdMS_TO_TICKS(1000));
+ vTaskDelay(pdMS_TO_TICKS(1000));
- pn532_get_firmware_version();
+ pn532_get_firmware_version();
- vTaskDelay(pdMS_TO_TICKS(100));
+ vTaskDelay(pdMS_TO_TICKS(100));
- if (!pn532_sam_config()) {
- ESP_LOGE(TAG, "PN532 initialization failed");
- return;
- }
+ if (!pn532_sam_config()) {
+ ESP_LOGE(TAG, "PN532 initialization failed");
+ return;
+ }
- vTaskDelay(pdMS_TO_TICKS(100));
+ vTaskDelay(pdMS_TO_TICKS(100));
- while (1) {
- pn532_poll_card();
- vTaskDelay(pdMS_TO_TICKS(300));
- }
+ while (1) {
+ pn532_poll_card();
+ vTaskDelay(pdMS_TO_TICKS(300));
+ }
}
diff --git a/esp32/main/reader.h b/esp32/main/reader.h
index 262e20d..f82c253 100644
--- a/esp32/main/reader.h
+++ b/esp32/main/reader.h
@@ -9,55 +9,60 @@
#define NFC_UID_MAX 10
typedef struct {
- uint8_t uid[NFC_UID_MAX];
- uint8_t uid_len;
+ uint8_t uid[NFC_UID_MAX];
+ uint8_t uid_len;
+ uint8_t target;
+ uint8_t sak;
} nfc_card_t;
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. */
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,
+ uint8_t *out, size_t capacity, size_t *out_length);
static inline bool nfc_uid_valid(size_t length)
{
- return length == 4 || length == 7 || length == 10;
+ return length == 4 || length == 7 || length == 10;
}
static inline bool nfc_card_equal(const nfc_card_t *a, const nfc_card_t *b)
{
- return a->uid_len == b->uid_len &&
- memcmp(a->uid, b->uid, a->uid_len) == 0;
+ return a->uid_len == b->uid_len &&
+ memcmp(a->uid, b->uid, a->uid_len) == 0;
}
typedef struct {
- nfc_card_t last;
- bool present;
- unsigned misses;
+ nfc_card_t last;
+ bool present;
+ unsigned misses;
} nfc_presence_t;
/* Only successful no-target scans count toward removal. */
static inline bool nfc_presence_update(nfc_presence_t *state, int result,
- const nfc_card_t *card)
+ const nfc_card_t *card)
{
- if (result == 1) {
- state->misses = 0;
- return !state->present || !nfc_card_equal(&state->last, card);
- }
- if (result == 0) {
- if (state->present && ++state->misses >= 2) {
- state->present = false;
- }
- } else {
- state->misses = 0;
- }
- return false;
+ if (result == 1) {
+ state->misses = 0;
+ return !state->present || !nfc_card_equal(&state->last, card);
+ }
+ if (result == 0) {
+ if (state->present && ++state->misses >= 2) {
+ state->present = false;
+ }
+ } else {
+ state->misses = 0;
+ }
+ return false;
}
static inline void nfc_presence_accept(nfc_presence_t *state,
- const nfc_card_t *card)
+ const nfc_card_t *card)
{
- state->last = *card;
- state->present = true;
+ state->last = *card;
+ state->present = true;
}
/* Nonconnectable legacy ADV: one 128-bit service-data AD, at most 31 bytes.
@@ -65,24 +70,24 @@ static inline void nfc_presence_accept(nfc_presence_t *state,
* Flags/name/type are omitted to fit a full 10-byte UID.
*/
static inline size_t build_card_adv(uint8_t adv[31], const nfc_card_t *card,
- uint16_t event_seq)
+ uint16_t event_seq)
{
- static const uint8_t uuid[16] = {
- 0xab, 0x89, 0x67, 0x45, 0x23, 0x01, 0xbc, 0x9a,
- 0x11, 0x4a, 0x1e, 0x7b, 0x01, 0x00, 0x1e, 0x7b,
- };
- if (!nfc_uid_valid(card->uid_len)) {
- return 0;
- }
- adv[0] = (uint8_t)(20 + card->uid_len);
- adv[1] = 0x21;
- memcpy(adv + 2, uuid, sizeof(uuid));
+ static const uint8_t uuid[16] = {
+ 0xab, 0x89, 0x67, 0x45, 0x23, 0x01, 0xbc, 0x9a,
+ 0x11, 0x4a, 0x1e, 0x7b, 0x01, 0x00, 0x1e, 0x7b,
+ };
+ if (!nfc_uid_valid(card->uid_len)) {
+ return 0;
+ }
+ adv[0] = (uint8_t)(20 + card->uid_len);
+ adv[1] = 0x21;
+ memcpy(adv + 2, uuid, sizeof(uuid));
// Protocol Version
- adv[18] = 1;
- adv[19] = (uint8_t)event_seq;
- adv[20] = (uint8_t)(event_seq >> 8);
- memcpy(adv + 21, card->uid, card->uid_len);
- return 21 + card->uid_len;
+ adv[18] = 1;
+ adv[19] = (uint8_t)event_seq;
+ adv[20] = (uint8_t)(event_seq >> 8);
+ memcpy(adv + 21, card->uid, card->uid_len);
+ return 21 + card->uid_len;
}
#endif
diff --git a/esp32/main/relay.c b/esp32/main/relay.c
new file mode 100644
index 0000000..ae1993a
--- /dev/null
+++ b/esp32/main/relay.c
@@ -0,0 +1,211 @@
+#include "relay.h"
+#include "esp_log.h"
+#include "esp_random.h"
+#include "esp_timer.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/queue.h"
+#include "freertos/semphr.h"
+#include "freertos/task.h"
+
+#define SESSION_TIMEOUT_US (15000000LL)
+typedef struct {
+ uint32_t session;
+ uint16_t seq, length;
+ uint8_t data[RELAY_APDU_MAX];
+} request_t;
+static SemaphoreHandle_t mutex;
+static QueueHandle_t requests;
+static bool connected, active, busy, ending;
+static uint32_t session_counter, session;
+static nfc_card_t current;
+static request_t assembling;
+static size_t received;
+static uint16_t last_seq;
+static uint8_t response[RELAY_VALUE_MAX];
+static size_t response_len;
+
+static uint16_t be16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); }
+static uint32_t be32(const uint8_t *p)
+{
+ return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
+}
+static void put16(uint8_t *p, uint16_t n)
+{
+ p[0] = n >> 8;
+ p[1] = n;
+}
+static void put32(uint8_t *p, uint32_t n)
+{
+ p[0] = n >> 24;
+ p[1] = n >> 16;
+ p[2] = n >> 8;
+ p[3] = n;
+}
+static void header(uint8_t kind, uint32_t sid, uint16_t seq, uint16_t len)
+{
+ memset(response, 0, RELAY_HEADER);
+ response[0] = 1;
+ response[1] = kind;
+ put32(response + 2, sid);
+ put16(response + 6, seq);
+ put16(response + 10, len);
+ response_len = RELAY_HEADER + len;
+}
+void relay_init(void)
+{
+ mutex = xSemaphoreCreateMutex();
+ requests = xQueueCreate(1, sizeof(request_t));
+ configASSERT(mutex && requests);
+ session_counter = esp_random();
+}
+void relay_connected(bool value)
+{
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ connected = value;
+ active = false;
+ ending = true;
+ busy = false;
+ received = 0;
+ response_len = 0;
+ xQueueReset(requests);
+ xSemaphoreGive(mutex);
+}
+size_t relay_status(uint8_t out[18])
+{
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ memset(out, 0, 18);
+ out[0] = 1;
+ out[1] = active ? 1 : 0;
+ if (active) {
+ out[2] = current.uid_len;
+ out[3] = current.sak;
+ put32(out + 4, session);
+ memcpy(out + 8, current.uid, current.uid_len);
+ }
+ xSemaphoreGive(mutex);
+ return 18;
+}
+size_t relay_response(uint8_t out[RELAY_VALUE_MAX])
+{
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ size_t n = response_len;
+ memcpy(out, response, n);
+ xSemaphoreGive(mutex);
+ return n;
+}
+bool relay_write(const uint8_t *p, size_t n)
+{
+ if (n < RELAY_HEADER || p[0] != 1)
+ return false;
+ uint32_t sid = be32(p + 2);
+ uint16_t seq = be16(p + 6), off = be16(p + 8), total = be16(p + 10);
+ bool ok = false;
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ if (!connected || !active || ending || sid != session)
+ goto done;
+ if (p[1] == 2) {
+ if (n == RELAY_HEADER && off == 0 && total == 0) {
+ ending = true;
+ ok = true;
+ }
+ goto done;
+ }
+ if (p[1] != 1 || busy || total < 4 || total > RELAY_APDU_MAX || n == RELAY_HEADER ||
+ off > total || n - RELAY_HEADER > (size_t)(total - off))
+ goto done;
+ if (off == 0) {
+ if (seq == 0 || seq <= last_seq || received != 0)
+ goto done;
+ assembling.session = sid;
+ assembling.seq = seq;
+ assembling.length = total;
+ }
+ if (assembling.session != sid || assembling.seq != seq || assembling.length != total ||
+ off != received)
+ goto done;
+ memcpy(assembling.data + off, p + RELAY_HEADER, n - RELAY_HEADER);
+ received += n - RELAY_HEADER;
+ ok = true;
+ if (received == total) {
+ if (xQueueSend(requests, &assembling, 0) != pdTRUE) {
+ received = 0;
+ ok = false;
+ goto done;
+ }
+ busy = true;
+ last_seq = seq;
+ received = 0;
+ header(0x10, sid, seq, 0);
+ }
+done:
+ xSemaphoreGive(mutex);
+ return ok;
+}
+/* Runs synchronously inside the NFC callback: no scans or RF-off commands
+ * may run until this session has ended. No NimBLE calls from this task. */
+bool relay_card(const nfc_card_t *card)
+{
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ if (!connected) {
+ xSemaphoreGive(mutex);
+ return false;
+ }
+ current = *card;
+ session = ++session_counter;
+ if (session == 0)
+ session = ++session_counter;
+ uint32_t sid = session;
+ active = true;
+ ending = false;
+ busy = false;
+ received = 0;
+ last_seq = 0;
+ response_len = 0;
+ xQueueReset(requests);
+ xSemaphoreGive(mutex);
+ ESP_LOGI("homekey-reader", "NFC session %lu opened; sak=0x%02x", (unsigned long)sid, card->sak);
+ int64_t deadline = esp_timer_get_time() + SESSION_TIMEOUT_US;
+ for (;;) {
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ bool stop = !active || !connected || ending || session != sid;
+ xSemaphoreGive(mutex);
+ if (stop || esp_timer_get_time() >= deadline)
+ break;
+ request_t req;
+ if (xQueueReceive(requests, &req, pdMS_TO_TICKS(20)) != pdTRUE)
+ continue;
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ bool valid = active && connected && !ending && session == sid && req.session == sid;
+ xSemaphoreGive(mutex);
+ if (!valid)
+ break;
+ uint8_t data[RELAY_APDU_MAX];
+ size_t length = 0;
+ int error = pn532_exchange(card->target, req.data, req.length, data, sizeof(data), &length);
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ if (active && connected && session == sid && !ending) {
+ if (error == 0) {
+ header(0x11, sid, req.seq, length);
+ memcpy(response + RELAY_HEADER, data, length);
+ } else {
+ header(0x12, sid, req.seq, 1);
+ response[RELAY_HEADER] = (uint8_t)error;
+ }
+ busy = false;
+ }
+ xSemaphoreGive(mutex);
+ ESP_LOGI("homekey-reader", "APDU session=%lu seq=%u length=%u result=%d",
+ (unsigned long)sid, req.seq, (unsigned)length, error);
+ if (error != 0)
+ break;
+ }
+ xSemaphoreTake(mutex, portMAX_DELAY);
+ if (session == sid) {
+ active = false;
+ busy = false;
+ received = 0;
+ }
+ xSemaphoreGive(mutex);
+ ESP_LOGI("homekey-reader", "NFC session %lu closed", (unsigned long)sid);
+ return true;
+}
diff --git a/esp32/main/relay.h b/esp32/main/relay.h
new file mode 100644
index 0000000..cc640a5
--- /dev/null
+++ b/esp32/main/relay.h
@@ -0,0 +1,15 @@
+#ifndef HOMEKEY_RELAY_H
+#define HOMEKEY_RELAY_H
+#include "reader.h"
+
+#define RELAY_APDU_MAX 240
+#define RELAY_HEADER 12
+#define RELAY_VALUE_MAX (RELAY_HEADER + RELAY_APDU_MAX)
+/* GATT callbacks are nonblocking. PN532/UART work stays in its owner task. */
+void relay_init(void);
+void relay_connected(bool connected);
+size_t relay_status(uint8_t out[18]);
+size_t relay_response(uint8_t out[RELAY_VALUE_MAX]);
+bool relay_write(const uint8_t *data, size_t length);
+bool relay_card(const nfc_card_t *card);
+#endif
diff --git a/tests/include/driver/uart.h b/tests/include/driver/uart.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/driver/uart.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/esp_err.h b/tests/include/esp_err.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/esp_err.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/esp_log.h b/tests/include/esp_log.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/esp_log.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/esp_random.h b/tests/include/esp_random.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/esp_random.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/esp_timer.h b/tests/include/esp_timer.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/esp_timer.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/freertos/FreeRTOS.h b/tests/include/freertos/FreeRTOS.h
new file mode 100644
index 0000000..ca173d9
--- /dev/null
+++ b/tests/include/freertos/FreeRTOS.h
@@ -0,0 +1,3 @@
+#include "mock.h"
+#define portMAX_DELAY ((TickType_t)-1)
+#define configASSERT assert
diff --git a/tests/include/freertos/queue.h b/tests/include/freertos/queue.h
new file mode 100644
index 0000000..ecf3234
--- /dev/null
+++ b/tests/include/freertos/queue.h
@@ -0,0 +1,2 @@
+#include "mock.h"
+int xQueueReset(QueueHandle_t);
diff --git a/tests/include/freertos/semphr.h b/tests/include/freertos/semphr.h
new file mode 100644
index 0000000..378174b
--- /dev/null
+++ b/tests/include/freertos/semphr.h
@@ -0,0 +1,5 @@
+#include "mock.h"
+typedef void *SemaphoreHandle_t;
+SemaphoreHandle_t xSemaphoreCreateMutex(void);
+int xSemaphoreTake(SemaphoreHandle_t,TickType_t);
+int xSemaphoreGive(SemaphoreHandle_t);
diff --git a/tests/include/freertos/task.h b/tests/include/freertos/task.h
new file mode 100644
index 0000000..9820ce8
--- /dev/null
+++ b/tests/include/freertos/task.h
@@ -0,0 +1 @@
+#include "mock.h"
diff --git a/tests/include/mock.h b/tests/include/mock.h
new file mode 100644
index 0000000..7d58e95
--- /dev/null
+++ b/tests/include/mock.h
@@ -0,0 +1,47 @@
+#ifndef MOCK_H
+#define MOCK_H
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdarg.h>
+typedef int esp_err_t;
+typedef unsigned TickType_t;
+#define ESP_OK 0
+#define ESP_ERR_NVS_NO_FREE_PAGES 1
+#define ESP_ERR_NVS_NEW_VERSION_FOUND 2
+#define ESP_LOG_INFO 1
+#define ESP_LOG_DEBUG 2
+#define ESP_ERROR_CHECK(e) assert((e)==ESP_OK)
+static inline void mock_log(const char *tag, const char *fmt, ...) {(void)tag;(void)fmt;}
+#define ESP_LOGI mock_log
+#define ESP_LOGW mock_log
+#define ESP_LOGE mock_log
+#define ESP_LOG_BUFFER_HEX_LEVEL(t,d,n,l) do {(void)(t);(void)(d);(void)(n);(void)(l);} while(0)
+#define pdMS_TO_TICKS(x) ((TickType_t)(x))
+#define pdTRUE 1
+#define UART_NUM_1 1
+#define UART_DATA_8_BITS 8
+#define UART_PARITY_DISABLE 0
+#define UART_STOP_BITS_1 1
+#define UART_HW_FLOWCTRL_DISABLE 0
+#define UART_SCLK_DEFAULT 0
+#define UART_PIN_NO_CHANGE -1
+typedef struct {int baud_rate,data_bits,parity,stop_bits,flow_ctrl,source_clk;} uart_config_t;
+int uart_driver_install(int,int,int,int,void*,int);
+int uart_param_config(int,const uart_config_t*);
+int uart_set_pin(int,int,int,int,int);
+int uart_flush_input(int);
+int uart_write_bytes(int,const void*,size_t);
+int uart_wait_tx_done(int,TickType_t);
+int uart_read_bytes(int,void*,size_t,TickType_t);
+int64_t esp_timer_get_time(void);
+void vTaskDelay(TickType_t);
+uint32_t esp_random(void);
+int nvs_flash_init(void);
+int nvs_flash_erase(void);
+typedef void *QueueHandle_t;
+QueueHandle_t xQueueCreate(unsigned,unsigned);
+int xQueueSend(QueueHandle_t,const void*,TickType_t);
+int xQueueReceive(QueueHandle_t,void*,TickType_t);
+#endif
diff --git a/tests/pn532_test.c b/tests/pn532_test.c
new file mode 100644
index 0000000..2e88dc7
--- /dev/null
+++ b/tests/pn532_test.c
@@ -0,0 +1,175 @@
+#include <stdio.h>
+#include "../esp32/main/pn532.c"
+static uint8_t stream[1024];
+static size_t total, pos, chunk = 1;
+static int64_t now;
+static unsigned writes;
+static uint8_t last_tx[300];
+static size_t last_tx_len;
+int64_t esp_timer_get_time(void) { return now; }
+void vTaskDelay(TickType_t t) { now += t * 1000; }
+int uart_read_bytes(int u, void *dst, size_t len, TickType_t t)
+{
+ (void)u;
+ now += 100;
+ if (pos == total) {
+ now += t * 1000;
+ return 0;
+ }
+ size_t n = total - pos;
+ if (n > len)
+ n = len;
+ if (n > chunk)
+ n = chunk;
+ memcpy(dst, stream + pos, n);
+ pos += n;
+ return (int)n;
+}
+int uart_write_bytes(int u, const void *d, size_t n)
+{
+ (void)u;
+ assert(n <= sizeof(last_tx));
+ memcpy(last_tx, d, n);
+ last_tx_len = n;
+ ++writes;
+ return (int)n;
+}
+int uart_wait_tx_done(int u, TickType_t t)
+{
+ (void)u;
+ (void)t;
+ return 0;
+}
+int uart_flush_input(int u)
+{
+ (void)u;
+ pos = total;
+ return 0;
+}
+int uart_driver_install(int a, int b, int c, int d, void *e, int f)
+{
+ (void)a;
+ (void)b;
+ (void)c;
+ (void)d;
+ (void)e;
+ (void)f;
+ return 0;
+}
+int uart_param_config(int u, const uart_config_t *c)
+{
+ (void)u;
+ (void)c;
+ return 0;
+}
+int uart_set_pin(int a, int b, int c, int d, int e)
+{
+ (void)a;
+ (void)b;
+ (void)c;
+ (void)d;
+ (void)e;
+ return 0;
+}
+static void reset(void)
+{
+ total = pos = 0;
+ now = 0;
+ writes = 0;
+}
+static void append(const uint8_t *d, size_t n)
+{
+ memcpy(stream + total, d, n);
+ total += n;
+}
+static void response(const uint8_t *d, uint8_t n)
+{
+ uint8_t h[] = {0, 0, 255, n, (uint8_t)(0 - n)};
+ append(ack, sizeof(ack));
+ append(h, 5);
+ append(d, n);
+ uint8_t sum = 0;
+ for (unsigned i = 0; i < n; i++)
+ sum += d[i];
+ uint8_t tail[] = {(uint8_t)(0 - sum), 0};
+ append(tail, 2);
+}
+int main(void)
+{
+ uint8_t out[128];
+ const uint8_t fw[] = {0xd5, 3, 0x32, 1, 6, 7}, cmd[] = {2};
+ for (chunk = 1; chunk <= 64; chunk *= 2) {
+ reset();
+ response(fw, 6);
+ assert(command(cmd, 1, out) == 6);
+ assert(memcmp(out, fw, 6) == 0);
+ assert(writes == 1);
+ assert(last_tx_len == 14 && memcmp(last_tx, "\x55\x55\0\0\0", 5) == 0);
+ }
+ reset();
+ response(fw, 6);
+ stream[total - 2] ^= 1;
+ assert(command(cmd, 1, out) == -1);
+ assert(writes == 2);
+ reset();
+ response(fw, 6);
+ stream[sizeof(ack) + 4] ^= 1;
+ assert(command(cmd, 1, out) == -1);
+ reset();
+ response(fw, 6);
+ --total;
+ assert(command(cmd, 1, out) == -1);
+ reset();
+ append(ack, sizeof(ack));
+ assert(command(cmd, 1, out) == -1);
+ const uint8_t rf[] = {0xd5, 0x33};
+ const unsigned lens[] = {4, 7, 10};
+ for (unsigned k = 0; k < 3; k++) {
+ uint8_t target[18] = {0xd5, 0x4b, 1, 1, 0, 4, 0};
+ target[7] = lens[k];
+ for (unsigned j = 0; j < lens[k]; j++)
+ target[8 + j] = j + 1;
+ reset();
+ response(target, 8 + lens[k]);
+ response(rf, 2);
+ nfc_card_t card = {0};
+ assert(poll_card(&card) == 1);
+ assert(card.uid_len == lens[k]);
+ assert(memcmp(card.uid, target + 8, lens[k]) == 0);
+ }
+ uint8_t absent[] = {0xd5, 0x4b, 0};
+ reset();
+ response(absent, 3);
+ response(rf, 2);
+ nfc_card_t card = {0};
+ assert(poll_card(&card) == 0);
+ uint8_t short_uid[] = {0xd5, 0x4b, 1, 1, 0, 4, 0, 10, 1};
+ reset();
+ response(short_uid, sizeof(short_uid));
+ response(rf, 2);
+ assert(poll_card(&card) == -1);
+ reset();
+ response(absent, 3);
+ stream[total - 2] ^= 1;
+ assert(poll_card(&card) == -1);
+ uint8_t apdu[114] = {0x80, 0x80, 0, 1}, reply[] = {0xd5, 0x41, 0, 0x90, 0};
+ size_t out_length = 0;
+ reset();
+ response(reply, sizeof(reply));
+ assert(pn532_exchange(1, apdu, sizeof(apdu), out, sizeof(out), &out_length) == 0);
+ assert(out_length == 2 && out[0] == 0x90);
+ assert(last_tx_len == sizeof(apdu) + 10);
+ assert(last_tx[5] == 0xd4 && last_tx[6] == 0x40 && last_tx[7] == 1);
+ assert(memcmp(last_tx + 8, apdu, sizeof(apdu)) == 0);
+ reset();
+ reply[2] = 1;
+ response(reply, sizeof(reply));
+ assert(pn532_exchange(1, apdu, sizeof(apdu), out, sizeof(out), &out_length) == 3);
+ reset();
+ reply[2] = 0x40;
+ response(reply, sizeof(reply));
+ assert(pn532_exchange(1, apdu, sizeof(apdu), out, sizeof(out), &out_length) == 3);
+ assert(pn532_exchange(1, apdu, RELAY_APDU_MAX + 1, out, sizeof(out), &out_length) == 1);
+ puts("PN532: startup prefix preserved; APDU >64 bytes; status/chaining rejection; fragmented "
+ "frames, checksums, timeout and UID parsing passed");
+}
diff --git a/tests/relay_test.c b/tests/relay_test.c
new file mode 100644
index 0000000..0fa85a8
--- /dev/null
+++ b/tests/relay_test.c
@@ -0,0 +1,123 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "../esp32/main/relay.c"
+static request_t queued;
+static bool have_queued;
+static int phase, scenario, exchanges;
+static int64_t clock_us;
+SemaphoreHandle_t xSemaphoreCreateMutex(void) { return (void *)1; }
+int xSemaphoreTake(SemaphoreHandle_t m, TickType_t t)
+{
+ (void)m;
+ (void)t;
+ return pdTRUE;
+}
+int xSemaphoreGive(SemaphoreHandle_t m)
+{
+ (void)m;
+ return pdTRUE;
+}
+QueueHandle_t xQueueCreate(unsigned n, unsigned size)
+{
+ assert(n == 1 && size == sizeof(request_t));
+ return (void *)1;
+}
+int xQueueReset(QueueHandle_t q)
+{
+ (void)q;
+ have_queued = false;
+ return pdTRUE;
+}
+int xQueueSend(QueueHandle_t q, const void *p, TickType_t t)
+{
+ (void)q;
+ (void)t;
+ if (have_queued)
+ return 0;
+ queued = *(const request_t *)p;
+ have_queued = true;
+ return pdTRUE;
+}
+uint32_t esp_random(void) { return 100; }
+int64_t esp_timer_get_time(void) { return clock_us; }
+static size_t make_request(uint8_t out[32], uint8_t kind, uint32_t sid, uint16_t seq, uint16_t off,
+ uint16_t len, const uint8_t *d, size_t n)
+{
+ memset(out, 0, RELAY_HEADER);
+ out[0] = 1;
+ out[1] = kind;
+ put32(out + 2, sid);
+ put16(out + 6, seq);
+ put16(out + 8, off);
+ put16(out + 10, len);
+ if (n)
+ memcpy(out + 12, d, n);
+ return 12 + n;
+}
+int xQueueReceive(QueueHandle_t q, void *p, TickType_t t)
+{
+ (void)q;
+ clock_us += t * 1000;
+ if (phase == 0) {
+ uint8_t s[18], b[32], d[] = {0, 0xa4, 4, 0};
+ relay_status(s);
+ assert(s[1] == 1 && be32(s + 4) == session && s[3] == 0x20);
+ assert(!relay_write(b, make_request(b, 1, session - 1, 1, 0, 4, d, 4)));
+ assert(!relay_write(b, make_request(b, 1, session, 1, 2, 4, d + 2, 2)));
+ assert(relay_write(b, make_request(b, 1, session, 1, 0, 4, d, 2)));
+ assert(!relay_write(b, make_request(b, 1, session, 1, 0, 4, d, 2))); // Duplicate offset.
+ assert(!relay_write(b, make_request(b, 1, session, 2, 2, 4, d + 2, 2))); // Wrong sequence.
+ assert(relay_write(b, make_request(b, 1, session, 1, 2, 4, d + 2, 2)));
+ assert(!relay_write(b, make_request(b, 1, session, 2, 0, 4, d, 4))); // Busy.
+ uint8_t r[RELAY_VALUE_MAX];
+ assert(relay_response(r) == 12 && r[1] == 0x10);
+ phase = 1;
+ } else if (phase == 1) {
+ uint8_t r[RELAY_VALUE_MAX], b[32], d[] = {0x80, 0x3c, 0, 0};
+ size_t n = relay_response(r);
+ assert(n == 14 && r[1] == 0x11 && be16(r + 6) == 1 && r[12] == 0x90 && r[13] == 0);
+ assert(!relay_write(b, make_request(b, 1, session, 1, 0, 4, d, 4))); // Replay.
+ if (scenario == 1) {
+ relay_connected(false);
+ } else if (scenario == 2) {
+ clock_us += SESSION_TIMEOUT_US;
+ } else {
+ assert(relay_write(b, make_request(b, 2, session, 0, 0, 0, NULL, 0)));
+ }
+ phase = 2;
+ }
+ if (!have_queued)
+ return 0;
+ *(request_t *)p = queued;
+ have_queued = false;
+ return pdTRUE;
+}
+int pn532_exchange(uint8_t target, const uint8_t *apdu, size_t len, uint8_t *out, size_t capacity,
+ size_t *n)
+{
+ assert(target == 1 && len == 4 && apdu[1] == 0xa4 && capacity >= 2);
+ exchanges++;
+ out[0] = 0x90;
+ out[1] = 0;
+ *n = 2;
+ return 0;
+}
+int main(void)
+{
+ relay_init();
+ nfc_card_t card = {.uid = {1, 2, 3, 4}, .uid_len = 4, .target = 1, .sak = 0x20};
+ assert(!relay_card(&card));
+ for (scenario = 0; scenario < 3; scenario++) {
+ relay_connected(true);
+ phase = exchanges = 0;
+ clock_us = 0;
+ assert(relay_card(&card));
+ assert(exchanges == 1 && !active);
+ uint8_t s[18];
+ relay_status(s);
+ assert(s[1] == 0);
+ }
+ puts("Relay: fragment order, busy rejection, stale session, replay, response, release, "
+ "disconnect and timeout passed");
+}
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100755
index 0000000..8cc7a98
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+cd "$(dirname "$0")"
+build_dir=$(mktemp -d)
+trap 'rm -rf "$build_dir"' EXIT
+for name in pn532 relay; do
+ cc -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -g -Iinclude "${name}_test.c" -o "$build_dir/$name"
+ "$build_dir/$name"
+done