feat(firmware): add nfc and update all documentation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# VSCodium / VSCode
|
||||
# ----------------------------
|
||||
.vscode/
|
||||
OpenParcelBox.code-workspace
|
||||
|
||||
# ----------------------------
|
||||
# Codex
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+56
-9
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+61
-31
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
This behavior is fully handled by this module and should remain transparent to higher-level drivers.
|
||||
|
||||
@@ -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.
|
||||
- Unit tests.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+32
-19
@@ -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.
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
</defs>
|
||||
|
||||
<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="88" text-anchor="middle" class="txt">Seeed Studio XIAO BLE nRF52840 • serrure KR-S79 • stack 8×AAA ≈ 13 V</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 • KR-S79 lock • 8×AAA pack ≈ 13 V</text>
|
||||
|
||||
<!-- XIAO -->
|
||||
<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="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"/>
|
||||
<text x="95" y="505" class="label">GND</text>
|
||||
<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 -->
|
||||
<path d="M400 292 H500" class="logic"/>
|
||||
@@ -67,7 +67,7 @@
|
||||
<path d="M1190 640 H1270" class="wire"/>
|
||||
<path d="M1205 655 H1255" 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 -->
|
||||
<path d="M845 235 H930" class="wire"/>
|
||||
@@ -105,34 +105,34 @@
|
||||
|
||||
<!-- Lock 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="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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<circle cx="1290" cy="500" r="7" class="node"/>
|
||||
|
||||
<!-- Functional notes -->
|
||||
<rect x="70" y="630" width="700" height="300" class="box"/>
|
||||
<text x="95" y="675" class="h2">Fonctionnement</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="770" class="txt">D7 = HIGH : Q2 conduit → courant limité par R2 → base de Q1 abaissée d’environ 0,7 V</text>
|
||||
<text x="100" y="805" class="txt">sous son émetteur → Q1 conduit → environ +13 V appliqué sur 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="900" class="note">Important : ne jamais relier directement D7 à la base du S8550.</text>
|
||||
<text x="95" y="675" class="h2">Operation</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">D9 = HIGH: Q2 conducts → R2 limits current → Q1 base about 0.7 V below its emitter</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 keeps Q2 off during startup or if D9 is floating.</text>
|
||||
<text x="100" y="900" class="note">Important: never connect D9 directly to the S8550 base.</text>
|
||||
|
||||
<!-- Verification notes -->
|
||||
<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="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="760" class="txt">• Les boîtiers S8050/S8550 ne garantissent pas tous le même ordre de broches.</text>
|
||||
<text x="840" y="800" class="txt">• Vérifier la polarité du stack : rouge = +13 V, noir = GND.</text>
|
||||
<text x="840" y="840" class="txt">• Relier le GND du XIAO au GND du stack avant d’activer D7.</text>
|
||||
<text x="840" y="880" class="txt">• Tester d’abord avec une impulsion courte (≈100 ms) et mesurer SIG au multimètre.</text>
|
||||
<text x="840" y="915" class="note">Ce schéma ne suppose aucun brochage physique gauche/centre/droite.</text>
|
||||
</svg>
|
||||
<text x="835" y="675" class="h2">Checks before wiring</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">• S8050/S8550 packages do not all guarantee the same pin order.</text>
|
||||
<text x="840" y="800" class="txt">• Check pack polarity: red = +13 V, black = GND.</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">• Firmware applies a 150 ms pulse; measure SIG with a multimeter.</text>
|
||||
<text x="840" y="915" class="note">This diagram assumes no physical left/center/right transistor pin order.</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.7 KiB |
+17
-11
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -17,7 +17,7 @@
|
||||
#include <zephyr/kernel.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. */
|
||||
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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
+125
-1
@@ -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");
|
||||
|
||||
+260
-12
@@ -4,39 +4,287 @@
|
||||
* OpenParcelBox
|
||||
* Copyright (c) 2026
|
||||
*
|
||||
* NFC scan mode interface.
|
||||
* PN532 NFC reader over I2C.
|
||||
*/
|
||||
|
||||
#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>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
+59
-11
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
+9
-8
@@ -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.
|
||||
|
||||
+8
-3
@@ -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.
|
||||
|
||||
+12
-7
@@ -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
|
||||
|
||||
+4
-1
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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<void> startNfcEnrollment(String name) => send(
|
||||
OpenParcelBoxCommand('start_nfc_enrollment', <String, Object?>{
|
||||
'name': name.trim(),
|
||||
}),
|
||||
);
|
||||
Future<void> startNfcEnrollment() =>
|
||||
send(const OpenParcelBoxCommand('start_nfc_enrollment'));
|
||||
|
||||
Future<void> cancelNfcEnrollment() =>
|
||||
send(const OpenParcelBoxCommand('cancel_nfc_enrollment'));
|
||||
|
||||
Future<void> removeNfcTag(NfcTag tag) => send(
|
||||
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}) {
|
||||
_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 [])) {
|
||||
if (item is! Map<String, dynamic> ||
|
||||
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() {
|
||||
|
||||
+198
-34
@@ -607,37 +607,44 @@ class _OpenParcelBoxHomeState extends State<OpenParcelBoxHome> {
|
||||
(code) => _DataRowCard(
|
||||
icon: Icons.password,
|
||||
title: code.code,
|
||||
actions: hasModifyPermission
|
||||
? <Widget>[
|
||||
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: <Widget>[
|
||||
IconButton(
|
||||
key: ValueKey<String>('copy-code-${code.id}'),
|
||||
onPressed: () =>
|
||||
Clipboard.setData(ClipboardData(text: code.code)),
|
||||
icon: const Icon(Icons.copy_outlined),
|
||||
tooltip: strings.text('copy_code'),
|
||||
),
|
||||
if (hasModifyPermission) ...<Widget>[
|
||||
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 <Widget>[],
|
||||
}
|
||||
: 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<OpenParcelBoxHome> {
|
||||
}
|
||||
|
||||
Future<void> _addNfcFromBox(StateSetter refreshModal) async {
|
||||
final name = await _askText(
|
||||
strings.text('tag_name'),
|
||||
description: strings.text('pairing_name_help'),
|
||||
final added = await showDialog<bool>(
|
||||
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<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 {
|
||||
const _RegistrationStatus({
|
||||
required this.error,
|
||||
|
||||
@@ -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 <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', (
|
||||
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<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', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user