feat(firmware): add open lock

This commit is contained in:
2026-07-15 14:29:32 +02:00
parent bebe0c6b2c
commit dbbf79f7f6
23 changed files with 835 additions and 65 deletions
+9
View File
@@ -22,7 +22,13 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
- Firmware component documentation.
- Firmware debugging documentation.
- Firmware buzzer documentation.
- Firmware lock control documentation.
- English architecture documentation in `docs/architecture.md`.
- Persistent six-digit access code storage.
- Lock control pulse on XIAO pin `D9`.
- Lock state feedback detection using KR-S79 `COM/NC` on XIAO `D7` / `D8`.
- Long buzzer success beep for accepted unlock codes.
- Door opened and door closed UART messages.
### Changed
@@ -36,6 +42,8 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
- Aligned hardware documentation with the active buzzer transistor driver.
- Aligned firmware documentation with C17 and the current Zephyr build tools.
- Updated project tracking documents to reflect completed RGB LED, GPIO expander, keypad, and active buzzer work.
- Updated keypad unlock user feedback with off-at-rest LEDs, green-open indication, red invalid-code feedback, and `B` entry cancellation.
- Updated open-lock reminder beep interval to 2 seconds.
### Fixed
@@ -46,6 +54,7 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
- Removed obsolete documentation references to PWM buzzer control.
- Removed stale development-environment references from project documentation.
- Removed corrupted text encoding artifacts from Markdown documentation.
- Prevented unintended lock command pulses at startup or after flashing by declaring D9 in Devicetree and configuring it directly inactive.
---
+14 -2
View File
@@ -27,18 +27,24 @@ Completed or validated:
- PCF8574 GPIO expander driver
- Development keypad scanning
- Active buzzer feedback
- Persistent six-digit access codes
- Keypad unlock flow with default development code `784512`
- Lock command pulse on XIAO `D9`
- KR-S79 `COM/NC` lock state feedback on XIAO `D7` / `D8`
- Green-open, off-closed, red-invalid LED feedback
- Short key beeps, long valid-code beep, invalid-code beep sequence, and 2s open reminder beep
- Door opened / closed serial debug messages
- Firmware debug output over serial console
In progress:
- Original hardware reverse engineering
- Lock driver
- Production keypad mapping
- Hardware validation
Planned:
- Access control
- Administrator access management
- NFC
- Battery management
- Zigbee
@@ -79,6 +85,9 @@ Current firmware modules:
- PCF8574 GPIO expander
- Matrix keypad
- Active buzzer
- Lock control
- Lock state feedback
- Persistent access codes
Firmware documentation is available in:
@@ -94,6 +103,9 @@ Current development platform:
- Seeed Studio XIAO BLE nRF52840
- PCF8574 GPIO expander
- Freenove 4x4 matrix keypad for development validation
- KR-S79 lock command on XIAO `D9`
- KR-S79 `NC` feedback on XIAO `D7`
- KR-S79 `COM` feedback reference on XIAO `D8`
The production hardware is planned around a custom PCB and the original parcel box mechanical parts.
+28 -4
View File
@@ -39,7 +39,9 @@ The firmware is based on Zephyr RTOS and follows a modular architecture to simpl
| GPIO expander driver | Done |
| Matrix keypad driver | Done |
| Active buzzer driver | Done |
| Lock driver | Pending |
| Lock driver | Done |
| Door or lock state feedback | Done |
| Persistent access code storage | Done |
| Battery driver | Pending |
| NFC driver | Pending |
| Power management | Pending |
@@ -99,6 +101,9 @@ Firmware modules:
- GPIO expander.
- Matrix keypad.
- Active buzzer.
- Lock control.
- Door or lock state feedback.
- Persistent access codes.
Status: in progress.
@@ -110,13 +115,21 @@ Objective: validate every reusable or newly added hardware component before inte
Components:
- Lock.
- Door or lock state feedback.
- Lock command pulse.
- Door or lock state feedback through the KR-S79 `COM/NC` contact.
- Battery monitoring.
- NFC antenna.
- Buzzer.
- External LEDs.
Validated so far:
- KR-S79 opening command through XIAO `D9`.
- KR-S79 `COM/NC` state detection through XIAO `D7` / `D8`.
- Door opened and door closed UART events.
- Open-state reminder beep every 2 seconds.
- Valid-code long beep and invalid-code beep sequence.
Status: in progress.
---
@@ -132,6 +145,7 @@ Components:
- Keypad.
- Lock.
- Door or lock state feedback.
- Persistent six-digit access codes.
- Battery.
- Buzzer.
- NFC.
@@ -139,11 +153,12 @@ Components:
Security:
- Administrator PIN.
- Development six-digit unlock code.
- PIN timeout.
- Anti brute-force logic.
- Configuration storage.
Status: planned.
Status: in progress.
---
@@ -195,6 +210,15 @@ Required features:
- No cloud.
- No subscription.
Implemented MVP pieces:
- Keypad-based unlock flow.
- Persistent six-digit code table.
- Default development unlock code.
- Lock command pulse.
- Lock state feedback.
- Audible and visual feedback for valid codes, invalid codes, and open state.
At this stage, the original proprietary electronics are no longer required.
---
+17 -17
View File
@@ -69,8 +69,8 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] GPIO expander driver.
- [x] Matrix keypad driver.
- [x] Active buzzer driver.
- [ ] Lock driver.
- [ ] Door or lock state driver.
- [x] Lock driver.
- [x] Door or lock state driver.
- [ ] Battery driver.
- [ ] NFC driver.
@@ -81,7 +81,7 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] buzzer.md.
- [x] debug.md.
- [ ] led.md.
- [ ] lock.md.
- [x] lock.md.
- [ ] battery.md.
- [ ] nfc.md.
@@ -91,16 +91,16 @@ Tasks are grouped by development phase and updated throughout the project.
### Lock
- [ ] Drive lock through the selected circuit.
- [ ] Validate opening pulse.
- [ ] Validate opening detection.
- [x] Drive lock through the selected circuit.
- [x] Validate opening pulse.
- [x] Validate opening detection.
- [ ] Validate timeout.
- [ ] Validate error handling.
### Door or Lock State
- [ ] Read feedback contact.
- [ ] Validate state changes.
- [x] Read feedback contact.
- [x] Validate state changes.
- [ ] Validate debounce.
### Battery
@@ -120,8 +120,8 @@ Tasks are grouped by development phase and updated throughout the project.
- [x] Single keypad beep.
- [ ] Double beep.
- [ ] Error beep.
- [ ] Success beep.
- [x] Error beep.
- [x] Success beep.
---
@@ -136,15 +136,15 @@ Tasks are grouped by development phase and updated throughout the project.
### Access Control
- [ ] Administrator PIN.
- [ ] PIN verification.
- [x] PIN verification.
- [ ] PIN timeout.
- [ ] Anti brute-force logic.
- [ ] Lock opening sequence.
- [ ] Door or lock state validation.
- [x] Lock opening sequence.
- [x] Door or lock state validation.
### Configuration
- [ ] Persistent storage.
- [x] Persistent storage.
- [ ] Factory reset.
- [ ] Version information.
@@ -165,12 +165,12 @@ Tasks are grouped by development phase and updated throughout the project.
## MVP - Standalone Firmware
- [ ] Complete firmware startup.
- [ ] Lock opening.
- [ ] Door or lock feedback.
- [x] Lock opening.
- [x] Door or lock feedback.
- [ ] Battery monitoring.
- [ ] Administrator PIN.
- [ ] Administrator NFC badge.
- [ ] Audible feedback.
- [x] Audible feedback.
- [ ] Standalone operation.
- [ ] Battery-powered operation.
- [ ] No cloud.
+7 -2
View File
@@ -2,7 +2,7 @@
## Purpose
This module provides short audible feedback for accepted keypad presses.
This module provides audible feedback for keypad presses and access-code events.
The current hardware uses an active buzzer. The firmware does not generate PWM; it only drives a GPIO high for a short pulse.
@@ -28,6 +28,8 @@ void buzzer_on(void);
void buzzer_beep_key(void);
void buzzer_beep_success(void);
void buzzer_off(void);
```
@@ -37,7 +39,10 @@ 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.
- 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.
- While the lock state feedback reports open, the main loop emits one short reminder beep every 2 seconds.
---
@@ -45,4 +50,4 @@ void buzzer_off(void);
Because the buzzer is active, tone generation is handled by the buzzer itself.
Future sound patterns, such as success and error feedback, should be implemented as higher-level sequences built from GPIO on/off pulses.
Future sound patterns should stay as higher-level sequences built from GPIO on/off pulses.
+23 -21
View File
@@ -45,25 +45,21 @@ The onboard RGB LED is used to quickly identify the firmware state.
## Startup
| Color | Meaning |
| ------- | ----------------------------------- |
| Red | Firmware initialization in progress |
| Green | Firmware initialized successfully |
| Magenta | Initialization failed |
| Color | Meaning |
| ------- | ----------------------- |
| Off | Startup in progress |
| Magenta | Initialization failed |
---
## Runtime
| Color | Meaning |
| ------- | ------------------------- |
| Green | Idle / Ready |
| Blue | User interaction detected |
| Yellow | Warning |
| Cyan | Reserved |
| White | Reserved |
| Red | Reserved |
| Magenta | Fatal error |
| Color | Meaning |
| ------- | ----------------------------------- |
| Off | Idle / lock closed |
| Green | Lock state feedback reports open |
| Red | Invalid six-digit access code |
| Magenta | Fatal initialization error |
---
@@ -72,14 +68,18 @@ The onboard RGB LED is used to quickly identify the firmware state.
Current startup sequence:
1. Initialize RGB LED
2. Turn LED Red
2. Keep LED off
3. Initialize peripherals
4. If initialization fails:
- Turn LED Magenta
- Stop execution
5. Turn LED Green
5. Show green only if the lock state feedback already reports open
6. Start main application loop
While the lock state feedback reports open, the firmware emits one short reminder beep every 2 seconds.
A valid six-digit access code triggers one long success beep before the opening pulse.
---
# UART Examples
@@ -89,14 +89,16 @@ Keypad:
```text
Key pressed: 1
Key pressed: 5
Key pressed: #
Key pressed: 6
Received valid access code
Door opened
```
Future lock module:
Lock state:
```text
Opening lock...
Lock opened.
Door opened
Door closed
```
Future NFC module:
@@ -158,4 +160,4 @@ Planned debugging features:
Debugging features should remain lightweight to minimize firmware size and power consumption.
Development-only diagnostics should be removable without affecting the application logic.
Development-only diagnostics should be removable without affecting the application logic.
+59
View File
@@ -0,0 +1,59 @@
# Lock Control
## Purpose
This module commands the KR-S79 lock opening input.
The lock itself remains powered by the battery pack. The firmware only drives the control stage connected to the lock `SIG` input.
---
## Hardware
- Seeed Studio XIAO nRF52840.
- Control signal: XIAO pin `D9`.
- D9 mapping in Zephyr: `GPIO1 pin 14`.
- D9 is declared in the application Devicetree overlay as `GPIO_ACTIVE_HIGH`.
- External S8050/S8550 driver stage.
- Active-high signal.
The GPIO is configured directly with `GPIO_OUTPUT_INACTIVE`. The idle state is low to avoid accidental opening at startup or after flashing.
---
## Public API
```c
int lock_control_init(void);
int lock_control_open(void);
```
---
## Current Behavior
- `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)`.
- All GPIO return codes used by the lock control path are logged.
- No PWM is used.
---
## Access Code Flow
The main loop collects numeric keypad input.
- Codes are six digits long.
- Up to 10 codes can be stored.
- Codes are persisted through Zephyr settings with the NVS backend.
- The development default code is `784512`.
- A valid six-digit entry is the only current path that triggers `lock_control_open()`.
- `*`, `#`, 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.
- 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`.
- LEDs stay off during startup and while the lock is closed.
+138
View File
@@ -0,0 +1,138 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
<defs>
<style>
.wire { stroke:#111; stroke-width:4; fill:none; }
.power { stroke:#b00020; stroke-width:5; fill:none; }
.signal { stroke:#c47a00; stroke-width:5; fill:none; }
.logic { stroke:#006b3c; stroke-width:5; fill:none; }
.thin { stroke:#111; stroke-width:3; fill:none; }
.box { fill:#fff; stroke:#111; stroke-width:3; rx:16; }
.title { font:700 34px sans-serif; fill:#111; }
.h2 { font:700 24px sans-serif; fill:#111; }
.txt { font:20px sans-serif; fill:#111; }
.small { font:17px sans-serif; fill:#111; }
.label { font:700 19px sans-serif; fill:#111; }
.note { font:18px sans-serif; fill:#7a0012; }
.node { fill:#111; }
.res { fill:#fff; stroke:#111; stroke-width:3; }
</style>
</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>
<!-- 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>
<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>
<!-- NPN base resistor -->
<path d="M400 292 H500" class="logic"/>
<rect x="500" y="267" width="120" height="50" class="res"/>
<text x="560" y="300" text-anchor="middle" class="label">R1 4,7 kΩ</text>
<path d="M620 292 H735" class="logic"/>
<!-- NPN transistor symbol -->
<circle cx="810" cy="400" r="75" class="thin"/>
<path d="M735 292 L775 332" class="thin"/>
<path d="M775 332 V468" class="thin"/>
<path d="M775 365 L845 325" class="thin"/>
<path d="M775 435 L845 475" class="thin"/>
<path d="M828 464 L845 475 L837 455" fill="#111"/>
<path d="M845 325 V235" class="wire"/>
<path d="M845 475 V560" class="wire"/>
<text x="900" y="370" class="h2">Q2 — S8050</text>
<text x="900" y="402" class="txt">NPN</text>
<text x="710" y="278" class="label">B</text>
<text x="855" y="225" class="label">C</text>
<text x="855" y="590" class="label">E</text>
<!-- NPN base pulldown -->
<path d="M700 292 V365" class="wire"/>
<rect x="675" y="365" width="50" height="110" class="res"/>
<text x="660" y="425" text-anchor="end" class="label">R4 100 kΩ</text>
<path d="M700 475 V560" class="wire"/>
<!-- GND rail -->
<path d="M400 500 H400 V560 H1290" class="wire"/>
<circle cx="700" cy="560" r="7" class="node"/>
<circle cx="845" cy="560" r="7" class="node"/>
<circle cx="1290" cy="560" r="7" class="node"/>
<path d="M1230 560 V640" class="wire"/>
<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>
<!-- PNP drive resistor -->
<path d="M845 235 H930" class="wire"/>
<rect x="930" y="210" width="120" height="50" class="res"/>
<text x="990" y="243" text-anchor="middle" class="label">R2 2,2 kΩ</text>
<path d="M1050 235 H1120" class="wire"/>
<!-- PNP transistor -->
<circle cx="1195" cy="235" r="75" class="thin"/>
<path d="M1120 235 H1155" class="thin"/>
<path d="M1155 167 V303" class="thin"/>
<path d="M1155 195 L1225 155" class="thin"/>
<path d="M1155 275 L1225 315" class="thin"/>
<path d="M1170 188 L1155 195 L1176 202" fill="#111"/>
<path d="M1225 155 V125" class="power"/>
<path d="M1225 315 V390" class="signal"/>
<text x="1285" y="210" class="h2">Q1 — S8550</text>
<text x="1285" y="242" class="txt">PNP</text>
<text x="1100" y="220" class="label">B</text>
<text x="1238" y="115" class="label">E</text>
<text x="1238" y="420" class="label">C</text>
<!-- +13V rail -->
<path d="M1225 125 H1430" class="power"/>
<path d="M1070 125 H1225" class="power"/>
<text x="1435" y="132" class="label" fill="#b00020">+13 V stack</text>
<!-- PNP base pullup -->
<path d="M1120 235 V150" class="wire"/>
<rect x="1095" y="125" width="50" height="90" class="res"/>
<text x="1080" y="170" text-anchor="end" class="label">R3 10 kΩ</text>
<path d="M1120 125 H1225" class="power"/>
<circle cx="1120" cy="235" r="7" class="node"/>
<circle cx="1225" cy="125" r="7" class="node"/>
<!-- 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="322" text-anchor="middle" class="small">KERONG KR-S79_20-JST</text>
<text x="1315" y="385" class="label">SIG jaune</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>
<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>
<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 denviron 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>
<!-- 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 dactiver D7.</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="915" class="note">Ce schéma ne suppose aucun brochage physique gauche/centre/droite.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

+18 -2
View File
@@ -83,6 +83,18 @@ Development keypad matrix scanning through the GPIO expander.
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`.
### Lock State
KR-S79 `COM/NC` feedback contact detection using `NC` on XIAO `D7` and `COM` on XIAO `D8`.
### Access Codes
Persistent storage for up to 10 six-digit unlock codes.
---
## Development Philosophy
@@ -107,13 +119,17 @@ Implemented:
- PCF8574 GPIO expander access.
- 4x4 development keypad scanning.
- Active buzzer feedback on accepted key presses.
- Long success beep on valid access code.
- Three short beeps on invalid access code.
- Persistent six-digit access code storage.
- Lock command pulse on XIAO pin `D9`.
- Lock state feedback through the KR-S79 `COM/NC` contact.
- Door opened / closed debug output.
- Serial debug output.
In progress or planned:
- Production keypad mapping.
- Access control.
- Lock driver.
- Battery monitoring.
- NFC.
- Zigbee.
+2
View File
@@ -12,6 +12,8 @@ target_sources(app PRIVATE
src/main.c
src/led.c
src/buzzer.c
src/lock_control.c
src/access_codes.c
src/lock_state.c
src/gpio_expander.c
src/keypad.c
+10 -1
View File
@@ -6,6 +6,15 @@
* Development board configuration.
*/
/ {
openparcelbox {
lock_control: lock-control {
compatible = "openparcelbox,lock-control";
gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>;
};
};
};
&i2c1 {
status = "okay";
};
};
@@ -0,0 +1,9 @@
description: OpenParcelBox lock control GPIO
compatible: "openparcelbox,lock-control"
properties:
gpios:
type: phandle-array
required: true
description: Active-high lock command GPIO.
@@ -0,0 +1 @@
openparcelbox OpenParcelBox
+4
View File
@@ -30,6 +30,10 @@
#define BUZZER_GPIO_PIN 2
#define BUZZER_GPIO_FLAGS GPIO_ACTIVE_HIGH
/* Lock control */
#define LOCK_CONTROL_GPIO_NODE DT_NODELABEL(lock_control)
/* Lock state feedback */
#define LOCK_STATE_NC_GPIO_NODE DT_NODELABEL(gpio1)
+6 -1
View File
@@ -1,8 +1,13 @@
CONFIG_GPIO=y
CONFIG_I2C=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
CONFIG_I2C_SHELL=y
CONFIG_SHELL=y
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_DEBUG_THREAD_INFO=y
CONFIG_DEBUG_THREAD_INFO=y
+222
View File
@@ -0,0 +1,222 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Persistent access code storage.
*/
#include "access_codes.h"
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/settings/settings.h>
#define ACCESS_CODES_SETTINGS_ROOT "codes"
#define ACCESS_CODES_SETTINGS_TABLE "table"
#define ACCESS_CODES_SETTINGS_TABLE_PATH "codes/table"
#define ACCESS_CODES_MAGIC 0x4f504243U
#define ACCESS_CODES_VERSION 1U
struct access_code_slot {
bool enabled;
char code[ACCESS_CODE_LENGTH + 1];
};
struct access_code_table {
uint32_t magic;
uint16_t version;
uint16_t count;
struct access_code_slot slots[ACCESS_CODE_MAX_COUNT];
};
static struct access_code_table code_table;
static bool code_table_loaded;
static bool access_code_is_digit_string(const char *code, size_t length) {
if (length != ACCESS_CODE_LENGTH) {
return false;
}
for (size_t i = 0; i < ACCESS_CODE_LENGTH; i++) {
if (code[i] < '0' || code[i] > '9') {
return false;
}
}
return true;
}
static bool access_code_slot_is_valid(const struct access_code_slot *slot) {
if (!slot->enabled) {
return true;
}
if (slot->code[ACCESS_CODE_LENGTH] != '\0') {
return false;
}
return access_code_is_digit_string(slot->code, ACCESS_CODE_LENGTH);
}
static bool access_code_table_is_valid(const struct access_code_table *table) {
size_t enabled_count = 0;
if (table->magic != ACCESS_CODES_MAGIC ||
table->version != ACCESS_CODES_VERSION ||
table->count > ACCESS_CODE_MAX_COUNT) {
return false;
}
for (size_t i = 0; i < ACCESS_CODE_MAX_COUNT; i++) {
if (!access_code_slot_is_valid(&table->slots[i])) {
return false;
}
if (table->slots[i].enabled) {
enabled_count++;
}
}
return enabled_count == table->count;
}
static void access_codes_set_default_table(void) {
memset(&code_table, 0, sizeof(code_table));
code_table.magic = ACCESS_CODES_MAGIC;
code_table.version = ACCESS_CODES_VERSION;
code_table.count = 1;
code_table.slots[0].enabled = true;
memcpy(code_table.slots[0].code, "784512", ACCESS_CODE_LENGTH + 1);
}
static int access_codes_save(void) {
return settings_save_one(ACCESS_CODES_SETTINGS_TABLE_PATH, &code_table,
sizeof(code_table));
}
static int access_codes_settings_set(const char *key, size_t len,
settings_read_cb read_cb, void *cb_arg) {
struct access_code_table loaded_table;
ssize_t bytes_read;
if (strcmp(key, ACCESS_CODES_SETTINGS_TABLE) != 0) {
return -ENOENT;
}
if (len != sizeof(loaded_table)) {
return -EINVAL;
}
bytes_read = read_cb(cb_arg, &loaded_table, sizeof(loaded_table));
if (bytes_read != sizeof(loaded_table)) {
return -EIO;
}
if (!access_code_table_is_valid(&loaded_table)) {
return 0;
}
code_table = loaded_table;
code_table_loaded = true;
return 0;
}
static struct settings_handler access_codes_settings = {
.name = ACCESS_CODES_SETTINGS_ROOT,
.h_set = access_codes_settings_set,
};
int access_codes_init(void) {
int ret;
access_codes_set_default_table();
code_table_loaded = false;
ret = settings_subsys_init();
if (ret < 0) {
return ret;
}
ret = settings_register(&access_codes_settings);
if (ret < 0) {
return ret;
}
ret = settings_load_subtree(ACCESS_CODES_SETTINGS_ROOT);
if (ret < 0) {
return ret;
}
if (!code_table_loaded || !access_code_table_is_valid(&code_table)) {
access_codes_set_default_table();
return access_codes_save();
}
if (code_table.count == 0) {
access_codes_set_default_table();
return access_codes_save();
}
return 0;
}
bool access_codes_is_valid(const char *code, size_t length) {
if (!access_code_is_digit_string(code, length)) {
return false;
}
for (size_t i = 0; i < ACCESS_CODE_MAX_COUNT; i++) {
if (!code_table.slots[i].enabled) {
continue;
}
if (memcmp(code_table.slots[i].code, code, ACCESS_CODE_LENGTH) == 0) {
return true;
}
}
return false;
}
int access_codes_set(size_t slot, const char *code) {
if (slot >= ACCESS_CODE_MAX_COUNT) {
return -EINVAL;
}
if (!access_code_is_digit_string(code, ACCESS_CODE_LENGTH)) {
return -EINVAL;
}
if (!code_table.slots[slot].enabled) {
code_table.count++;
}
code_table.slots[slot].enabled = true;
memcpy(code_table.slots[slot].code, code, ACCESS_CODE_LENGTH);
code_table.slots[slot].code[ACCESS_CODE_LENGTH] = '\0';
return access_codes_save();
}
int access_codes_clear(size_t slot) {
if (slot >= ACCESS_CODE_MAX_COUNT) {
return -EINVAL;
}
if (!code_table.slots[slot].enabled) {
return 0;
}
code_table.slots[slot].enabled = false;
memset(code_table.slots[slot].code, 0, sizeof(code_table.slots[slot].code));
code_table.count--;
return access_codes_save();
}
+58
View File
@@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Persistent access code storage.
*/
#ifndef ACCESS_CODES_H
#define ACCESS_CODES_H
#include <stdbool.h>
#include <stddef.h>
#define ACCESS_CODE_LENGTH 6
#define ACCESS_CODE_MAX_COUNT 10
/**
* @brief Initialize persistent access code storage.
*
* Loads stored codes from non-volatile settings. If no valid table exists yet,
* the development code 784512 is stored in slot 0.
*
* @return 0 on success, negative value on error.
*/
int access_codes_init(void);
/**
* @brief Validate a six-digit access code.
*
* @param code Six ASCII digits, not necessarily null-terminated.
* @param length Number of characters available in code.
*
* @return true when the code matches an enabled slot.
*/
bool access_codes_is_valid(const char *code, size_t length);
/**
* @brief Store or replace a six-digit code in a slot.
*
* @param slot Slot index from 0 to ACCESS_CODE_MAX_COUNT - 1.
* @param code Six ASCII digits.
*
* @return 0 on success, negative value on error.
*/
int access_codes_set(size_t slot, const char *code);
/**
* @brief Disable one stored code slot.
*
* @param slot Slot index from 0 to ACCESS_CODE_MAX_COUNT - 1.
*
* @return 0 on success, negative value on error.
*/
int access_codes_clear(size_t slot);
#endif /* ACCESS_CODES_H */
+8
View File
@@ -15,6 +15,7 @@
#include <zephyr/kernel.h>
#define BUZZER_KEY_BEEP_MS 50
#define BUZZER_SUCCESS_BEEP_MS 400
/* D0 on the Seeed Studio XIAO nRF52840 is mapped to GPIO0 pin 2. */
static const struct gpio_dt_spec buzzer_gpio = {
@@ -52,4 +53,11 @@ void buzzer_beep_key(void) {
buzzer_off();
}
void buzzer_beep_success(void) {
/* Longer active pulse used when an unlock code is accepted. */
buzzer_on();
k_msleep(BUZZER_SUCCESS_BEEP_MS);
buzzer_off();
}
void buzzer_off(void) { gpio_pin_set_raw(buzzer_gpio.port, buzzer_gpio.pin, 0); }
+8
View File
@@ -40,6 +40,14 @@ void buzzer_on(void);
*/
void buzzer_beep_key(void);
/**
* @brief Emit a long success beep.
*
* This feedback is used when a valid access code is accepted before opening
* the lock.
*/
void buzzer_beep_success(void);
/**
* @brief Turn off the buzzer output.
*/
+78
View File
@@ -0,0 +1,78 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Lock control driver.
*/
#include "lock_control.h"
#include "board_config.h"
#include <errno.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define LOCK_CONTROL_OPEN_PULSE_MS 100
/* D9 is declared in the application devicetree overlay as GPIO_ACTIVE_HIGH. */
static const struct gpio_dt_spec lock_control_gpio =
GPIO_DT_SPEC_GET(LOCK_CONTROL_GPIO_NODE, gpios);
int lock_control_init(void) {
int ret;
if (!gpio_is_ready_dt(&lock_control_gpio)) {
printk("Lock control GPIO is not ready\n");
return -ENODEV;
}
printk("Lock control GPIO ready\n");
ret = gpio_pin_configure_dt(&lock_control_gpio, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
printk("Lock control GPIO configure inactive failed: %d\n", ret);
return ret;
}
printk("Lock control GPIO configured inactive: %d\n", ret);
return 0;
}
int lock_control_open(void) {
int ret;
int inactive_ret;
ret = gpio_pin_set_dt(&lock_control_gpio, 1);
if (ret < 0) {
printk("Lock control GPIO active set failed: %d\n", ret);
inactive_ret = gpio_pin_set_dt(&lock_control_gpio, 0);
if (inactive_ret < 0) {
printk("Lock control GPIO inactive recovery failed: %d\n", inactive_ret);
} else {
printk("Lock control GPIO inactive recovery: %d\n", inactive_ret);
}
return ret;
}
printk("Lock control GPIO active set: %d\n", ret);
k_msleep(LOCK_CONTROL_OPEN_PULSE_MS);
ret = gpio_pin_set_dt(&lock_control_gpio, 0);
if (ret < 0) {
printk("Lock control GPIO inactive set failed: %d\n", ret);
return ret;
}
printk("Lock control GPIO inactive set: %d\n", ret);
return 0;
}
+31
View File
@@ -0,0 +1,31 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Lock control driver.
*/
#ifndef LOCK_CONTROL_H
#define LOCK_CONTROL_H
/**
* @brief Initialize the lock control output.
*
* Configures D9 as a push-pull GPIO output and leaves it low.
*
* @return 0 on success, negative value on error.
*/
int lock_control_init(void);
/**
* @brief Send an unlock pulse to the lock.
*
* Drives D9 high for 100 ms, then returns it low.
*
* @return 0 on success, negative value on error.
*/
int lock_control_open(void);
#endif /* LOCK_CONTROL_H */
+84 -14
View File
@@ -9,14 +9,16 @@
* Current test:
* - RGB LED
* - Buzzer
* - Lock state feedback
* - Lock control and state feedback
* - GPIO Expander
* - 4x4 Keypad
*/
#include "access_codes.h"
#include "buzzer.h"
#include "keypad.h"
#include "led.h"
#include "lock_control.h"
#include "lock_state.h"
#include <zephyr/kernel.h>
@@ -30,19 +32,44 @@
#define MAIN_LOOP_DELAY_MS 20
#define KEY_PRESS_LED_MS 100
#define LOCK_OPEN_BEEP_INTERVAL_MS 2000
#define INVALID_CODE_LED_MS 1000
#define INVALID_CODE_BEEP_COUNT 3
#define INVALID_CODE_BEEP_MS 50
#define INVALID_CODE_BEEP_PAUSE_MS 80
#define INVALID_CODE_BEEP_SEQUENCE_MS \
((INVALID_CODE_BEEP_COUNT * INVALID_CODE_BEEP_MS) + \
((INVALID_CODE_BEEP_COUNT - 1) * INVALID_CODE_BEEP_PAUSE_MS))
/* --------------------------------------------------------------------------
* Private helpers
* -------------------------------------------------------------------------- */
static void set_idle_led(bool lock_open) {
static void set_lock_led(bool lock_open) {
if (lock_open) {
led_set_red();
} else {
led_set_green();
} else {
led_off();
}
}
static bool key_is_digit(char key) { return key >= '0' && key <= '9'; }
static void signal_invalid_code(void) {
led_set_red();
for (int i = 0; i < INVALID_CODE_BEEP_COUNT; i++) {
buzzer_on();
k_msleep(INVALID_CODE_BEEP_MS);
buzzer_off();
if (i < (INVALID_CODE_BEEP_COUNT - 1)) {
k_msleep(INVALID_CODE_BEEP_PAUSE_MS);
}
}
k_msleep(INVALID_CODE_LED_MS - INVALID_CODE_BEEP_SEQUENCE_MS);
}
/* --------------------------------------------------------------------------
* Public API
* -------------------------------------------------------------------------- */
@@ -52,13 +79,22 @@ int main(void) {
bool lock_open;
bool previous_lock_open;
int64_t next_lock_open_beep_ms;
char entered_code[ACCESS_CODE_LENGTH];
size_t entered_code_length;
if (led_init() < 0) {
return 0;
}
led_set_red();
k_msleep(1000);
led_off();
if (lock_control_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (buzzer_init() < 0) {
led_set_magenta();
@@ -76,6 +112,14 @@ int main(void) {
}
}
if (access_codes_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (keypad_init() < 0) {
led_set_magenta();
@@ -87,22 +131,23 @@ int main(void) {
lock_open = lock_state_is_open();
previous_lock_open = lock_open;
next_lock_open_beep_ms = k_uptime_get();
entered_code_length = 0;
set_idle_led(lock_open);
set_lock_led(lock_open);
printf("\n");
printf("========================================\n");
printf("OpenParcelBox Firmware\n");
printf("Hardware Test: RGB LED + Buzzer + Lock State + Keypad\n");
printf("Hardware Test: RGB LED + Buzzer + Lock + Keypad\n");
printf("========================================\n");
set_idle_led(lock_open);
set_lock_led(lock_open);
while (1) {
lock_open = lock_state_is_open();
if (lock_open != previous_lock_open) {
set_idle_led(lock_open);
set_lock_led(lock_open);
previous_lock_open = lock_open;
if (lock_open) {
@@ -125,14 +170,39 @@ int main(void) {
printf("Key pressed: %c\n", key);
if (!lock_open) {
/* keypad_get_key() returns once per press, so holding a key will not beep continuously. */
buzzer_beep_key();
if (key == '*' || key == '#' || key == 'B') {
entered_code_length = 0;
buzzer_beep_key();
} else if (key_is_digit(key)) {
entered_code[entered_code_length] = key;
entered_code_length++;
led_set_blue();
if (entered_code_length == ACCESS_CODE_LENGTH) {
if (access_codes_is_valid(entered_code, entered_code_length)) {
printf("Received valid access code\n");
buzzer_beep_success();
if (lock_control_open() < 0) {
printf("Lock control open failed\n");
}
} else {
printf("Invalid access code\n");
signal_invalid_code();
}
entered_code_length = 0;
} else {
/*
* keypad_get_key() returns once per press, so holding a key will
* not beep continuously.
*/
buzzer_beep_key();
}
}
k_msleep(KEY_PRESS_LED_MS);
led_set_green();
set_lock_led(lock_state_is_open());
}
}
+1 -1
View File
@@ -23,6 +23,7 @@ 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 |
| PCF8574 I2C SDA | I/O | SDA | Development keypad GPIO expander |
| PCF8574 I2C SCL | Output | SCL | Development keypad GPIO expander |
| RGB LED red | Output | Board LED alias `led0` | On-board validation LED |
@@ -35,7 +36,6 @@ Seeed Studio XIAO BLE nRF52840
These signals still need to be assigned after hardware validation:
- Lock `SIG` command.
- Production keypad matrix.
- NFC antenna and matching network.
- Battery voltage measurement.