feat(firmware): add secure context, BLE connexion and refactor code
This commit is contained in:
@@ -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
@@ -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/`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user