feat(firmware): add nfc and update all documentation

This commit is contained in:
2026-07-22 19:33:04 +02:00
parent 201187b752
commit 44b5b49df1
33 changed files with 1186 additions and 222 deletions
+1
View File
@@ -2,6 +2,7 @@
# VSCodium / VSCode # VSCodium / VSCode
# ---------------------------- # ----------------------------
.vscode/ .vscode/
OpenParcelBox.code-workspace
# ---------------------------- # ----------------------------
# Codex # Codex
+21
View File
@@ -10,6 +10,21 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
### Fixed ### 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 - Prevented generated permanent and temporary codes from colliding with any
active code, including across code kinds, and made firmware reject exclusive active code, including across code kinds, and made firmware reject exclusive
creation when the value already exists. 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
- 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 - Internal identity-reset button on XIAO `P1.11`: holding it for three seconds
clears administrator and guest app identities plus BLE bonds without erasing clears administrator and guest app identities plus BLE bonds without erasing
access codes, NFC tags, history, or the box name. access codes, NFC tags, history, or the box name.
+10 -6
View File
@@ -45,11 +45,14 @@ Completed or validated:
- BLE LE Secure Connections, encrypted GATT access, and 128-bit phone identities - BLE LE Secure Connections, encrypted GATT access, and 128-bit phone identities
- First-phone administrator provisioning and named guest identities - First-phone administrator provisioning and named guest identities
- App-triggered NFC enrollment with stored tag names - 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` - KR-S79 `COM/NC` lock state feedback on XIAO `D7` / `D8`
- Internal identity-reset button on XIAO `P1.11` - Internal identity-reset button on XIAO `P1.11`
- Green-open, off-closed, red-invalid LED feedback - 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 - Door opened / closed serial debug messages
- Firmware debug output over serial console - Firmware debug output over serial console
- One-second white RGB confirmation when the local keypad path is ready - One-second white RGB confirmation when the local keypad path is ready
@@ -58,14 +61,13 @@ In progress:
- Original hardware reverse engineering - Original hardware reverse engineering
- Production keypad mapping - Production keypad mapping
- NFC credential reader hardware selection - PN532 reader hardware validation and production integration
- Hardware validation - Hardware validation
- BLE protocol documentation and hardening - BLE security and hardware interoperability validation
Planned: Planned:
- Firmware update from the mobile application - Firmware update from the mobile application
- NFC badge unlock with a dedicated reader circuit
- Battery management - Battery management
- Zigbee - Zigbee
- OTA updates - OTA updates
@@ -144,7 +146,9 @@ Current development platform:
- KR-S79 `COM` feedback reference on XIAO `D8` - KR-S79 `COM` feedback reference on XIAO `D8`
- Internal identity-reset button between `GND` and XIAO `P1.11` - 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. The production hardware is planned around a custom PCB and the original parcel box mechanical parts.
+56 -9
View File
@@ -44,7 +44,7 @@ The firmware is based on Zephyr RTOS and follows a modular architecture to simpl
| Persistent access code storage | Done | | Persistent access code storage | Done |
| Battery driver | Pending | | Battery driver | Pending |
| NFC credential storage and scan API | Done | | NFC credential storage and scan API | Done |
| Dedicated NFC reader driver | Pending | | PN532 NFC reader driver | Done |
| Power management | Pending | | Power management | Pending |
| Bluetooth configuration | In progress | | Bluetooth configuration | In progress |
| Zigbee integration | Pending | | Zigbee integration | Pending |
@@ -117,10 +117,10 @@ Objective: validate every reusable or newly added hardware component before inte
Components: 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. - Door or lock state feedback through the KR-S79 `COM/NC` contact.
- Battery monitoring. - Battery monitoring.
- Dedicated NFC reader circuit and antenna. - PN532 development reader and production antenna/placement validation.
- Buzzer. - Buzzer.
- External LEDs. - External LEDs.
@@ -151,7 +151,7 @@ Components:
- Battery. - Battery.
- Buzzer. - Buzzer.
- NFC credential storage. - NFC credential storage.
- Dedicated NFC reader backend. - PN532 I2C reader backend.
Security: Security:
@@ -231,9 +231,12 @@ Implemented MVP pieces:
- Keypad-based unlock flow. - Keypad-based unlock flow.
- Persistent six-digit code table. - Persistent six-digit code table.
- Default development unlock code. - Default development unlock code.
- Lock command pulse. - 150 ms active-high lock command pulse with no startup pulse.
- Lock state feedback. - Lock state feedback.
- Audible and visual feedback for valid codes, invalid codes, and open state. - 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. At this stage, the original proprietary electronics are no longer required.
@@ -264,6 +267,7 @@ Implemented so far:
- Permanent code management screen. - Permanent code management screen.
- One-time temporary code management screen. - One-time temporary code management screen.
- NFC tag add/remove 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. - Stored code and NFC tag readback from the firmware BLE state characteristic.
- Branded Android launcher icon and native splash screen. - Branded Android launcher icon and native splash screen.
- Automatic French/English selection with a manual override. - Automatic French/English selection with a manual override.
@@ -273,6 +277,10 @@ Implemented so far:
- AES-256-GCM password-protected application backup and restore. - AES-256-GCM password-protected application backup and restore.
- Opening history, code limits, phone-side NFC scans, and destructive-action - Opening history, code limits, phone-side NFC scans, and destructive-action
warnings. 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. Status: in progress.
@@ -290,7 +298,18 @@ Features:
- Permanent NFC badges through dedicated reader hardware. - Permanent NFC badges through dedicated reader hardware.
- User permissions. - 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 opened.
- Door closed. - 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. - One-time PINs.
- Temporary NFC badges through dedicated reader hardware. - 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. - Hardware documentation.
- Firmware 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.
--- ---
+13 -11
View File
@@ -72,7 +72,7 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] Lock driver. - [x] Lock driver.
- [x] Door or lock state driver. - [x] Door or lock state driver.
- [ ] Battery driver. - [ ] Battery driver.
- [ ] NFC driver. - [x] PN532 NFC driver over I2C.
### Documentation ### Documentation
@@ -83,7 +83,7 @@ Tasks are grouped by development phase and updated throughout the project.
- [ ] led.md. - [ ] led.md.
- [x] lock.md. - [x] lock.md.
- [ ] battery.md. - [ ] battery.md.
- [ ] nfc.md. - [x] nfc.md.
--- ---
@@ -111,10 +111,10 @@ Tasks are grouped by development phase and updated throughout the project.
### NFC ### NFC
- [ ] Select dedicated NFC reader IC. - [x] Select PN532 development NFC reader.
- [ ] Design reader antenna. - [ ] Design reader antenna.
- [ ] Validate reader tuning network. - [ ] Validate reader tuning network.
- [ ] Read UID through the dedicated reader. - [x] Read UID through the PN532 reader.
- [ ] Detect badge removal. - [ ] Detect badge removal.
### Buzzer ### Buzzer
@@ -130,8 +130,8 @@ Tasks are grouped by development phase and updated throughout the project.
### Initialization ### Initialization
- [ ] Complete startup sequence. - [x] Keep the local keypad path available before Settings and BLE startup.
- [ ] Initialize all peripherals. - [x] Initialize all currently implemented peripherals.
- [ ] Standardize error handling. - [ ] Standardize error handling.
### Access Control ### Access Control
@@ -170,12 +170,12 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] Door or lock feedback. - [x] Door or lock feedback.
- [ ] Battery monitoring. - [ ] Battery monitoring.
- [ ] Administrator PIN. - [ ] Administrator PIN.
- [ ] Administrator NFC badge through dedicated reader hardware. - [x] Administrator NFC badge through the PN532 reader.
- [x] Audible feedback. - [x] Audible feedback.
- [ ] Standalone operation. - [x] Standalone local keypad and NFC operation.
- [ ] Battery-powered operation. - [ ] Battery-powered operation.
- [ ] No cloud. - [x] No mandatory cloud dependency.
- [ ] No subscription. - [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 Bluetooth scan and connect UI.
- [x] Mobile clock-sync command. - [x] Mobile clock-sync command.
- [x] Mobile random permanent and one-time code management UI. - [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 NFC tag add/remove UI.
- [x] Mobile direct lock-open command UI. - [x] Mobile direct lock-open command UI.
- [x] Mobile stored-state readback from firmware. - [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. - [ ] Validate pairing recovery and guest invitation on Android and iOS hardware.
- [ ] Zigbee connectivity. - [ ] Zigbee connectivity.
- [ ] Home Assistant integration. - [ ] Home Assistant integration.
- [ ] Temporary access. - [x] One-time temporary PINs.
- [ ] Time-limited and usage-count temporary credentials.
- [ ] Dedicated PCB. - [ ] Dedicated PCB.
- [ ] Field validation. - [ ] Field validation.
- [ ] Complete user, maker, and developer documentation. - [ ] Complete user, maker, and developer documentation.
+61 -31
View File
@@ -20,21 +20,18 @@ Main goals:
## Overview ## Overview
```text ```text
Home Assistant Flutter mobile application
| |
Zigbee (ZHA / Zigbee2MQTT) Secure BLE administration
| |
OpenParcelBox OpenParcelBox firmware on Zephyr RTOS
| +-- Keypad and access codes
+-- Zephyr RTOS +-- PN532 NFC reader and stored tags
+-- Seeed Studio XIAO nRF52840 +-- Lock command and COM/NC feedback
+-- Access Control +-- Audible, visual, and serial diagnostics
+-- Keypad
+-- NFC Planned: Home Assistant via Zigbee (ZHA / Zigbee2MQTT)
+-- Lock Controller Planned: battery manager and secure firmware update
+-- Power Manager
+-- Diagnostics
+-- OTA
``` ```
--- ---
@@ -82,11 +79,14 @@ Responsibilities:
- Keypad scanning. - Keypad scanning.
- NFC credential coordination. - NFC credential coordination.
- Lock control. - Lock control.
- Battery monitoring.
- RGB LED status. - RGB LED status.
- Active buzzer feedback. - Active buzzer feedback.
Planned responsibilities:
- Battery monitoring and system sleep.
- Zigbee communication. - Zigbee communication.
- OTA updates. - Secure firmware updates.
--- ---
@@ -114,37 +114,60 @@ The firmware is organized into independent modules.
### NFC ### NFC
- UID validation. - 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 ### Access Control
- Administrator codes. - Eight permanent six-digit codes.
- Permanent codes. - Twenty one-time six-digit codes.
- Temporary codes. - Ten named NFC tags.
- Anti brute-force logic. - Administrator and named guest mobile identities.
Attempt limiting and more granular credential ownership remain planned.
### Lock Controller ### Lock Controller
- Lock actuation. - 150 ms lock actuation pulse.
- Opening validation. - Continuous `COM/NC` state feedback.
- Error handling. - 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. - Battery monitoring.
- Deep sleep. - Deep sleep.
- Wake-up handling. - Wake-up handling.
- Power optimization. - Power optimization.
### Zigbee ### Zigbee (Planned)
- Home Assistant communication. - Home Assistant communication.
- State publication. - State publication.
- Command reception. - Command reception.
### OTA ### OTA (Planned)
- Download. - Download.
- Verification. - 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: Sequence:
@@ -229,8 +258,9 @@ Core build tools:
## Future Work ## Future Work
- Local event history.
- Advanced user management. - Advanced user management.
- Hardware diagnostics. - 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. - Support for multiple parcel box variants.
+19 -3
View File
@@ -314,6 +314,18 @@ Example:
"name": "Alice" "name": "Alice"
} }
], ],
"nfc_enrollment": {
"active": false,
"uid": "",
"error": ""
},
"history": [
{
"unix_ms": 1784135045000,
"kind": 2,
"actor": "Alice"
}
],
"guests": [ "guests": [
{ {
"name": "Delivery team", "name": "Delivery team",
@@ -357,12 +369,16 @@ NFC enrollment is started with:
```json ```json
{ {
"command": "start_nfc_enrollment", "command": "start_nfc_enrollment",
"identity_key": "<32 hexadecimal characters>", "identity_key": "<32 hexadecimal characters>"
"name": "Alice"
} }
``` ```
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 When the phone has synchronized the clock, lock-opening logs include a Unix
timestamp in milliseconds. timestamp in milliseconds.
+7 -2
View File
@@ -39,10 +39,15 @@ void buzzer_off(void);
- `buzzer_init()` configures the buzzer control GPIO and leaves the buzzer off. - `buzzer_init()` configures the buzzer control GPIO and leaves the buzzer off.
- `buzzer_beep_key()` emits a short blocking beep. - `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. - 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. - 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.
--- ---
+7 -9
View File
@@ -15,13 +15,12 @@ The firmware currently supports the following debugging methods:
- UART console (`printf`) - UART console (`printf`)
- On-board RGB LED - On-board RGB LED
- Bluetooth state and command diagnostics - Bluetooth state and command diagnostics
- Zephyr Shell (when enabled) - PN532/NFC UART diagnostics
Additional methods may be added later: Additional methods may be added later:
- Zigbee diagnostics - Zigbee diagnostics
- Home Assistant diagnostic entities - Home Assistant diagnostic entities
- NFC diagnostics
--- ---
@@ -46,10 +45,9 @@ The onboard RGB LED is used to quickly identify the firmware state.
## Startup ## Startup
| Color | Meaning | 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
| Off | Startup in progress | do not currently use a dedicated fatal LED state.
| Magenta | Initialization failed |
--- ---
@@ -71,7 +69,6 @@ and keypad initialization is retried once per second.
| Green | Lock state feedback reports open | | Green | Lock state feedback reports open |
| Blue | NFC credential scan mode active | | Blue | NFC credential scan mode active |
| Red | Invalid six-digit access code | | Red | Invalid six-digit access code |
| Magenta | Fatal initialization error |
--- ---
@@ -112,11 +109,12 @@ Door opened timestamp_ms=1784135045050
Lock state: Lock state:
```text ```text
Door opened Door opened timestamp_ms=1784135045050
Door open reminder beep
Door closed Door closed
``` ```
Future dedicated NFC reader backend: PN532 reader:
```text ```text
Scan NFC: ON Scan NFC: ON
+4 -3
View File
@@ -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. 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 ## Used By
- Keypad - Keypad
- Future lock module (if additional GPIO are required)
- Future peripherals connected through an external GPIO expander - 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. 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. This behavior is fully handled by this module and should remain transparent to higher-level drivers.
+4 -2
View File
@@ -69,6 +69,9 @@ Each key press is reported only once.
- Single key detection - Single key detection
- Anti-repeat - Anti-repeat
- Modular GPIO Expander interface - 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. - Support for the production 2x6 keypad.
- Configurable key mapping. - Configurable key mapping.
- Optional audible feedback handled by the buzzer module. - Unit tests.
- Unit tests.
+6 -4
View File
@@ -35,7 +35,7 @@ int lock_control_open(void);
- `lock_control_init()` configures D9 directly as an inactive GPIO output. - `lock_control_init()` configures D9 directly as an inactive GPIO output.
- `lock_control_init()` never sends an unlock pulse. - `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. - All GPIO return codes used by the lock control path are logged.
- No PWM is used. - 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. - Codes are persisted through Zephyr settings with the NVS backend.
- The development default code is `784512`. - The development default code is `784512`.
- A valid six-digit entry triggers `lock_control_open()`. - 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. - `*`, `#`, and `B` clear the current entry.
- A valid six-digit entry triggers one long success beep before the opening pulse. - 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. - 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. - The first keypad press starts one non-renewable 15-second NFC scan window,
- 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. 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. - 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. - 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`. - 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. - LEDs stay off during startup and while the lock is closed.
+32 -19
View File
@@ -4,11 +4,16 @@
This module provides the application-level NFC credential flow and persistent NFC tag UID storage. 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. 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 ## Scan Mode
The NFC reader polling flow is temporarily disabled in the production main loop The firmware keeps the PN532 in its I2C-wakeable power-down mode by default.
while the local keypad/BLE startup baseline is being validated. Persistent tag
storage and the BLE management protocol remain compiled and initialized.
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 start a separate 15-second enrollment scan
from the mobile application. This explicit administration flow is the only
An authenticated administrator can also start enrollment from the mobile exception to keypad-triggered scanning. A new UID is returned through BLE but
application. The next UID reported by the reader is stored with the supplied is not authorized until the administrator enters a name and validates it in
name and scan mode stops immediately. 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. 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. - 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. - A valid UID triggers the same success beep and unlock path as a valid keypad code.
- After a valid NFC unlock through the dedicated reader backend, scan mode is disabled immediately. - After a valid NFC unlock, scan mode is disabled immediately.
- An invalid UID is reported over UART and scan mode remains active until timeout or a valid tag. - 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: 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. - Interface selection: `I2C 1 0` on the module switches.
- Bus: XIAO `SDA` / `SCL`, shared with the PCF8574 at `0x20`.
The hardware-specific tag detection backend is still a placeholder. It currently returns no detected UID until a dedicated NFC reader implementation is added. - 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.
+23 -23
View File
@@ -19,17 +19,17 @@
</defs> </defs>
<rect width="1600" height="1000" fill="#fafafa"/> <rect width="1600" height="1000" fill="#fafafa"/>
<text x="800" y="55" text-anchor="middle" class="title">OpenParcelBox — commande temporaire de SIG avec S8550 + S8050</text> <text x="800" y="55" text-anchor="middle" class="title">OpenParcelBox — temporary SIG driver with S8550 + S8050</text>
<text x="800" y="88" text-anchor="middle" class="txt">Seeed Studio XIAO BLE nRF52840 • serrure KR-S79 • stack 8×AAA ≈ 13 V</text> <text x="800" y="88" text-anchor="middle" class="txt">Seeed Studio XIAO BLE nRF52840 • KR-S79 lock • 8×AAA pack ≈ 13 V</text>
<!-- XIAO --> <!-- XIAO -->
<rect x="70" y="170" width="330" height="390" class="box"/> <rect x="70" y="170" width="330" height="390" class="box"/>
<text x="235" y="215" text-anchor="middle" class="h2">XIAO BLE nRF52840</text> <text x="235" y="215" text-anchor="middle" class="h2">XIAO BLE nRF52840</text>
<text x="95" y="300" class="label">D7 — commande serrure</text> <text x="95" y="300" class="label">D9 lock command</text>
<circle cx="400" cy="292" r="7" class="node"/> <circle cx="400" cy="292" r="7" class="node"/>
<text x="95" y="505" class="label">GND</text> <text x="95" y="505" class="label">GND</text>
<circle cx="400" cy="500" r="7" class="node"/> <circle cx="400" cy="500" r="7" class="node"/>
<text x="95" y="545" class="small">Alimentation USB-C pendant le développement</text> <text x="95" y="545" class="small">USB-C power during development</text>
<!-- NPN base resistor --> <!-- NPN base resistor -->
<path d="M400 292 H500" class="logic"/> <path d="M400 292 H500" class="logic"/>
@@ -67,7 +67,7 @@
<path d="M1190 640 H1270" class="wire"/> <path d="M1190 640 H1270" class="wire"/>
<path d="M1205 655 H1255" class="wire"/> <path d="M1205 655 H1255" class="wire"/>
<path d="M1220 670 H1240" class="wire"/> <path d="M1220 670 H1240" class="wire"/>
<text x="1285" y="595" class="label">GND commun</text> <text x="1285" y="595" class="label">Common GND</text>
<!-- PNP drive resistor --> <!-- PNP drive resistor -->
<path d="M845 235 H930" class="wire"/> <path d="M845 235 H930" class="wire"/>
@@ -105,34 +105,34 @@
<!-- Lock box --> <!-- Lock box -->
<rect x="1290" y="250" width="250" height="300" class="box"/> <rect x="1290" y="250" width="250" height="300" class="box"/>
<text x="1415" y="292" text-anchor="middle" class="h2">SERRURE</text> <text x="1415" y="292" text-anchor="middle" class="h2">LOCK</text>
<text x="1415" y="322" text-anchor="middle" class="small">KERONG KR-S79_20-JST</text> <text x="1415" y="322" text-anchor="middle" class="small">KERONG KR-S79_20-JST</text>
<text x="1315" y="385" class="label">SIG jaune</text> <text x="1315" y="385" class="label">Yellow SIG</text>
<circle cx="1290" cy="390" r="7" class="node"/> <circle cx="1290" cy="390" r="7" class="node"/>
<path d="M1225 390 H1290" class="signal"/> <path d="M1225 390 H1290" class="signal"/>
<text x="1315" y="445" class="label">+ rouge → +13 V</text> <text x="1315" y="445" class="label">Red + → +13 V</text>
<path d="M1430 125 V425 H1290" class="power"/> <path d="M1430 125 V425 H1290" class="power"/>
<circle cx="1290" cy="425" r="7" class="node"/> <circle cx="1290" cy="425" r="7" class="node"/>
<text x="1315" y="505" class="label"> orange → GND</text> <text x="1315" y="505" class="label">Orange → GND</text>
<path d="M1290 500 V560" class="wire"/> <path d="M1290 500 V560" class="wire"/>
<circle cx="1290" cy="500" r="7" class="node"/> <circle cx="1290" cy="500" r="7" class="node"/>
<!-- Functional notes --> <!-- Functional notes -->
<rect x="70" y="630" width="700" height="300" class="box"/> <rect x="70" y="630" width="700" height="300" class="box"/>
<text x="95" y="675" class="h2">Fonctionnement</text> <text x="95" y="675" class="h2">Operation</text>
<text x="100" y="720" class="txt">D7 = LOW : Q2 bloqué → R3 remonte la base de Q1 à +13 V → Q1 bloqué → SIG non alimenté.</text> <text x="100" y="720" class="txt">D9 = LOW: Q2 off → R3 pulls the Q1 base to +13 V → Q1 off → SIG not powered.</text>
<text x="100" y="770" class="txt">D7 = HIGH : Q2 conduit → courant limité par R2 → base de Q1 abaissée denviron 0,7 V</text> <text x="100" y="770" class="txt">D9 = HIGH: Q2 conducts → R2 limits current → Q1 base about 0.7 V below its emitter</text>
<text x="100" y="805" class="txt">sous son émetteur → Q1 conduit → environ +13 V appliqué sur SIG.</text> <text x="100" y="805" class="txt">→ Q1 conducts → approximately +13 V is applied to SIG.</text>
<text x="100" y="855" class="txt">R4 maintient Q2 bloqué pendant le démarrage ou si D7 est flottante.</text> <text x="100" y="855" class="txt">R4 keeps Q2 off during startup or if D9 is floating.</text>
<text x="100" y="900" class="note">Important : ne jamais relier directement D7 à la base du S8550.</text> <text x="100" y="900" class="note">Important: never connect D9 directly to the S8550 base.</text>
<!-- Verification notes --> <!-- Verification notes -->
<rect x="810" y="630" width="730" height="300" class="box"/> <rect x="810" y="630" width="730" height="300" class="box"/>
<text x="835" y="675" class="h2">Vérifications avant câblage</text> <text x="835" y="675" class="h2">Checks before wiring</text>
<text x="840" y="720" class="txt">• Identifier physiquement E, B et C de chaque transistor avec la fiche du fabricant ou un multimètre.</text> <text x="840" y="720" class="txt">• Identify E, B, and C on each transistor from its datasheet or with a multimeter.</text>
<text x="840" y="760" class="txt">Les boîtiers S8050/S8550 ne garantissent pas tous le même ordre de broches.</text> <text x="840" y="760" class="txt">S8050/S8550 packages do not all guarantee the same pin order.</text>
<text x="840" y="800" class="txt">Vérifier la polarité du stack : rouge = +13 V, noir = GND.</text> <text x="840" y="800" class="txt">Check pack polarity: red = +13 V, black = GND.</text>
<text x="840" y="840" class="txt">Relier le GND du XIAO au GND du stack avant dactiver D7.</text> <text x="840" y="840" class="txt">Connect XIAO GND to pack GND before driving D9.</text>
<text x="840" y="880" class="txt">Tester dabord avec une impulsion courte (≈100 ms) et mesurer SIG au multimètre.</text> <text x="840" y="880" class="txt">Firmware applies a 150 ms pulse; measure SIG with a multimeter.</text>
<text x="840" y="915" class="note">Ce schéma ne suppose aucun brochage physique gauche/centre/droite.</text> <text x="840" y="915" class="note">This diagram assumes no physical left/center/right transistor pin order.</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

+17 -11
View File
@@ -11,11 +11,15 @@ The firmware is responsible for:
- Keypad management. - Keypad management.
- Access code handling. - Access code handling.
- Lock mechanism control. - Lock mechanism control.
- Battery monitoring.
- Zigbee communication.
- OTA updates.
- Home Assistant integration.
- Audible and visual feedback. - 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 ### 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 ### Lock State
@@ -98,9 +103,9 @@ unlock codes.
### NFC Tags ### NFC Tags
Persistent storage for NFC tag UIDs and the application-level scan-mode API. 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
The XIAO nRF52840 integrated NFCT peripheral is tag-side NFC-A hardware. Passive badge UID reading will require a dedicated NFC reader backend. 15-second scan window; the reader remains in power-down mode otherwise.
### Bluetooth ### Bluetooth
@@ -135,13 +140,14 @@ Implemented:
- Active buzzer feedback on accepted key presses. - Active buzzer feedback on accepted key presses.
- Long success beep on valid access code. - Long success beep on valid access code.
- Three short beeps on invalid 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 six-digit access code storage.
- Persistent NFC tag UID storage with default development UID `60:4F:E2:B5`. - 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. - 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. - One-time access codes removed automatically after first successful keypad use.
- Runtime lock-opening logs with phone-synchronized timestamps when available. - Runtime lock-opening logs with phone-synchronized timestamps when available.
- NFC credential scan-mode API retained while production polling remains - PN532 UID polling, mobile enrollment, stored-tag unlock, history, and
temporarily disabled pending a dedicated reader backend. I2C-wakeable power-down outside fixed 15-second scan windows.
- Lock command pulse on XIAO pin `D9`. - Lock command pulse on XIAO pin `D9`.
- Lock state feedback through the KR-S79 `COM/NC` contact. - Lock state feedback through the KR-S79 `COM/NC` contact.
- Door opened / closed debug output. - Door opened / closed debug output.
@@ -151,6 +157,6 @@ In progress or planned:
- Production keypad mapping. - Production keypad mapping.
- Battery monitoring. - Battery monitoring.
- Dedicated NFC reader backend for passive badge UID detection. - Physical PN532 range/interference validation and production reader layout.
- Zigbee. - Zigbee.
- OTA updates. - OTA updates.
+5
View File
@@ -24,6 +24,11 @@
#define GPIO_EXPANDER_I2C_NODE DT_NODELABEL(i2c1) #define GPIO_EXPANDER_I2C_NODE DT_NODELABEL(i2c1)
#define GPIO_EXPANDER_ADDRESS 0x20 #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 */ /* Buzzer */
#define BUZZER_GPIO_NODE DT_NODELABEL(gpio0) #define BUZZER_GPIO_NODE DT_NODELABEL(gpio0)
Binary file not shown.
+3 -2
View File
@@ -17,7 +17,7 @@
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/sys/printk.h> #include <zephyr/sys/printk.h>
#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. */ /* D9 is declared in the application devicetree overlay as GPIO_ACTIVE_HIGH. */
static const struct gpio_dt_spec lock_control_gpio = static const struct gpio_dt_spec lock_control_gpio =
@@ -62,7 +62,8 @@ int lock_control_open(void) {
return ret; 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); k_msleep(LOCK_CONTROL_OPEN_PULSE_MS);
+1 -1
View File
@@ -22,7 +22,7 @@ int lock_control_init(void);
/** /**
* @brief Send an unlock pulse to the lock. * @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. * @return 0 on success, negative value on error.
*/ */
+125 -1
View File
@@ -31,6 +31,7 @@
#define KEYPAD_RETRY_MS 1000 #define KEYPAD_RETRY_MS 1000
#define SERVICE_INIT_STACK_SIZE 4096 #define SERVICE_INIT_STACK_SIZE 4096
#define SERVICE_INIT_PRIORITY 8 #define SERVICE_INIT_PRIORITY 8
#define NFC_SCAN_WINDOW_MS 15000
#define LOCK_OPEN_BEEP_INTERVAL_MS 2000 #define LOCK_OPEN_BEEP_INTERVAL_MS 2000
#define INVALID_CODE_LED_MS 1000 #define INVALID_CODE_LED_MS 1000
#define INVALID_CODE_BEEP_COUNT 3 #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); 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) { static int request_lock_open(const char *source, const char *actor) {
if (!lock_control_ready) { if (!lock_control_ready) {
printf("Lock open rejected: control GPIO unavailable\n"); 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; if (ret < 0 && first_error == 0) first_error = ret;
ret = nfc_init(); ret = nfc_init();
printf("Startup nfc=%d\n", ret); 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; return first_error;
} }
@@ -210,6 +251,8 @@ int main(void) {
int64_t reset_button_pressed_since_ms = 0; int64_t reset_button_pressed_since_ms = 0;
int64_t identity_reset_blink_until_ms = 0; int64_t identity_reset_blink_until_ms = 0;
int64_t next_identity_reset_blink_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 * These two initializers are RAM-only. They guarantee that keypad access and
@@ -253,6 +296,23 @@ int main(void) {
while (1) { while (1) {
char key; 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) { if (!keypad_ready && k_uptime_get() >= next_keypad_retry_ms) {
keypad_ready = keypad_init() == 0; keypad_ready = keypad_init() == 0;
next_keypad_retry_ms = k_uptime_get() + KEYPAD_RETRY_MS; 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) { if (door_open && k_uptime_get() >= next_open_beep_ms) {
printf("Door open reminder beep\n");
beep_key(); beep_key();
next_open_beep_ms = k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS; 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; key = keypad_ready ? keypad_get_key() : 0;
if (key == 0) { if (key == 0) {
k_msleep(MAIN_LOOP_DELAY_MS); k_msleep(MAIN_LOOP_DELAY_MS);
@@ -339,6 +458,9 @@ int main(void) {
printf("Key pressed: %c\n", key); 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') { if (key == '*' || key == '#' || key == 'B') {
entered_length = 0; entered_length = 0;
beep_key(); beep_key();
@@ -377,6 +499,8 @@ int main(void) {
(void)access_codes_consume_if_one_time(entered_code, (void)access_codes_consume_if_one_time(entered_code,
entered_length); entered_length);
opb_ble_notify_state_changed(); opb_ble_notify_state_changed();
stop_nfc_scan(&nfc_scan_deadline_ms,
lock_state_ready ? lock_state_is_open() : false);
} }
} else { } else {
printf("Invalid access code\n"); printf("Invalid access code\n");
+260 -12
View File
@@ -4,39 +4,287 @@
* OpenParcelBox * OpenParcelBox
* Copyright (c) 2026 * Copyright (c) 2026
* *
* NFC scan mode interface. * PN532 NFC reader over I2C.
*/ */
#include "nfc.h" #include "nfc.h"
#include <errno.h> #include "board_config.h"
#include <errno.h>
#include <string.h>
#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/util.h> #include <zephyr/sys/util.h>
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) { 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; return 0;
} }
int nfc_scan_enable(void) { 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; return 0;
} }
int nfc_scan_disable(void) { int nfc_scan_disable(void) {
scan_enabled = false; static const uint8_t parameters[] = {PN532_I2C_WAKEUP_ENABLE};
return 0; 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, bool nfc_read_detected_uid(uint8_t *uid, size_t uid_capacity,
size_t *uid_length) { size_t *uid_length) {
ARG_UNUSED(uid); if (uid == NULL || uid_length == NULL) return false;
ARG_UNUSED(uid_capacity);
ARG_UNUSED(uid_length);
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;
} }
+59 -11
View File
@@ -58,8 +58,11 @@ static char state_json[OPB_BLE_STATE_MAX];
static size_t state_snapshot_length; static size_t state_snapshot_length;
static size_t state_page_start; static size_t state_page_start;
static bool state_page_active; static bool state_page_active;
static char enrollment_name[NFC_TAG_NAME_MAX_LENGTH + 1];
static bool enrollment_requested; 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 bt_conn *authenticated_conn;
static struct app_identity authenticated_identity; static struct app_identity authenticated_identity;
static bool authenticated_identity_valid; 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; first = true;
for (size_t i = 0; i < open_history_count(); i++) { for (size_t i = 0; i < open_history_count(); i++) {
struct open_history_event event; 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 (strcmp(command, "start_nfc_enrollment") == 0) {
if (identity.role != APP_IDENTITY_ADMIN || if (identity.role != APP_IDENTITY_ADMIN) {
!json_get_string(json, "name", enrollment_name,
sizeof(enrollment_name))) {
return -EACCES; return -EACCES;
} }
enrollment_uid[0] = '\0';
enrollment_error[0] = '\0';
enrollment_active = true;
enrollment_requested = true; enrollment_requested = true;
return 0; 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) { if (strcmp(command, "add_guest") == 0) {
char name[APP_IDENTITY_NAME_MAX_LENGTH + 1]; char name[APP_IDENTITY_NAME_MAX_LENGTH + 1];
char guest_key[APP_IDENTITY_KEY_HEX_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; return -ENOTSUP;
} }
bool opb_ble_take_nfc_enrollment(char *name, size_t name_size) { bool opb_ble_take_nfc_enrollment(void) {
if (!enrollment_requested || name == NULL || name_size == 0) { if (!enrollment_requested) return false;
return false;
}
strncpy(name, enrollment_name, name_size - 1);
name[name_size - 1] = '\0';
enrollment_requested = false; enrollment_requested = false;
return true; 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, static ssize_t command_write(struct bt_conn *conn,
const struct bt_gatt_attr *attr, const void *buf, const struct bt_gatt_attr *attr, const void *buf,
uint16_t len, uint16_t offset, uint8_t flags) { uint16_t len, uint16_t offset, uint8_t flags) {
+4 -1
View File
@@ -39,7 +39,10 @@ bool opb_ble_get_unix_time_ms(int64_t *unix_ms);
* @brief Notify connected clients that the readable state changed. * @brief Notify connected clients that the readable state changed.
*/ */
void opb_ble_notify_state_changed(void); 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); void opb_ble_clear_bonds_after_reset(void);
#endif /* OPB_BLE_H */ #endif /* OPB_BLE_H */
+9 -8
View File
@@ -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. 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 | | Description | Quantity | Status |
| --- | --- | --- | | --- | --- | --- |
| NFC reader IC | 1 | To select | | PN532 NFC/RFID I2C module with antenna | 1 | Integrated for development |
| NFC reader antenna, 13.56 MHz | 1 | To design | | Production NFC reader implementation | 1 | To design from PN532 validation |
| NFC reader matching network | 1 | To design |
--- ---
@@ -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 | | Reference | Description | Quantity | Status |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Logic-level P-channel MOSFET or equivalent high-side driver | `SIG` input driver | 1 | To select | | S8050 NPN + S8550 PNP stage | Development `SIG` high-side driver | 1 each | Integrated for development |
| Bias resistors | According to schematic | TBD | To define | | 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 ### NFC
- Do not rely on the nRF52840 NFCT peripheral for passive badge UID reading. - 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. - 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: High priority:
- NFC reader IC selection. - Production implementation based on PN532 validation.
- NFC reader antenna design. - NFC reader antenna design.
- High-side lock driver selection. - High-side lock driver selection.
- Buck converter selection. - Buck converter selection.
+8 -3
View File
@@ -2,15 +2,20 @@
This directory contains the hardware documentation for OpenParcelBox. 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 ## Planned Content
- KiCad schematics. - KiCad schematics.
- PCB files. - PCB files.
- Gerber files. - Gerber files.
- Bill of materials.
- 3D models. - 3D models.
- Mechanical drawings. - Mechanical drawings.
- Wiring documentation. - Production wiring and assembly documentation.
- Reverse-engineering notes.
The goal is to make the electronics fully reproducible while reusing as much of the original Boks One hardware as possible. The goal is to make the electronics fully reproducible while reusing as much of the original Boks One hardware as possible.
+12 -7
View File
@@ -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. 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: Functions:
@@ -38,7 +40,9 @@ Functions:
- Badge validation. - Badge validation.
- Unlock request trigger. - 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: Command:
- Positive pulse on `SIG`. - 150 ms positive pulse on `SIG` in the current development firmware.
Validation: 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. Target validation sequence:
Sequence:
```text ```text
SIG command SIG command
+4 -1
View File
@@ -23,9 +23,12 @@ Seeed Studio XIAO BLE nRF52840
| Active buzzer control | Output | D0 | GPIO signal to transistor driver | | Active buzzer control | Output | D0 | GPIO signal to transistor driver |
| Lock NC feedback | Input | D7 | KR-S79 NC contact, internal pull-up enabled | | 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 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 SDA | I/O | SDA | Development keypad GPIO expander |
| PCF8574 I2C SCL | Output | SCL | 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 red | Output | Board LED alias `led0` | On-board validation LED |
| RGB LED green | Output | Board LED alias `led1` | 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 | | RGB LED blue | Output | Board LED alias `led2` | On-board validation LED |
@@ -172,7 +172,9 @@ To be completed.
The lock is controlled by the Seeed Studio XIAO BLE nRF52840. 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`. 1. Generate a positive pulse on `SIG`.
2. Start a timeout. 2. Start a timeout.
+6 -2
View File
@@ -91,9 +91,13 @@ unused record when needed.
modify permanent codes. Random generation excludes every currently active modify permanent codes. Random generation excludes every currently active
permanent and temporary value. Administrators can also edit permanent codes permanent and temporary value. Administrators can also edit permanent codes
through the six-digit numeric editor; choosing an existing temporary value 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, - 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. - Named guest creation, invitation QR display, and revocation.
- Firmware clock synchronization after connection. - Firmware clock synchronization after connection.
- Language override, encrypted backup/restore, factory reset, and local removal - Language override, encrypted backup/restore, factory reset, and local removal
+26
View File
@@ -34,6 +34,7 @@ class AppStrings {
'add': {'fr': 'Ajouter', 'en': 'Add'}, 'add': {'fr': 'Ajouter', 'en': 'Add'},
'delete': {'fr': 'Supprimer', 'en': 'Delete'}, 'delete': {'fr': 'Supprimer', 'en': 'Delete'},
'edit': {'fr': 'Modifier', 'en': 'Edit'}, 'edit': {'fr': 'Modifier', 'en': 'Edit'},
'copy_code': {'fr': 'Copier le code', 'en': 'Copy code'},
'cancel': {'fr': 'Annuler', 'en': 'Cancel'}, 'cancel': {'fr': 'Annuler', 'en': 'Cancel'},
'save': {'fr': 'Enregistrer', 'en': 'Save'}, 'save': {'fr': 'Enregistrer', 'en': 'Save'},
'scan': {'fr': 'Scanner', 'en': 'Scan'}, 'scan': {'fr': 'Scanner', 'en': 'Scan'},
@@ -129,6 +130,31 @@ class AppStrings {
'tag_name': {'fr': 'Nom du tag', 'en': 'Tag name'}, 'tag_name': {'fr': 'Nom du tag', 'en': 'Tag name'},
'tag_uid': {'fr': 'UID du tag', 'en': 'Tag UID'}, 'tag_uid': {'fr': 'UID du tag', 'en': 'Tag UID'},
'pairing_mode': {'fr': 'Mode appairage', 'en': 'Pairing mode'}, 'pairing_mode': {'fr': 'Mode appairage', 'en': 'Pairing mode'},
'nfc_pairing_title': {
'fr': 'Appairage dun 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 lautoriser.',
'en': 'Tag detected. Choose a name before authorizing it.',
},
'nfc_pairing_timeout': {
'fr': 'Aucun tag na é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'}, 'add_manually': {'fr': 'Ajouter manuellement', 'en': 'Add manually'},
'nfc_unavailable': { 'nfc_unavailable': {
'fr': 'Le NFC est indisponible sur ce téléphone.', 'fr': 'Le NFC est indisponible sur ce téléphone.',
+23 -5
View File
@@ -90,6 +90,9 @@ class OpenParcelBoxBleController extends ChangeNotifier {
bool isScanning = false; bool isScanning = false;
bool isBusy = false; bool isBusy = false;
bool isAuthenticated = false; bool isAuthenticated = false;
bool nfcEnrollmentActive = false;
String? nfcEnrollmentUid;
String? nfcEnrollmentError;
String status = 'disconnected'; String status = 'disconnected';
String? errorMessage; String? errorMessage;
@@ -719,11 +722,11 @@ class OpenParcelBoxBleController extends ChangeNotifier {
}), }),
); );
Future<void> startNfcEnrollment(String name) => send( Future<void> startNfcEnrollment() =>
OpenParcelBoxCommand('start_nfc_enrollment', <String, Object?>{ send(const OpenParcelBoxCommand('start_nfc_enrollment'));
'name': name.trim(),
}), Future<void> cancelNfcEnrollment() =>
); send(const OpenParcelBoxCommand('cancel_nfc_enrollment'));
Future<void> removeNfcTag(NfcTag tag) => send( Future<void> removeNfcTag(NfcTag tag) => send(
OpenParcelBoxCommand('remove_nfc_tag', <String, Object?>{'uid': tag.uid}), OpenParcelBoxCommand('remove_nfc_tag', <String, Object?>{'uid': tag.uid}),
@@ -932,6 +935,18 @@ class OpenParcelBoxBleController extends ChangeNotifier {
void _applyState(Map<String, dynamic> state, {bool notify = true}) { void _applyState(Map<String, dynamic> state, {bool notify = true}) {
_clearCollections(); _clearCollections();
final enrollment = state['nfc_enrollment'];
if (enrollment is Map<String, dynamic>) {
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<dynamic>? ?? const [])) { for (final item in (state['codes'] as List<dynamic>? ?? const [])) {
if (item is! Map<String, dynamic> || if (item is! Map<String, dynamic> ||
item['code'] is! String || item['code'] is! String ||
@@ -1041,6 +1056,9 @@ class OpenParcelBoxBleController extends ChangeNotifier {
nfcTags.clear(); nfcTags.clear();
history.clear(); history.clear();
guests.clear(); guests.clear();
nfcEnrollmentActive = false;
nfcEnrollmentUid = null;
nfcEnrollmentError = null;
} }
String _generateUniqueCode() { String _generateUniqueCode() {
+198 -34
View File
@@ -607,37 +607,44 @@ class _OpenParcelBoxHomeState extends State<OpenParcelBoxHome> {
(code) => _DataRowCard( (code) => _DataRowCard(
icon: Icons.password, icon: Icons.password,
title: code.code, title: code.code,
actions: hasModifyPermission actions: <Widget>[
? <Widget>[ IconButton(
if (kind == CredentialKind.permanent) key: ValueKey<String>('copy-code-${code.id}'),
IconButton( onPressed: () =>
onPressed: canModify Clipboard.setData(ClipboardData(text: code.code)),
? () async { icon: const Icon(Icons.copy_outlined),
final changed = await _editPermanentCode( tooltip: strings.text('copy_code'),
code, ),
); if (hasModifyPermission) ...<Widget>[
if (changed) { if (kind == CredentialKind.permanent)
setModalState(() {}); IconButton(
} onPressed: canModify
} ? () async {
: null, final changed = await _editPermanentCode(
icon: const Icon(Icons.edit_outlined), code,
tooltip: strings.text('edit_code'), );
), if (changed) {
IconButton(
onPressed: canModify
? () async {
await _guarded(
() => _controller.removeCode(code),
);
setModalState(() {}); setModalState(() {});
} }
: null, }
icon: const Icon(Icons.delete_outline), : null,
tooltip: strings.text('delete'), icon: const Icon(Icons.edit_outlined),
), tooltip: strings.text('edit_code'),
] ),
: const <Widget>[], 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<OpenParcelBoxHome> {
} }
Future<void> _addNfcFromBox(StateSetter refreshModal) async { Future<void> _addNfcFromBox(StateSetter refreshModal) async {
final name = await _askText( final added = await showDialog<bool>(
strings.text('tag_name'), context: context,
description: strings.text('pairing_name_help'), barrierDismissible: false,
builder: (dialogContext) =>
NfcEnrollmentDialog(controller: _controller, strings: strings),
); );
if (name != null) { if (added == true && mounted) {
await _guarded(() => _controller.startNfcEnrollment(name));
refreshModal(() {}); 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<NfcEnrollmentDialog> createState() => _NfcEnrollmentDialogState();
}
class _NfcEnrollmentDialogState extends State<NfcEnrollmentDialog> {
final TextEditingController _nameController = TextEditingController();
String? _localError;
bool _saving = false;
bool _completed = false;
@override
void initState() {
super.initState();
unawaited(_start());
}
Future<void> _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<void> _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<void> _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<String>('nfc-enrollment-modal'),
title: Text(widget.strings.text('nfc_pairing_title')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (uid == null && error == null) ...<Widget>[
const Center(child: CircularProgressIndicator()),
const SizedBox(height: 18),
Text(widget.strings.text('nfc_pairing_wait')),
],
if (uid != null) ...<Widget>[
Text(widget.strings.text('nfc_pairing_detected')),
const SizedBox(height: 8),
Text(uid, key: const ValueKey<String>('nfc-enrollment-uid')),
const SizedBox(height: 12),
TextField(
key: const ValueKey<String>('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) ...<Widget>[
Text(
error,
key: const ValueKey<String>('nfc-enrollment-error'),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
),
actions: <Widget>[
TextButton(
onPressed: _saving ? null : _cancel,
child: Text(
error == null
? widget.strings.text('cancel')
: widget.strings.text('close'),
),
),
if (uid != null)
FilledButton(
key: const ValueKey<String>('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 { class _RegistrationStatus extends StatelessWidget {
const _RegistrationStatus({ const _RegistrationStatus({
required this.error, required this.error,
+159
View File
@@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:app/backup_service.dart'; import 'package:app/backup_service.dart';
import 'package:app/app_strings.dart';
import 'package:app/ble_controller.dart'; import 'package:app/ble_controller.dart';
import 'package:app/main.dart'; import 'package:app/main.dart';
import 'package:app/models.dart'; import 'package:app/models.dart';
@@ -531,6 +532,82 @@ void main() {
controller.dispose(); 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 <SavedBox>[box],
cachedStates: <String, Map<String, dynamic>>{
box.identityKey: <String, dynamic>{
'nfc_enrollment': <String, dynamic>{
'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(<int>[]));
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(<int>[]))
..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<String>('nfc-enrollment-modal')),
findsOneWidget,
);
expect(
find.byKey(const ValueKey<String>('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<String>('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', ( testWidgets('administrator compact actions share one bottom row', (
WidgetTester tester, WidgetTester tester,
) async { ) async {
@@ -689,6 +766,88 @@ void main() {
expect(tester.takeException(), isNull); 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<Object?, Object?>)['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>[
SavedBox(
remoteId: 'AA:BB',
name: 'Front gate',
identityKey: 'ADMIN',
role: BoxRole.administrator,
),
],
cachedStates: <String, Map<String, dynamic>>{
'ADMIN': <String, dynamic>{
'codes': <Map<String, dynamic>>[
<String, dynamic>{
'slot': 0,
'code': '123456',
'kind': 'permanent',
},
<String, dynamic>{
'slot': 1,
'code': '654321',
'kind': 'one_time',
},
],
},
},
),
splashDuration: Duration.zero,
),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('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<String>('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', ( testWidgets('registration opens the styled setup modal', (
WidgetTester tester, WidgetTester tester,
) async { ) async {