From 44b5b49df10a8526750b9cc003f17bfd58afc107 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 22 Jul 2026 19:33:04 +0200 Subject: [PATCH] feat(firmware): add nfc and update all documentation --- .gitignore | 1 + CHANGELOG.md | 21 ++ README.md | 16 +- ROADMAP.md | 65 ++++- TODO.md | 24 +- docs/architecture.md | 92 ++++-- docs/firmware/bluetooth.md | 22 +- docs/firmware/buzzer.md | 9 +- docs/firmware/debug.md | 16 +- docs/firmware/gpio_expander.md | 7 +- docs/firmware/keypad.md | 6 +- docs/firmware/lock.md | 10 +- docs/firmware/nfc.md | 51 ++-- docs/hardware/Commande temporaire SIG.svg | 46 +-- firmware/README.md | 28 +- firmware/app/include/board_config.h | 5 + firmware/app/openparcelbox.uf2 | Bin 446464 -> 458240 bytes firmware/app/src/lock_control.c | 5 +- firmware/app/src/lock_control.h | 2 +- firmware/app/src/main.c | 126 +++++++- firmware/app/src/nfc.c | 272 +++++++++++++++++- firmware/app/src/opb_ble.c | 70 ++++- firmware/app/src/opb_ble.h | 5 +- hardware/BOM.md | 17 +- hardware/README.md | 11 +- hardware/interfaces.md | 19 +- hardware/pinout.md | 5 +- .../reverse-engineering.md | 4 +- mobile-app/app/README.md | 8 +- mobile-app/app/lib/app_strings.dart | 26 ++ mobile-app/app/lib/ble_controller.dart | 28 +- mobile-app/app/lib/main.dart | 232 ++++++++++++--- mobile-app/app/test/widget_test.dart | 159 ++++++++++ 33 files changed, 1186 insertions(+), 222 deletions(-) diff --git a/.gitignore b/.gitignore index 81aef0b..50d8141 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # VSCodium / VSCode # ---------------------------- .vscode/ +OpenParcelBox.code-workspace # ---------------------------- # Codex diff --git a/CHANGELOG.md b/CHANGELOG.md index 90998ca..081c6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,21 @@ The format is based on Keep a Changelog and this project follows Semantic Versio ### Fixed +- Aligned project documentation with the implemented PN532, BLE, mobile, + opening-history, user-role, one-time-code, 150 ms lock-pulse, and diagnostic + behavior, while keeping unfinished timeout, power, Zigbee, and Home Assistant + work explicitly marked as pending. +- Restored explicit 2-second open-door reminder diagnostics and increased the + D9 lock-control pulse from 100 ms to 150 ms so mechanical relay stages have + more time to present `SIG` without excessive coil-on time. +- Added a clipboard action to permanent and temporary access-code rows in the + mobile application, positioned before the edit action. +- Matched NFC access feedback to keypad-code feedback: authorized badges use + the long success beep, while unauthorized badges and duplicate enrollment + attempts use the same three-beep invalid sequence. +- Reworked box-reader NFC enrollment into an administrator-only 15-second BLE + flow with an active waiting modal, in-modal errors, post-detection naming, + cancellation, duplicate detection, and authoritative tag-list refresh. - Prevented generated permanent and temporary codes from colliding with any active code, including across code kinds, and made firmware reject exclusive creation when the value already exists. @@ -39,6 +54,12 @@ The format is based on Keep a Changelog and this project follows Semantic Versio ### Added +- Added PN532 passive-tag detection over the shared I2C bus at 7-bit address + `0x24`, including firmware-version probing, ISO/IEC 14443-A UID reads, mobile + enrollment, stored-tag unlock, history, and UART diagnostics. +- Added a fixed 15-second NFC scan window started only by the first keypad press; + additional presses cannot extend it, and the PN532 enters I2C-wakeable + power-down mode outside scan windows. - Internal identity-reset button on XIAO `P1.11`: holding it for three seconds clears administrator and guest app identities plus BLE bonds without erasing access codes, NFC tags, history, or the box name. diff --git a/README.md b/README.md index 55e4476..67e8b38 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,14 @@ Completed or validated: - BLE LE Secure Connections, encrypted GATT access, and 128-bit phone identities - First-phone administrator provisioning and named guest identities - App-triggered NFC enrollment with stored tag names -- Lock command pulse on XIAO `D9` +- PN532 passive-badge reader over I2C at `0x24`, with a fixed non-renewable + 15-second keypad-triggered scan window and power-down between windows +- 150 ms lock command pulse on XIAO `D9`, inactive throughout startup - KR-S79 `COM/NC` lock state feedback on XIAO `D7` / `D8` - Internal identity-reset button on XIAO `P1.11` - Green-open, off-closed, red-invalid LED feedback -- Short key beeps, long valid-code beep, invalid-code beep sequence, and 2s open reminder beep +- Short key beeps, long valid-code beep, invalid-code beep sequence, and 2-second + open reminder beep - Door opened / closed serial debug messages - Firmware debug output over serial console - One-second white RGB confirmation when the local keypad path is ready @@ -58,14 +61,13 @@ In progress: - Original hardware reverse engineering - Production keypad mapping -- NFC credential reader hardware selection +- PN532 reader hardware validation and production integration - Hardware validation -- BLE protocol documentation and hardening +- BLE security and hardware interoperability validation Planned: - Firmware update from the mobile application -- NFC badge unlock with a dedicated reader circuit - Battery management - Zigbee - OTA updates @@ -144,7 +146,9 @@ Current development platform: - KR-S79 `COM` feedback reference on XIAO `D8` - Internal identity-reset button between `GND` and XIAO `P1.11` -The XIAO nRF52840 exposes the Nordic NFCT pins (`NFC1/P0.09` and `NFC2/P0.10`). This peripheral is tag-side NFC-A hardware, not a standalone active reader for passive badges. Badge UID reading requires a dedicated NFC reader circuit on the production hardware. +The XIAO nRF52840 exposes tag-side Nordic NFCT pins, which are not used as a +passive-badge reader. Badge UID reading uses a PN532 module on the shared I2C +bus at address `0x24` during development. The production hardware is planned around a custom PCB and the original parcel box mechanical parts. diff --git a/ROADMAP.md b/ROADMAP.md index a4b63a8..75e4340 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,7 +44,7 @@ The firmware is based on Zephyr RTOS and follows a modular architecture to simpl | Persistent access code storage | Done | | Battery driver | Pending | | NFC credential storage and scan API | Done | -| Dedicated NFC reader driver | Pending | +| PN532 NFC reader driver | Done | | Power management | Pending | | Bluetooth configuration | In progress | | Zigbee integration | Pending | @@ -117,10 +117,10 @@ Objective: validate every reusable or newly added hardware component before inte Components: -- Lock command pulse. +- 150 ms active-high lock command pulse, inactive throughout startup. - Door or lock state feedback through the KR-S79 `COM/NC` contact. - Battery monitoring. -- Dedicated NFC reader circuit and antenna. +- PN532 development reader and production antenna/placement validation. - Buzzer. - External LEDs. @@ -151,7 +151,7 @@ Components: - Battery. - Buzzer. - NFC credential storage. -- Dedicated NFC reader backend. +- PN532 I2C reader backend. Security: @@ -231,9 +231,12 @@ Implemented MVP pieces: - Keypad-based unlock flow. - Persistent six-digit code table. - Default development unlock code. -- Lock command pulse. +- 150 ms active-high lock command pulse with no startup pulse. - Lock state feedback. - Audible and visual feedback for valid codes, invalid codes, and open state. +- PN532 badge unlock with persistent named tags and low-power scan windows. +- Local BLE administration with administrator and guest identities. +- One-time access codes and persistent opening history. At this stage, the original proprietary electronics are no longer required. @@ -264,6 +267,7 @@ Implemented so far: - Permanent code management screen. - One-time temporary code management screen. - NFC tag add/remove screen. +- Box-reader NFC enrollment with timeout, duplicate handling, and tag naming. - Stored code and NFC tag readback from the firmware BLE state characteristic. - Branded Android launcher icon and native splash screen. - Automatic French/English selection with a manual override. @@ -273,6 +277,10 @@ Implemented so far: - AES-256-GCM password-protected application backup and restore. - Opening history, code limits, phone-side NFC scans, and destructive-action warnings. +- Permanent-code editing and clipboard actions for permanent and temporary + codes. +- Secure offline state cache keyed by application identity and BLE rediscovery + after backup restoration. Status: in progress. @@ -290,7 +298,18 @@ Features: - Permanent NFC badges through dedicated reader hardware. - User permissions. -Status: planned. +Implemented so far: + +- One administrator identity and named guest identities stored in firmware. +- Administrator-generated guest invitation QR codes and guest revocation. +- Firmware-enforced administrator/guest permissions. + +Remaining work: + +- Associate individual keypad codes and NFC tags with user records. +- Expand permission granularity beyond the current administrator/guest roles. + +Status: in progress. --- @@ -308,7 +327,18 @@ Events: - Door opened. - Door closed. -Status: planned. +Implemented so far: + +- Persistent history of the newest 64 successful openings. +- Seven-day retention with permanent-code, temporary-code, NFC-tag, or mobile + identity source information. +- Mobile readback and offline caching. + +Remaining work: + +- Invalid credential, low-battery, and explicit door transition events. + +Status: in progress. --- @@ -366,7 +396,18 @@ Features: - One-time PINs. - Temporary NFC badges through dedicated reader hardware. -Status: planned. +Implemented so far: + +- One-time six-digit PINs removed after their first successful keypad use. +- Guest creation and removal of temporary PINs. + +Remaining work: + +- Date/time expiration. +- Configurable usage counters. +- Temporary NFC badges. + +Status: in progress. --- @@ -413,7 +454,13 @@ Scope: - Hardware documentation. - Firmware documentation. -Status: planned. +Implemented so far: + +- Firmware module and BLE protocol documentation. +- Development hardware pinout, BOM, interfaces, and lock reverse engineering. +- Mobile application behavior and build documentation. + +Status: in progress. --- diff --git a/TODO.md b/TODO.md index a28f04f..3cf4e38 100644 --- a/TODO.md +++ b/TODO.md @@ -72,7 +72,7 @@ Tasks are grouped by development phase and updated throughout the project. - [x] Lock driver. - [x] Door or lock state driver. - [ ] Battery driver. -- [ ] NFC driver. +- [x] PN532 NFC driver over I2C. ### Documentation @@ -83,7 +83,7 @@ Tasks are grouped by development phase and updated throughout the project. - [ ] led.md. - [x] lock.md. - [ ] battery.md. -- [ ] nfc.md. +- [x] nfc.md. --- @@ -111,10 +111,10 @@ Tasks are grouped by development phase and updated throughout the project. ### NFC -- [ ] Select dedicated NFC reader IC. +- [x] Select PN532 development NFC reader. - [ ] Design reader antenna. - [ ] Validate reader tuning network. -- [ ] Read UID through the dedicated reader. +- [x] Read UID through the PN532 reader. - [ ] Detect badge removal. ### Buzzer @@ -130,8 +130,8 @@ Tasks are grouped by development phase and updated throughout the project. ### Initialization -- [ ] Complete startup sequence. -- [ ] Initialize all peripherals. +- [x] Keep the local keypad path available before Settings and BLE startup. +- [x] Initialize all currently implemented peripherals. - [ ] Standardize error handling. ### Access Control @@ -170,12 +170,12 @@ Tasks are grouped by development phase and updated throughout the project. - [x] Door or lock feedback. - [ ] Battery monitoring. - [ ] Administrator PIN. -- [ ] Administrator NFC badge through dedicated reader hardware. +- [x] Administrator NFC badge through the PN532 reader. - [x] Audible feedback. -- [ ] Standalone operation. +- [x] Standalone local keypad and NFC operation. - [ ] Battery-powered operation. -- [ ] No cloud. -- [ ] No subscription. +- [x] No mandatory cloud dependency. +- [x] No subscription. --- @@ -186,6 +186,7 @@ Tasks are grouped by development phase and updated throughout the project. - [x] Mobile Bluetooth scan and connect UI. - [x] Mobile clock-sync command. - [x] Mobile random permanent and one-time code management UI. +- [x] Mobile permanent-code editing and clipboard actions for both code lists. - [x] Mobile NFC tag add/remove UI. - [x] Mobile direct lock-open command UI. - [x] Mobile stored-state readback from firmware. @@ -203,7 +204,8 @@ Tasks are grouped by development phase and updated throughout the project. - [ ] Validate pairing recovery and guest invitation on Android and iOS hardware. - [ ] Zigbee connectivity. - [ ] Home Assistant integration. -- [ ] Temporary access. +- [x] One-time temporary PINs. +- [ ] Time-limited and usage-count temporary credentials. - [ ] Dedicated PCB. - [ ] Field validation. - [ ] Complete user, maker, and developer documentation. diff --git a/docs/architecture.md b/docs/architecture.md index 50304b7..3f9d0fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,21 +20,18 @@ Main goals: ## Overview ```text -Home Assistant +Flutter mobile application | -Zigbee (ZHA / Zigbee2MQTT) +Secure BLE administration | -OpenParcelBox - | - +-- Zephyr RTOS - +-- Seeed Studio XIAO nRF52840 - +-- Access Control - +-- Keypad - +-- NFC - +-- Lock Controller - +-- Power Manager - +-- Diagnostics - +-- OTA +OpenParcelBox firmware on Zephyr RTOS + +-- Keypad and access codes + +-- PN532 NFC reader and stored tags + +-- Lock command and COM/NC feedback + +-- Audible, visual, and serial diagnostics + +Planned: Home Assistant via Zigbee (ZHA / Zigbee2MQTT) +Planned: battery manager and secure firmware update ``` --- @@ -82,11 +79,14 @@ Responsibilities: - Keypad scanning. - NFC credential coordination. - Lock control. -- Battery monitoring. - RGB LED status. - Active buzzer feedback. + +Planned responsibilities: + +- Battery monitoring and system sleep. - Zigbee communication. -- OTA updates. +- Secure firmware updates. --- @@ -114,37 +114,60 @@ The firmware is organized into independent modules. ### NFC - UID validation. -- Low-power wake-up. +- Fixed 15-second keypad-triggered scan windows. +- Administrator-triggered enrollment through the mobile application. +- PN532 power-down outside active scan windows. -The nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware. Passive badge UID reading requires a dedicated NFC reader circuit on the production hardware. +The nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware. Passive +badge UID reading currently uses a PN532 module on the shared I2C bus; its +production placement and antenna implementation remain to be validated. ### Access Control -- Administrator codes. -- Permanent codes. -- Temporary codes. -- Anti brute-force logic. +- Eight permanent six-digit codes. +- Twenty one-time six-digit codes. +- Ten named NFC tags. +- Administrator and named guest mobile identities. + +Attempt limiting and more granular credential ownership remain planned. ### Lock Controller -- Lock actuation. -- Opening validation. -- Error handling. +- 150 ms lock actuation pulse. +- Continuous `COM/NC` state feedback. +- Open-state LED, serial, and buzzer feedback. -### Power Manager +The current implementation provides actuation and continuous feedback. The +time-bounded command-result validation and explicit error path remain planned. + +### Bluetooth + +- BLE LE Secure Connections and encrypted GATT access. +- Administrator and named guest application identities. +- Paginated authoritative state readback for the Flutter application. +- Commands for opening, clock synchronization, credentials, guests, NFC + enrollment, and factory reset. + +### Opening History + +- Persistent storage of the newest 64 successful openings. +- Seven-day retention. +- Permanent-code, temporary-code, NFC-tag, or mobile-identity source. + +### Power Manager (Planned) - Battery monitoring. - Deep sleep. - Wake-up handling. - Power optimization. -### Zigbee +### Zigbee (Planned) - Home Assistant communication. - State publication. - Command reception. -### OTA +### OTA (Planned) - Download. - Verification. @@ -152,9 +175,15 @@ The nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware. Passive badg --- -## Opening Validation +## Opening Feedback and Planned Validation -Opening must never be validated by a fixed delay alone. +The current firmware sends a 150 ms command pulse and independently monitors +the KR-S79 `COM/NC` contact. That feedback drives the green open indicator, +door transition logs, and the reminder beep every 2 seconds. + +Correlating an opening request with a time-bounded `COM/NC` transition and +reporting an explicit opening failure remain to be implemented. The target +sequence is: Sequence: @@ -229,8 +258,9 @@ Core build tools: ## Future Work -- Local event history. - Advanced user management. - Hardware diagnostics. -- Bluetooth Low Energy configuration. +- Battery monitoring and power optimization. +- Zigbee and Home Assistant integration. +- Secure firmware updates. - Support for multiple parcel box variants. diff --git a/docs/firmware/bluetooth.md b/docs/firmware/bluetooth.md index 70f0c17..73105c9 100644 --- a/docs/firmware/bluetooth.md +++ b/docs/firmware/bluetooth.md @@ -314,6 +314,18 @@ Example: "name": "Alice" } ], + "nfc_enrollment": { + "active": false, + "uid": "", + "error": "" + }, + "history": [ + { + "unix_ms": 1784135045000, + "kind": 2, + "actor": "Alice" + } + ], "guests": [ { "name": "Delivery team", @@ -357,12 +369,16 @@ NFC enrollment is started with: ```json { "command": "start_nfc_enrollment", - "identity_key": "<32 hexadecimal characters>", - "name": "Alice" + "identity_key": "<32 hexadecimal characters>" } ``` -The next UID reported by the dedicated reader is stored with that name. +The PN532 scans immediately for 15 seconds. Authenticated state exposes +`nfc_enrollment.active`, then either a new `uid` or an `error` (`timeout`, +`already_exists`, or `reader_unavailable`). The application asks for a name +only after receiving a new UID, then stores it with the regular `add_nfc_tag` +command. `cancel_nfc_enrollment` stops an active scan and returns the reader to +power-down mode. All enrollment commands remain administrator-only. When the phone has synchronized the clock, lock-opening logs include a Unix timestamp in milliseconds. diff --git a/docs/firmware/buzzer.md b/docs/firmware/buzzer.md index f0a0936..a47612b 100644 --- a/docs/firmware/buzzer.md +++ b/docs/firmware/buzzer.md @@ -39,10 +39,15 @@ void buzzer_off(void); - `buzzer_init()` configures the buzzer control GPIO and leaves the buzzer off. - `buzzer_beep_key()` emits a short blocking beep. -- `buzzer_beep_success()` emits a longer blocking beep when a valid access code is accepted. +- `buzzer_beep_success()` emits a longer blocking beep when a valid access code + or NFC badge is accepted. - The keypad calls `buzzer_beep_key()` only when a new key press is accepted, so holding a key does not produce a continuous beep. -- Invalid six-digit access codes are signaled by a higher-level sequence of three short GPIO pulses. +- Invalid six-digit access codes, unauthorized NFC badges, and duplicate tags + during enrollment are signaled by the same higher-level sequence of three + short GPIO pulses. - While the lock state feedback reports open, the main loop emits one short reminder beep every 2 seconds. +- Each open-state reminder is also logged as `Door open reminder beep` so the + feedback contact and buzzer behavior can be distinguished during diagnosis. --- diff --git a/docs/firmware/debug.md b/docs/firmware/debug.md index 107dca0..e2a7835 100644 --- a/docs/firmware/debug.md +++ b/docs/firmware/debug.md @@ -15,13 +15,12 @@ The firmware currently supports the following debugging methods: - UART console (`printf`) - On-board RGB LED - Bluetooth state and command diagnostics -- Zephyr Shell (when enabled) +- PN532/NFC UART diagnostics Additional methods may be added later: - Zigbee diagnostics - Home Assistant diagnostic entities -- NFC diagnostics --- @@ -46,10 +45,9 @@ The onboard RGB LED is used to quickly identify the firmware state. ## Startup -| Color | Meaning | -| ------- | ----------------------- | -| Off | Startup in progress | -| Magenta | Initialization failed | +The LED remains off during early startup, then turns white for one second when +the local keypad path is ready. Peripheral failures are reported over UART and +do not currently use a dedicated fatal LED state. --- @@ -71,7 +69,6 @@ and keypad initialization is retried once per second. | Green | Lock state feedback reports open | | Blue | NFC credential scan mode active | | Red | Invalid six-digit access code | -| Magenta | Fatal initialization error | --- @@ -112,11 +109,12 @@ Door opened timestamp_ms=1784135045050 Lock state: ```text -Door opened +Door opened timestamp_ms=1784135045050 +Door open reminder beep Door closed ``` -Future dedicated NFC reader backend: +PN532 reader: ```text Scan NFC: ON diff --git a/docs/firmware/gpio_expander.md b/docs/firmware/gpio_expander.md index fdf6cc0..8fac5a6 100644 --- a/docs/firmware/gpio_expander.md +++ b/docs/firmware/gpio_expander.md @@ -6,7 +6,9 @@ This module provides a hardware abstraction layer (HAL) for the external GPIO ex The current implementation targets the PCF8574 connected over the I²C bus. Other GPIO expanders (PCF8575, MCP23017, etc.) can later be supported by modifying only this module. -All higher-level modules (keypad, lock, etc.) must use this API instead of directly accessing the I²C bus. +Higher-level modules connected to the PCF8574 use this API instead of directly +accessing the I²C bus. Direct XIAO GPIO peripherals such as lock control do not +pass through the expander. --- @@ -68,7 +70,6 @@ Only `gpio_expander.c` should require modifications. ## Used By - Keypad -- Future lock module (if additional GPIO are required) - Future peripherals connected through an external GPIO expander --- @@ -79,4 +80,4 @@ The PCF8574 uses quasi-bidirectional GPIOs. A pin must be written HIGH before it can be used as an input. -This behavior is fully handled by this module and should remain transparent to higher-level drivers. \ No newline at end of file +This behavior is fully handled by this module and should remain transparent to higher-level drivers. diff --git a/docs/firmware/keypad.md b/docs/firmware/keypad.md index 4e9fca3..f8eb6b8 100644 --- a/docs/firmware/keypad.md +++ b/docs/firmware/keypad.md @@ -69,6 +69,9 @@ Each key press is reported only once. - Single key detection - Anti-repeat - Modular GPIO Expander interface +- Higher-level short beep on each accepted key press +- `B`, `*`, or `#` cancellation of an in-progress access-code entry +- First-key trigger for the fixed, non-renewable 15-second NFC scan window --- @@ -76,5 +79,4 @@ Each key press is reported only once. - Support for the production 2x6 keypad. - Configurable key mapping. -- Optional audible feedback handled by the buzzer module. -- Unit tests. \ No newline at end of file +- Unit tests. diff --git a/docs/firmware/lock.md b/docs/firmware/lock.md index d3f3da3..9ec58ec 100644 --- a/docs/firmware/lock.md +++ b/docs/firmware/lock.md @@ -35,7 +35,7 @@ int lock_control_open(void); - `lock_control_init()` configures D9 directly as an inactive GPIO output. - `lock_control_init()` never sends an unlock pulse. -- `lock_control_open()` drives D9 high for 100 ms, then immediately returns it inactive with `gpio_pin_set_dt(..., 0)`. +- `lock_control_open()` drives D9 high for 150 ms, then immediately returns it inactive with `gpio_pin_set_dt(..., 0)`. The additional 50 ms compared with the original pulse gives the mechanical relay more time to pull in while limiting coil-on time and battery use. - All GPIO return codes used by the lock control path are logged. - No PWM is used. @@ -50,13 +50,15 @@ The main loop collects numeric keypad input. - Codes are persisted through Zephyr settings with the NVS backend. - The development default code is `784512`. - A valid six-digit entry triggers `lock_control_open()`. -- A future dedicated NFC reader backend may trigger `lock_control_open()` after a valid stored tag UID is detected. +- The PN532 reader triggers `lock_control_open()` after a valid stored tag UID is detected. - `*`, `#`, and `B` clear the current entry. - A valid six-digit entry triggers one long success beep before the opening pulse. - An invalid six-digit entry triggers three short close beeps and keeps the red LED on for 1 second. -- NFC credential scan mode starts on keypad activity, remains active for 1 minute, and is shown with the blue LED. -- A valid NFC tag UID will use the same success beep and lock opening path as a valid keypad code once the dedicated reader backend is added. +- The first keypad press starts one non-renewable 15-second NFC scan window, + shown with the blue LED; later presses cannot extend the active window. +- A valid NFC tag UID uses the same success beep and lock opening path as a valid keypad code. - The green LED stays on while the lock state feedback reports the lock open. - A short reminder beep is emitted every 2 seconds while the lock state feedback reports open. - Door state changes are reported over UART as `Door opened` and `Door closed`. +- Each periodic reminder is reported over UART as `Door open reminder beep`. - LEDs stay off during startup and while the lock is closed. diff --git a/docs/firmware/nfc.md b/docs/firmware/nfc.md index 2eb9a9d..09796ef 100644 --- a/docs/firmware/nfc.md +++ b/docs/firmware/nfc.md @@ -4,11 +4,16 @@ This module provides the application-level NFC credential flow and persistent NFC tag UID storage. -NFC tag UIDs are stored in non-volatile settings, like access codes. They remain available after power loss and expose add, replace, and clear APIs for the future mobile application and Home Assistant integration. +NFC tag UIDs are stored in non-volatile settings, like access codes. They +remain available after power loss and expose add, replace, and clear APIs used +by the mobile application and reserved for the future Home Assistant +integration. The Seeed Studio XIAO nRF52840 exposes the Nordic NFCT pins (`NFC1/P0.09` and `NFC2/P0.10`). The integrated NFCT peripheral is tag-side NFC-A hardware and is not a standalone active reader for passive badges. -Passive badge UID reading requires a dedicated NFC reader circuit on the production hardware. +Passive badge UID reading is provided by a PN532 module connected to the same +I2C bus as the development keypad expander. The module uses the fixed 7-bit +address `0x24` (`0x48` in NXP's 8-bit address notation). --- @@ -35,26 +40,29 @@ int nfc_tags_clear(size_t slot); ## Scan Mode -The NFC reader polling flow is temporarily disabled in the production main loop -while the local keypad/BLE startup baseline is being validated. Persistent tag -storage and the BLE management protocol remain compiled and initialized. +The firmware keeps the PN532 in its I2C-wakeable power-down mode by default. -The intended firmware flow keeps NFC credential scan mode disabled by default. +The first keypad key press wakes the PN532 and starts one fixed 15-second scan +window. Additional key presses during that window never extend or restart its +deadline. A later key press can start a new window only after the previous one +has ended. -The main loop enables scan mode when a keypad key is pressed. Scan mode remains active for 1 minute after the latest key press, then the firmware disables NFC credential scanning again. - -An authenticated administrator can also start enrollment from the mobile -application. The next UID reported by the reader is stored with the supplied -name and scan mode stops immediately. +An authenticated administrator can start a separate 15-second enrollment scan +from the mobile application. This explicit administration flow is the only +exception to keypad-triggered scanning. A new UID is returned through BLE but +is not authorized until the administrator enters a name and validates it in +the active modal. A duplicate tag or timeout is returned as an enrollment +error, and cancellation powers the reader down immediately. While scan mode is active, the RGB LED is blue. -When a future NFC reader backend detects a tag: +When the PN532 detects an ISO/IEC 14443-A tag: - The UID is printed over UART. -- A valid UID triggers the same unlock path as a valid keypad code: success beep, lock open pulse, and regular lock state feedback. -- After a valid NFC unlock through the dedicated reader backend, scan mode is disabled immediately. -- An invalid UID is reported over UART and scan mode remains active until timeout or a valid tag. +- A valid UID triggers the same success beep and unlock path as a valid keypad code. +- After a valid NFC unlock, scan mode is disabled immediately. +- An invalid UID triggers the same three-beep error sequence as an invalid + six-digit code, then scan mode remains active until timeout or a valid tag. Runtime UART messages: @@ -68,8 +76,13 @@ NFC not valid --- -## Current Driver Status +## PN532 Driver -The current `nfc` module defines the scan-mode API and keeps NFC disabled at startup. - -The hardware-specific tag detection backend is still a placeholder. It currently returns no detected UID until a dedicated NFC reader implementation is added. +- Interface selection: `I2C 1 0` on the module switches. +- Bus: XIAO `SDA` / `SCL`, shared with the PCF8574 at `0x20`. +- Address used by Zephyr: `0x24`. +- Reader initialization verifies the PN532 firmware response and configures SAM + normal mode plus bounded passive-target retries. +- Disabling scan sends the PN532 `PowerDown` command with I2C wake enabled. +- If the reader is absent, local keypad access, persistent storage, and BLE + continue operating; UART reports the NFC initialization/start error. diff --git a/docs/hardware/Commande temporaire SIG.svg b/docs/hardware/Commande temporaire SIG.svg index e87c92d..3a95730 100644 --- a/docs/hardware/Commande temporaire SIG.svg +++ b/docs/hardware/Commande temporaire SIG.svg @@ -19,17 +19,17 @@ - OpenParcelBox — commande temporaire de SIG avec S8550 + S8050 - Seeed Studio XIAO BLE nRF52840 • serrure KR-S79 • stack 8×AAA ≈ 13 V + OpenParcelBox — temporary SIG driver with S8550 + S8050 + Seeed Studio XIAO BLE nRF52840 • KR-S79 lock • 8×AAA pack ≈ 13 V XIAO BLE nRF52840 - D7 — commande serrure + D9 — lock command GND - Alimentation USB-C pendant le développement + USB-C power during development @@ -67,7 +67,7 @@ - GND commun + Common GND @@ -105,34 +105,34 @@ - SERRURE + LOCK KERONG KR-S79_20-JST - SIG jaune + Yellow SIG - + rouge → +13 V + Red + → +13 V - − orange → GND + Orange − → GND - Fonctionnement - D7 = LOW : Q2 bloqué → R3 remonte la base de Q1 à +13 V → Q1 bloqué → SIG non alimenté. - D7 = HIGH : Q2 conduit → courant limité par R2 → base de Q1 abaissée d’environ 0,7 V - sous son émetteur → Q1 conduit → environ +13 V appliqué sur SIG. - R4 maintient Q2 bloqué pendant le démarrage ou si D7 est flottante. - Important : ne jamais relier directement D7 à la base du S8550. + Operation + D9 = LOW: Q2 off → R3 pulls the Q1 base to +13 V → Q1 off → SIG not powered. + D9 = HIGH: Q2 conducts → R2 limits current → Q1 base about 0.7 V below its emitter + → Q1 conducts → approximately +13 V is applied to SIG. + R4 keeps Q2 off during startup or if D9 is floating. + Important: never connect D9 directly to the S8550 base. - Vérifications avant câblage - • Identifier physiquement E, B et C de chaque transistor avec la fiche du fabricant ou un multimètre. - • Les boîtiers S8050/S8550 ne garantissent pas tous le même ordre de broches. - • Vérifier la polarité du stack : rouge = +13 V, noir = GND. - • Relier le GND du XIAO au GND du stack avant d’activer D7. - • Tester d’abord avec une impulsion courte (≈100 ms) et mesurer SIG au multimètre. - Ce schéma ne suppose aucun brochage physique gauche/centre/droite. - \ No newline at end of file + Checks before wiring + • Identify E, B, and C on each transistor from its datasheet or with a multimeter. + • S8050/S8550 packages do not all guarantee the same pin order. + • Check pack polarity: red = +13 V, black = GND. + • Connect XIAO GND to pack GND before driving D9. + • Firmware applies a 150 ms pulse; measure SIG with a multimeter. + This diagram assumes no physical left/center/right transistor pin order. + diff --git a/firmware/README.md b/firmware/README.md index bc4945b..2c63d84 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -11,11 +11,15 @@ The firmware is responsible for: - Keypad management. - Access code handling. - Lock mechanism control. -- Battery monitoring. -- Zigbee communication. -- OTA updates. -- Home Assistant integration. - Audible and visual feedback. +- PN532 NFC badge reading. +- Secure BLE administration. + +Planned responsibilities: + +- Battery monitoring and low-power system modes. +- Zigbee and Home Assistant integration. +- Secure firmware updates. --- @@ -85,7 +89,8 @@ Active buzzer control through a GPIO-driven transistor. ### Lock Control -KR-S79 unlock command through a 100 ms active-high pulse on XIAO pin `D9`. +KR-S79 unlock command through a 150 ms active-high pulse on XIAO pin `D9`. +The output is configured inactive and never pulses during initialization. ### Lock State @@ -98,9 +103,9 @@ unlock codes. ### NFC Tags -Persistent storage for NFC tag UIDs and the application-level scan-mode API. - -The XIAO nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware. Passive badge UID reading will require a dedicated NFC reader backend. +Persistent storage for NFC tag UIDs and a PN532 passive-badge reader on the +shared I2C bus at address `0x24`. The first keypad press opens one fixed +15-second scan window; the reader remains in power-down mode otherwise. ### Bluetooth @@ -135,13 +140,14 @@ Implemented: - Active buzzer feedback on accepted key presses. - Long success beep on valid access code. - Three short beeps on invalid access code. +- One short reminder beep every 2 seconds while `COM/NC` reports open. - Persistent six-digit access code storage. - Persistent NFC tag UID storage with default development UID `60:4F:E2:B5`. - BLE administration service for clock sync, direct opening, access-code management, NFC tag management, and state readback. - One-time access codes removed automatically after first successful keypad use. - Runtime lock-opening logs with phone-synchronized timestamps when available. -- NFC credential scan-mode API retained while production polling remains - temporarily disabled pending a dedicated reader backend. +- PN532 UID polling, mobile enrollment, stored-tag unlock, history, and + I2C-wakeable power-down outside fixed 15-second scan windows. - Lock command pulse on XIAO pin `D9`. - Lock state feedback through the KR-S79 `COM/NC` contact. - Door opened / closed debug output. @@ -151,6 +157,6 @@ In progress or planned: - Production keypad mapping. - Battery monitoring. -- Dedicated NFC reader backend for passive badge UID detection. +- Physical PN532 range/interference validation and production reader layout. - Zigbee. - OTA updates. diff --git a/firmware/app/include/board_config.h b/firmware/app/include/board_config.h index 4962906..c21bb95 100644 --- a/firmware/app/include/board_config.h +++ b/firmware/app/include/board_config.h @@ -24,6 +24,11 @@ #define GPIO_EXPANDER_I2C_NODE DT_NODELABEL(i2c1) #define GPIO_EXPANDER_ADDRESS 0x20 +/* PN532 NFC reader (7-bit I2C address; NXP documents 0x48 including R/W). */ + +#define PN532_I2C_NODE DT_NODELABEL(i2c1) +#define PN532_I2C_ADDRESS 0x24 + /* Buzzer */ #define BUZZER_GPIO_NODE DT_NODELABEL(gpio0) diff --git a/firmware/app/openparcelbox.uf2 b/firmware/app/openparcelbox.uf2 index 31b59b09e9dc1d7730009654fd16924c85637661..0b98c40e481adbeafe57ad96d244430aeeacbc40 100644 GIT binary patch delta 93935 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]i*IwPqb6H8(8&#ft_j_uQPss@0-e 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Ɓc<(htcxQwv~<;v&?b(!cU`l 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 AwEzGB diff --git a/firmware/app/src/lock_control.c b/firmware/app/src/lock_control.c index 1628706..aa91f63 100644 --- a/firmware/app/src/lock_control.c +++ b/firmware/app/src/lock_control.c @@ -17,7 +17,7 @@ #include #include -#define LOCK_CONTROL_OPEN_PULSE_MS 100 +#define LOCK_CONTROL_OPEN_PULSE_MS 150 /* D9 is declared in the application devicetree overlay as GPIO_ACTIVE_HIGH. */ static const struct gpio_dt_spec lock_control_gpio = @@ -62,7 +62,8 @@ int lock_control_open(void) { return ret; } - printk("Lock control GPIO active set: %d\n", ret); + printk("Lock control GPIO active for %d ms\n", + LOCK_CONTROL_OPEN_PULSE_MS); k_msleep(LOCK_CONTROL_OPEN_PULSE_MS); diff --git a/firmware/app/src/lock_control.h b/firmware/app/src/lock_control.h index 2d9184f..188bd9b 100644 --- a/firmware/app/src/lock_control.h +++ b/firmware/app/src/lock_control.h @@ -22,7 +22,7 @@ int lock_control_init(void); /** * @brief Send an unlock pulse to the lock. * - * Drives D9 high for 100 ms, then returns it low. + * Drives D9 high for 150 ms, then returns it low. * * @return 0 on success, negative value on error. */ diff --git a/firmware/app/src/main.c b/firmware/app/src/main.c index 8b2c8c8..ee2de42 100644 --- a/firmware/app/src/main.c +++ b/firmware/app/src/main.c @@ -31,6 +31,7 @@ #define KEYPAD_RETRY_MS 1000 #define SERVICE_INIT_STACK_SIZE 4096 #define SERVICE_INIT_PRIORITY 8 +#define NFC_SCAN_WINDOW_MS 15000 #define LOCK_OPEN_BEEP_INTERVAL_MS 2000 #define INVALID_CODE_LED_MS 1000 #define INVALID_CODE_BEEP_COUNT 3 @@ -87,6 +88,46 @@ static void signal_invalid_code(void) { k_msleep(INVALID_CODE_LED_MS - INVALID_CODE_SEQUENCE_MS); } +static void print_nfc_uid(const uint8_t *uid, size_t uid_length) { + printf("NFC detected:"); + for (size_t i = 0; i < uid_length; i++) { + printf("%s%02X", i == 0 ? " " : ":", uid[i]); + } + printf("\n"); +} + +static bool start_nfc_scan(int64_t *deadline_ms) { + if (nfc_scan_is_enabled()) { + return true; + } + + int ret = nfc_scan_enable(); + if (ret < 0) { + printf("NFC scan start failed: %d\n", ret); + return false; + } + + *deadline_ms = k_uptime_get() + NFC_SCAN_WINDOW_MS; + printf("Scan NFC: ON\n"); + if (led_ready) led_set_blue(); + return true; +} + +static void stop_nfc_scan(int64_t *deadline_ms, bool door_open) { + if (!nfc_scan_is_enabled()) { + *deadline_ms = 0; + return; + } + + int ret = nfc_scan_disable(); + if (ret < 0) { + printf("NFC power-down failed: %d\n", ret); + } + *deadline_ms = 0; + printf("Scan NFC: OFF\n"); + set_idle_led(door_open); +} + static int request_lock_open(const char *source, const char *actor) { if (!lock_control_ready) { printf("Lock open rejected: control GPIO unavailable\n"); @@ -154,7 +195,7 @@ static int initialize_persistent_modules(void) { if (ret < 0 && first_error == 0) first_error = ret; ret = nfc_init(); printf("Startup nfc=%d\n", ret); - if (ret < 0 && first_error == 0) first_error = ret; + /* A missing optional reader must not disable persistence, keypad, or BLE. */ return first_error; } @@ -210,6 +251,8 @@ int main(void) { int64_t reset_button_pressed_since_ms = 0; int64_t identity_reset_blink_until_ms = 0; int64_t next_identity_reset_blink_ms = 0; + int64_t nfc_scan_deadline_ms = 0; + bool nfc_enrollment_pending = false; /* * These two initializers are RAM-only. They guarantee that keypad access and @@ -253,6 +296,23 @@ int main(void) { while (1) { char key; + if (opb_ble_take_nfc_enrollment()) { + if (nfc_scan_is_enabled()) { + stop_nfc_scan(&nfc_scan_deadline_ms, door_open); + } + nfc_enrollment_pending = true; + if (!start_nfc_scan(&nfc_scan_deadline_ms)) { + nfc_enrollment_pending = false; + opb_ble_set_nfc_enrollment_error("reader_unavailable"); + } else { + printf("NFC enrollment scan started\n"); + } + } + if (opb_ble_take_nfc_enrollment_cancel()) { + nfc_enrollment_pending = false; + stop_nfc_scan(&nfc_scan_deadline_ms, door_open); + } + if (!keypad_ready && k_uptime_get() >= next_keypad_retry_ms) { keypad_ready = keypad_init() == 0; next_keypad_retry_ms = k_uptime_get() + KEYPAD_RETRY_MS; @@ -307,6 +367,7 @@ int main(void) { } if (door_open && k_uptime_get() >= next_open_beep_ms) { + printf("Door open reminder beep\n"); beep_key(); next_open_beep_ms = k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS; } @@ -331,6 +392,64 @@ int main(void) { } } + if (nfc_scan_is_enabled() && + k_uptime_get() >= nfc_scan_deadline_ms) { + if (nfc_enrollment_pending) { + nfc_enrollment_pending = false; + opb_ble_set_nfc_enrollment_error("timeout"); + } + stop_nfc_scan(&nfc_scan_deadline_ms, door_open); + } + + if (nfc_scan_is_enabled()) { + uint8_t uid[NFC_TAG_UID_MAX_LENGTH]; + size_t uid_length; + + bool uid_detected = + nfc_read_detected_uid(uid, sizeof(uid), &uid_length); + if (k_uptime_get() >= nfc_scan_deadline_ms) { + /* Never accept a response that completed after the fixed deadline. */ + if (nfc_enrollment_pending) { + nfc_enrollment_pending = false; + opb_ble_set_nfc_enrollment_error("timeout"); + } + stop_nfc_scan(&nfc_scan_deadline_ms, door_open); + } else if (uid_detected) { + char tag_name[NFC_TAG_NAME_MAX_LENGTH + 1] = ""; + print_nfc_uid(uid, uid_length); + + if (nfc_enrollment_pending) { + nfc_enrollment_pending = false; + if (nfc_tags_find(uid, uid_length, tag_name)) { + printf("NFC enrollment rejected: tag already exists\n"); + signal_invalid_code(); + opb_ble_set_nfc_enrollment_error("already_exists"); + } else { + printf("NFC enrollment tag detected\n"); + beep_success(); + opb_ble_set_nfc_enrollment_uid(uid, uid_length); + } + stop_nfc_scan(&nfc_scan_deadline_ms, door_open); + } else if (nfc_tags_find(uid, uid_length, tag_name)) { + printf("NFC valid\n"); + beep_success(); + if (request_lock_open("nfc", tag_name) == 0) { + if (atomic_get(&persistent_services_ready) != 0) { + (void)open_history_append(OPEN_HISTORY_NFC_TAG, tag_name, + opb_clock_now_ms()); + } + opb_ble_notify_state_changed(); + } + stop_nfc_scan(&nfc_scan_deadline_ms, + lock_state_ready ? lock_state_is_open() : false); + } else { + printf("NFC not valid\n"); + signal_invalid_code(); + if (nfc_scan_is_enabled() && led_ready) led_set_blue(); + } + } + } + key = keypad_ready ? keypad_get_key() : 0; if (key == 0) { k_msleep(MAIN_LOOP_DELAY_MS); @@ -339,6 +458,9 @@ int main(void) { printf("Key pressed: %c\n", key); + /* Only the first key press opens the fixed 15-second NFC window. */ + (void)start_nfc_scan(&nfc_scan_deadline_ms); + if (key == '*' || key == '#' || key == 'B') { entered_length = 0; beep_key(); @@ -377,6 +499,8 @@ int main(void) { (void)access_codes_consume_if_one_time(entered_code, entered_length); opb_ble_notify_state_changed(); + stop_nfc_scan(&nfc_scan_deadline_ms, + lock_state_ready ? lock_state_is_open() : false); } } else { printf("Invalid access code\n"); diff --git a/firmware/app/src/nfc.c b/firmware/app/src/nfc.c index 44fbf69..529ecd1 100644 --- a/firmware/app/src/nfc.c +++ b/firmware/app/src/nfc.c @@ -4,39 +4,287 @@ * OpenParcelBox * Copyright (c) 2026 * - * NFC scan mode interface. + * PN532 NFC reader over I2C. */ #include "nfc.h" -#include +#include "board_config.h" +#include +#include + +#include +#include +#include +#include #include -static bool scan_enabled; +#define PN532_HOST_TO_PN532 0xd4 +#define PN532_PN532_TO_HOST 0xd5 +#define PN532_COMMAND_GET_FIRMWARE_VERSION 0x02 +#define PN532_COMMAND_SAM_CONFIGURATION 0x14 +#define PN532_COMMAND_POWER_DOWN 0x16 +#define PN532_COMMAND_RF_CONFIGURATION 0x32 +#define PN532_COMMAND_IN_LIST_PASSIVE_TARGET 0x4a +#define PN532_RESPONSE_TIMEOUT_MS 250 +#define PN532_WAKE_DELAY_MS 5 +#define PN532_READY_POLL_MS 5 +#define PN532_FRAME_MAX 40 +#define PN532_I2C_WAKEUP_ENABLE 0x80 +#define PN532_THREAD_STACK_SIZE 2048 +#define PN532_THREAD_PRIORITY 9 +#define PN532_IDLE_POLL_MS 20 + +static const struct device *i2c_dev = DEVICE_DT_GET(PN532_I2C_NODE); +static bool reader_ready; +static atomic_t scan_enabled; +static uint8_t last_uid[NFC_TAG_UID_MAX_LENGTH]; +static size_t last_uid_length; +static uint8_t pending_uid[NFC_TAG_UID_MAX_LENGTH]; +static size_t pending_uid_length; +static struct k_mutex command_mutex; +static struct k_mutex uid_mutex; +static K_THREAD_STACK_DEFINE(pn532_thread_stack, PN532_THREAD_STACK_SIZE); +static struct k_thread pn532_thread; + +static void pn532_thread_entry(void *unused1, void *unused2, void *unused3); + +static int pn532_wait_ready(int timeout_ms) { + int64_t deadline = k_uptime_get() + timeout_ms; + uint8_t status; + + do { + int ret = i2c_read(i2c_dev, &status, sizeof(status), PN532_I2C_ADDRESS); + if (ret == 0 && status == 0x01) return 0; + k_msleep(PN532_READY_POLL_MS); + } while (k_uptime_get() < deadline); + return -ETIMEDOUT; +} + +static int pn532_read_frame(uint8_t expected_response, uint8_t *payload, + size_t payload_capacity, size_t *payload_length) { + uint8_t frame[PN532_FRAME_MAX]; + uint8_t checksum = 0; + int ret = i2c_read(i2c_dev, frame, sizeof(frame), PN532_I2C_ADDRESS); + if (ret < 0) return ret; + if (frame[0] != 0x01 || frame[1] != 0x00 || frame[2] != 0x00 || + frame[3] != 0xff) return -EIO; + + size_t data_length = frame[4]; + if (data_length < 2 || data_length + 8 > sizeof(frame) || + (uint8_t)(frame[4] + frame[5]) != 0 || + frame[6] != PN532_PN532_TO_HOST || frame[7] != expected_response) { + return -EIO; + } + for (size_t i = 0; i < data_length; i++) { + checksum = (uint8_t)(checksum + frame[6 + i]); + } + if ((uint8_t)(checksum + frame[6 + data_length]) != 0) return -EIO; + + size_t response_payload_length = data_length - 2; + if (response_payload_length > payload_capacity) return -EMSGSIZE; + if (response_payload_length > 0 && payload != NULL) { + memcpy(payload, &frame[8], response_payload_length); + } + if (payload_length != NULL) *payload_length = response_payload_length; + return 0; +} + +static int pn532_command(uint8_t command, const uint8_t *parameters, + size_t parameter_length, uint8_t *response, + size_t response_capacity, size_t *response_length) { + static const uint8_t expected_ack[] = {0x01, 0x00, 0x00, 0xff, + 0x00, 0xff, 0x00}; + uint8_t frame[PN532_FRAME_MAX]; + uint8_t ack[sizeof(expected_ack)]; + size_t data_length = parameter_length + 2; + uint8_t checksum = PN532_HOST_TO_PN532; + if (parameter_length + 9 > sizeof(frame)) return -EMSGSIZE; + + frame[0] = 0x00; + frame[1] = 0x00; + frame[2] = 0xff; + frame[3] = (uint8_t)data_length; + frame[4] = (uint8_t)(0U - data_length); + frame[5] = PN532_HOST_TO_PN532; + frame[6] = command; + if (parameter_length > 0) memcpy(&frame[7], parameters, parameter_length); + checksum = (uint8_t)(checksum + command); + for (size_t i = 0; i < parameter_length; i++) { + checksum = (uint8_t)(checksum + parameters[i]); + } + frame[7 + parameter_length] = (uint8_t)(0U - checksum); + frame[8 + parameter_length] = 0x00; + + int ret = i2c_write(i2c_dev, frame, parameter_length + 9, + PN532_I2C_ADDRESS); + if (ret < 0) return ret; + ret = pn532_wait_ready(PN532_RESPONSE_TIMEOUT_MS); + if (ret < 0) return ret; + ret = i2c_read(i2c_dev, ack, sizeof(ack), PN532_I2C_ADDRESS); + if (ret < 0 || memcmp(ack, expected_ack, sizeof(ack)) != 0) return -EIO; + ret = pn532_wait_ready(PN532_RESPONSE_TIMEOUT_MS); + if (ret < 0) return ret; + return pn532_read_frame((uint8_t)(command + 1), response, + response_capacity, response_length); +} + +static int pn532_configure_reader(void) { + static const uint8_t sam_parameters[] = {0x01, 0x14, 0x01}; + static const uint8_t retry_parameters[] = {0x05, 0xff, 0x01, 0x01}; + uint8_t response[8]; + size_t response_length; + int ret = pn532_command(PN532_COMMAND_SAM_CONFIGURATION, sam_parameters, + sizeof(sam_parameters), response, sizeof(response), + &response_length); + if (ret < 0) return ret; + return pn532_command(PN532_COMMAND_RF_CONFIGURATION, retry_parameters, + sizeof(retry_parameters), response, sizeof(response), + &response_length); +} int nfc_init(void) { - scan_enabled = false; + uint8_t response[8]; + size_t response_length; + atomic_clear(&scan_enabled); + reader_ready = false; + last_uid_length = 0; + pending_uid_length = 0; + k_mutex_init(&command_mutex); + k_mutex_init(&uid_mutex); + if (!device_is_ready(i2c_dev)) return -ENODEV; + + int ret = pn532_command(PN532_COMMAND_GET_FIRMWARE_VERSION, NULL, 0, + response, sizeof(response), &response_length); + if (ret < 0 || response_length < 4) return ret < 0 ? ret : -EIO; + ret = pn532_configure_reader(); + if (ret < 0) return ret; + reader_ready = true; + ret = nfc_scan_disable(); + if (ret < 0) return ret; + + k_tid_t thread_id = + k_thread_create(&pn532_thread, pn532_thread_stack, + K_THREAD_STACK_SIZEOF(pn532_thread_stack), + pn532_thread_entry, NULL, NULL, NULL, + PN532_THREAD_PRIORITY, 0, K_NO_WAIT); + if (thread_id == NULL) return -ENOMEM; + (void)k_thread_name_set(&pn532_thread, "pn532"); return 0; } int nfc_scan_enable(void) { - scan_enabled = true; + uint8_t status; + if (!reader_ready) return -ENODEV; + if (atomic_get(&scan_enabled) != 0) return 0; + + k_mutex_lock(&command_mutex, K_FOREVER); + /* Addressing the PN532 wakes it from I2C power-down mode. */ + (void)i2c_read(i2c_dev, &status, sizeof(status), PN532_I2C_ADDRESS); + k_msleep(PN532_WAKE_DELAY_MS); + int ret = pn532_configure_reader(); + k_mutex_unlock(&command_mutex); + if (ret < 0) return ret; + last_uid_length = 0; + k_mutex_lock(&uid_mutex, K_FOREVER); + pending_uid_length = 0; + k_mutex_unlock(&uid_mutex); + atomic_set(&scan_enabled, 1); return 0; } int nfc_scan_disable(void) { - scan_enabled = false; - return 0; + static const uint8_t parameters[] = {PN532_I2C_WAKEUP_ENABLE}; + uint8_t response[4]; + size_t response_length; + atomic_clear(&scan_enabled); + last_uid_length = 0; + k_mutex_lock(&uid_mutex, K_FOREVER); + pending_uid_length = 0; + k_mutex_unlock(&uid_mutex); + if (!reader_ready) return 0; + k_mutex_lock(&command_mutex, K_FOREVER); + int ret = pn532_command(PN532_COMMAND_POWER_DOWN, parameters, + sizeof(parameters), response, sizeof(response), + &response_length); + k_mutex_unlock(&command_mutex); + return ret; } -bool nfc_scan_is_enabled(void) { return scan_enabled; } +bool nfc_scan_is_enabled(void) { return atomic_get(&scan_enabled) != 0; } + +static bool pn532_poll_uid(uint8_t *uid, size_t uid_capacity, + size_t *uid_length) { + static const uint8_t parameters[] = {0x01, 0x00}; + uint8_t response[24]; + size_t response_length; + if (atomic_get(&scan_enabled) == 0 || uid == NULL || uid_length == NULL) { + return false; + } + + int ret = pn532_command(PN532_COMMAND_IN_LIST_PASSIVE_TARGET, parameters, + sizeof(parameters), response, sizeof(response), + &response_length); + if (ret < 0 || response_length < 1 || response[0] == 0) { + last_uid_length = 0; + return false; + } + if (response_length < 6) return false; + + size_t detected_length = response[5]; + if (detected_length == 0 || detected_length > NFC_TAG_UID_MAX_LENGTH || + detected_length > uid_capacity || response_length < 6 + detected_length) { + return false; + } + if (last_uid_length == detected_length && + memcmp(last_uid, &response[6], detected_length) == 0) return false; + + memcpy(uid, &response[6], detected_length); + memcpy(last_uid, &response[6], detected_length); + last_uid_length = detected_length; + *uid_length = detected_length; + return true; +} + +static void pn532_thread_entry(void *unused1, void *unused2, void *unused3) { + uint8_t uid[NFC_TAG_UID_MAX_LENGTH]; + size_t uid_length; + + ARG_UNUSED(unused1); + ARG_UNUSED(unused2); + ARG_UNUSED(unused3); + + while (1) { + if (atomic_get(&scan_enabled) == 0) { + k_msleep(PN532_IDLE_POLL_MS); + continue; + } + + k_mutex_lock(&command_mutex, K_FOREVER); + bool detected = pn532_poll_uid(uid, sizeof(uid), &uid_length); + k_mutex_unlock(&command_mutex); + if (!detected || atomic_get(&scan_enabled) == 0) continue; + + k_mutex_lock(&uid_mutex, K_FOREVER); + memcpy(pending_uid, uid, uid_length); + pending_uid_length = uid_length; + k_mutex_unlock(&uid_mutex); + } +} bool nfc_read_detected_uid(uint8_t *uid, size_t uid_capacity, size_t *uid_length) { - ARG_UNUSED(uid); - ARG_UNUSED(uid_capacity); - ARG_UNUSED(uid_length); + if (uid == NULL || uid_length == NULL) return false; - return false; + k_mutex_lock(&uid_mutex, K_FOREVER); + if (pending_uid_length == 0 || pending_uid_length > uid_capacity) { + k_mutex_unlock(&uid_mutex); + return false; + } + memcpy(uid, pending_uid, pending_uid_length); + *uid_length = pending_uid_length; + pending_uid_length = 0; + k_mutex_unlock(&uid_mutex); + return true; } diff --git a/firmware/app/src/opb_ble.c b/firmware/app/src/opb_ble.c index 29c11a9..52ca4aa 100644 --- a/firmware/app/src/opb_ble.c +++ b/firmware/app/src/opb_ble.c @@ -58,8 +58,11 @@ static char state_json[OPB_BLE_STATE_MAX]; static size_t state_snapshot_length; static size_t state_page_start; static bool state_page_active; -static char enrollment_name[NFC_TAG_NAME_MAX_LENGTH + 1]; static bool enrollment_requested; +static bool enrollment_cancel_requested; +static bool enrollment_active; +static char enrollment_uid[3 * NFC_TAG_UID_MAX_LENGTH]; +static char enrollment_error[32]; static struct bt_conn *authenticated_conn; static struct app_identity authenticated_identity; static bool authenticated_identity_valid; @@ -396,7 +399,15 @@ static size_t build_state_json(void) { } } - append_text(state_json, sizeof(state_json), &offset, "],\"history\":["); + append_formatted( + state_json, sizeof(state_json), &offset, + "],\"nfc_enrollment\":{\"active\":%s,\"uid\":\"%s\",\"error\":\"%s\"},\"history\":[", + authenticated_identity.role == APP_IDENTITY_ADMIN && enrollment_active + ? "true" + : "false", + authenticated_identity.role == APP_IDENTITY_ADMIN ? enrollment_uid : "", + authenticated_identity.role == APP_IDENTITY_ADMIN ? enrollment_error + : ""); first = true; for (size_t i = 0; i < open_history_count(); i++) { struct open_history_event event; @@ -706,15 +717,26 @@ static int handle_command(struct bt_conn *conn, const char *json) { } if (strcmp(command, "start_nfc_enrollment") == 0) { - if (identity.role != APP_IDENTITY_ADMIN || - !json_get_string(json, "name", enrollment_name, - sizeof(enrollment_name))) { + if (identity.role != APP_IDENTITY_ADMIN) { return -EACCES; } + enrollment_uid[0] = '\0'; + enrollment_error[0] = '\0'; + enrollment_active = true; enrollment_requested = true; return 0; } + if (strcmp(command, "cancel_nfc_enrollment") == 0) { + if (identity.role != APP_IDENTITY_ADMIN) return -EACCES; + enrollment_active = false; + enrollment_requested = false; + enrollment_cancel_requested = true; + enrollment_uid[0] = '\0'; + enrollment_error[0] = '\0'; + return 0; + } + if (strcmp(command, "add_guest") == 0) { char name[APP_IDENTITY_NAME_MAX_LENGTH + 1]; char guest_key[APP_IDENTITY_KEY_HEX_LENGTH + 1]; @@ -753,16 +775,42 @@ static int handle_command(struct bt_conn *conn, const char *json) { return -ENOTSUP; } -bool opb_ble_take_nfc_enrollment(char *name, size_t name_size) { - if (!enrollment_requested || name == NULL || name_size == 0) { - return false; - } - strncpy(name, enrollment_name, name_size - 1); - name[name_size - 1] = '\0'; +bool opb_ble_take_nfc_enrollment(void) { + if (!enrollment_requested) return false; enrollment_requested = false; return true; } +bool opb_ble_take_nfc_enrollment_cancel(void) { + if (!enrollment_cancel_requested) return false; + enrollment_cancel_requested = false; + return true; +} + +void opb_ble_set_nfc_enrollment_uid(const uint8_t *uid, size_t uid_length) { + size_t offset = 0; + enrollment_uid[0] = '\0'; + for (size_t i = 0; uid != NULL && i < uid_length; i++) { + int written = snprintf(enrollment_uid + offset, + sizeof(enrollment_uid) - offset, "%s%02X", + i == 0 ? "" : ":", uid[i]); + if (written < 0 || (size_t)written >= sizeof(enrollment_uid) - offset) break; + offset += (size_t)written; + } + enrollment_active = false; + enrollment_error[0] = '\0'; + opb_ble_notify_state_changed(); +} + +void opb_ble_set_nfc_enrollment_error(const char *error) { + enrollment_active = false; + enrollment_uid[0] = '\0'; + strncpy(enrollment_error, error == NULL ? "reader_error" : error, + sizeof(enrollment_error) - 1); + enrollment_error[sizeof(enrollment_error) - 1] = '\0'; + opb_ble_notify_state_changed(); +} + static ssize_t command_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { diff --git a/firmware/app/src/opb_ble.h b/firmware/app/src/opb_ble.h index 128f328..e11c9a6 100644 --- a/firmware/app/src/opb_ble.h +++ b/firmware/app/src/opb_ble.h @@ -39,7 +39,10 @@ bool opb_ble_get_unix_time_ms(int64_t *unix_ms); * @brief Notify connected clients that the readable state changed. */ void opb_ble_notify_state_changed(void); -bool opb_ble_take_nfc_enrollment(char *name, size_t name_size); +bool opb_ble_take_nfc_enrollment(void); +bool opb_ble_take_nfc_enrollment_cancel(void); +void opb_ble_set_nfc_enrollment_uid(const uint8_t *uid, size_t uid_length); +void opb_ble_set_nfc_enrollment_error(const char *error); void opb_ble_clear_bonds_after_reset(void); #endif /* OPB_BLE_H */ diff --git a/hardware/BOM.md b/hardware/BOM.md index 2e94af9..2346d18 100644 --- a/hardware/BOM.md +++ b/hardware/BOM.md @@ -41,13 +41,13 @@ The original keypad PCB is replaced by a new PCB compatible with the original fr The nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware and is not a standalone active reader for passive badges. -Badge UID reading requires a dedicated NFC reader circuit on the production PCB. +Badge UID reading uses a PN532 I2C module during development. A production PCB +implementation still needs placement, range, antenna, and interference validation. | Description | Quantity | Status | | --- | --- | --- | -| NFC reader IC | 1 | To select | -| NFC reader antenna, 13.56 MHz | 1 | To design | -| NFC reader matching network | 1 | To design | +| PN532 NFC/RFID I2C module with antenna | 1 | Integrated for development | +| Production NFC reader implementation | 1 | To design from PN532 validation | --- @@ -57,8 +57,9 @@ The lock opens when a positive 8 to 24 V pulse is applied to the `SIG` input. | Reference | Description | Quantity | Status | | --- | --- | --- | --- | -| Logic-level P-channel MOSFET or equivalent high-side driver | `SIG` input driver | 1 | To select | -| Bias resistors | According to schematic | TBD | To define | +| S8050 NPN + S8550 PNP stage | Development `SIG` high-side driver | 1 each | Integrated for development | +| Bias resistors | Development transistor stage | 4 | Integrated for development | +| Production high-side driver | Final `SIG` input driver | 1 | To select after validation | --- @@ -138,7 +139,7 @@ The original PCB LEDs are not considered reusable in the current plan. ### NFC - Do not rely on the nRF52840 NFCT peripheral for passive badge UID reading. -- Select a dedicated NFC reader circuit for badge unlock. +- Validate the PN532 module for badge-unlock range and interference. - Integrate and tune the NFC reader antenna on the production PCB. --- @@ -147,7 +148,7 @@ The original PCB LEDs are not considered reusable in the current plan. High priority: -- NFC reader IC selection. +- Production implementation based on PN532 validation. - NFC reader antenna design. - High-side lock driver selection. - Buck converter selection. diff --git a/hardware/README.md b/hardware/README.md index f975108..625fe31 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -2,15 +2,20 @@ This directory contains the hardware documentation for OpenParcelBox. +## Current Documentation + +- `BOM.md`: development and planned production components. +- `interfaces.md`: board-independent electrical interfaces. +- `pinout.md`: current XIAO development assignments. +- `reverse-engineering/`: validated Boks One and KR-S79 measurements. + ## Planned Content - KiCad schematics. - PCB files. - Gerber files. -- Bill of materials. - 3D models. - Mechanical drawings. -- Wiring documentation. -- Reverse-engineering notes. +- Production wiring and assembly documentation. The goal is to make the electronics fully reproducible while reusing as much of the original Boks One hardware as possible. diff --git a/hardware/interfaces.md b/hardware/interfaces.md index 6a515e2..9ef942a 100644 --- a/hardware/interfaces.md +++ b/hardware/interfaces.md @@ -30,7 +30,9 @@ The nRF52840 integrated NFC peripheral is NFCT hardware. It supports the tag-sid It is not a standalone active NFC reader for passive badges. -Credential badge reading therefore requires a dedicated NFC reader circuit on the production PCB. +Credential badge reading currently uses a PN532 module in I2C mode (`1 0`) at +the fixed 7-bit address `0x24`. It shares XIAO `SDA` and `SCL` with the PCF8574 +keypad expander at `0x20`. Functions: @@ -38,7 +40,9 @@ Functions: - Badge validation. - Unlock request trigger. -The reader IC, antenna, and matching network still need to be selected, designed, and validated. +The PN532 module and its integrated antenna provide the development reader. +Reader placement, range, interference, and the production PCB implementation +still need physical validation. --- @@ -46,15 +50,16 @@ The reader IC, antenna, and matching network still need to be selected, designed Command: -- Positive pulse on `SIG`. +- 150 ms positive pulse on `SIG` in the current development firmware. Validation: -The firmware must not validate opening based on a fixed delay. +The current firmware monitors the `COM/NC` contact independently for open-state +LED, serial, and buzzer feedback. Correlating that transition with the command +pulse and reporting a timeout is planned; opening must not ultimately be +validated from a fixed delay alone. -Opening validation relies on the `COM/NC` contact state change. - -Sequence: +Target validation sequence: ```text SIG command diff --git a/hardware/pinout.md b/hardware/pinout.md index 59d3ee8..c71b674 100644 --- a/hardware/pinout.md +++ b/hardware/pinout.md @@ -23,9 +23,12 @@ Seeed Studio XIAO BLE nRF52840 | Active buzzer control | Output | D0 | GPIO signal to transistor driver | | Lock NC feedback | Input | D7 | KR-S79 NC contact, internal pull-up enabled | | Lock COM feedback reference | Output | D8 | KR-S79 COM contact, driven low for NC continuity detection | -| Lock SIG command | Output | D9 | Active-high GPIO pulse to S8050/S8550 driver, no PWM | +| Lock SIG command | Output | D9 | 150 ms active-high GPIO pulse to S8050/S8550 driver, no PWM | +| Internal identity reset | Input | P1.11 | Active low with internal pull-up; button to GND | | PCF8574 I2C SDA | I/O | SDA | Development keypad GPIO expander | | PCF8574 I2C SCL | Output | SCL | Development keypad GPIO expander | +| PN532 I2C SDA | I/O | SDA | Shared bus, fixed 7-bit address `0x24` | +| PN532 I2C SCL | Output | SCL | Shared bus, module switches `1 0` | | RGB LED red | Output | Board LED alias `led0` | On-board validation LED | | RGB LED green | Output | Board LED alias `led1` | On-board validation LED | | RGB LED blue | Output | Board LED alias `led2` | On-board validation LED | diff --git a/hardware/reverse-engineering/reverse-engineering.md b/hardware/reverse-engineering/reverse-engineering.md index 5bdd50f..fe688e0 100644 --- a/hardware/reverse-engineering/reverse-engineering.md +++ b/hardware/reverse-engineering/reverse-engineering.md @@ -172,7 +172,9 @@ To be completed. The lock is controlled by the Seeed Studio XIAO BLE nRF52840. -Selected principle: +The current development firmware generates a 150 ms positive `SIG` pulse and +monitors `COM/NC` independently for open-state feedback. The remaining target +for command-result validation is: 1. Generate a positive pulse on `SIG`. 2. Start a timeout. diff --git a/mobile-app/app/README.md b/mobile-app/app/README.md index 5d3ce04..dfa3dcb 100644 --- a/mobile-app/app/README.md +++ b/mobile-app/app/README.md @@ -91,9 +91,13 @@ unused record when needed. modify permanent codes. Random generation excludes every currently active permanent and temporary value. Administrators can also edit permanent codes through the six-digit numeric editor; choosing an existing temporary value - requires confirmation before converting it to permanent. + requires confirmation before converting it to permanent. Every permanent or + temporary code row has a local clipboard action before the edit action; it + remains available while the box is offline. - NFC tag listing, renaming, deletion, manual UID entry, phone NFC scanning, - and enrollment through the box reader. + and a box-reader enrollment modal that scans for 15 seconds, keeps errors + visible, asks for a name only after detecting a new UID, and refreshes the + authoritative tag list after validation. - Named guest creation, invitation QR display, and revocation. - Firmware clock synchronization after connection. - Language override, encrypted backup/restore, factory reset, and local removal diff --git a/mobile-app/app/lib/app_strings.dart b/mobile-app/app/lib/app_strings.dart index e043547..e061a58 100644 --- a/mobile-app/app/lib/app_strings.dart +++ b/mobile-app/app/lib/app_strings.dart @@ -34,6 +34,7 @@ class AppStrings { 'add': {'fr': 'Ajouter', 'en': 'Add'}, 'delete': {'fr': 'Supprimer', 'en': 'Delete'}, 'edit': {'fr': 'Modifier', 'en': 'Edit'}, + 'copy_code': {'fr': 'Copier le code', 'en': 'Copy code'}, 'cancel': {'fr': 'Annuler', 'en': 'Cancel'}, 'save': {'fr': 'Enregistrer', 'en': 'Save'}, 'scan': {'fr': 'Scanner', 'en': 'Scan'}, @@ -129,6 +130,31 @@ class AppStrings { 'tag_name': {'fr': 'Nom du tag', 'en': 'Tag name'}, 'tag_uid': {'fr': 'UID du tag', 'en': 'Tag UID'}, 'pairing_mode': {'fr': 'Mode appairage', 'en': 'Pairing mode'}, + 'nfc_pairing_title': { + 'fr': 'Appairage d’un tag NFC', + 'en': 'Pair an NFC tag', + }, + 'nfc_pairing_wait': { + 'fr': + 'Présentez un nouveau tag au lecteur de la box dans les 15 secondes.', + 'en': 'Hold a new tag near the box reader within 15 seconds.', + }, + 'nfc_pairing_detected': { + 'fr': 'Tag détecté. Choisissez un nom avant de l’autoriser.', + 'en': 'Tag detected. Choose a name before authorizing it.', + }, + 'nfc_pairing_timeout': { + 'fr': 'Aucun tag n’a été détecté dans les 15 secondes.', + 'en': 'No tag was detected within 15 seconds.', + }, + 'nfc_pairing_exists': { + 'fr': 'Ce tag est déjà présent dans la liste des tags autorisés.', + 'en': 'This tag is already in the authorized tag list.', + }, + 'nfc_reader_unavailable': { + 'fr': 'Le lecteur NFC de la box est indisponible.', + 'en': 'The box NFC reader is unavailable.', + }, 'add_manually': {'fr': 'Ajouter manuellement', 'en': 'Add manually'}, 'nfc_unavailable': { 'fr': 'Le NFC est indisponible sur ce téléphone.', diff --git a/mobile-app/app/lib/ble_controller.dart b/mobile-app/app/lib/ble_controller.dart index 2c688ad..e0d2e3a 100644 --- a/mobile-app/app/lib/ble_controller.dart +++ b/mobile-app/app/lib/ble_controller.dart @@ -90,6 +90,9 @@ class OpenParcelBoxBleController extends ChangeNotifier { bool isScanning = false; bool isBusy = false; bool isAuthenticated = false; + bool nfcEnrollmentActive = false; + String? nfcEnrollmentUid; + String? nfcEnrollmentError; String status = 'disconnected'; String? errorMessage; @@ -719,11 +722,11 @@ class OpenParcelBoxBleController extends ChangeNotifier { }), ); - Future startNfcEnrollment(String name) => send( - OpenParcelBoxCommand('start_nfc_enrollment', { - 'name': name.trim(), - }), - ); + Future startNfcEnrollment() => + send(const OpenParcelBoxCommand('start_nfc_enrollment')); + + Future cancelNfcEnrollment() => + send(const OpenParcelBoxCommand('cancel_nfc_enrollment')); Future removeNfcTag(NfcTag tag) => send( OpenParcelBoxCommand('remove_nfc_tag', {'uid': tag.uid}), @@ -932,6 +935,18 @@ class OpenParcelBoxBleController extends ChangeNotifier { void _applyState(Map state, {bool notify = true}) { _clearCollections(); + final enrollment = state['nfc_enrollment']; + if (enrollment is Map) { + nfcEnrollmentActive = enrollment['active'] == true; + final uid = enrollment['uid']; + nfcEnrollmentUid = uid is String && uid.isNotEmpty ? uid : null; + final error = enrollment['error']; + nfcEnrollmentError = error is String && error.isNotEmpty ? error : null; + } else { + nfcEnrollmentActive = false; + nfcEnrollmentUid = null; + nfcEnrollmentError = null; + } for (final item in (state['codes'] as List? ?? const [])) { if (item is! Map || item['code'] is! String || @@ -1041,6 +1056,9 @@ class OpenParcelBoxBleController extends ChangeNotifier { nfcTags.clear(); history.clear(); guests.clear(); + nfcEnrollmentActive = false; + nfcEnrollmentUid = null; + nfcEnrollmentError = null; } String _generateUniqueCode() { diff --git a/mobile-app/app/lib/main.dart b/mobile-app/app/lib/main.dart index a6d5fcb..1322111 100644 --- a/mobile-app/app/lib/main.dart +++ b/mobile-app/app/lib/main.dart @@ -607,37 +607,44 @@ class _OpenParcelBoxHomeState extends State { (code) => _DataRowCard( icon: Icons.password, title: code.code, - actions: hasModifyPermission - ? [ - if (kind == CredentialKind.permanent) - IconButton( - onPressed: canModify - ? () async { - final changed = await _editPermanentCode( - code, - ); - if (changed) { - setModalState(() {}); - } - } - : null, - icon: const Icon(Icons.edit_outlined), - tooltip: strings.text('edit_code'), - ), - IconButton( - onPressed: canModify - ? () async { - await _guarded( - () => _controller.removeCode(code), - ); + actions: [ + IconButton( + key: ValueKey('copy-code-${code.id}'), + onPressed: () => + Clipboard.setData(ClipboardData(text: code.code)), + icon: const Icon(Icons.copy_outlined), + tooltip: strings.text('copy_code'), + ), + if (hasModifyPermission) ...[ + if (kind == CredentialKind.permanent) + IconButton( + onPressed: canModify + ? () async { + final changed = await _editPermanentCode( + code, + ); + if (changed) { setModalState(() {}); } - : null, - icon: const Icon(Icons.delete_outline), - tooltip: strings.text('delete'), - ), - ] - : const [], + } + : null, + icon: const Icon(Icons.edit_outlined), + tooltip: strings.text('edit_code'), + ), + IconButton( + onPressed: canModify + ? () async { + await _guarded( + () => _controller.removeCode(code), + ); + setModalState(() {}); + } + : null, + icon: const Icon(Icons.delete_outline), + tooltip: strings.text('delete'), + ), + ], + ], ), ), ], @@ -831,12 +838,13 @@ class _OpenParcelBoxHomeState extends State { } Future _addNfcFromBox(StateSetter refreshModal) async { - final name = await _askText( - strings.text('tag_name'), - description: strings.text('pairing_name_help'), + final added = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => + NfcEnrollmentDialog(controller: _controller, strings: strings), ); - if (name != null) { - await _guarded(() => _controller.startNfcEnrollment(name)); + if (added == true && mounted) { refreshModal(() {}); } } @@ -2006,6 +2014,162 @@ class _EmptyRow extends StatelessWidget { ); } +class NfcEnrollmentDialog extends StatefulWidget { + const NfcEnrollmentDialog({ + required this.controller, + required this.strings, + super.key, + }); + + final OpenParcelBoxBleController controller; + final AppStrings strings; + + @override + State createState() => _NfcEnrollmentDialogState(); +} + +class _NfcEnrollmentDialogState extends State { + final TextEditingController _nameController = TextEditingController(); + String? _localError; + bool _saving = false; + bool _completed = false; + + @override + void initState() { + super.initState(); + unawaited(_start()); + } + + Future _start() async { + try { + await widget.controller.startNfcEnrollment(); + } catch (error) { + if (mounted) setState(() => _localError = '$error'); + } + } + + String _firmwareError(String error) => switch (error) { + 'timeout' => widget.strings.text('nfc_pairing_timeout'), + 'already_exists' => widget.strings.text('nfc_pairing_exists'), + 'reader_unavailable' => widget.strings.text('nfc_reader_unavailable'), + _ => error, + }; + + Future _cancel() async { + try { + await widget.controller.cancelNfcEnrollment(); + } catch (_) { + // Closing the local modal must remain possible after a disconnect. + } + if (mounted) Navigator.of(context).pop(false); + } + + Future _save(String uid) async { + final name = _nameController.text.trim(); + if (name.isEmpty || _saving) return; + setState(() { + _saving = true; + _localError = null; + }); + try { + await widget.controller.addNfcTag(uid, name); + _completed = true; + if (mounted) Navigator.of(context).pop(true); + } catch (error) { + if (mounted) { + setState(() { + _saving = false; + _localError = '$error'; + }); + } + } + } + + @override + void dispose() { + _nameController.dispose(); + if (!_completed && widget.controller.nfcEnrollmentActive) { + unawaited(widget.controller.cancelNfcEnrollment().catchError((_) {})); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) => ListenableBuilder( + listenable: widget.controller, + builder: (context, child) { + final uid = widget.controller.nfcEnrollmentUid; + final firmwareError = widget.controller.nfcEnrollmentError; + final error = + _localError ?? + (firmwareError == null ? null : _firmwareError(firmwareError)); + + return AlertDialog( + key: const ValueKey('nfc-enrollment-modal'), + title: Text(widget.strings.text('nfc_pairing_title')), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (uid == null && error == null) ...[ + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 18), + Text(widget.strings.text('nfc_pairing_wait')), + ], + if (uid != null) ...[ + Text(widget.strings.text('nfc_pairing_detected')), + const SizedBox(height: 8), + Text(uid, key: const ValueKey('nfc-enrollment-uid')), + const SizedBox(height: 12), + TextField( + key: const ValueKey('nfc-enrollment-name'), + controller: _nameController, + autofocus: true, + maxLength: 31, + decoration: InputDecoration( + labelText: widget.strings.text('tag_name'), + ), + onChanged: (_) => setState(() {}), + onSubmitted: (_) => _save(uid), + ), + ], + if (error != null) ...[ + Text( + error, + key: const ValueKey('nfc-enrollment-error'), + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: _saving ? null : _cancel, + child: Text( + error == null + ? widget.strings.text('cancel') + : widget.strings.text('close'), + ), + ), + if (uid != null) + FilledButton( + key: const ValueKey('nfc-enrollment-save'), + onPressed: _saving || _nameController.text.trim().isEmpty + ? null + : () => _save(uid), + child: _saving + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(widget.strings.text('save')), + ), + ], + ); + }, + ); +} + class _RegistrationStatus extends StatelessWidget { const _RegistrationStatus({ required this.error, diff --git a/mobile-app/app/test/widget_test.dart b/mobile-app/app/test/widget_test.dart index 8c2925d..b5d56bc 100644 --- a/mobile-app/app/test/widget_test.dart +++ b/mobile-app/app/test/widget_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:app/backup_service.dart'; +import 'package:app/app_strings.dart'; import 'package:app/ble_controller.dart'; import 'package:app/main.dart'; import 'package:app/models.dart'; @@ -531,6 +532,82 @@ void main() { controller.dispose(); }); + test('NFC enrollment state and commands use the two-step box flow', () async { + const box = SavedBox( + remoteId: 'AA:BB', + name: 'Front gate', + identityKey: '1234', + role: BoxRole.administrator, + ); + final controller = OpenParcelBoxBleController( + store: _MemoryStore( + boxes: const [box], + cachedStates: >{ + box.identityKey: { + 'nfc_enrollment': { + 'active': false, + 'uid': '60:4F:E2:B5', + 'error': '', + }, + }, + }, + ), + reconnectOnInitialize: false, + ); + await controller.initialize(); + expect(controller.nfcEnrollmentUid, '60:4F:E2:B5'); + expect(controller.nfcEnrollmentError, isNull); + controller.dispose(); + + final recording = _RecordingController(random: _SequenceRandom([])); + await recording.startNfcEnrollment(); + expect(recording.lastCommand?.name, 'start_nfc_enrollment'); + expect(recording.lastCommand?.payload, isEmpty); + await recording.cancelNfcEnrollment(); + expect(recording.lastCommand?.name, 'cancel_nfc_enrollment'); + recording.dispose(); + }); + + testWidgets('NFC enrollment keeps errors visible then shows naming field', ( + WidgetTester tester, + ) async { + final controller = _RecordingController(random: _SequenceRandom([])) + ..nfcEnrollmentError = 'timeout'; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: NfcEnrollmentDialog( + controller: controller, + strings: AppStrings(const Locale('en')), + ), + ), + ), + ); + await tester.pump(); + expect( + find.byKey(const ValueKey('nfc-enrollment-modal')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('nfc-enrollment-error')), + findsOneWidget, + ); + expect(find.text('No tag was detected within 15 seconds.'), findsOneWidget); + + controller + ..nfcEnrollmentError = null + ..nfcEnrollmentUid = '60:4F:E2:B5' + ..notifyListeners(); + await tester.pump(); + expect( + find.byKey(const ValueKey('nfc-enrollment-name')), + findsOneWidget, + ); + expect(find.text('60:4F:E2:B5'), findsOneWidget); + await tester.pumpWidget(const SizedBox.shrink()); + controller.dispose(); + }); + testWidgets('administrator compact actions share one bottom row', ( WidgetTester tester, ) async { @@ -689,6 +766,88 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('permanent and temporary codes can be copied', ( + WidgetTester tester, + ) async { + String? clipboardText; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (MethodCall call) async { + if (call.method == 'Clipboard.setData') { + clipboardText = + (call.arguments as Map)['text'] as String?; + } + return null; + }, + ); + addTearDown( + () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + null, + ), + ); + await tester.binding.setSurfaceSize(const Size(400, 800)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + MyApp( + store: _MemoryStore( + boxes: const [ + SavedBox( + remoteId: 'AA:BB', + name: 'Front gate', + identityKey: 'ADMIN', + role: BoxRole.administrator, + ), + ], + cachedStates: >{ + 'ADMIN': { + 'codes': >[ + { + 'slot': 0, + 'code': '123456', + 'kind': 'permanent', + }, + { + 'slot': 1, + 'code': '654321', + 'kind': 'one_time', + }, + ], + }, + }, + ), + splashDuration: Duration.zero, + ), + ); + await tester.pumpAndSettle(); + + await tester.tap( + find.byKey(const ValueKey('permanent-code-action')), + ); + await tester.pumpAndSettle(); + final copyButton = find.byTooltip('Copy code'); + expect(copyButton, findsOneWidget); + expect( + tester.getRect(copyButton).left, + lessThan(tester.getRect(find.byTooltip('Edit code')).left), + ); + await tester.tap(copyButton); + await tester.pump(); + expect(clipboardText, '123456'); + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + + await tester.tap( + find.byKey(const ValueKey('temporary-code-action')), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byTooltip('Copy code')); + await tester.pump(); + expect(clipboardText, '654321'); + expect(tester.takeException(), isNull); + }); + testWidgets('registration opens the styled setup modal', ( WidgetTester tester, ) async {