feat(firmware): add secure context, BLE connexion and refactor code

This commit is contained in:
2026-07-16 17:34:48 +02:00
parent 1c987a3512
commit ddae86ea6f
23 changed files with 2531 additions and 274 deletions
+281
View File
@@ -0,0 +1,281 @@
# Firmware Bluetooth
## Purpose
The XIAO nRF52840 firmware exposes a local Bluetooth Low Energy administration
service for the Flutter application.
This service is used to:
- synchronize the firmware clock;
- open the lock through the normal lock-control path;
- add or remove stored six-digit access codes;
- enroll, name, update, or remove stored NFC tags;
- read the current stored configuration and seven-day opening history;
- provision named administrator and guest phone identities.
Before phone synchronization, the clock starts at `2026-06-01T00:00:00Z`, or
at the newest persisted opening timestamp after a reboot.
## Security
Pairing is restricted to BLE LE Secure Connections. BLE AES-CCM link encryption
protects every GATT command, state read, and notification, and Zephyr persists
bonding keys.
At startup, the controller is enabled first, the Zephyr `bt/*` settings subtree
is then restored, and advertising starts only after the Bluetooth identity has
made the host stack ready. Reversing these last two operations causes Zephyr to
reject advertising with `-EAGAIN` (`-11`).
This complete sequence runs in a dedicated thread after local keypad
initialization. Bluetooth failure or slow bond restoration therefore cannot
prevent offline keypad access or lock operation.
Each phone also owns a random 128-bit identity key stored in its secure
keystore. The first phone can send `provision_admin` only while no administrator
exists. Afterwards every command requires a valid administrator or guest
`identity_key`. Until the connection authenticates, state reads expose only the
box name and `admin_exists`; codes, tags, identities, and history remain hidden.
The box never returns the administrator key. After administrator
authentication, it returns guest names and guest keys so the administrator can
redisplay or back up invitation QR codes. A guest never receives identity
records.
An administrator can register a named guest using a random key generated by the
administrator application and transferred through a versioned invitation QR
code.
---
## GATT Service
Device name:
```text
OpenParcelBox
```
The static name is intentionally retained while the per-board naming path is
being isolated from the validated startup sequence.
Service UUID:
```text
f2a00000-8e7a-4f8d-9b1d-7d8e4b7a0001
```
Characteristics:
| Characteristic | UUID | Properties |
| --- | --- | --- |
| Command | `f2a00001-8e7a-4f8d-9b1d-7d8e4b7a0001` | Write |
| State | `f2a00002-8e7a-4f8d-9b1d-7d8e4b7a0001` | Read, Notify |
---
## Command Format
Commands are UTF-8 JSON objects written to the command characteristic.
Except for initial administrator provisioning, each command includes
`identity_key`.
Initial administrator provisioning also stores the box name:
```json
{
"command": "provision_admin",
"identity_key": "<32 hexadecimal characters>",
"name": "Administrator",
"box_name": "Front gate"
}
```
An existing phone authenticates immediately after reconnecting:
```json
{
"command": "authenticate",
"identity_key": "<32 hexadecimal characters>"
}
```
Clock synchronization:
```json
{
"command": "sync_clock",
"iso_utc": "2026-07-15T17:04:00.000Z",
"unix_ms": 1784135040000,
"timezone_offset_minutes": 120
}
```
Open the lock:
```json
{
"command": "open_lock"
}
```
Add a permanent code:
```json
{
"command": "add_code",
"code": "123456",
"kind": "permanent"
}
```
Add a one-time code:
```json
{
"command": "add_code",
"code": "654321",
"kind": "one_time"
}
```
Remove a code:
```json
{
"command": "remove_code",
"code": "123456"
}
```
Add or remove an NFC tag UID:
```json
{
"command": "add_nfc_tag",
"uid": "60:4F:E2:B5",
"name": "Alice"
}
```
```json
{
"command": "remove_nfc_tag",
"uid": "60:4F:E2:B5"
}
```
Add or revoke a guest:
```json
{
"command": "add_guest",
"name": "Alice",
"guest_key": "<32 hexadecimal characters>"
}
```
```json
{
"command": "remove_guest",
"guest_key": "<32 hexadecimal characters>"
}
```
An administrator can restore all persistent tables to their defaults and clear
every BLE bond:
```json
{
"command": "factory_reset"
}
```
---
## State Format
The state characteristic returns UTF-8 JSON.
Example:
```json
{
"box_name": "Front gate",
"role": "administrator",
"clock_synced": true,
"unix_ms": 1784135045000,
"codes": [
{
"slot": 0,
"code": "784512",
"kind": "permanent"
}
],
"nfc_tags": [
{
"slot": 0,
"uid": "60:4F:E2:B5",
"name": "Alice"
}
],
"guests": [
{
"name": "Delivery team",
"key": "<32 hexadecimal characters>"
}
],
"admin_exists": true
}
```
The firmware also sends a notification on the state characteristic after a
successful write command when notifications are enabled by the client.
---
## Access Code Behavior
Access codes remain six-digit numeric strings.
The firmware stores up to eight permanent codes and twenty temporary codes.
Permanent codes remain active until removed. One-time codes are removed
automatically after the first successful keypad use.
An older persistent code table without code-kind metadata is migrated at boot;
existing codes become permanent codes.
---
## Runtime Logs
Successful openings are persisted with a timestamp and source: permanent code,
temporary code, named NFC tag, or named mobile identity. Events older than
seven days are removed and the newest 64 events are retained.
NFC enrollment is started with:
```json
{
"command": "start_nfc_enrollment",
"identity_key": "<32 hexadecimal characters>",
"name": "Alice"
}
```
The next UID reported by the dedicated reader is stored with that name.
When the phone has synchronized the clock, lock-opening logs include a Unix
timestamp in milliseconds.
Example:
```text
Lock open requested source=keypad timestamp_ms=1784135045000
```
Before clock synchronization, the same log is emitted with:
```text
timestamp_ms=unsynced
```
+39 -24
View File
@@ -14,6 +14,7 @@ The firmware currently supports the following debugging methods:
- UART console (`printf`)
- On-board RGB LED
- Bluetooth state and command diagnostics
- Zephyr Shell (when enabled)
Additional methods may be added later:
@@ -54,6 +55,16 @@ The onboard RGB LED is used to quickly identify the firmware state.
## Runtime
When the local hardware path is ready, the red, green, and blue LED channels
turn on together (white) for one second immediately before the keypad loop
starts. Persistent storage and Bluetooth are deliberately initialized
afterwards in background threads, so neither can prevent this indication.
RGB LED failure is non-fatal: the firmware continues with keypad and Bluetooth
startup instead of returning silently. Persistent credential/history failures
are also non-fatal; keypad access falls back to the in-memory development code
and keypad initialization is retried once per second.
| Color | Meaning |
| ------- | ----------------------------------- |
| Off | Idle / lock closed |
@@ -68,14 +79,16 @@ The onboard RGB LED is used to quickly identify the firmware state.
Current startup sequence:
1. Initialize RGB LED
2. Keep LED off
3. Initialize peripherals
4. If initialization fails:
- Turn LED Magenta
- Stop execution
5. Show green only if the lock state feedback already reports open
6. Start main application loop
1. Install the fallback code and default clock in RAM without reading flash.
2. Initialize the RGB LED, lock output, buzzer, lock feedback, and keypad.
3. Keep D9 inactive throughout initialization.
4. Show white for one second when the local keypad path is ready.
5. Start persistent Settings/NVS loading in a dedicated services thread.
6. Start Bluetooth in its own thread after the stored application state loads.
7. Enter the keypad loop regardless of storage or Bluetooth progress.
Persistent Settings callbacks use static staging buffers. Large history and
identity records are never allocated on a thread stack during boot.
While the lock state feedback reports open, the firmware emits one short reminder beep every 2 seconds.
@@ -92,7 +105,8 @@ Key pressed: 1
Key pressed: 5
Key pressed: 6
Received valid access code
Door opened
Lock open requested source=keypad timestamp_ms=1784135045000
Door opened timestamp_ms=1784135045050
```
Lock state:
@@ -108,28 +122,29 @@ Future dedicated NFC reader backend:
Scan NFC: ON
NFC detected: 60:4F:E2:B5
NFC valid
Lock open requested source=nfc timestamp_ms=1784135045000
Scan NFC: OFF
```
Bluetooth command path:
```text
BLE advertising: OpenParcelBox
BLE clock synchronized: 1784135040000
Lock open requested source=ble timestamp_ms=1784135045000
```
---
# Zephyr Shell
# Serial Console
The Zephyr Shell is enabled during development.
The firmware uses the USB CDC serial console for `printf` and `printk`
diagnostics. The interactive Zephyr shell and I2C shell are intentionally
disabled so they cannot compete with the console for the same CDC backend.
Typical commands:
```text
uart:~$ i2c scan i2c@40004000
```
This allows verification of:
- I²C devices
- GPIO configuration
- Driver availability
The shell is intended for development only and may be disabled in production builds.
Hardware-level I2C, keypad, LED, flash, lock-feedback, settings, and secure-BLE
checks are provided by the standalone diagnostic UF2 under
`firmware/diagnostic/`.
---
+1 -1
View File
@@ -46,7 +46,7 @@ int lock_control_open(void);
The main loop collects numeric keypad input.
- Codes are six digits long.
- Up to 10 codes can be stored.
- Up to 8 permanent and 20 temporary/one-time 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 triggers `lock_control_open()`.
+10 -2
View File
@@ -14,7 +14,7 @@ Passive badge UID reading requires a dedicated NFC reader circuit on the product
## Persistent Tags
- Up to 10 NFC tag UIDs can be stored.
- Up to 10 NFC tag UIDs can be stored, each with a 31-character name.
- UID length can be 1 to 10 bytes.
- The first default development UID is `60:4F:E2:B5`.
- A valid stored table can contain zero enabled tags. This allows future applications to delete all tags without having the default test UID recreated after reboot.
@@ -35,10 +35,18 @@ int nfc_tags_clear(size_t slot);
## Scan Mode
The firmware keeps the NFC credential scan flow disabled by default.
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 intended firmware flow keeps NFC credential scan mode disabled by default.
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.
While scan mode is active, the RGB LED is blue.
When a future NFC reader backend detects a tag:
+12 -2
View File
@@ -93,7 +93,8 @@ KR-S79 `COM/NC` feedback contact detection using `NC` on XIAO `D7` and `COM` on
### Access Codes
Persistent storage for up to 10 six-digit unlock codes.
Persistent storage for up to 8 permanent and 20 temporary/one-time six-digit
unlock codes.
### NFC Tags
@@ -101,6 +102,11 @@ 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.
### Bluetooth
BLE administration service for the Flutter application, with a JSON command
characteristic and a readable/notifiable state characteristic.
---
## Development Philosophy
@@ -129,7 +135,11 @@ Implemented:
- Three short beeps on invalid access code.
- Persistent six-digit access code storage.
- Persistent NFC tag UID storage with default development UID `60:4F:E2:B5`.
- NFC credential scan-mode API enabled for 1 minute after keypad activity, with blue LED feedback.
- 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.
- Lock command pulse on XIAO pin `D9`.
- Lock state feedback through the KR-S79 `COM/NC` contact.
- Door opened / closed debug output.
+5
View File
@@ -15,6 +15,11 @@ target_sources(app PRIVATE
src/lock_control.c
src/access_codes.c
src/nfc_tags.c
src/opb_ble.c
src/opb_clock.c
src/open_history.c
src/app_identities.c
src/box_settings.c
src/lock_state.c
src/nfc.c
src/gpio_expander.c
Binary file not shown.
+21 -3
View File
@@ -5,9 +5,27 @@ CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
CONFIG_I2C_SHELL=y
CONFIG_SHELL=y
# Keep a single USB CDC console owner. The interactive Zephyr shell and its
# serial backend are intentionally disabled because the firmware uses printf
# diagnostics only.
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_DEBUG_THREAD_INFO=y
CONFIG_PRINTK=y
CONFIG_STDOUT_CONSOLE=y
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="OpenParcelBox"
CONFIG_BT_DEVICE_APPEARANCE=0
CONFIG_BT_MAX_CONN=1
CONFIG_BT_SMP=y
CONFIG_BT_BONDABLE=y
CONFIG_BT_SETTINGS=y
CONFIG_BT_SMP_SC_ONLY=y
CONFIG_BT_PRIVACY=y
CONFIG_BT_RX_STACK_SIZE=2048
CONFIG_ENTROPY_GENERATOR=y
+318 -9
View File
@@ -20,9 +20,18 @@
#define ACCESS_CODES_SETTINGS_TABLE_PATH "codes/table"
#define ACCESS_CODES_MAGIC 0x4f504243U
#define ACCESS_CODES_VERSION 1U
#define ACCESS_CODES_VERSION 3U
#define ACCESS_CODES_V2_VERSION 2U
#define ACCESS_CODES_OLD_VERSION 1U
#define ACCESS_CODES_OLD_MAX_COUNT 10
struct access_code_slot {
bool enabled;
uint8_t kind;
char code[ACCESS_CODE_LENGTH + 1];
};
struct access_code_slot_v1 {
bool enabled;
char code[ACCESS_CODE_LENGTH + 1];
};
@@ -34,7 +43,24 @@ struct access_code_table {
struct access_code_slot slots[ACCESS_CODE_MAX_COUNT];
};
struct access_code_table_v1 {
uint32_t magic;
uint16_t version;
uint16_t count;
struct access_code_slot_v1 slots[ACCESS_CODES_OLD_MAX_COUNT];
};
struct access_code_table_v2 {
uint32_t magic;
uint16_t version;
uint16_t count;
struct access_code_slot slots[ACCESS_CODES_OLD_MAX_COUNT];
};
static struct access_code_table code_table;
static struct access_code_table loaded_table;
static struct access_code_table_v1 loaded_table_v1;
static struct access_code_table_v2 loaded_table_v2;
static bool code_table_loaded;
static bool access_code_is_digit_string(const char *code, size_t length) {
@@ -56,6 +82,11 @@ static bool access_code_slot_is_valid(const struct access_code_slot *slot) {
return true;
}
if (slot->kind != ACCESS_CODE_KIND_PERMANENT &&
slot->kind != ACCESS_CODE_KIND_ONE_TIME) {
return false;
}
if (slot->code[ACCESS_CODE_LENGTH] != '\0') {
return false;
}
@@ -65,6 +96,8 @@ static bool access_code_slot_is_valid(const struct access_code_slot *slot) {
static bool access_code_table_is_valid(const struct access_code_table *table) {
size_t enabled_count = 0;
size_t permanent_count = 0;
size_t temporary_count = 0;
if (table->magic != ACCESS_CODES_MAGIC ||
table->version != ACCESS_CODES_VERSION ||
@@ -79,12 +112,106 @@ static bool access_code_table_is_valid(const struct access_code_table *table) {
if (table->slots[i].enabled) {
enabled_count++;
if (table->slots[i].kind == ACCESS_CODE_KIND_PERMANENT) {
permanent_count++;
} else {
temporary_count++;
}
}
}
return enabled_count == table->count &&
permanent_count <= ACCESS_CODE_PERMANENT_MAX_COUNT &&
temporary_count <= ACCESS_CODE_TEMPORARY_MAX_COUNT;
}
static bool access_code_table_v1_is_valid(
const struct access_code_table_v1 *table) {
size_t enabled_count = 0;
if (table->magic != ACCESS_CODES_MAGIC ||
table->version != ACCESS_CODES_OLD_VERSION ||
table->count > ACCESS_CODES_OLD_MAX_COUNT) {
return false;
}
for (size_t i = 0; i < ACCESS_CODES_OLD_MAX_COUNT; i++) {
if (!table->slots[i].enabled) {
continue;
}
if (table->slots[i].code[ACCESS_CODE_LENGTH] != '\0' ||
!access_code_is_digit_string(table->slots[i].code,
ACCESS_CODE_LENGTH)) {
return false;
}
enabled_count++;
}
return enabled_count == table->count;
}
static void access_codes_migrate_v1(
const struct access_code_table_v1 *old_table) {
memset(&code_table, 0, sizeof(code_table));
code_table.magic = ACCESS_CODES_MAGIC;
code_table.version = ACCESS_CODES_VERSION;
for (size_t i = 0; i < ACCESS_CODES_OLD_MAX_COUNT; i++) {
if (!old_table->slots[i].enabled ||
code_table.count >= ACCESS_CODE_PERMANENT_MAX_COUNT) {
continue;
}
struct access_code_slot *slot = &code_table.slots[code_table.count++];
slot->enabled = true;
slot->kind = ACCESS_CODE_KIND_PERMANENT;
memcpy(slot->code, old_table->slots[i].code,
ACCESS_CODE_LENGTH + 1);
}
}
static bool access_code_table_v2_is_valid(
const struct access_code_table_v2 *table) {
if (table->magic != ACCESS_CODES_MAGIC ||
table->version != ACCESS_CODES_V2_VERSION ||
table->count > ACCESS_CODES_OLD_MAX_COUNT) {
return false;
}
for (size_t i = 0; i < ACCESS_CODES_OLD_MAX_COUNT; i++) {
if (!access_code_slot_is_valid(&table->slots[i])) {
return false;
}
}
return true;
}
static void access_codes_migrate_v2(
const struct access_code_table_v2 *old_table) {
size_t permanent_count = 0;
size_t temporary_count = 0;
memset(&code_table, 0, sizeof(code_table));
code_table.magic = ACCESS_CODES_MAGIC;
code_table.version = ACCESS_CODES_VERSION;
for (size_t i = 0; i < ACCESS_CODES_OLD_MAX_COUNT; i++) {
if (!old_table->slots[i].enabled) {
continue;
}
if (old_table->slots[i].kind == ACCESS_CODE_KIND_PERMANENT) {
if (permanent_count >= ACCESS_CODE_PERMANENT_MAX_COUNT) continue;
permanent_count++;
} else {
if (temporary_count >= ACCESS_CODE_TEMPORARY_MAX_COUNT) continue;
temporary_count++;
}
code_table.slots[code_table.count++] = old_table->slots[i];
}
}
static void access_codes_set_default_table(void) {
memset(&code_table, 0, sizeof(code_table));
@@ -92,25 +219,64 @@ static void access_codes_set_default_table(void) {
code_table.version = ACCESS_CODES_VERSION;
code_table.count = 1;
code_table.slots[0].enabled = true;
code_table.slots[0].kind = ACCESS_CODE_KIND_PERMANENT;
memcpy(code_table.slots[0].code, "784512", ACCESS_CODE_LENGTH + 1);
}
void access_codes_prepare_defaults(void) { access_codes_set_default_table(); }
static int access_codes_save(void) {
#ifdef OPB_DIAGNOSTIC_READ_ONLY
return 0;
#else
return settings_save_one(ACCESS_CODES_SETTINGS_TABLE_PATH, &code_table,
sizeof(code_table));
#endif
}
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_v1)) {
bytes_read = read_cb(cb_arg, &loaded_table_v1, sizeof(loaded_table_v1));
if (bytes_read != sizeof(loaded_table_v1)) {
return -EIO;
}
if (!access_code_table_v1_is_valid(&loaded_table_v1)) {
return 0;
}
access_codes_migrate_v1(&loaded_table_v1);
code_table_loaded = true;
return 0;
}
if (len == sizeof(loaded_table_v2)) {
bytes_read = read_cb(cb_arg, &loaded_table_v2, sizeof(loaded_table_v2));
if (bytes_read != sizeof(loaded_table_v2)) {
return -EIO;
}
if (!access_code_table_v2_is_valid(&loaded_table_v2)) {
return 0;
}
access_codes_migrate_v2(&loaded_table_v2);
code_table_loaded = true;
return 0;
}
if (len != sizeof(loaded_table)) {
return -EINVAL;
/*
* A previous development firmware may have persisted another table
* layout. Ignore it so init can install the safe default table instead of
* preventing the keypad application from starting.
*/
return 0;
}
bytes_read = read_cb(cb_arg, &loaded_table, sizeof(loaded_table));
@@ -156,18 +322,23 @@ int access_codes_init(void) {
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;
}
/*
* Never write or migrate settings during boot. The local keypad must become
* available even when flash contents are stale. The next real credential
* change persists the current table.
*/
return 0;
}
bool access_codes_is_valid(const char *code, size_t length) {
return access_codes_find(code, length, NULL);
}
bool access_codes_find(const char *code, size_t length,
enum access_code_kind *kind) {
if (!access_code_is_digit_string(code, length)) {
return false;
}
@@ -178,6 +349,9 @@ bool access_codes_is_valid(const char *code, size_t length) {
}
if (memcmp(code_table.slots[i].code, code, ACCESS_CODE_LENGTH) == 0) {
if (kind != NULL) {
*kind = code_table.slots[i].kind;
}
return true;
}
}
@@ -185,7 +359,61 @@ bool access_codes_is_valid(const char *code, size_t length) {
return false;
}
bool access_codes_check_and_consume(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) {
continue;
}
if (code_table.slots[i].kind == ACCESS_CODE_KIND_ONE_TIME) {
(void)access_codes_clear(i);
}
return true;
}
return false;
}
int access_codes_consume_if_one_time(const char *code, size_t length) {
if (!access_code_is_digit_string(code, length)) {
return -EINVAL;
}
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) {
continue;
}
if (code_table.slots[i].kind == ACCESS_CODE_KIND_ONE_TIME) {
return access_codes_clear(i);
}
return 0;
}
return 0;
}
int access_codes_set(size_t slot, const char *code) {
return access_codes_set_with_kind(slot, code, ACCESS_CODE_KIND_PERMANENT);
}
int access_codes_set_with_kind(size_t slot, const char *code,
enum access_code_kind kind) {
size_t kind_count = 0;
if (slot >= ACCESS_CODE_MAX_COUNT) {
return -EINVAL;
}
@@ -194,17 +422,61 @@ int access_codes_set(size_t slot, const char *code) {
return -EINVAL;
}
if (kind != ACCESS_CODE_KIND_PERMANENT &&
kind != ACCESS_CODE_KIND_ONE_TIME) {
return -EINVAL;
}
for (size_t i = 0; i < ACCESS_CODE_MAX_COUNT; i++) {
if (code_table.slots[i].enabled && i != slot &&
code_table.slots[i].kind == kind) {
kind_count++;
}
}
if ((kind == ACCESS_CODE_KIND_PERMANENT &&
kind_count >= ACCESS_CODE_PERMANENT_MAX_COUNT) ||
(kind == ACCESS_CODE_KIND_TEMPORARY &&
kind_count >= ACCESS_CODE_TEMPORARY_MAX_COUNT)) {
return -ENOMEM;
}
if (!code_table.slots[slot].enabled) {
code_table.count++;
}
code_table.slots[slot].enabled = true;
code_table.slots[slot].kind = kind;
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_upsert(const char *code, enum access_code_kind kind) {
size_t first_free_slot = ACCESS_CODE_MAX_COUNT;
if (!access_code_is_digit_string(code, ACCESS_CODE_LENGTH)) {
return -EINVAL;
}
for (size_t i = 0; i < ACCESS_CODE_MAX_COUNT; i++) {
if (code_table.slots[i].enabled) {
if (memcmp(code_table.slots[i].code, code, ACCESS_CODE_LENGTH) == 0) {
return access_codes_set_with_kind(i, code, kind);
}
} else if (first_free_slot == ACCESS_CODE_MAX_COUNT) {
first_free_slot = i;
}
}
if (first_free_slot == ACCESS_CODE_MAX_COUNT) {
return -ENOMEM;
}
return access_codes_set_with_kind(first_free_slot, code, kind);
}
int access_codes_clear(size_t slot) {
if (slot >= ACCESS_CODE_MAX_COUNT) {
return -EINVAL;
@@ -215,8 +487,45 @@ int access_codes_clear(size_t slot) {
}
code_table.slots[slot].enabled = false;
code_table.slots[slot].kind = ACCESS_CODE_KIND_PERMANENT;
memset(code_table.slots[slot].code, 0, sizeof(code_table.slots[slot].code));
code_table.count--;
return access_codes_save();
}
int access_codes_clear_code(const char *code) {
if (!access_code_is_digit_string(code, ACCESS_CODE_LENGTH)) {
return -EINVAL;
}
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 access_codes_clear(i);
}
}
return -ENOENT;
}
bool access_codes_get(size_t slot, char code[ACCESS_CODE_LENGTH + 1],
enum access_code_kind *kind) {
if (slot >= ACCESS_CODE_MAX_COUNT || code == NULL || kind == NULL ||
!code_table.slots[slot].enabled) {
return false;
}
memcpy(code, code_table.slots[slot].code, ACCESS_CODE_LENGTH + 1);
*kind = code_table.slots[slot].kind;
return true;
}
int access_codes_factory_reset(void) {
access_codes_set_default_table();
return access_codes_save();
}
+89 -3
View File
@@ -14,13 +14,34 @@
#include <stddef.h>
#define ACCESS_CODE_LENGTH 6
#define ACCESS_CODE_MAX_COUNT 10
#define ACCESS_CODE_PERMANENT_MAX_COUNT 8
#define ACCESS_CODE_TEMPORARY_MAX_COUNT 20
#define ACCESS_CODE_MAX_COUNT \
(ACCESS_CODE_PERMANENT_MAX_COUNT + ACCESS_CODE_TEMPORARY_MAX_COUNT)
enum access_code_kind {
ACCESS_CODE_KIND_PERMANENT = 0,
ACCESS_CODE_KIND_TEMPORARY = 1,
ACCESS_CODE_KIND_ONE_TIME = ACCESS_CODE_KIND_TEMPORARY,
};
/**
* @brief Install the built-in local fallback code in RAM.
*
* This function does not access flash and is safe to call before the settings
* subsystem starts. It guarantees local keypad access while persistent
* credentials are loaded by the background services thread.
*/
void access_codes_prepare_defaults(void);
bool access_codes_find(const char *code, size_t length,
enum access_code_kind *kind);
/**
* @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.
* Loads stored codes from non-volatile settings. If no valid table exists, the
* in-memory development code 784512 remains available in slot 0.
*
* @return 0 on success, negative value on error.
*/
@@ -36,6 +57,27 @@ int access_codes_init(void);
*/
bool access_codes_is_valid(const char *code, size_t length);
/**
* @brief Validate a code and consume it when it is a one-time 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_check_and_consume(const char *code, size_t length);
/**
* @brief Remove a matching code only when it is a one-time code.
*
* @param code Six ASCII digits, not necessarily null-terminated.
* @param length Number of characters available in code.
*
* @return 0 when the code is permanent, consumed, or absent; negative value on
* storage error.
*/
int access_codes_consume_if_one_time(const char *code, size_t length);
/**
* @brief Store or replace a six-digit code in a slot.
*
@@ -46,6 +88,28 @@ bool access_codes_is_valid(const char *code, size_t length);
*/
int access_codes_set(size_t slot, const char *code);
/**
* @brief Store or replace a six-digit code in a slot with an explicit kind.
*
* @param slot Slot index from 0 to ACCESS_CODE_MAX_COUNT - 1.
* @param code Six ASCII digits.
* @param kind Permanent or one-time code behavior.
*
* @return 0 on success, negative value on error.
*/
int access_codes_set_with_kind(size_t slot, const char *code,
enum access_code_kind kind);
/**
* @brief Add a code to the first suitable slot or update its existing slot.
*
* @param code Six ASCII digits.
* @param kind Permanent or one-time code behavior.
*
* @return 0 on success, negative value on error.
*/
int access_codes_upsert(const char *code, enum access_code_kind kind);
/**
* @brief Disable one stored code slot.
*
@@ -55,4 +119,26 @@ int access_codes_set(size_t slot, const char *code);
*/
int access_codes_clear(size_t slot);
/**
* @brief Disable the slot containing a code.
*
* @param code Six ASCII digits.
*
* @return 0 on success, negative value on error.
*/
int access_codes_clear_code(const char *code);
/**
* @brief Read one enabled stored code slot.
*
* @param slot Slot index from 0 to ACCESS_CODE_MAX_COUNT - 1.
* @param code Output buffer of ACCESS_CODE_LENGTH + 1 bytes.
* @param kind Output code kind.
*
* @return true when the slot is enabled and outputs were populated.
*/
bool access_codes_get(size_t slot, char code[ACCESS_CODE_LENGTH + 1],
enum access_code_kind *kind);
int access_codes_factory_reset(void);
#endif /* ACCESS_CODES_H */
+218
View File
@@ -0,0 +1,218 @@
#include "app_identities.h"
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/random/random.h>
#include <zephyr/settings/settings.h>
#define IDENTITIES_MAGIC 0x4f504249U
#define IDENTITIES_VERSION 1U
struct identities_table {
uint32_t magic;
uint16_t version;
uint16_t count;
struct app_identity slots[APP_IDENTITY_MAX_COUNT];
};
static struct identities_table table;
static struct identities_table loaded_table;
static bool loaded;
static bool key_is_valid(const char *key) {
if (key[APP_IDENTITY_KEY_HEX_LENGTH] != '\0') {
return false;
}
for (size_t i = 0; i < APP_IDENTITY_KEY_HEX_LENGTH; i++) {
bool digit = key[i] >= '0' && key[i] <= '9';
bool upper = key[i] >= 'A' && key[i] <= 'F';
bool lower = key[i] >= 'a' && key[i] <= 'f';
if (!digit && !upper && !lower) return false;
}
return true;
}
static bool name_is_valid(const char *name) {
if (name == NULL || strlen(name) > APP_IDENTITY_NAME_MAX_LENGTH) {
return false;
}
return strchr(name, '"') == NULL && strchr(name, '\\') == NULL;
}
static bool table_is_valid(const struct identities_table *candidate) {
size_t count = 0;
size_t admin_count = 0;
if (candidate->magic != IDENTITIES_MAGIC ||
candidate->version != IDENTITIES_VERSION ||
candidate->count > APP_IDENTITY_MAX_COUNT) {
return false;
}
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++) {
const struct app_identity *identity = &candidate->slots[i];
if (!identity->enabled) continue;
if ((identity->role != APP_IDENTITY_ADMIN &&
identity->role != APP_IDENTITY_GUEST) ||
identity->name[APP_IDENTITY_NAME_MAX_LENGTH] != '\0' ||
!key_is_valid(identity->key)) {
return false;
}
count++;
if (identity->role == APP_IDENTITY_ADMIN) admin_count++;
}
return count == candidate->count && admin_count <= 1;
}
static int save(void) {
#ifdef OPB_DIAGNOSTIC_READ_ONLY
return 0;
#else
return settings_save_one("identities/table", &table, sizeof(table));
#endif
}
static int settings_set(const char *key, size_t len, settings_read_cb read_cb,
void *cb_arg) {
if (strcmp(key, "table") != 0 || len != sizeof(table)) return 0;
if (read_cb(cb_arg, &loaded_table, sizeof(loaded_table)) !=
sizeof(loaded_table) ||
!table_is_valid(&loaded_table)) {
return 0;
}
table = loaded_table;
loaded = true;
return 0;
}
static struct settings_handler handler = {.name = "identities",
.h_set = settings_set};
int app_identities_init(void) {
memset(&table, 0, sizeof(table));
table.magic = IDENTITIES_MAGIC;
table.version = IDENTITIES_VERSION;
loaded = false;
int ret = settings_register(&handler);
if (ret < 0) return ret;
ret = settings_load_subtree("identities");
if (ret < 0) return ret;
/* The first real provisioning command persists the default empty table. */
return 0;
}
bool app_identities_has_admin(void) {
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++)
if (table.slots[i].enabled &&
table.slots[i].role == APP_IDENTITY_ADMIN) return true;
return false;
}
bool app_identities_authenticate(const char *key, struct app_identity *identity) {
if (key == NULL || strlen(key) != APP_IDENTITY_KEY_HEX_LENGTH ||
!key_is_valid(key)) return false;
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++) {
if (table.slots[i].enabled &&
strcmp(table.slots[i].key, key) == 0) {
if (identity != NULL) *identity = table.slots[i];
return true;
}
}
return false;
}
static int create(enum app_identity_role role, const char *name, char *key) {
size_t slot = APP_IDENTITY_MAX_COUNT;
uint8_t random[16];
static const char hex[] = "0123456789ABCDEF";
if (key == NULL || !name_is_valid(name)) return -EINVAL;
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++)
if (!table.slots[i].enabled) { slot = i; break; }
if (slot == APP_IDENTITY_MAX_COUNT) return -ENOMEM;
if (sys_csrand_get(random, sizeof(random)) < 0) return -EIO;
struct app_identity *item = &table.slots[slot];
memset(item, 0, sizeof(*item));
item->enabled = true;
item->role = role;
for (size_t i = 0; i < sizeof(random); i++) {
item->key[i * 2] = hex[random[i] >> 4];
item->key[i * 2 + 1] = hex[random[i] & 0x0f];
}
strncpy(item->name, name, APP_IDENTITY_NAME_MAX_LENGTH);
table.count++;
int ret = save();
if (ret == 0) memcpy(key, item->key, sizeof(item->key));
return ret;
}
static int store(enum app_identity_role role, const char *name,
const char *key) {
if (key == NULL || strlen(key) != APP_IDENTITY_KEY_HEX_LENGTH ||
!key_is_valid(key) || !name_is_valid(name)) return -EINVAL;
size_t slot = APP_IDENTITY_MAX_COUNT;
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++) {
if (table.slots[i].enabled && strcmp(table.slots[i].key, key) == 0)
return -EEXIST;
if (!table.slots[i].enabled && slot == APP_IDENTITY_MAX_COUNT) slot = i;
}
if (slot == APP_IDENTITY_MAX_COUNT) return -ENOMEM;
struct app_identity *item = &table.slots[slot];
memset(item, 0, sizeof(*item));
item->enabled = true;
item->role = role;
memcpy(item->key, key, APP_IDENTITY_KEY_HEX_LENGTH + 1);
strncpy(item->name, name, APP_IDENTITY_NAME_MAX_LENGTH);
table.count++;
return save();
}
int app_identities_create_admin(
const char *name, char key[APP_IDENTITY_KEY_HEX_LENGTH + 1]) {
if (app_identities_has_admin()) return -EACCES;
return create(APP_IDENTITY_ADMIN, name, key);
}
int app_identities_store_admin(const char *name, const char *key) {
if (app_identities_has_admin()) return -EACCES;
return store(APP_IDENTITY_ADMIN, name, key);
}
int app_identities_add_guest(
const char *name, char key[APP_IDENTITY_KEY_HEX_LENGTH + 1]) {
return create(APP_IDENTITY_GUEST, name, key);
}
int app_identities_store_guest(const char *name, const char *key) {
return store(APP_IDENTITY_GUEST, name, key);
}
int app_identities_remove(const char *key) {
struct app_identity identity;
if (!app_identities_authenticate(key, &identity)) return -ENOENT;
if (identity.role == APP_IDENTITY_ADMIN) return -EACCES;
for (size_t i = 0; i < APP_IDENTITY_MAX_COUNT; i++) {
if (table.slots[i].enabled && strcmp(table.slots[i].key, key) == 0) {
memset(&table.slots[i], 0, sizeof(table.slots[i]));
table.count--;
return save();
}
}
return -ENOENT;
}
bool app_identities_get(size_t slot, struct app_identity *identity) {
if (slot >= APP_IDENTITY_MAX_COUNT || identity == NULL ||
!table.slots[slot].enabled) return false;
*identity = table.slots[slot];
return true;
}
int app_identities_factory_reset(void) {
memset(&table, 0, sizeof(table));
table.magic = IDENTITIES_MAGIC;
table.version = IDENTITIES_VERSION;
return save();
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef APP_IDENTITIES_H
#define APP_IDENTITIES_H
#include <stdbool.h>
#include <stddef.h>
#define APP_IDENTITY_KEY_HEX_LENGTH 32
#define APP_IDENTITY_NAME_MAX_LENGTH 31
#define APP_IDENTITY_MAX_COUNT 16
enum app_identity_role { APP_IDENTITY_ADMIN = 0, APP_IDENTITY_GUEST = 1 };
struct app_identity {
bool enabled;
unsigned char role;
char key[APP_IDENTITY_KEY_HEX_LENGTH + 1];
char name[APP_IDENTITY_NAME_MAX_LENGTH + 1];
};
int app_identities_init(void);
bool app_identities_has_admin(void);
bool app_identities_authenticate(const char *key, struct app_identity *identity);
int app_identities_create_admin(const char *name,
char key[APP_IDENTITY_KEY_HEX_LENGTH + 1]);
int app_identities_store_admin(const char *name, const char *key);
int app_identities_add_guest(const char *name,
char key[APP_IDENTITY_KEY_HEX_LENGTH + 1]);
int app_identities_store_guest(const char *name, const char *key);
int app_identities_remove(const char *key);
bool app_identities_get(size_t slot, struct app_identity *identity);
int app_identities_factory_reset(void);
#endif
+87
View File
@@ -0,0 +1,87 @@
#include "box_settings.h"
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/settings/settings.h>
#define BOX_SETTINGS_MAGIC 0x4f504253U
#define BOX_SETTINGS_VERSION 1U
#define BOX_SETTINGS_PATH "box/config"
struct box_config {
uint32_t magic;
uint16_t version;
char name[BOX_NAME_MAX_LENGTH + 1];
};
static struct box_config config;
static struct box_config loaded_config;
static bool loaded;
static void set_defaults(void) {
memset(&config, 0, sizeof(config));
config.magic = BOX_SETTINGS_MAGIC;
config.version = BOX_SETTINGS_VERSION;
memcpy(config.name, "OpenParcelBox", sizeof("OpenParcelBox"));
}
static int save(void) {
#ifdef OPB_DIAGNOSTIC_READ_ONLY
return 0;
#else
return settings_save_one(BOX_SETTINGS_PATH, &config, sizeof(config));
#endif
}
static int settings_set(const char *key, size_t len, settings_read_cb read_cb,
void *cb_arg) {
if (strcmp(key, "config") != 0 || len != sizeof(config)) {
return 0;
}
if (read_cb(cb_arg, &loaded_config, sizeof(loaded_config)) !=
sizeof(loaded_config) ||
loaded_config.magic != BOX_SETTINGS_MAGIC ||
loaded_config.version != BOX_SETTINGS_VERSION ||
loaded_config.name[BOX_NAME_MAX_LENGTH] != '\0') {
return 0;
}
config = loaded_config;
loaded = true;
return 0;
}
static struct settings_handler handler = {
.name = "box",
.h_set = settings_set,
};
int box_settings_init(void) {
set_defaults();
loaded = false;
int ret = settings_register(&handler);
if (ret < 0) return ret;
ret = settings_load_subtree("box");
if (ret < 0) return ret;
/* The default box name remains in RAM until explicitly changed. */
return 0;
}
const char *box_settings_get_name(void) { return config.name; }
int box_settings_set_name(const char *name) {
if (name == NULL || name[0] == '\0' || strlen(name) > BOX_NAME_MAX_LENGTH ||
strchr(name, '"') != NULL || strchr(name, '\\') != NULL) {
return -EINVAL;
}
memset(config.name, 0, sizeof(config.name));
strncpy(config.name, name, BOX_NAME_MAX_LENGTH);
return save();
}
int box_settings_factory_reset(void) {
set_defaults();
return save();
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef BOX_SETTINGS_H
#define BOX_SETTINGS_H
#include <stddef.h>
#define BOX_NAME_MAX_LENGTH 31
int box_settings_init(void);
const char *box_settings_get_name(void);
int box_settings_set_name(const char *name);
int box_settings_factory_reset(void);
#endif
+226 -223
View File
@@ -1,20 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Main firmware entry point.
*
* Current test:
* - RGB LED
* - Buzzer
* - Lock control and state feedback
* - GPIO Expander
* - 4x4 Keypad
* OpenParcelBox resilient application entry point.
*/
#include "access_codes.h"
#include "app_identities.h"
#include "box_settings.h"
#include "buzzer.h"
#include "keypad.h"
#include "led.h"
@@ -22,280 +14,291 @@
#include "lock_state.h"
#include "nfc.h"
#include "nfc_tags.h"
#include "opb_ble.h"
#include "opb_clock.h"
#include "open_history.h"
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/kernel.h>
#include <stdio.h>
/* --------------------------------------------------------------------------
* Defines
* -------------------------------------------------------------------------- */
#define MAIN_LOOP_DELAY_MS 20
#define KEY_PRESS_LED_MS 100
#define NFC_SCAN_TIMEOUT_MS 60000
#define KEYPAD_RETRY_MS 1000
#define SERVICE_INIT_STACK_SIZE 4096
#define SERVICE_INIT_PRIORITY 8
#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) + \
#define INVALID_CODE_SEQUENCE_MS \
((INVALID_CODE_BEEP_COUNT * INVALID_CODE_BEEP_MS) + \
((INVALID_CODE_BEEP_COUNT - 1) * INVALID_CODE_BEEP_PAUSE_MS))
/* --------------------------------------------------------------------------
* Private helpers
* -------------------------------------------------------------------------- */
static bool led_ready;
static bool buzzer_ready;
static bool lock_control_ready;
static bool lock_state_ready;
static atomic_t persistent_services_ready;
static K_THREAD_STACK_DEFINE(service_init_stack, SERVICE_INIT_STACK_SIZE);
static struct k_thread service_init_thread;
static void set_status_led(bool lock_open, bool nfc_scan_active) {
if (nfc_scan_active) {
led_set_blue();
} else if (lock_open) {
static bool is_digit(char key) { return key >= '0' && key <= '9'; }
static void set_idle_led(bool door_open) {
if (!led_ready) return;
if (door_open) {
led_set_green();
} else {
led_off();
}
}
static bool key_is_digit(char key) { return key >= '0' && key <= '9'; }
static void print_nfc_uid(const uint8_t *uid, size_t length) {
for (size_t i = 0; i < length; i++) {
printf("%02X", uid[i]);
if (i < (length - 1)) {
printf(":");
}
}
static void beep_key(void) {
if (buzzer_ready) buzzer_beep_key();
}
static void stop_nfc_scan(bool *nfc_scan_active, bool lock_open) {
if (!*nfc_scan_active) {
return;
}
if (nfc_scan_disable() < 0) {
printf("NFC scan disable failed\n");
}
*nfc_scan_active = false;
printf("Scan NFC: OFF\n");
set_status_led(lock_open, *nfc_scan_active);
static void beep_success(void) {
if (buzzer_ready) buzzer_beep_success();
}
static void signal_invalid_code(void) {
led_set_red();
if (led_ready) 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);
if (buzzer_ready) {
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);
k_msleep(INVALID_CODE_LED_MS - INVALID_CODE_SEQUENCE_MS);
}
/* --------------------------------------------------------------------------
* Public API
* -------------------------------------------------------------------------- */
static int request_lock_open(const char *source, const char *actor) {
if (!lock_control_ready) {
printf("Lock open rejected: control GPIO unavailable\n");
return -ENODEV;
}
printf("Lock open requested source=%s timestamp_ms=%lld\n",
source == NULL ? "unknown" : source,
(long long)opb_clock_now_ms());
int ret = lock_control_open();
if (ret < 0) {
printf("Lock control open failed: %d\n", ret);
return ret;
}
if (source != NULL && strcmp(source, "ble") == 0) {
(void)open_history_append(OPEN_HISTORY_APP, actor, opb_clock_now_ms());
}
return 0;
}
static int initialize_persistent_modules(void) {
int first_error = 0;
int ret;
ret = access_codes_init();
printf("Startup access_codes=%d\n", ret);
if (ret < 0 && first_error == 0) first_error = ret;
ret = nfc_tags_init();
printf("Startup nfc_tags=%d\n", ret);
if (ret < 0 && first_error == 0) first_error = ret;
ret = open_history_init();
printf("Startup open_history=%d\n", ret);
if (ret < 0 && first_error == 0) first_error = ret;
ret = opb_clock_init(open_history_last_timestamp_ms());
printf("Startup clock=%d\n", ret);
if (ret < 0 && first_error == 0) first_error = ret;
ret = app_identities_init();
printf("Startup identities=%d\n", ret);
if (ret < 0 && first_error == 0) first_error = ret;
ret = box_settings_init();
printf("Startup box_settings=%d\n", ret);
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;
return first_error;
}
static void service_init_thread_entry(void *unused1, void *unused2,
void *unused3) {
int ret;
ARG_UNUSED(unused1);
ARG_UNUSED(unused2);
ARG_UNUSED(unused3);
ret = initialize_persistent_modules();
if (ret == 0) {
atomic_set(&persistent_services_ready, 1);
} else {
printf("Persistent services degraded: %d\n", ret);
}
ret = opb_ble_init(request_lock_open);
if (ret < 0) {
printf("BLE thread creation failed; local access remains active: %d\n",
ret);
}
}
static int start_service_init_thread(void) {
k_tid_t thread_id =
k_thread_create(&service_init_thread, service_init_stack,
K_THREAD_STACK_SIZEOF(service_init_stack),
service_init_thread_entry, NULL, NULL, NULL,
SERVICE_INIT_PRIORITY, 0, K_NO_WAIT);
if (thread_id == NULL) {
return -ENOMEM;
}
(void)k_thread_name_set(&service_init_thread, "opb_services");
return 0;
}
int main(void) {
char key;
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;
bool nfc_scan_active;
int64_t nfc_scan_deadline_ms;
uint8_t nfc_uid[NFC_TAG_UID_MAX_LENGTH];
size_t nfc_uid_length;
size_t entered_length = 0;
bool keypad_ready;
bool door_open;
bool previous_door_open;
int64_t next_keypad_retry_ms;
int64_t next_open_beep_ms;
if (led_init() < 0) {
return 0;
/*
* These two initializers are RAM-only. They guarantee that keypad access and
* timestamps are usable even if flash settings or Bluetooth never start.
*/
access_codes_prepare_defaults();
(void)opb_clock_init(0);
led_ready = led_init() == 0;
if (led_ready) led_off();
lock_control_ready = lock_control_init() == 0;
buzzer_ready = buzzer_init() == 0;
lock_state_ready = lock_state_init() == 0;
keypad_ready = keypad_init() == 0;
next_keypad_retry_ms = k_uptime_get() + KEYPAD_RETRY_MS;
/*
* Local-ready indication happens before Bluetooth starts. No Bluetooth or
* settings condition can prevent the keypad loop from being reached.
*/
if (led_ready) {
led_set_white();
k_msleep(1000);
led_off();
}
led_off();
printf("OpenParcelBox local runtime ready\n");
if (lock_control_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
if (start_service_init_thread() < 0) {
printf("Service thread creation failed; local access remains active\n");
}
if (buzzer_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (lock_state_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (access_codes_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (nfc_tags_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (nfc_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
if (keypad_init() < 0) {
led_set_magenta();
while (1) {
k_msleep(1000);
}
}
lock_open = lock_state_is_open();
previous_lock_open = lock_open;
next_lock_open_beep_ms = k_uptime_get();
entered_code_length = 0;
nfc_scan_active = false;
nfc_scan_deadline_ms = 0;
set_status_led(lock_open, nfc_scan_active);
printf("\n");
printf("========================================\n");
printf("OpenParcelBox Firmware\n");
printf("Hardware Test: RGB LED + Buzzer + Lock + Keypad + NFC\n");
printf("========================================\n");
printf("Scan NFC: OFF\n");
set_status_led(lock_open, nfc_scan_active);
door_open = lock_state_ready ? lock_state_is_open() : false;
previous_door_open = door_open;
next_open_beep_ms = k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS;
set_idle_led(door_open);
while (1) {
lock_open = lock_state_is_open();
char key;
if (lock_open != previous_lock_open) {
set_status_led(lock_open, nfc_scan_active);
previous_lock_open = lock_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;
if (keypad_ready) printf("Keypad initialization recovered\n");
}
if (lock_open) {
printf("Door opened\n");
next_lock_open_beep_ms = k_uptime_get();
door_open = lock_state_ready ? lock_state_is_open() : false;
if (door_open != previous_door_open) {
previous_door_open = door_open;
set_idle_led(door_open);
if (door_open) {
printf("Door opened timestamp_ms=%lld\n",
(long long)opb_clock_now_ms());
next_open_beep_ms =
k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS;
} else {
printf("Door closed\n");
}
}
if (lock_open && k_uptime_get() >= next_lock_open_beep_ms) {
buzzer_beep_key();
next_lock_open_beep_ms = k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS;
if (door_open && k_uptime_get() >= next_open_beep_ms) {
beep_key();
next_open_beep_ms = k_uptime_get() + LOCK_OPEN_BEEP_INTERVAL_MS;
}
if (nfc_scan_active && k_uptime_get() >= nfc_scan_deadline_ms) {
stop_nfc_scan(&nfc_scan_active, lock_open);
key = keypad_ready ? keypad_get_key() : 0;
if (key == 0) {
k_msleep(MAIN_LOOP_DELAY_MS);
continue;
}
if (nfc_scan_active &&
nfc_read_detected_uid(nfc_uid, sizeof(nfc_uid), &nfc_uid_length)) {
printf("NFC detected: ");
print_nfc_uid(nfc_uid, nfc_uid_length);
printf("\n");
printf("Key pressed: %c\n", key);
if (nfc_tags_is_valid(nfc_uid, nfc_uid_length)) {
printf("NFC valid\n");
buzzer_beep_success();
if (lock_control_open() < 0) {
printf("Lock control open failed\n");
}
stop_nfc_scan(&nfc_scan_active, lock_state_is_open());
} else {
printf("NFC not valid\n");
}
if (key == '*' || key == '#' || key == 'B') {
entered_length = 0;
beep_key();
continue;
}
key = keypad_get_key();
if (!is_digit(key)) {
beep_key();
continue;
}
if (key != 0) {
/*
* Accept keypad input regardless of the feedback contact state. A faulty
* or disconnected door sensor must not disable local credential entry.
*/
entered_code[entered_length++] = key;
printf("Key pressed: %c\n", key);
if (entered_length < ACCESS_CODE_LENGTH) {
beep_key();
continue;
}
if (!nfc_scan_active) {
if (nfc_scan_enable() < 0) {
printf("NFC scan enable failed\n");
} else {
nfc_scan_active = true;
printf("Scan NFC: ON\n");
}
}
nfc_scan_deadline_ms = k_uptime_get() + NFC_SCAN_TIMEOUT_MS;
set_status_led(lock_open, nfc_scan_active);
if (!lock_open) {
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++;
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");
}
stop_nfc_scan(&nfc_scan_active, lock_state_is_open());
} 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();
{
enum access_code_kind kind;
if (access_codes_find(entered_code, entered_length, &kind)) {
printf("Received valid access code\n");
beep_success();
if (request_lock_open("keypad", NULL) == 0) {
if (atomic_get(&persistent_services_ready) != 0) {
(void)open_history_append(
kind == ACCESS_CODE_KIND_PERMANENT
? OPEN_HISTORY_PERMANENT_CODE
: OPEN_HISTORY_TEMPORARY_CODE,
"", opb_clock_now_ms());
}
(void)access_codes_consume_if_one_time(entered_code,
entered_length);
opb_ble_notify_state_changed();
}
k_msleep(KEY_PRESS_LED_MS);
set_status_led(lock_state_is_open(), nfc_scan_active);
} else {
printf("Invalid access code\n");
signal_invalid_code();
}
}
entered_length = 0;
set_idle_led(lock_state_ready ? lock_state_is_open() : false);
k_msleep(MAIN_LOOP_DELAY_MS);
}
+154 -6
View File
@@ -20,12 +20,20 @@
#define NFC_TAGS_SETTINGS_TABLE_PATH "nfc_tags/table"
#define NFC_TAGS_MAGIC 0x4f504254U
#define NFC_TAGS_VERSION 1U
#define NFC_TAGS_VERSION 2U
#define NFC_TAGS_OLD_VERSION 1U
struct nfc_tag_slot {
bool enabled;
uint8_t length;
uint8_t uid[NFC_TAG_UID_MAX_LENGTH];
char name[NFC_TAG_NAME_MAX_LENGTH + 1];
};
struct nfc_tag_slot_v1 {
bool enabled;
uint8_t length;
uint8_t uid[NFC_TAG_UID_MAX_LENGTH];
};
struct nfc_tag_table {
@@ -35,21 +43,39 @@ struct nfc_tag_table {
struct nfc_tag_slot slots[NFC_TAG_MAX_COUNT];
};
struct nfc_tag_table_v1 {
uint32_t magic;
uint16_t version;
uint16_t count;
struct nfc_tag_slot_v1 slots[NFC_TAG_MAX_COUNT];
};
static const uint8_t default_test_uid[] = {0x60, 0x4f, 0xe2, 0xb5};
static struct nfc_tag_table tag_table;
static struct nfc_tag_table loaded_table;
static struct nfc_tag_table_v1 loaded_table_v1;
static bool tag_table_loaded;
static bool nfc_tag_uid_length_is_valid(size_t length) {
return length > 0 && length <= NFC_TAG_UID_MAX_LENGTH;
}
static bool nfc_tag_name_is_valid(const char *name) {
if (name == NULL || strlen(name) > NFC_TAG_NAME_MAX_LENGTH) {
return false;
}
return strchr(name, '"') == NULL && strchr(name, '\\') == NULL;
}
static bool nfc_tag_slot_is_valid(const struct nfc_tag_slot *slot) {
if (!slot->enabled) {
return true;
}
return nfc_tag_uid_length_is_valid(slot->length);
return nfc_tag_uid_length_is_valid(slot->length) &&
slot->name[NFC_TAG_NAME_MAX_LENGTH] == '\0';
}
static bool nfc_tag_table_is_valid(const struct nfc_tag_table *table) {
@@ -85,21 +111,46 @@ static void nfc_tags_set_default_table(void) {
}
static int nfc_tags_save(void) {
#ifdef OPB_DIAGNOSTIC_READ_ONLY
return 0;
#else
return settings_save_one(NFC_TAGS_SETTINGS_TABLE_PATH, &tag_table,
sizeof(tag_table));
#endif
}
static int nfc_tags_settings_set(const char *key, size_t len,
settings_read_cb read_cb, void *cb_arg) {
struct nfc_tag_table loaded_table;
ssize_t bytes_read;
if (strcmp(key, NFC_TAGS_SETTINGS_TABLE) != 0) {
return -ENOENT;
}
if (len == sizeof(loaded_table_v1)) {
bytes_read =
read_cb(cb_arg, &loaded_table_v1, sizeof(loaded_table_v1));
if (bytes_read != sizeof(loaded_table_v1) ||
loaded_table_v1.magic != NFC_TAGS_MAGIC ||
loaded_table_v1.version != NFC_TAGS_OLD_VERSION ||
loaded_table_v1.count > NFC_TAG_MAX_COUNT) {
return 0;
}
nfc_tags_set_default_table();
memset(tag_table.slots, 0, sizeof(tag_table.slots));
tag_table.count = loaded_table_v1.count;
for (size_t i = 0; i < NFC_TAG_MAX_COUNT; i++) {
tag_table.slots[i].enabled = loaded_table_v1.slots[i].enabled;
tag_table.slots[i].length = loaded_table_v1.slots[i].length;
memcpy(tag_table.slots[i].uid, loaded_table_v1.slots[i].uid,
sizeof(loaded_table_v1.slots[i].uid));
}
tag_table_loaded = true;
return 0;
}
if (len != sizeof(loaded_table)) {
return -EINVAL;
return 0;
}
bytes_read = read_cb(cb_arg, &loaded_table, sizeof(loaded_table));
@@ -140,13 +191,19 @@ int nfc_tags_init(void) {
if (!tag_table_loaded || !nfc_tag_table_is_valid(&tag_table)) {
nfc_tags_set_default_table();
return nfc_tags_save();
return 0;
}
/* Persistence is updated by an explicit tag change, never during boot. */
return 0;
}
bool nfc_tags_is_valid(const uint8_t *uid, size_t length) {
return nfc_tags_find(uid, length, NULL);
}
bool nfc_tags_find(const uint8_t *uid, size_t length,
char name[NFC_TAG_NAME_MAX_LENGTH + 1]) {
if (uid == NULL || !nfc_tag_uid_length_is_valid(length)) {
return false;
}
@@ -157,7 +214,10 @@ bool nfc_tags_is_valid(const uint8_t *uid, size_t length) {
}
if (tag_table.slots[i].length == length &&
memcmp(tag_table.slots[i].uid, uid, length) == 0) {
memcmp(tag_table.slots[i].uid, uid, length) == 0) {
if (name != NULL) {
memcpy(name, tag_table.slots[i].name, sizeof(tag_table.slots[i].name));
}
return true;
}
}
@@ -183,6 +243,47 @@ int nfc_tags_set(size_t slot, const uint8_t *uid, size_t length) {
return nfc_tags_save();
}
int nfc_tags_upsert(const uint8_t *uid, size_t length) {
return nfc_tags_upsert_named(uid, length, "");
}
int nfc_tags_upsert_named(const uint8_t *uid, size_t length, const char *name) {
size_t first_free_slot = NFC_TAG_MAX_COUNT;
if (uid == NULL || !nfc_tag_uid_length_is_valid(length) ||
!nfc_tag_name_is_valid(name)) {
return -EINVAL;
}
for (size_t i = 0; i < NFC_TAG_MAX_COUNT; i++) {
if (tag_table.slots[i].enabled) {
if (tag_table.slots[i].length == length &&
memcmp(tag_table.slots[i].uid, uid, length) == 0) {
memset(tag_table.slots[i].name, 0,
sizeof(tag_table.slots[i].name));
strncpy(tag_table.slots[i].name, name, NFC_TAG_NAME_MAX_LENGTH);
return nfc_tags_save();
}
} else if (first_free_slot == NFC_TAG_MAX_COUNT) {
first_free_slot = i;
}
}
if (first_free_slot == NFC_TAG_MAX_COUNT) {
return -ENOMEM;
}
struct nfc_tag_slot *slot = &tag_table.slots[first_free_slot];
memset(slot, 0, sizeof(*slot));
slot->enabled = true;
slot->length = length;
memcpy(slot->uid, uid, length);
strncpy(slot->name, name,
NFC_TAG_NAME_MAX_LENGTH);
tag_table.count++;
return nfc_tags_save();
}
int nfc_tags_clear(size_t slot) {
if (slot >= NFC_TAG_MAX_COUNT) {
return -EINVAL;
@@ -195,7 +296,54 @@ int nfc_tags_clear(size_t slot) {
tag_table.slots[slot].enabled = false;
tag_table.slots[slot].length = 0;
memset(tag_table.slots[slot].uid, 0, sizeof(tag_table.slots[slot].uid));
memset(tag_table.slots[slot].name, 0, sizeof(tag_table.slots[slot].name));
tag_table.count--;
return nfc_tags_save();
}
int nfc_tags_clear_uid(const uint8_t *uid, size_t length) {
if (uid == NULL || !nfc_tag_uid_length_is_valid(length)) {
return -EINVAL;
}
for (size_t i = 0; i < NFC_TAG_MAX_COUNT; i++) {
if (!tag_table.slots[i].enabled) {
continue;
}
if (tag_table.slots[i].length == length &&
memcmp(tag_table.slots[i].uid, uid, length) == 0) {
return nfc_tags_clear(i);
}
}
return -ENOENT;
}
bool nfc_tags_get(size_t slot, uint8_t uid[NFC_TAG_UID_MAX_LENGTH],
size_t *length) {
return nfc_tags_get_named(slot, uid, length, NULL);
}
bool nfc_tags_get_named(size_t slot, uint8_t uid[NFC_TAG_UID_MAX_LENGTH],
size_t *length,
char name[NFC_TAG_NAME_MAX_LENGTH + 1]) {
if (slot >= NFC_TAG_MAX_COUNT || uid == NULL || length == NULL ||
!tag_table.slots[slot].enabled) {
return false;
}
*length = tag_table.slots[slot].length;
memcpy(uid, tag_table.slots[slot].uid, *length);
if (name != NULL) {
memcpy(name, tag_table.slots[slot].name, sizeof(tag_table.slots[slot].name));
}
return true;
}
int nfc_tags_factory_reset(void) {
nfc_tags_set_default_table();
return nfc_tags_save();
}
+41 -1
View File
@@ -16,12 +16,13 @@
#define NFC_TAG_UID_MAX_LENGTH 10
#define NFC_TAG_MAX_COUNT 10
#define NFC_TAG_NAME_MAX_LENGTH 31
/**
* @brief Initialize persistent NFC tag UID storage.
*
* Loads stored NFC tag UIDs from non-volatile settings. If no valid table
* exists yet, the development test UID 60:4F:E2:B5 is stored in slot 0.
* exists, the in-memory development UID 60:4F:E2:B5 remains in slot 0.
*
* @return 0 on success, negative value on error.
*/
@@ -48,6 +49,19 @@ bool nfc_tags_is_valid(const uint8_t *uid, size_t length);
*/
int nfc_tags_set(size_t slot, const uint8_t *uid, size_t length);
/**
* @brief Add a UID to the first suitable slot or update its existing slot.
*
* @param uid UID bytes.
* @param length Number of UID bytes.
*
* @return 0 on success, negative value on error.
*/
int nfc_tags_upsert(const uint8_t *uid, size_t length);
int nfc_tags_upsert_named(const uint8_t *uid, size_t length, const char *name);
bool nfc_tags_find(const uint8_t *uid, size_t length,
char name[NFC_TAG_NAME_MAX_LENGTH + 1]);
/**
* @brief Disable one stored NFC tag UID slot.
*
@@ -57,4 +71,30 @@ int nfc_tags_set(size_t slot, const uint8_t *uid, size_t length);
*/
int nfc_tags_clear(size_t slot);
/**
* @brief Disable the slot containing a UID.
*
* @param uid UID bytes.
* @param length Number of UID bytes.
*
* @return 0 on success, negative value on error.
*/
int nfc_tags_clear_uid(const uint8_t *uid, size_t length);
/**
* @brief Read one enabled stored NFC tag slot.
*
* @param slot Slot index from 0 to NFC_TAG_MAX_COUNT - 1.
* @param uid Output UID bytes.
* @param length Output UID length.
*
* @return true when the slot is enabled and outputs were populated.
*/
bool nfc_tags_get(size_t slot, uint8_t uid[NFC_TAG_UID_MAX_LENGTH],
size_t *length);
bool nfc_tags_get_named(size_t slot, uint8_t uid[NFC_TAG_UID_MAX_LENGTH],
size_t *length,
char name[NFC_TAG_NAME_MAX_LENGTH + 1]);
int nfc_tags_factory_reset(void);
#endif /* NFC_TAGS_H */
+726
View File
@@ -0,0 +1,726 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Bluetooth Low Energy administration service.
*/
#include "opb_ble.h"
#include "access_codes.h"
#include "app_identities.h"
#include "box_settings.h"
#include "nfc_tags.h"
#include "opb_clock.h"
#include "open_history.h"
#include <ctype.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <zephyr/bluetooth/att.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/util.h>
#define OPB_BLE_RX_MAX 256
#define OPB_BLE_STATE_MAX 12288
#define OPB_BLE_INIT_STACK_SIZE 4096
#define OPB_BLE_INIT_PRIORITY 7
#define OPB_BLE_ADV_RETRY_COUNT 20
#define OPB_BLE_ADV_RETRY_DELAY_MS 100
#define OPB_UUID_SERVICE \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0xf2a00000, 0x8e7a, 0x4f8d, 0x9b1d, \
0x7d8e4b7a0001))
#define OPB_UUID_COMMAND \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0xf2a00001, 0x8e7a, 0x4f8d, 0x9b1d, \
0x7d8e4b7a0001))
#define OPB_UUID_STATE \
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0xf2a00002, 0x8e7a, 0x4f8d, 0x9b1d, \
0x7d8e4b7a0001))
static opb_ble_open_lock_cb_t open_lock_cb;
static bool state_notifications_enabled;
static char state_json[OPB_BLE_STATE_MAX];
static char enrollment_name[NFC_TAG_NAME_MAX_LENGTH + 1];
static bool enrollment_requested;
static struct bt_conn *authenticated_conn;
static struct app_identity authenticated_identity;
static bool authenticated_identity_valid;
static struct k_work_delayable factory_reset_work;
static K_THREAD_STACK_DEFINE(opb_ble_init_stack, OPB_BLE_INIT_STACK_SIZE);
static struct k_thread opb_ble_init_thread;
static bool opb_ble_init_thread_started;
static ssize_t state_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset);
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);
static void state_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value);
static void factory_reset_work_handler(struct k_work *work) {
ARG_UNUSED(work);
(void)bt_unpair(BT_ID_DEFAULT, NULL);
printf("BLE bonds cleared after factory reset\n");
}
static void disconnected(struct bt_conn *conn, uint8_t reason) {
ARG_UNUSED(reason);
if (conn == authenticated_conn) {
authenticated_conn = NULL;
memset(&authenticated_identity, 0, sizeof(authenticated_identity));
authenticated_identity_valid = false;
}
}
BT_CONN_CB_DEFINE(opb_conn_callbacks) = {
.disconnected = disconnected,
};
BT_GATT_SERVICE_DEFINE(
opb_service, BT_GATT_PRIMARY_SERVICE(OPB_UUID_SERVICE),
BT_GATT_CHARACTERISTIC(OPB_UUID_COMMAND, BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE_ENCRYPT, NULL, command_write, NULL),
BT_GATT_CHARACTERISTIC(OPB_UUID_STATE,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ_ENCRYPT, state_read, NULL, NULL),
BT_GATT_CCC(state_ccc_changed,
BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT));
static const struct bt_data advertising_data[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
BT_UUID_128_ENCODE(0xf2a00000, 0x8e7a, 0x4f8d, 0x9b1d,
0x7d8e4b7a0001)),
};
static const struct bt_data scan_response_data[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
static const char *json_skip_ws(const char *cursor) {
while (*cursor != '\0' && isspace((unsigned char)*cursor)) {
cursor++;
}
return cursor;
}
static bool json_get_string(const char *json, const char *field, char *value,
size_t value_size) {
char pattern[32];
const char *cursor;
size_t i = 0;
if (snprintf(pattern, sizeof(pattern), "\"%s\"", field) >=
(int)sizeof(pattern)) {
return false;
}
cursor = strstr(json, pattern);
if (cursor == NULL) {
return false;
}
cursor += strlen(pattern);
cursor = json_skip_ws(cursor);
if (*cursor != ':') {
return false;
}
cursor = json_skip_ws(cursor + 1);
if (*cursor != '"') {
return false;
}
cursor++;
while (*cursor != '\0' && *cursor != '"' && i < (value_size - 1)) {
value[i] = *cursor;
cursor++;
i++;
}
if (*cursor != '"') {
return false;
}
value[i] = '\0';
return true;
}
static bool json_get_int64(const char *json, const char *field, int64_t *value) {
char pattern[32];
const char *cursor;
char *endptr;
if (snprintf(pattern, sizeof(pattern), "\"%s\"", field) >=
(int)sizeof(pattern)) {
return false;
}
cursor = strstr(json, pattern);
if (cursor == NULL) {
return false;
}
cursor += strlen(pattern);
cursor = json_skip_ws(cursor);
if (*cursor != ':') {
return false;
}
cursor = json_skip_ws(cursor + 1);
*value = strtoll(cursor, &endptr, 10);
return endptr != cursor;
}
static int hex_value(char value) {
if (value >= '0' && value <= '9') {
return value - '0';
}
value = (char)toupper((unsigned char)value);
if (value >= 'A' && value <= 'F') {
return value - 'A' + 10;
}
return -1;
}
static bool parse_uid_string(const char *text, uint8_t *uid, size_t uid_size,
size_t *uid_length) {
size_t length = 0;
while (*text != '\0') {
int high;
int low;
if (*text == ':' || *text == '-' || isspace((unsigned char)*text)) {
text++;
continue;
}
high = hex_value(*text++);
if (high < 0 || *text == '\0') {
return false;
}
low = hex_value(*text++);
if (low < 0 || length >= uid_size) {
return false;
}
uid[length++] = (uint8_t)((high << 4) | low);
}
if (length == 0) {
return false;
}
*uid_length = length;
return true;
}
static void append_text(char *buffer, size_t buffer_size, size_t *offset,
const char *text) {
int written;
if (*offset >= buffer_size) {
return;
}
written = snprintf(buffer + *offset, buffer_size - *offset, "%s", text);
if (written < 0) {
return;
}
*offset += MIN((size_t)written, buffer_size - *offset);
}
static void append_formatted(char *buffer, size_t buffer_size, size_t *offset,
const char *format, ...) {
va_list args;
int written;
if (*offset >= buffer_size) {
return;
}
va_start(args, format);
written = vsnprintf(buffer + *offset, buffer_size - *offset, format, args);
va_end(args);
if (written < 0) {
return;
}
*offset += MIN((size_t)written, buffer_size - *offset);
}
static size_t build_state_json(void) {
size_t offset = 0;
bool first = true;
int64_t unix_ms = 0;
append_formatted(state_json, sizeof(state_json), &offset,
"{\"box_name\":\"%s\",\"role\":\"%s\",\"clock_synced\":",
box_settings_get_name(),
authenticated_identity.role == APP_IDENTITY_ADMIN
? "administrator"
: "guest");
append_text(state_json, sizeof(state_json), &offset,
opb_clock_is_phone_synced() ? "true" : "false");
if (opb_ble_get_unix_time_ms(&unix_ms)) {
append_formatted(state_json, sizeof(state_json), &offset,
",\"unix_ms\":%lld", (long long)unix_ms);
}
/*
* Guests only need enough state to confirm their authenticated session and
* open the lock. Credential tables, tag names, history, and other guests are
* administrator data.
*/
if (authenticated_identity.role != APP_IDENTITY_ADMIN) {
append_text(state_json, sizeof(state_json), &offset,
",\"admin_exists\":true}");
return strlen(state_json);
}
append_text(state_json, sizeof(state_json), &offset, ",\"codes\":[");
for (size_t slot = 0; slot < ACCESS_CODE_MAX_COUNT; slot++) {
char code[ACCESS_CODE_LENGTH + 1];
enum access_code_kind kind;
if (!access_codes_get(slot, code, &kind)) {
continue;
}
append_formatted(state_json, sizeof(state_json), &offset,
"%s{\"slot\":%u,\"code\":\"%s\",\"kind\":\"%s\"}",
first ? "" : ",", (unsigned int)slot, code,
kind == ACCESS_CODE_KIND_ONE_TIME ? "one_time"
: "permanent");
first = false;
}
append_text(state_json, sizeof(state_json), &offset, "],\"nfc_tags\":[");
first = true;
for (size_t slot = 0; slot < NFC_TAG_MAX_COUNT; slot++) {
uint8_t uid[NFC_TAG_UID_MAX_LENGTH];
size_t uid_length;
char name[NFC_TAG_NAME_MAX_LENGTH + 1];
if (!nfc_tags_get_named(slot, uid, &uid_length, name)) {
continue;
}
append_formatted(state_json, sizeof(state_json), &offset,
"%s{\"slot\":%u,\"uid\":\"", first ? "" : ",",
(unsigned int)slot);
for (size_t i = 0; i < uid_length; i++) {
append_formatted(state_json, sizeof(state_json), &offset, "%s%02X",
i == 0 ? "" : ":", uid[i]);
}
append_formatted(state_json, sizeof(state_json), &offset,
"\",\"name\":\"%s\"}", name);
first = false;
}
append_text(state_json, sizeof(state_json), &offset, "],\"history\":[");
first = true;
for (size_t i = 0; i < open_history_count(); i++) {
struct open_history_event event;
if (!open_history_get(i, &event)) continue;
append_formatted(state_json, sizeof(state_json), &offset,
"%s{\"unix_ms\":%lld,\"kind\":%u,\"actor\":\"%s\"}",
first ? "" : ",", (long long)event.unix_ms,
event.kind, event.actor);
first = false;
}
append_text(state_json, sizeof(state_json), &offset, "],\"guests\":[");
first = true;
if (authenticated_identity.role == APP_IDENTITY_ADMIN) {
for (size_t slot = 0; slot < APP_IDENTITY_MAX_COUNT; slot++) {
struct app_identity identity;
if (!app_identities_get(slot, &identity) ||
identity.role != APP_IDENTITY_GUEST) {
continue;
}
append_formatted(state_json, sizeof(state_json), &offset,
"%s{\"name\":\"%s\",\"key\":\"%s\"}",
first ? "" : ",", identity.name, identity.key);
first = false;
}
}
append_formatted(state_json, sizeof(state_json), &offset,
"],\"admin_exists\":%s}",
app_identities_has_admin() ? "true" : "false");
return strlen(state_json);
}
static size_t build_public_state_json(void) {
return (size_t)snprintf(
state_json, sizeof(state_json),
"{\"box_name\":\"%s\",\"admin_exists\":%s}",
box_settings_get_name(),
app_identities_has_admin() ? "true" : "false");
}
static ssize_t state_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset) {
size_t state_len = conn == authenticated_conn && authenticated_identity_valid
? build_state_json()
: build_public_state_json();
return bt_gatt_attr_read(conn, attr, buf, len, offset, state_json, state_len);
}
static void state_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) {
state_notifications_enabled = (value == BT_GATT_CCC_NOTIFY);
}
void opb_ble_notify_state_changed(void) {
static const char changed_notification[] = "{\"changed\":true}";
if (!state_notifications_enabled || authenticated_conn == NULL) {
return;
}
/*
* A full state document can be several kilobytes and does not fit in one ATT
* notification. Notify only that state changed; clients then perform the
* existing long read on the state characteristic.
*/
(void)bt_gatt_notify(authenticated_conn, &opb_service.attrs[4],
changed_notification,
sizeof(changed_notification) - 1);
}
bool opb_ble_get_unix_time_ms(int64_t *unix_ms) {
if (unix_ms == NULL) {
return false;
}
*unix_ms = opb_clock_now_ms();
return true;
}
static int handle_sync_clock(const char *json) {
int64_t unix_ms;
if (!json_get_int64(json, "unix_ms", &unix_ms)) {
return -EINVAL;
}
opb_clock_sync(unix_ms);
printf("BLE clock synchronized: %lld\n", (long long)unix_ms);
return 0;
}
static int handle_add_code(const char *json) {
char code[ACCESS_CODE_LENGTH + 1];
char kind_text[16];
enum access_code_kind kind = ACCESS_CODE_KIND_PERMANENT;
if (!json_get_string(json, "code", code, sizeof(code))) {
return -EINVAL;
}
if (json_get_string(json, "kind", kind_text, sizeof(kind_text)) &&
strcmp(kind_text, "one_time") == 0) {
kind = ACCESS_CODE_KIND_ONE_TIME;
}
return access_codes_upsert(code, kind);
}
static int handle_remove_code(const char *json) {
char code[ACCESS_CODE_LENGTH + 1];
if (!json_get_string(json, "code", code, sizeof(code))) {
return -EINVAL;
}
return access_codes_clear_code(code);
}
static int handle_add_nfc_tag(const char *json) {
char uid_text[40];
uint8_t uid[NFC_TAG_UID_MAX_LENGTH];
size_t uid_length;
if (!json_get_string(json, "uid", uid_text, sizeof(uid_text)) ||
!parse_uid_string(uid_text, uid, sizeof(uid), &uid_length)) {
return -EINVAL;
}
char name[NFC_TAG_NAME_MAX_LENGTH + 1] = "";
(void)json_get_string(json, "name", name, sizeof(name));
return nfc_tags_upsert_named(uid, uid_length, name);
}
static int handle_remove_nfc_tag(const char *json) {
char uid_text[40];
uint8_t uid[NFC_TAG_UID_MAX_LENGTH];
size_t uid_length;
if (!json_get_string(json, "uid", uid_text, sizeof(uid_text)) ||
!parse_uid_string(uid_text, uid, sizeof(uid), &uid_length)) {
return -EINVAL;
}
return nfc_tags_clear_uid(uid, uid_length);
}
static int handle_command(struct bt_conn *conn, const char *json) {
char command[32];
char key[APP_IDENTITY_KEY_HEX_LENGTH + 1];
struct app_identity identity;
if (!json_get_string(json, "command", command, sizeof(command))) {
return -EINVAL;
}
if (strcmp(command, "provision_admin") == 0) {
char name[APP_IDENTITY_NAME_MAX_LENGTH + 1] = "Administrator";
char box_name[BOX_NAME_MAX_LENGTH + 1] = "OpenParcelBox";
if (app_identities_has_admin() ||
!json_get_string(json, "identity_key", key, sizeof(key))) {
return -EACCES;
}
(void)json_get_string(json, "name", name, sizeof(name));
(void)json_get_string(json, "box_name", box_name, sizeof(box_name));
int ret = box_settings_set_name(box_name);
if (ret < 0) return ret;
ret = app_identities_store_admin(name, key);
if (ret == 0) {
authenticated_conn = conn;
(void)app_identities_authenticate(key, &authenticated_identity);
authenticated_identity_valid = true;
}
return ret;
}
if (!json_get_string(json, "identity_key", key, sizeof(key)) ||
!app_identities_authenticate(key, &identity)) {
return -EACCES;
}
authenticated_conn = conn;
authenticated_identity = identity;
authenticated_identity_valid = true;
if (identity.role == APP_IDENTITY_GUEST &&
strcmp(command, "open_lock") != 0 &&
strcmp(command, "sync_clock") != 0 &&
strcmp(command, "authenticate") != 0) {
return -EACCES;
}
if (strcmp(command, "authenticate") == 0) {
return 0;
}
if (strcmp(command, "sync_clock") == 0) {
return handle_sync_clock(json);
}
if (strcmp(command, "open_lock") == 0) {
if (open_lock_cb == NULL) {
return -ENOSYS;
}
return open_lock_cb("ble", identity.name);
}
if (strcmp(command, "add_code") == 0) {
return handle_add_code(json);
}
if (strcmp(command, "remove_code") == 0) {
return handle_remove_code(json);
}
if (strcmp(command, "add_nfc_tag") == 0) {
return handle_add_nfc_tag(json);
}
if (strcmp(command, "remove_nfc_tag") == 0) {
return handle_remove_nfc_tag(json);
}
if (strcmp(command, "start_nfc_enrollment") == 0) {
if (identity.role != APP_IDENTITY_ADMIN ||
!json_get_string(json, "name", enrollment_name,
sizeof(enrollment_name))) {
return -EACCES;
}
enrollment_requested = true;
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];
if (identity.role != APP_IDENTITY_ADMIN ||
!json_get_string(json, "name", name, sizeof(name)) ||
!json_get_string(json, "guest_key", guest_key, sizeof(guest_key))) {
return -EACCES;
}
return app_identities_store_guest(name, guest_key);
}
if (strcmp(command, "remove_guest") == 0) {
char guest_key[APP_IDENTITY_KEY_HEX_LENGTH + 1];
if (identity.role != APP_IDENTITY_ADMIN ||
!json_get_string(json, "guest_key", guest_key, sizeof(guest_key))) {
return -EACCES;
}
return app_identities_remove(guest_key);
}
if (strcmp(command, "factory_reset") == 0) {
int ret;
if (identity.role != APP_IDENTITY_ADMIN) {
return -EACCES;
}
ret = access_codes_factory_reset();
if (ret < 0) return ret;
ret = nfc_tags_factory_reset();
if (ret < 0) return ret;
ret = open_history_clear();
if (ret < 0) return ret;
ret = box_settings_factory_reset();
if (ret < 0) return ret;
ret = app_identities_factory_reset();
if (ret == 0) {
authenticated_conn = NULL;
authenticated_identity_valid = false;
memset(&authenticated_identity, 0, sizeof(authenticated_identity));
(void)k_work_schedule(&factory_reset_work, K_MSEC(250));
}
return ret;
}
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';
enrollment_requested = false;
return true;
}
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) {
char json[OPB_BLE_RX_MAX];
int ret;
if (offset != 0) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if (len >= sizeof(json)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
memcpy(json, buf, len);
json[len] = '\0';
ret = handle_command(conn, json);
if (ret < 0) {
printf("BLE command failed: %d\n", ret);
return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
}
opb_ble_notify_state_changed();
return len;
}
static void opb_ble_init_thread_entry(void *unused1, void *unused2,
void *unused3) {
int ret;
ARG_UNUSED(unused1);
ARG_UNUSED(unused2);
ARG_UNUSED(unused3);
/*
* CONFIG_BT_SETTINGS deliberately leaves BT_DEV_READY clear after
* bt_enable() until the Bluetooth settings subtree has been loaded. Starting
* advertising before this step returns -EAGAIN.
*/
ret = bt_enable(NULL);
if (ret < 0) {
printf("BLE initialization failed: %d\n", ret);
return;
}
ret = settings_load_subtree("bt");
if (ret < 0) {
printf("BLE settings load failed: %d\n", ret);
}
for (int attempt = 0; attempt < OPB_BLE_ADV_RETRY_COUNT; attempt++) {
ret = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, advertising_data,
ARRAY_SIZE(advertising_data), scan_response_data,
ARRAY_SIZE(scan_response_data));
if (ret != -EAGAIN) {
break;
}
k_msleep(OPB_BLE_ADV_RETRY_DELAY_MS);
}
if (ret < 0) {
printf("BLE advertising failed: %d\n", ret);
return;
}
printf("BLE advertising: OpenParcelBox\n");
}
int opb_ble_init(opb_ble_open_lock_cb_t callback) {
k_tid_t thread_id;
open_lock_cb = callback;
k_work_init_delayable(&factory_reset_work, factory_reset_work_handler);
if (opb_ble_init_thread_started) {
return -EALREADY;
}
opb_ble_init_thread_started = true;
thread_id = k_thread_create(
&opb_ble_init_thread, opb_ble_init_stack,
K_THREAD_STACK_SIZEOF(opb_ble_init_stack), opb_ble_init_thread_entry,
NULL, NULL, NULL, OPB_BLE_INIT_PRIORITY, 0, K_NO_WAIT);
if (thread_id == NULL) {
opb_ble_init_thread_started = false;
return -ENOMEM;
}
(void)k_thread_name_set(&opb_ble_init_thread, "opb_ble_init");
return 0;
}
+44
View File
@@ -0,0 +1,44 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* OpenParcelBox
* Copyright (c) 2026
*
* Bluetooth Low Energy administration service.
*/
#ifndef OPB_BLE_H
#define OPB_BLE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef int (*opb_ble_open_lock_cb_t)(const char *source, const char *actor);
/**
* @brief Initialize BLE advertising and the OpenParcelBox GATT service.
*
* @param open_lock_cb Callback used by the BLE command service to request a
* lock opening through the normal application path.
*
* @return 0 on success, negative value on error.
*/
int opb_ble_init(opb_ble_open_lock_cb_t open_lock_cb);
/**
* @brief Get the current synchronized Unix time.
*
* @param unix_ms Output Unix timestamp in milliseconds.
*
* @return true when the phone has synchronized the clock since boot.
*/
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);
#endif /* OPB_BLE_H */
+29
View File
@@ -0,0 +1,29 @@
#include "opb_clock.h"
#include <zephyr/kernel.h>
#define OPB_DEFAULT_TIME_MS 1780272000000LL /* 2026-06-01T00:00:00Z */
static int64_t reference_unix_ms;
static int64_t reference_uptime_ms;
static bool phone_synced;
int opb_clock_init(int64_t last_event_ms) {
reference_unix_ms =
last_event_ms > OPB_DEFAULT_TIME_MS ? last_event_ms : OPB_DEFAULT_TIME_MS;
reference_uptime_ms = k_uptime_get();
phone_synced = false;
return 0;
}
int64_t opb_clock_now_ms(void) {
return reference_unix_ms + (k_uptime_get() - reference_uptime_ms);
}
void opb_clock_sync(int64_t unix_ms) {
reference_unix_ms = unix_ms;
reference_uptime_ms = k_uptime_get();
phone_synced = true;
}
bool opb_clock_is_phone_synced(void) { return phone_synced; }
+12
View File
@@ -0,0 +1,12 @@
#ifndef OPB_CLOCK_H
#define OPB_CLOCK_H
#include <stdbool.h>
#include <stdint.h>
int opb_clock_init(int64_t last_event_ms);
int64_t opb_clock_now_ms(void);
void opb_clock_sync(int64_t unix_ms);
bool opb_clock_is_phone_synced(void);
#endif
+140
View File
@@ -0,0 +1,140 @@
#include "open_history.h"
#include <errno.h>
#include <string.h>
#include <zephyr/settings/settings.h>
#define HISTORY_MAGIC 0x4f504248U
#define HISTORY_VERSION 1U
#define HISTORY_PATH "history/table"
#define HISTORY_ROOT "history"
#define HISTORY_TABLE "table"
#define SEVEN_DAYS_MS 604800000LL
struct history_table {
uint32_t magic;
uint16_t version;
uint16_t count;
struct open_history_event events[OPEN_HISTORY_MAX_COUNT];
};
static struct history_table table;
static struct history_table loaded_table;
static bool loaded;
static bool history_table_is_valid(const struct history_table *candidate) {
if (candidate->magic != HISTORY_MAGIC ||
candidate->version != HISTORY_VERSION ||
candidate->count > OPEN_HISTORY_MAX_COUNT) {
return false;
}
for (size_t i = 0; i < candidate->count; i++) {
const struct open_history_event *event = &candidate->events[i];
if (event->kind > OPEN_HISTORY_APP ||
event->actor[OPEN_HISTORY_ACTOR_MAX_LENGTH] != '\0' ||
(i > 0 && event->unix_ms < candidate->events[i - 1].unix_ms)) {
return false;
}
}
return true;
}
static int history_save(void) {
#ifdef OPB_DIAGNOSTIC_READ_ONLY
return 0;
#else
return settings_save_one(HISTORY_PATH, &table, sizeof(table));
#endif
}
static int history_set(const char *key, size_t len, settings_read_cb read_cb,
void *cb_arg) {
if (strcmp(key, HISTORY_TABLE) != 0 || len != sizeof(table)) {
return 0;
}
if (read_cb(cb_arg, &loaded_table, sizeof(loaded_table)) !=
sizeof(loaded_table) ||
!history_table_is_valid(&loaded_table)) {
return 0;
}
table = loaded_table;
loaded = true;
return 0;
}
static struct settings_handler handler = {.name = HISTORY_ROOT,
.h_set = history_set};
int open_history_init(void) {
memset(&table, 0, sizeof(table));
table.magic = HISTORY_MAGIC;
table.version = HISTORY_VERSION;
loaded = false;
int ret = settings_register(&handler);
if (ret < 0) return ret;
ret = settings_load_subtree(HISTORY_ROOT);
if (ret < 0) return ret;
/* An empty default history does not need a flash write during boot. */
return 0;
}
int open_history_append(enum open_history_kind kind, const char *actor,
int64_t unix_ms) {
size_t first = 0;
if (kind < OPEN_HISTORY_PERMANENT_CODE || kind > OPEN_HISTORY_APP) {
return -EINVAL;
}
/*
* Keep the persisted table chronological even if a phone sends an older
* clock value. A descending timestamp would invalidate the complete history
* on the following boot.
*/
if (table.count > 0 &&
unix_ms < table.events[table.count - 1].unix_ms) {
unix_ms = table.events[table.count - 1].unix_ms;
}
while (first < table.count &&
unix_ms - table.events[first].unix_ms > SEVEN_DAYS_MS) {
first++;
}
if (first > 0) {
memmove(table.events, table.events + first,
(table.count - first) * sizeof(table.events[0]));
table.count -= first;
}
if (table.count == OPEN_HISTORY_MAX_COUNT) {
memmove(table.events, table.events + 1,
(OPEN_HISTORY_MAX_COUNT - 1) * sizeof(table.events[0]));
table.count--;
}
struct open_history_event *event = &table.events[table.count++];
memset(event, 0, sizeof(*event));
event->unix_ms = unix_ms;
event->kind = kind;
if (actor != NULL) {
strncpy(event->actor, actor, OPEN_HISTORY_ACTOR_MAX_LENGTH);
}
return history_save();
}
size_t open_history_count(void) { return table.count; }
bool open_history_get(size_t index, struct open_history_event *event) {
if (event == NULL || index >= table.count) return false;
*event = table.events[index];
return true;
}
int64_t open_history_last_timestamp_ms(void) {
return table.count == 0 ? 0 : table.events[table.count - 1].unix_ms;
}
int open_history_clear(void) {
memset(&table, 0, sizeof(table));
table.magic = HISTORY_MAGIC;
table.version = HISTORY_VERSION;
return history_save();
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef OPEN_HISTORY_H
#define OPEN_HISTORY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define OPEN_HISTORY_MAX_COUNT 64
#define OPEN_HISTORY_ACTOR_MAX_LENGTH 31
enum open_history_kind {
OPEN_HISTORY_PERMANENT_CODE,
OPEN_HISTORY_TEMPORARY_CODE,
OPEN_HISTORY_NFC_TAG,
OPEN_HISTORY_APP,
};
struct open_history_event {
int64_t unix_ms;
uint8_t kind;
char actor[OPEN_HISTORY_ACTOR_MAX_LENGTH + 1];
};
int open_history_init(void);
int open_history_append(enum open_history_kind kind, const char *actor,
int64_t unix_ms);
size_t open_history_count(void);
bool open_history_get(size_t index, struct open_history_event *event);
int64_t open_history_last_timestamp_ms(void);
int open_history_clear(void);
#endif