implement openthread
Signed-off-by: Peter Siegmund <mars3142@users.noreply.github.com>
This commit is contained in:
@@ -12,3 +12,4 @@ firmware/.vscode/c_cpp_properties.json
|
||||
hardware/mcu_board/.history/
|
||||
*.lck
|
||||
wiki/
|
||||
.cache/
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"additionalDirectories": [
|
||||
"/Volumes/Coding/git.mars3142.dev/model-railway/system-control"
|
||||
],
|
||||
"allow": [
|
||||
"Bash(xargs ls -la)"
|
||||
]
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -4,10 +4,19 @@ compile: clean
|
||||
deploy: compile
|
||||
idf.py -B build-release -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release" flash monitor
|
||||
|
||||
# Write device name to NVS without reflashing the full firmware.
|
||||
# The name is sent as VendorData during Thread commissioning.
|
||||
#
|
||||
# make set-name NAME="Leuchtturm West"
|
||||
# make set-name NAME="Leuchtturm West" PORT=/dev/cu.usbmodem1101
|
||||
set-name:
|
||||
@[ -n "$(NAME)" ] || { echo "Usage: make set-name NAME=\"Leuchtturm West\" [PORT=/dev/cu.usbmodemXXXX]"; exit 1; }
|
||||
@bash scripts/set_device_name.sh "$(NAME)" $(PORT)
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
rm -rf build-release
|
||||
rm -rf sdkconfig
|
||||
rm -rf dependencies.lock
|
||||
|
||||
.PHONY: compile deploy clean
|
||||
.PHONY: compile deploy set-name clean
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
# Thread Integration — ESP32-H2 Lighthouse
|
||||
|
||||
This document describes the Thread network protocol used by the ESP32-H2 lighthouse
|
||||
firmware and what the ESP32-C6 coordinator must implement to discover and control it.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ESP32-C6 (Coordinator / Leader)
|
||||
│ Thread / IEEE 802.15.4
|
||||
└── ESP32-H2 (Lighthouse / Child or Router)
|
||||
CoAP server on port 5683
|
||||
Resources: /.well-known/core /beacon /outdoor /flicker /group
|
||||
```
|
||||
|
||||
The H2 is a **Full Thread Device (FTD)** with leader weight 1, so the C6 always wins
|
||||
leader election. The H2 operates as Child or Router — never Leader.
|
||||
|
||||
---
|
||||
|
||||
## 1 — Pre-provisioning: device name
|
||||
|
||||
Before first flash, write a human-readable name into the H2's NVS. This name is
|
||||
announced on the Thread network when the device attaches so the C6 can identify it.
|
||||
|
||||
```bash
|
||||
# requires IDF environment: . $IDF_PATH/export.sh
|
||||
./scripts/set_device_name.sh "Leuchtturm West"
|
||||
|
||||
# or via Make
|
||||
make set-name NAME="Leuchtturm West"
|
||||
make set-name NAME="Leuchtturm West" PORT=/dev/cu.usbmodem1101
|
||||
```
|
||||
|
||||
The name is stored in NVS namespace `thread`, key `name`. If no name is set the
|
||||
device uses `"Lighthouse"` as fallback.
|
||||
|
||||
---
|
||||
|
||||
## 2 — Thread network credentials
|
||||
|
||||
Both the H2 and the C6 use a **fixed, pre-provisioned dataset** — there is no runtime
|
||||
commissioning (no Joiner, no Commissioner). Both sides must be flashed with identical
|
||||
credentials.
|
||||
|
||||
| Parameter | Value | sdkconfig key |
|
||||
| ------------ | ---------------------------------- | ----------------------------------- |
|
||||
| Network name | `MaerklinNet` | `CONFIG_OPENTHREAD_NETWORK_NAME` |
|
||||
| Channel | `18` | `CONFIG_OPENTHREAD_NETWORK_CHANNEL` |
|
||||
| PAN ID | `0xBEEF` | `CONFIG_OPENTHREAD_NETWORK_PANID` |
|
||||
| ExtPAN ID | `dead00beef00cafe` | `CONFIG_OPENTHREAD_NETWORK_EXTPANID`|
|
||||
| Network key | `00112233445566778899aabbccddeeff` | `CONFIG_OPENTHREAD_NETWORK_MASTERKEY`|
|
||||
|
||||
**First-time flash procedure:**
|
||||
|
||||
Both devices must start with clean NVS to avoid stale dataset conflicts:
|
||||
|
||||
```bash
|
||||
idf.py erase-flash flash # run on C6 first, then H2
|
||||
```
|
||||
|
||||
**Boot order:** Flash and start the C6 first so it forms the network and wins leader
|
||||
election. The H2 can then boot at any time — simultaneous boot also works because the
|
||||
H2's leader weight (1) is always lower than the C6's.
|
||||
|
||||
**Dataset application logic (H2):** On boot, `thread_task` calls
|
||||
`otDatasetIsCommissioned()`. If the dataset is already stored (subsequent boots) it
|
||||
is used as-is. If not (first boot after erase-flash), `apply_fixed_dataset()` writes
|
||||
the credentials from sdkconfig into the active dataset.
|
||||
|
||||
---
|
||||
|
||||
## 3 — Device discovery
|
||||
|
||||
Once the H2 attaches to the network (`OT_DEVICE_ROLE_CHILD` or
|
||||
`OT_DEVICE_ROLE_ROUTER`), it sends a single **NON-CONFIRMABLE CoAP POST** to the
|
||||
mesh-local all-nodes multicast address:
|
||||
|
||||
```
|
||||
Destination : coap://[ff03::1]:5683/announce
|
||||
Payload : <device-name> e.g. "Leuchtturm West"
|
||||
```
|
||||
|
||||
This is sent exactly once per network attachment (`s_announced` flag). If the C6
|
||||
restarts it must wait for the next H2 reboot to receive the announce again.
|
||||
|
||||
**What the C6 must implement:**
|
||||
|
||||
```c
|
||||
otCoapAddResource(instance, &(otCoapResource){
|
||||
.mUriPath = "announce",
|
||||
.mHandler = on_announce, // store sender IPv6 + name
|
||||
.mContext = instance,
|
||||
});
|
||||
otCoapStart(instance, OT_DEFAULT_COAP_PORT);
|
||||
```
|
||||
|
||||
Record the sender's IPv6 address from `msg_info->mPeerAddr` — this is the unicast
|
||||
address used for all subsequent CoAP communication with that device.
|
||||
|
||||
---
|
||||
|
||||
## 4 — Capability discovery
|
||||
|
||||
After discovery, query the H2 for its available resources:
|
||||
|
||||
```
|
||||
GET coap://[<H2-addr>]:5683/.well-known/core
|
||||
```
|
||||
|
||||
Response (Content-Format 40 — `application/link-format`):
|
||||
|
||||
```
|
||||
</beacon>;rt="maerklin.switch";title="Blinken";sim;obs,
|
||||
</outdoor>;rt="maerklin.switch";title="Aussenlicht";sim;obs,
|
||||
</flicker>;rt="maerklin.value";min=0;max=100;step=5;title="Flackern in %";obs,
|
||||
</group>;rt="maerklin.group";title="Group"
|
||||
```
|
||||
|
||||
| Resource | Type | Semantics | Observable |
|
||||
| --------------------- | ------------------ | --------------------------------------------- | ---------- |
|
||||
| `maerklin.switch` | Boolean `"0"`/`"1"` | On/off toggle | yes |
|
||||
| `maerklin.value` | Integer string | Numeric value within min/max/step constraints | yes |
|
||||
| `maerklin.group` | — | Group membership management | no |
|
||||
|
||||
The `sim` attribute on `/beacon` and `/outdoor` indicates that these resources
|
||||
participate in simulated lighting (the `sim/on/...` and `sim/off/...` action
|
||||
namespace on the C6 side).
|
||||
|
||||
---
|
||||
|
||||
## 5 — Controlling resources
|
||||
|
||||
All unicast control requests use **CONFIRMABLE CoAP** to the H2's unicast IPv6
|
||||
address on port **5683**.
|
||||
|
||||
### `/beacon` — rotating lighthouse light
|
||||
|
||||
| Method | Observe option | Payload | Response code |
|
||||
| ------ | -------------- | ------- | ------------- |
|
||||
| GET | — | — | 2.05 Content, payload `"1"` or `"0"` |
|
||||
| GET | `0` (register) | — | 2.05 Content + Observe seq + current state |
|
||||
| GET | `1` (cancel) | — | 2.05 Content |
|
||||
| PUT | — | `"1"` | 2.04 Changed |
|
||||
| PUT | — | `"0"` | 2.04 Changed |
|
||||
|
||||
Starts **off** on every boot.
|
||||
|
||||
### `/outdoor` — outdoor lamp (flickering flame effect)
|
||||
|
||||
| Method | Observe option | Payload | Response code |
|
||||
| ------ | -------------- | ------- | ------------- |
|
||||
| GET | — | — | 2.05 Content, payload `"1"` or `"0"` |
|
||||
| GET | `0` (register) | — | 2.05 Content + Observe seq + current state |
|
||||
| GET | `1` (cancel) | — | 2.05 Content |
|
||||
| PUT | — | `"1"` | 2.04 Changed |
|
||||
| PUT | — | `"0"` | 2.04 Changed |
|
||||
|
||||
Starts **off** on every boot.
|
||||
|
||||
### `/flicker` — flicker intensity
|
||||
|
||||
Controls the random-flicker depth of the outdoor lamp as a percentage (0 = no
|
||||
flicker / steady, 100 = maximum flicker).
|
||||
|
||||
| Method | Observe option | Payload | Response code |
|
||||
| ------ | -------------- | ------------------- | ------------- |
|
||||
| GET | — | — | 2.05 Content, payload `"<0–100>"` |
|
||||
| GET | `0` (register) | — | 2.05 Content + Observe seq + current value |
|
||||
| GET | `1` (cancel) | — | 2.05 Content |
|
||||
| PUT | — | `"<0–100>"` (step 5) | 2.04 Changed |
|
||||
|
||||
Values above 100 are clamped to 100.
|
||||
|
||||
---
|
||||
|
||||
## 6 — Observing resources (RFC 7641)
|
||||
|
||||
`/beacon`, `/outdoor`, and `/flicker` support CoAP Observe. The C6 registers once
|
||||
per device; from that point on the H2 pushes a **NON-CONFIRMABLE** notification to
|
||||
every registered observer whenever the resource state changes.
|
||||
|
||||
### Capacity
|
||||
|
||||
Each resource holds up to **4 observers** (`MAX_OBSERVERS`). Registration beyond
|
||||
that limit is silently ignored.
|
||||
|
||||
### Re-registration handling
|
||||
|
||||
If the same peer registers with a new token (e.g. after a C6 restart), the H2
|
||||
updates the existing slot in place rather than consuming a new one. This means a
|
||||
restarted C6 can re-register without exhausting observer slots.
|
||||
|
||||
### Registration
|
||||
|
||||
```
|
||||
GET coap://[<H2-addr>]:5683/beacon
|
||||
Observe: 0
|
||||
Token: <client-chosen token>
|
||||
```
|
||||
|
||||
The H2 responds with the current state and the current Observe sequence number.
|
||||
Keep the token — all incoming notifications carry the same token.
|
||||
|
||||
### Cancellation
|
||||
|
||||
```
|
||||
GET coap://[<H2-addr>]:5683/beacon
|
||||
Observe: 1
|
||||
Token: <same token as registration>
|
||||
```
|
||||
|
||||
Alternatively send a **RST** in response to any incoming notification.
|
||||
|
||||
### Notification format
|
||||
|
||||
```
|
||||
NON 2.05 Content
|
||||
Observe: <monotonic 24-bit seq>
|
||||
Content-Format: text/plain
|
||||
Token: <registration token>
|
||||
Payload: "1" or "0" (for /flicker: "<integer>")
|
||||
```
|
||||
|
||||
Notifications are NON-CONFIRMABLE. The C6 does not need to ACK them. Observers
|
||||
are never automatically evicted — the C6 must cancel explicitly when a device goes
|
||||
away.
|
||||
|
||||
### C6 example
|
||||
|
||||
```c
|
||||
void register_observe(otInstance *inst, otIp6Address *h2_addr, const char *resource)
|
||||
{
|
||||
otMessage *msg = otCoapNewMessage(inst, NULL);
|
||||
otCoapMessageInit(msg, OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_GET);
|
||||
otCoapMessageGenerateToken(msg, OT_COAP_DEFAULT_TOKEN_LENGTH);
|
||||
otCoapMessageAppendObserveOption(msg, 0);
|
||||
otCoapMessageAppendUriPathOptions(msg, resource);
|
||||
|
||||
otMessageInfo info = {};
|
||||
info.mPeerAddr = *h2_addr;
|
||||
info.mPeerPort = OT_DEFAULT_COAP_PORT;
|
||||
|
||||
otCoapSendRequest(inst, msg, &info, on_observe_notification, NULL);
|
||||
}
|
||||
|
||||
void on_observe_notification(void *ctx, otMessage *msg,
|
||||
const otMessageInfo *info, otError err)
|
||||
{
|
||||
if (err != OT_ERROR_NONE) return;
|
||||
char buf[8] = {};
|
||||
otMessageRead(msg, otMessageGetOffset(msg), buf, sizeof(buf) - 1);
|
||||
// buf contains "1", "0", or a decimal integer (flicker)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7 — Group management
|
||||
|
||||
Groups use **IPv6 multicast**. The C6 assigns H2 devices to named groups by telling
|
||||
each device which multicast address to subscribe to. A group command is then a single
|
||||
CoAP request sent to the multicast address — all members receive it simultaneously.
|
||||
|
||||
### Joining a group
|
||||
|
||||
```
|
||||
PUT coap://[<H2-addr>]:5683/group
|
||||
Payload: "<multicast-addr>,1"
|
||||
|
||||
Example: "ff03::10,1"
|
||||
```
|
||||
|
||||
### Leaving a group
|
||||
|
||||
```
|
||||
PUT coap://[<H2-addr>]:5683/group
|
||||
Payload: "<multicast-addr>,0"
|
||||
|
||||
Example: "ff03::10,0"
|
||||
```
|
||||
|
||||
Response is `2.04 Changed` in both cases (no error response for invalid addresses —
|
||||
the parse silently fails).
|
||||
|
||||
### Sending a group command
|
||||
|
||||
```
|
||||
PUT coap://[ff03::10]:5683/outdoor
|
||||
Payload: "1"
|
||||
```
|
||||
|
||||
All H2 devices subscribed to `ff03::10` receive the packet and turn on their outdoor
|
||||
lamp simultaneously.
|
||||
|
||||
### Persistence
|
||||
|
||||
Group memberships are persisted in NVS (namespace `thread`, key `groups`) as a
|
||||
semicolon-separated list of IPv6 address strings. On every boot, after the device
|
||||
attaches to the network, all previously stored memberships are restored via
|
||||
`otIp6SubscribeMulticastAddress`. The C6 does not need to reassign memberships after
|
||||
an H2 reboot.
|
||||
|
||||
### Recommended group address range
|
||||
|
||||
Use addresses in the **mesh-local multicast scope** `ff03::/16`:
|
||||
|
||||
| Address | Suggested use |
|
||||
| ---------- | --------------------- |
|
||||
| `ff03::10` | All outdoor lamps |
|
||||
| `ff03::11` | All beacons |
|
||||
| `ff03::20` | Scene: harbour lights |
|
||||
|
||||
---
|
||||
|
||||
## 8 — Status LED
|
||||
|
||||
The H2's status LED (driven by `beacon_set_status`) reflects the Thread network state:
|
||||
|
||||
| LED state | `beacon_status_t` | Meaning |
|
||||
| ---------------------- | ------------------------ | -------------------------------- |
|
||||
| Blue blinking (500 ms) | `BEACON_STATUS_SEARCHING` | Detached — waiting for network |
|
||||
| Off | `BEACON_STATUS_CONNECTED` | Child or Router — ready |
|
||||
|
||||
`BEACON_STATUS_PAIRING` and `BEACON_STATUS_ERROR` are defined but not triggered by
|
||||
the current Thread stack (they were used by the former Joiner flow).
|
||||
|
||||
---
|
||||
|
||||
## Summary — C6 implementation checklist
|
||||
|
||||
- [ ] Use identical Thread credentials in sdkconfig (`MaerklinNet`, ch 18, PAN `0xBEEF`, …)
|
||||
- [ ] Erase flash on both devices before first use to clear stale NVS datasets
|
||||
- [ ] Start C6 first so it wins leader election
|
||||
- [ ] Listen for CoAP POST on `/announce` → store sender IPv6 + device name
|
||||
- [ ] On new device: GET `/.well-known/core` → parse resource types and `obs` attribute
|
||||
- [ ] Control via unicast PUT to `/beacon`, `/outdoor`, `/flicker`
|
||||
- [ ] After discovery: register as observer (GET + `Observe: 0`) on `/beacon`, `/outdoor`, `/flicker`
|
||||
- [ ] Handle incoming NON notifications → update device state in C6 model
|
||||
- [ ] For group commands: PUT to multicast address + resource path
|
||||
- [ ] Group membership is restored on H2 reboot — no reconciliation required
|
||||
@@ -4,8 +4,10 @@ idf_component_register(SRCS
|
||||
"src/outdoor.c"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
espressif__led_strip
|
||||
esp_driver_gpio
|
||||
esp_driver_gptimer
|
||||
esp_driver_ledc
|
||||
esp_timer
|
||||
persistence
|
||||
)
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
BEACON_STATUS_SEARCHING, // blue, 500ms blink
|
||||
BEACON_STATUS_PAIRING, // blue, steady
|
||||
BEACON_STATUS_CONNECTED, // off
|
||||
BEACON_STATUS_ERROR, // red, steady
|
||||
} beacon_status_t;
|
||||
|
||||
/**
|
||||
* @brief Sets the commissioning status LED.
|
||||
*
|
||||
* Controls the status LED to reflect the current Thread commissioning state.
|
||||
* Any active blink timer is stopped before applying the new state.
|
||||
*
|
||||
* @param status Desired status:
|
||||
* - BEACON_STATUS_SEARCHING: blue, 500 ms blink (joiner scanning)
|
||||
* - BEACON_STATUS_PAIRING: blue, steady (joiner handshake in progress)
|
||||
* - BEACON_STATUS_CONNECTED: off (joined successfully)
|
||||
* - BEACON_STATUS_ERROR: red, steady (max join attempts exceeded)
|
||||
*/
|
||||
void beacon_set_status(beacon_status_t status);
|
||||
|
||||
/**
|
||||
* @brief Initializes the beacon module.
|
||||
@@ -38,4 +61,24 @@ esp_err_t beacon_start(void);
|
||||
*/
|
||||
esp_err_t beacon_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Toggles the beacon and persists the new state.
|
||||
*
|
||||
* Reads the current enabled flag from NVS, inverts it, starts or stops the
|
||||
* beacon accordingly, and writes the updated flag back to NVS.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK: Toggle completed successfully.
|
||||
* - Error codes in case of failure, indicating the specific issue.
|
||||
*/
|
||||
esp_err_t beacon_toggle(void);
|
||||
|
||||
/**
|
||||
* @brief Returns whether the beacon is currently running.
|
||||
*
|
||||
* @return true if the beacon timer is active, false otherwise.
|
||||
*/
|
||||
bool beacon_is_running(void);
|
||||
|
||||
typedef void (*beacon_state_cb_t)(bool enabled);
|
||||
void beacon_register_state_cb(beacon_state_cb_t cb);
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "beacon.h"
|
||||
#include "led_strip.h"
|
||||
#include "outdoor.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
led_strip_handle_t led_strip;
|
||||
uint32_t size;
|
||||
} LedMatrix_t;
|
||||
|
||||
LedMatrix_t get_led_matrix(void);
|
||||
|
||||
/**
|
||||
* @brief Initializes the WLED module.
|
||||
* @brief Initializes the WS2812 LED strip via RMT.
|
||||
*
|
||||
* This function configures the WLED module for operation, preparing it for subsequent
|
||||
* usage such as enabling lighting effects or communication.
|
||||
* Configures the RMT peripheral for the data pin defined by
|
||||
* `CONFIG_WLED_DIN_PIN` and blanks all pixels. Must be called once before
|
||||
* any other LED operations.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK: Initialization completed successfully.
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void outdoor_init(void);
|
||||
esp_err_t outdoor_start(void);
|
||||
|
||||
esp_err_t outdoor_stop(void);
|
||||
bool outdoor_is_running(void);
|
||||
|
||||
uint8_t outdoor_get_flicker(void);
|
||||
esp_err_t outdoor_set_flicker(uint8_t percent);
|
||||
|
||||
@@ -2,16 +2,106 @@
|
||||
|
||||
#include "driver/gptimer.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "led_strip.h"
|
||||
#include "light.h"
|
||||
#include "light_priv.h"
|
||||
#include "persistence.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "semaphore.h"
|
||||
|
||||
static const char *TAG = "beacon";
|
||||
|
||||
static beacon_state_cb_t s_state_cb = NULL;
|
||||
static bool s_running = false;
|
||||
|
||||
// ─── LED helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
static void led_refresh(uint32_t brightness)
|
||||
{
|
||||
LedMatrix_t led_matrix = get_led_matrix();
|
||||
if (!led_matrix.led_strip) return;
|
||||
|
||||
for (uint32_t i = 0; i < led_matrix.size; i++)
|
||||
{
|
||||
led_strip_set_pixel(led_matrix.led_strip, i, 0, brightness, 0);
|
||||
}
|
||||
led_strip_refresh(led_matrix.led_strip);
|
||||
}
|
||||
|
||||
// ─── Commissioning status LED ─────────────────────────────────────────────────
|
||||
|
||||
static esp_timer_handle_t s_status_timer;
|
||||
static bool s_status_blink_on = false;
|
||||
|
||||
static void status_timer_cb(void *arg)
|
||||
{
|
||||
LedMatrix_t lm = get_led_matrix();
|
||||
if (!lm.led_strip) return;
|
||||
|
||||
s_status_blink_on = !s_status_blink_on;
|
||||
led_refresh(0); // clear green
|
||||
led_strip_set_pixel(lm.led_strip, 0, 0, 0, s_status_blink_on ? 20 : 0);
|
||||
led_strip_refresh(lm.led_strip);
|
||||
}
|
||||
|
||||
static void stop_status_timer(void)
|
||||
{
|
||||
if (!s_status_timer)
|
||||
return;
|
||||
esp_timer_stop(s_status_timer);
|
||||
esp_timer_delete(s_status_timer);
|
||||
s_status_timer = NULL;
|
||||
}
|
||||
|
||||
static void set_blue(uint8_t brightness)
|
||||
{
|
||||
LedMatrix_t lm = get_led_matrix();
|
||||
if (!lm.led_strip) return;
|
||||
led_strip_set_pixel(lm.led_strip, 0, 0, 0, brightness);
|
||||
led_strip_refresh(lm.led_strip);
|
||||
}
|
||||
|
||||
static void set_red(uint8_t brightness)
|
||||
{
|
||||
LedMatrix_t lm = get_led_matrix();
|
||||
if (!lm.led_strip) return;
|
||||
led_strip_set_pixel(lm.led_strip, 0, brightness, 0, 0);
|
||||
led_strip_refresh(lm.led_strip);
|
||||
}
|
||||
|
||||
void beacon_set_status(beacon_status_t status)
|
||||
{
|
||||
stop_status_timer();
|
||||
s_status_blink_on = false;
|
||||
|
||||
switch (status) {
|
||||
case BEACON_STATUS_SEARCHING: {
|
||||
esp_timer_create_args_t args = {.callback = status_timer_cb, .name = "status_blink"};
|
||||
esp_timer_create(&args, &s_status_timer);
|
||||
esp_timer_start_periodic(s_status_timer, 500000);
|
||||
set_blue(20);
|
||||
s_status_blink_on = true;
|
||||
break;
|
||||
}
|
||||
case BEACON_STATUS_PAIRING:
|
||||
set_blue(20);
|
||||
break;
|
||||
case BEACON_STATUS_CONNECTED:
|
||||
led_refresh(0);
|
||||
break;
|
||||
case BEACON_STATUS_ERROR:
|
||||
set_red(20);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void beacon_register_state_cb(beacon_state_cb_t cb)
|
||||
{
|
||||
s_state_cb = cb;
|
||||
}
|
||||
|
||||
static SemaphoreHandle_t timer_semaphore;
|
||||
gptimer_handle_t gptimer = NULL;
|
||||
|
||||
@@ -30,17 +120,6 @@ static bool IRAM_ATTR beacon_timer_callback(gptimer_handle_t timer, const gptime
|
||||
return true;
|
||||
}
|
||||
|
||||
static void led_refresh(uint32_t brightness)
|
||||
{
|
||||
LedMatrix_t led_matrix = get_led_matrix();
|
||||
|
||||
for (uint32_t i = 0; i < led_matrix.size; i++)
|
||||
{
|
||||
led_strip_set_pixel(led_matrix.led_strip, i, 0, brightness, 0);
|
||||
}
|
||||
led_strip_refresh(led_matrix.led_strip);
|
||||
}
|
||||
|
||||
static void beacon_timer_event_task(void *arg)
|
||||
{
|
||||
while (true)
|
||||
@@ -83,6 +162,7 @@ esp_err_t beacon_start(void)
|
||||
ESP_LOGE(TAG, "Failed to start gptimer: %s", esp_err_to_name(ret));
|
||||
}
|
||||
|
||||
s_running = true;
|
||||
ESP_LOGI(TAG, "GPTimer started.");
|
||||
return ret;
|
||||
}
|
||||
@@ -100,6 +180,7 @@ esp_err_t beacon_stop(void)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to stop gptimer: %s", esp_err_to_name(ret));
|
||||
}
|
||||
s_running = false;
|
||||
ESP_LOGI(TAG, "GPTimer stopped.");
|
||||
|
||||
ret = gptimer_disable(gptimer);
|
||||
@@ -175,6 +256,11 @@ exit:
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool beacon_is_running(void)
|
||||
{
|
||||
return s_running;
|
||||
}
|
||||
|
||||
esp_err_t beacon_toggle(void)
|
||||
{
|
||||
int8_t beacon_enabled = 0;
|
||||
@@ -193,5 +279,8 @@ esp_err_t beacon_toggle(void)
|
||||
beacon_enabled = 1 - beacon_enabled;
|
||||
persistence_save(VALUE_TYPE_INT8, "BEACON_ENABLED", &beacon_enabled);
|
||||
|
||||
if (s_state_cb)
|
||||
s_state_cb(beacon_enabled);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "light.h"
|
||||
#include "light_priv.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
@@ -32,8 +33,7 @@ esp_err_t wled_init(void)
|
||||
|
||||
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_matrix.led_strip));
|
||||
|
||||
for (uint32_t i = 0; i < led_matrix.size; i++)
|
||||
{
|
||||
for (uint32_t i = 0; i < led_matrix.size; i++) {
|
||||
led_strip_set_pixel(led_matrix.led_strip, i, 0, 0, 0);
|
||||
}
|
||||
led_strip_refresh(led_matrix.led_strip);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "led_strip.h"
|
||||
|
||||
/**
|
||||
* @brief Handle and pixel count for the WS2812 strip.
|
||||
*
|
||||
* Shared between the light, beacon, and outdoor modules so all of them
|
||||
* write through the same RMT handle.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
led_strip_handle_t led_strip;
|
||||
uint32_t size;
|
||||
} LedMatrix_t;
|
||||
|
||||
/**
|
||||
* @brief Returns the global LED matrix descriptor.
|
||||
*
|
||||
* The descriptor is populated by wled_init() and is valid for the lifetime
|
||||
* of the application.
|
||||
*/
|
||||
LedMatrix_t get_led_matrix(void);
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "outdoor.h"
|
||||
#include "persistence.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
@@ -7,51 +8,58 @@
|
||||
|
||||
static const char *TAG = "outdoor";
|
||||
|
||||
#define LEDC_RESOLUTION LEDC_TIMER_10_BIT // Timer resolution (10 bit = 1024 steps)
|
||||
#define MAX_DUTY 1023
|
||||
#define LEDC_RESOLUTION LEDC_TIMER_10_BIT
|
||||
#define MAX_DUTY 1023
|
||||
#define NORMAL_DUTY (MAX_DUTY * 0.9)
|
||||
#define FLICKER_COUNT 8
|
||||
|
||||
#define NORMAL_DUTY (MAX_DUTY * 0.9) // 90% brightness
|
||||
static uint8_t s_flicker_chance = 2;
|
||||
|
||||
#define FLICKER_CHANCE 2 // 2% chance of flickering per cycle
|
||||
#define FLICKER_COUNT 8 // Number of brightness changes during a flicker
|
||||
|
||||
TaskHandle_t outdoor_task_handle = NULL;
|
||||
|
||||
void outdoor_task(void *pvParameters)
|
||||
typedef struct
|
||||
{
|
||||
int gpio_num;
|
||||
ledc_channel_t channel;
|
||||
} outdoor_task_args_t;
|
||||
|
||||
int led_pin = *(int *)pvParameters;
|
||||
static TaskHandle_t s_task_left = NULL;
|
||||
static TaskHandle_t s_task_right = NULL;
|
||||
|
||||
ledc_timer_config_t ledc_timer = {.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.timer_num = LEDC_TIMER_0,
|
||||
static void outdoor_task(void *pvParameters)
|
||||
{
|
||||
outdoor_task_args_t *args = (outdoor_task_args_t *)pvParameters;
|
||||
int led_pin = args->gpio_num;
|
||||
ledc_channel_t channel = args->channel;
|
||||
|
||||
ledc_timer_config_t ledc_timer = {.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.timer_num = LEDC_TIMER_0,
|
||||
.duty_resolution = LEDC_RESOLUTION,
|
||||
.freq_hz = 5000,
|
||||
.clk_cfg = LEDC_AUTO_CLK};
|
||||
.freq_hz = 5000,
|
||||
.clk_cfg = LEDC_AUTO_CLK};
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
|
||||
|
||||
ledc_channel_config_t ledc_channel = {.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.channel = LEDC_CHANNEL_0,
|
||||
.timer_sel = LEDC_TIMER_0,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.gpio_num = led_pin,
|
||||
.duty = 0,
|
||||
.hpoint = 0};
|
||||
.channel = channel,
|
||||
.timer_sel = LEDC_TIMER_0,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.gpio_num = led_pin,
|
||||
.duty = 0,
|
||||
.hpoint = 0};
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
|
||||
|
||||
while (1)
|
||||
{
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, NORMAL_DUTY);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, channel, NORMAL_DUTY);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, channel);
|
||||
|
||||
uint32_t random_val = esp_random() % 100;
|
||||
|
||||
if (random_val < FLICKER_CHANCE)
|
||||
if (random_val < s_flicker_chance)
|
||||
{
|
||||
for (int i = 0; i < FLICKER_COUNT; i++)
|
||||
{
|
||||
uint32_t flicker_duty = (NORMAL_DUTY * 0.3) + (esp_random() % (uint32_t)(NORMAL_DUTY * 0.4));
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, flicker_duty);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, channel, flicker_duty);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, channel);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(20 + (esp_random() % 50)));
|
||||
}
|
||||
@@ -61,31 +69,57 @@ void outdoor_task(void *pvParameters)
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t outdoor_get_flicker(void)
|
||||
{
|
||||
return s_flicker_chance;
|
||||
}
|
||||
|
||||
esp_err_t outdoor_set_flicker(uint8_t percent)
|
||||
{
|
||||
if (percent > 100) percent = 100;
|
||||
s_flicker_chance = percent;
|
||||
persistence_save(VALUE_TYPE_INT8, "FLICKER_CHANCE", &s_flicker_chance);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void outdoor_init(void)
|
||||
{
|
||||
persistence_load(VALUE_TYPE_INT8, "FLICKER_CHANCE", &s_flicker_chance);
|
||||
}
|
||||
|
||||
esp_err_t outdoor_start(void)
|
||||
{
|
||||
if (s_task_left != NULL || s_task_right != NULL)
|
||||
return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Simulation of a defective light bulb started.");
|
||||
|
||||
static const int led_left_pin = CONFIG_LED_PIN_LEFT;
|
||||
xTaskCreate(outdoor_task, "outdoor_task_left", 2048, (void *)&led_left_pin, 5, &outdoor_task_handle);
|
||||
static outdoor_task_args_t args_left = {.gpio_num = CONFIG_LED_PIN_LEFT, .channel = LEDC_CHANNEL_0};
|
||||
static outdoor_task_args_t args_right = {.gpio_num = CONFIG_LED_PIN_RIGHT, .channel = LEDC_CHANNEL_1};
|
||||
|
||||
static const int led_right_pin = CONFIG_LED_PIN_RIGHT;
|
||||
xTaskCreate(outdoor_task, "outdoor_task_right", 2048, (void *)&led_right_pin, 5, &outdoor_task_handle);
|
||||
xTaskCreate(outdoor_task, "outdoor_left", 2048, &args_left, 5, &s_task_left);
|
||||
xTaskCreate(outdoor_task, "outdoor_right", 2048, &args_right, 5, &s_task_right);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t outdoor_stop(void)
|
||||
{
|
||||
if (outdoor_task_handle == NULL)
|
||||
{
|
||||
if (s_task_left == NULL && s_task_right == NULL)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
vTaskDelete(outdoor_task_handle);
|
||||
outdoor_task_handle = NULL;
|
||||
if (s_task_left) { vTaskDelete(s_task_left); s_task_left = NULL; }
|
||||
if (s_task_right) { vTaskDelete(s_task_right); s_task_right = NULL; }
|
||||
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
|
||||
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, 0);
|
||||
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool outdoor_is_running(void)
|
||||
{
|
||||
return s_task_left != NULL || s_task_right != NULL;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
idf_component_register(SRCS
|
||||
"src/matter.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
espressif__esp_matter
|
||||
light
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
esp_err_t matter_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "matter.h"
|
||||
|
||||
#include <app/clusters/on-off-server/on-off-server.h>
|
||||
#include <app/server/Server.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_matter.h>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include "beacon.h"
|
||||
#include "outdoor.h"
|
||||
}
|
||||
|
||||
static const char *TAG = "matter";
|
||||
|
||||
static uint16_t s_beacon_endpoint_id = 0;
|
||||
static uint16_t s_outdoor_endpoint_id = 0;
|
||||
|
||||
static esp_err_t identification_cb(esp_matter::identification::callback_type_t type, uint16_t endpoint_id,
|
||||
uint8_t effect_id, uint8_t effect_variant, void *priv_data)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t attribute_update_cb(esp_matter::attribute::callback_type_t type, uint16_t endpoint_id,
|
||||
uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val,
|
||||
void *priv_data)
|
||||
{
|
||||
if (type != esp_matter::attribute::PRE_UPDATE)
|
||||
return ESP_OK;
|
||||
|
||||
if (cluster_id != chip::app::Clusters::OnOff::Id ||
|
||||
attribute_id != chip::app::Clusters::OnOff::Attributes::OnOff::Id)
|
||||
return ESP_OK;
|
||||
|
||||
if (endpoint_id == s_beacon_endpoint_id)
|
||||
return val->val.b ? beacon_start() : beacon_stop();
|
||||
|
||||
if (endpoint_id == s_outdoor_endpoint_id)
|
||||
return val->val.b ? outdoor_start() : outdoor_stop();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void matter_event_cb(const ChipDeviceEvent *event, intptr_t arg)
|
||||
{
|
||||
}
|
||||
|
||||
extern "C" esp_err_t matter_init(void)
|
||||
{
|
||||
esp_matter::node::config_t node_config;
|
||||
esp_matter::node_t *node = esp_matter::node::create(&node_config, attribute_update_cb, identification_cb);
|
||||
if (!node)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create Matter node");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_matter::endpoint::on_off_light::config_t beacon_config;
|
||||
esp_matter::endpoint_t *beacon_ep =
|
||||
esp_matter::endpoint::on_off_light::create(node, &beacon_config, esp_matter::ENDPOINT_FLAG_NONE, NULL);
|
||||
if (!beacon_ep)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create beacon endpoint");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
s_beacon_endpoint_id = esp_matter::endpoint::get_id(beacon_ep);
|
||||
|
||||
esp_matter::endpoint::on_off_light::config_t outdoor_config;
|
||||
esp_matter::endpoint_t *outdoor_ep =
|
||||
esp_matter::endpoint::on_off_light::create(node, &outdoor_config, esp_matter::ENDPOINT_FLAG_NONE, NULL);
|
||||
if (!outdoor_ep)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create outdoor endpoint");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
s_outdoor_endpoint_id = esp_matter::endpoint::get_id(outdoor_ep);
|
||||
|
||||
ESP_LOGI(TAG, "beacon endpoint: %d, outdoor endpoint: %d", s_beacon_endpoint_id, s_outdoor_endpoint_id);
|
||||
|
||||
return esp_matter::start(matter_event_cb);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @brief Supported value types for NVS persistence.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
VALUE_TYPE_STRING,
|
||||
@@ -7,7 +10,45 @@ typedef enum
|
||||
VALUE_TYPE_INT32,
|
||||
} persistence_value_type_t;
|
||||
|
||||
/**
|
||||
* @brief Initializes the NVS flash and opens the given namespace.
|
||||
*
|
||||
* Erases and re-initialises the flash partition when a version mismatch or
|
||||
* no-free-pages error is detected. Creates the mutex used for thread-safe
|
||||
* access. Must be called once before any other persistence function.
|
||||
*
|
||||
* @param namespace_name NVS namespace to open (max 15 characters).
|
||||
*/
|
||||
void persistence_init(const char *namespace_name);
|
||||
|
||||
/**
|
||||
* @brief Saves a value to NVS under the given key.
|
||||
*
|
||||
* Thread-safe. Commits the value immediately so it survives a reboot.
|
||||
*
|
||||
* @param value_type Type of the value (STRING, INT8, or INT32).
|
||||
* @param key NVS key string (max 15 characters).
|
||||
* @param value Pointer to the value to store.
|
||||
*/
|
||||
void persistence_save(persistence_value_type_t value_type, const char *key, const void *value);
|
||||
|
||||
/**
|
||||
* @brief Loads a value from NVS into the caller-provided buffer.
|
||||
*
|
||||
* Thread-safe. Returns @p out unchanged when the key does not exist or an
|
||||
* error occurs (the error is logged but not propagated).
|
||||
*
|
||||
* @param value_type Type of the value (STRING, INT8, or INT32).
|
||||
* @param key NVS key string (max 15 characters).
|
||||
* @param out Buffer that receives the loaded value.
|
||||
* @return @p out for convenience.
|
||||
*/
|
||||
void *persistence_load(persistence_value_type_t value_type, const char *key, void *out);
|
||||
|
||||
/**
|
||||
* @brief Closes the NVS handle and deletes the mutex.
|
||||
*
|
||||
* Should be called on graceful shutdown. Subsequent calls to
|
||||
* persistence_save / persistence_load are no-ops.
|
||||
*/
|
||||
void persistence_deinit();
|
||||
|
||||
@@ -46,6 +46,7 @@ static const char *nvs_type_to_str(nvs_type_t type)
|
||||
}
|
||||
}
|
||||
|
||||
#if CONFIG_LOG_DEFAULT_LEVEL >= ESP_LOG_DEBUG
|
||||
void display_nvs_value(const char *namespace_name, const char *key, nvs_type_t type)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
@@ -138,6 +139,7 @@ static void list_all_nvs_entries(void)
|
||||
nvs_release_iterator(it);
|
||||
ESP_LOGI(TAG, "==================================");
|
||||
}
|
||||
#endif
|
||||
|
||||
static void check_nvs_stats(void)
|
||||
{
|
||||
@@ -164,7 +166,9 @@ void persistence_init(const char *namespace_name)
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
#if CONFIG_LOG_DEFAULT_LEVEL >= ESP_LOG_DEBUG
|
||||
list_all_nvs_entries();
|
||||
#endif
|
||||
check_nvs_stats();
|
||||
|
||||
ESP_ERROR_CHECK(nvs_open(namespace_name, NVS_READWRITE, &persistence_handle));
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
idf_component_register(SRCS
|
||||
"src/char_desc.c"
|
||||
"src/device_service.c"
|
||||
"src/light_service.c"
|
||||
"src/remote_control.c"
|
||||
"src/uart_service.c"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
bt
|
||||
esp_app_format
|
||||
light
|
||||
persistence
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "host/ble_gatt.h"
|
||||
#include <stdint.h>
|
||||
|
||||
int gatt_svr_desc_presentation_bool_access(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
int gatt_svr_desc_valid_range_bool_access(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include <stdio.h>
|
||||
|
||||
// 0x2A00 - Device Name
|
||||
int gatt_svr_chr_device_name_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
// 0x2A26 - Firmware Revision String
|
||||
int gatt_svr_chr_device_firmware_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
// 0x2A27 - Hardware Revision String
|
||||
int gatt_svr_chr_device_hardware_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
// 0x2A29 - Manufacturer Name String
|
||||
int gatt_svr_chr_device_manufacturer_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include <stdio.h>
|
||||
|
||||
// 0xA000 - Light Service
|
||||
int gatt_svr_chr_light_led_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
// 0xBEA0 - Beacon Control
|
||||
int gatt_svr_chr_light_beacon_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
/// Outdoor Light Descriptors
|
||||
int gatt_svr_desc_led_user_desc_access(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
/// Beacon Characteristic Descriptors
|
||||
int gatt_svr_desc_beacon_user_desc_access(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t conn_handle;
|
||||
bool is_connected;
|
||||
} ble_connection_t;
|
||||
|
||||
extern ble_connection_t g_connections[CONFIG_BT_NIMBLE_MAX_CONNECTIONS];
|
||||
|
||||
void remote_control_init(void);
|
||||
void remote_control_register_gatt(void);
|
||||
void remote_control_start_advertising(void);
|
||||
void remote_control_stop_advertising(void);
|
||||
bool is_any_device_connected(void);
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_sm.h"
|
||||
#include "host/ble_uuid.h"
|
||||
|
||||
// Unique UUIDs for UART-Service (compatible with Nordic UART Service)
|
||||
// Service UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
extern const ble_uuid128_t gatt_svr_svc_uart_uuid;
|
||||
|
||||
// RX Characteristic UUID: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
extern const ble_uuid128_t gatt_svr_chr_uart_rx_uuid;
|
||||
|
||||
// TX Characteristic UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
extern const ble_uuid128_t gatt_svr_chr_uart_tx_uuid;
|
||||
|
||||
extern uint16_t tx_chr_val_handle; // This is still needed as it's set once by the stack
|
||||
|
||||
int gatt_svr_chr_uart_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
void send_ble_data(const char *data);
|
||||
void uart_tx_task(void *param);
|
||||
@@ -1,31 +0,0 @@
|
||||
#include "char_desc.h"
|
||||
|
||||
int gatt_svr_desc_presentation_bool_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC)
|
||||
{
|
||||
// 7-Byte Format: [format, exponent, unit(2), namespace, description(2)]
|
||||
uint8_t fmt[7] = {
|
||||
0x01, // format = boolean
|
||||
0x00, // exponent
|
||||
0x00, 0x00, // unit = none
|
||||
0x01, // namespace = Bluetooth SIG
|
||||
0x00, 0x00 // description
|
||||
};
|
||||
return os_mbuf_append(ctxt->om, fmt, sizeof(fmt)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
int gatt_svr_desc_valid_range_bool_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC)
|
||||
{
|
||||
// for bool optional. but here as 1-Byte-Min/Max (0..1)
|
||||
uint8_t range[2] = {0x00, 0x01}; // min=0, max=1
|
||||
return os_mbuf_append(ctxt->om, range, sizeof(range)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#include "device_service.h"
|
||||
#include <esp_app_desc.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int gatt_svr_chr_device_name_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
char firmware_revision_str[33];
|
||||
const esp_app_desc_t *app_desc = esp_app_get_description();
|
||||
|
||||
if (app_desc->project_name[0] != '\0')
|
||||
{
|
||||
snprintf(firmware_revision_str, sizeof(firmware_revision_str), "%s", app_desc->project_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(firmware_revision_str, sizeof(firmware_revision_str), "undefined");
|
||||
}
|
||||
|
||||
os_mbuf_append(ctxt->om, firmware_revision_str, strlen(firmware_revision_str));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gatt_svr_chr_device_firmware_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
char firmware_revision_str[33];
|
||||
const esp_app_desc_t *app_desc = esp_app_get_description();
|
||||
|
||||
if (app_desc->version[0] != '\0')
|
||||
{
|
||||
snprintf(firmware_revision_str, sizeof(firmware_revision_str), "%s", app_desc->version);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(firmware_revision_str, sizeof(firmware_revision_str), "undefined");
|
||||
}
|
||||
|
||||
os_mbuf_append(ctxt->om, firmware_revision_str, strlen(firmware_revision_str));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gatt_svr_chr_device_hardware_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
char *hardware_revision = "rev1";
|
||||
os_mbuf_append(ctxt->om, hardware_revision, strlen(hardware_revision));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gatt_svr_chr_device_manufacturer_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
char *manufacturer = "mars3142";
|
||||
os_mbuf_append(ctxt->om, manufacturer, strlen(manufacturer));
|
||||
return 0;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
#include "light_service.h"
|
||||
#include "beacon.h"
|
||||
#include "persistence.h"
|
||||
|
||||
static uint8_t g_beacon_enabled = 0;
|
||||
static int8_t g_led_value = 0;
|
||||
|
||||
/// Characteristic Callbacks
|
||||
int gatt_svr_chr_light_led_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR)
|
||||
{
|
||||
char *data = "To be implemented later";
|
||||
os_mbuf_append(ctxt->om, data, strlen(data));
|
||||
return 0;
|
||||
}
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR)
|
||||
{
|
||||
int8_t led_value = 0;
|
||||
persistence_load(VALUE_TYPE_INT8, "LED_VALUE", &led_value);
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
int gatt_svr_chr_light_beacon_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR)
|
||||
{
|
||||
return os_mbuf_append(ctxt->om, &g_beacon_enabled, sizeof(g_beacon_enabled)) == 0
|
||||
? 0
|
||||
: BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR)
|
||||
{
|
||||
int8_t beacon_enabled = 0;
|
||||
persistence_load(VALUE_TYPE_INT8, "BEACON_ENABLED", &beacon_enabled);
|
||||
|
||||
// it has to be 1 Byte (0 or 1)
|
||||
if (OS_MBUF_PKTLEN(ctxt->om) != 1)
|
||||
{
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
uint8_t val;
|
||||
os_mbuf_copydata(ctxt->om, 0, 1, &val);
|
||||
if (val > 1)
|
||||
{
|
||||
return BLE_ATT_ERR_UNLIKELY; // or BLE_ATT_ERR_INVALID_ATTR_VALUE
|
||||
}
|
||||
|
||||
if (val == g_beacon_enabled) // value is already set
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_beacon_enabled = val;
|
||||
|
||||
if (g_beacon_enabled)
|
||||
{
|
||||
beacon_start();
|
||||
}
|
||||
else
|
||||
{
|
||||
beacon_stop();
|
||||
}
|
||||
persistence_save(VALUE_TYPE_INT8, "BEACON_ENABLED", &g_beacon_enabled);
|
||||
return 0;
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
// Characteristic User Descriptions
|
||||
int gatt_svr_desc_led_user_desc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC)
|
||||
{
|
||||
const char *desc = "Aussenbeleuchtung";
|
||||
return os_mbuf_append(ctxt->om, desc, strlen(desc)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
int gatt_svr_desc_beacon_user_desc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC)
|
||||
{
|
||||
const char *desc = "Leuchtfeuer";
|
||||
return os_mbuf_append(ctxt->om, desc, strlen(desc)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "remote_control.h"
|
||||
|
||||
#include "char_desc.h"
|
||||
#include "device_service.h"
|
||||
#include "light_service.h"
|
||||
#include "uart_service.h"
|
||||
#include <esp_event.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_mac.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <freertos/task.h>
|
||||
#include <host/ble_hs.h>
|
||||
#include <host/ble_sm.h>
|
||||
#include <host/ble_uuid.h>
|
||||
#include <nimble/nimble_port.h>
|
||||
#include <nimble/nimble_port_freertos.h>
|
||||
#include <sdkconfig.h>
|
||||
#include <services/gap/ble_svc_gap.h>
|
||||
#include <services/gatt/ble_svc_gatt.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
static const char *TAG = "remote_control";
|
||||
|
||||
static const ble_uuid16_t gatt_svr_svc_device_uuid = BLE_UUID16_INIT(0x180A);
|
||||
static const ble_uuid16_t gatt_svr_svc_light_uuid = BLE_UUID16_INIT(0xA000);
|
||||
static const ble_uuid16_t gatt_svr_svc_settings_uuid = BLE_UUID16_INIT(0xA999);
|
||||
|
||||
uint8_t ble_addr_type;
|
||||
|
||||
ble_connection_t g_connections[CONFIG_BT_NIMBLE_MAX_CONNECTIONS];
|
||||
|
||||
static void init_connection_pool()
|
||||
{
|
||||
for (int i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++)
|
||||
{
|
||||
g_connections[i].conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
g_connections[i].is_connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_any_device_connected(void)
|
||||
{
|
||||
for (int i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++)
|
||||
{
|
||||
if (g_connections[i].is_connected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ble_app_advertise(void);
|
||||
|
||||
// Descriptors for the Beacon Characteristic
|
||||
static struct ble_gatt_dsc_def beacon_char_desc[] = {
|
||||
{
|
||||
// User Description Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2901),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_beacon_user_desc_access,
|
||||
},
|
||||
{
|
||||
// Presentation Format Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2904),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_presentation_bool_access,
|
||||
},
|
||||
{
|
||||
// Valid Range Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2906),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_valid_range_bool_access,
|
||||
},
|
||||
{0},
|
||||
};
|
||||
|
||||
// Descriptors for the LED Characteristic
|
||||
static struct ble_gatt_dsc_def led_char_desc[] = {
|
||||
{
|
||||
// User Description Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2901),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_led_user_desc_access,
|
||||
},
|
||||
{
|
||||
// Presentation Format Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2904),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_presentation_bool_access,
|
||||
},
|
||||
{
|
||||
// Valid Range Descriptor
|
||||
.uuid = BLE_UUID16_DECLARE(0x2906),
|
||||
.att_flags = BLE_ATT_F_READ,
|
||||
.access_cb = gatt_svr_desc_valid_range_bool_access,
|
||||
},
|
||||
{0},
|
||||
};
|
||||
|
||||
// Array of pointers to service definitions
|
||||
static const struct ble_gatt_svc_def gatt_svcs[] = {
|
||||
{
|
||||
// Device Information Service
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &gatt_svr_svc_device_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{
|
||||
// Manufacturer String
|
||||
.uuid = BLE_UUID16_DECLARE(0x2A29),
|
||||
.flags = BLE_GATT_CHR_F_READ,
|
||||
.access_cb = gatt_svr_chr_device_manufacturer_access,
|
||||
},
|
||||
{
|
||||
// Hardware Revision String
|
||||
.uuid = BLE_UUID16_DECLARE(0x2A27),
|
||||
.flags = BLE_GATT_CHR_F_READ,
|
||||
.access_cb = gatt_svr_chr_device_hardware_access,
|
||||
},
|
||||
{
|
||||
// Firmware Revision String
|
||||
.uuid = BLE_UUID16_DECLARE(0x2A26),
|
||||
.flags = BLE_GATT_CHR_F_READ,
|
||||
.access_cb = gatt_svr_chr_device_firmware_access,
|
||||
},
|
||||
{
|
||||
// Device Name
|
||||
.uuid = BLE_UUID16_DECLARE(0x2A00),
|
||||
.flags = BLE_GATT_CHR_F_READ,
|
||||
.access_cb = gatt_svr_chr_device_name_access,
|
||||
},
|
||||
{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Light Service
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &gatt_svr_svc_light_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{
|
||||
// Beacon Characteristic
|
||||
.uuid = BLE_UUID16_DECLARE(0xBEA0),
|
||||
.flags =
|
||||
BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC,
|
||||
.access_cb = gatt_svr_chr_light_beacon_access,
|
||||
.descriptors = beacon_char_desc,
|
||||
},
|
||||
{
|
||||
// LED Characteristic
|
||||
.uuid = BLE_UUID16_DECLARE(0xF037),
|
||||
.flags =
|
||||
BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC,
|
||||
.access_cb = gatt_svr_chr_light_led_access,
|
||||
.descriptors = led_char_desc,
|
||||
},
|
||||
{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// UART Service
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &gatt_svr_svc_uart_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{
|
||||
// Characteristic: RX (Receiving of data)
|
||||
.uuid = &gatt_svr_chr_uart_rx_uuid.u,
|
||||
.access_cb = gatt_svr_chr_uart_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
||||
},
|
||||
{
|
||||
// Characteristic: TX (Sending of data)
|
||||
.uuid = &gatt_svr_chr_uart_tx_uuid.u,
|
||||
.access_cb = gatt_svr_chr_uart_access,
|
||||
.val_handle = &tx_chr_val_handle,
|
||||
.flags = BLE_GATT_CHR_F_NOTIFY,
|
||||
},
|
||||
{0},
|
||||
},
|
||||
},
|
||||
// Settings Service (empty for now)
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &gatt_svr_svc_settings_uuid.u,
|
||||
.characteristics = (struct ble_gatt_chr_def[]){{0}},
|
||||
},
|
||||
{0}};
|
||||
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[])
|
||||
{
|
||||
sprintf(addr_str, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
/* Local variables */
|
||||
char addr_str[18] = {0};
|
||||
|
||||
/* Connection handle */
|
||||
ESP_LOGI(TAG, "connection handle: %d", desc->conn_handle);
|
||||
|
||||
/* Local ID address */
|
||||
format_addr(addr_str, desc->our_id_addr.val);
|
||||
ESP_LOGI(TAG, "device id address: type=%d, value=%s", desc->our_id_addr.type, addr_str);
|
||||
|
||||
/* Peer ID address */
|
||||
format_addr(addr_str, desc->peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "peer id address: type=%d, value=%s", desc->peer_id_addr.type, addr_str);
|
||||
|
||||
/* Connection info */
|
||||
ESP_LOGI(TAG,
|
||||
"conn_itvl=%d, conn_latency=%d, supervision_timeout=%d, "
|
||||
"encrypted=%d, authenticated=%d, bonded=%d\n",
|
||||
desc->conn_itvl, desc->conn_latency, desc->supervision_timeout, desc->sec_state.encrypted,
|
||||
desc->sec_state.authenticated, desc->sec_state.bonded);
|
||||
}
|
||||
|
||||
// BLE event handling
|
||||
static int ble_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
esp_err_t rc;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
switch (event->type)
|
||||
{
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
ESP_LOGI(TAG, "connection %s; status=%d", event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
|
||||
/* Connection succeeded */
|
||||
if (event->connect.status == 0)
|
||||
{
|
||||
bool found_slot = false;
|
||||
for (int i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++)
|
||||
{
|
||||
if (!g_connections[i].is_connected)
|
||||
{
|
||||
g_connections[i].conn_handle = event->connect.conn_handle;
|
||||
g_connections[i].is_connected = true;
|
||||
found_slot = true;
|
||||
ESP_LOGI(TAG, "Connection stored in slot %d", i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_slot)
|
||||
{
|
||||
ESP_LOGW(TAG, "No free connection slot available!");
|
||||
}
|
||||
|
||||
/* Check connection handle */
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
if (rc != 0)
|
||||
{
|
||||
}
|
||||
|
||||
print_conn_desc(&desc);
|
||||
|
||||
/* Try to update connection parameters */
|
||||
struct ble_gap_upd_params params = {.itvl_min = desc.conn_itvl,
|
||||
.itvl_max = desc.conn_itvl,
|
||||
.latency = 3,
|
||||
.supervision_timeout = desc.supervision_timeout};
|
||||
rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
||||
if (rc != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "failed to update connection parameters, error code: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* Connection failed, restart advertising */
|
||||
else
|
||||
{
|
||||
ble_app_advertise();
|
||||
}
|
||||
return rc;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason);
|
||||
for (int i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++)
|
||||
{
|
||||
if (g_connections[i].conn_handle == event->disconnect.conn.conn_handle)
|
||||
{
|
||||
g_connections[i].is_connected = false;
|
||||
g_connections[i].conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
ESP_LOGI(TAG, "Connection from slot %d removed", i);
|
||||
ble_app_advertise(); // Restart advertising to allow new connections
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
||||
ESP_LOGI(TAG, "Passkey action required: %d", event->passkey.params.action);
|
||||
struct ble_sm_io pkey = {0};
|
||||
|
||||
switch (event->passkey.params.action)
|
||||
{
|
||||
case BLE_SM_IOACT_DISP:
|
||||
pkey.action = BLE_SM_IOACT_DISP;
|
||||
pkey.passkey = CONFIG_BONDING_PASSPHRASE;
|
||||
ESP_LOGI(TAG, "Displaying passkey: %06" PRIu32, pkey.passkey);
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
if (rc != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "failed to inject security manager io, error code: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unknown passkey action: %d", event->passkey.params.action);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
ESP_LOGI(TAG, "Encryption change event; status=%d", event->enc_change.status);
|
||||
|
||||
if (event->enc_change.status != 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "Encryption failed with status %d", event->enc_change.status);
|
||||
|
||||
rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc);
|
||||
if (rc == 0)
|
||||
{
|
||||
char addr_str[18] = {0};
|
||||
format_addr(addr_str, desc.peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "Deleting bond for peer: %s", addr_str);
|
||||
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
|
||||
ble_gap_terminate(event->enc_change.conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "Encryption successfully established");
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
||||
ESP_LOGI(TAG, "Repeat pairing requested");
|
||||
|
||||
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
if (rc == 0)
|
||||
{
|
||||
char addr_str[18] = {0};
|
||||
format_addr(addr_str, desc.peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "Deleting old bond for specific peer: %s", addr_str);
|
||||
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
}
|
||||
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
ESP_LOGI(TAG, "Advertising complete");
|
||||
ble_app_advertise();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Define the BLE connection
|
||||
static void ble_app_advertise(void)
|
||||
{
|
||||
int ret;
|
||||
|
||||
// GAP - advertising definition
|
||||
struct ble_hs_adv_fields fields;
|
||||
memset(&fields, 0, sizeof(fields));
|
||||
uint8_t mfg_data[] = {0xDE, 0xC0, 0x05, 0x10, 0x20, 0x25};
|
||||
static const ble_uuid16_t services[] = {gatt_svr_svc_device_uuid, gatt_svr_svc_light_uuid,
|
||||
gatt_svr_svc_settings_uuid};
|
||||
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
fields.uuids16 = services;
|
||||
fields.num_uuids16 = sizeof(services) / sizeof(services[0]);
|
||||
fields.uuids16_is_complete = 1;
|
||||
fields.mfg_data = mfg_data;
|
||||
fields.mfg_data_len = sizeof(mfg_data);
|
||||
|
||||
ret = ble_gap_adv_set_fields(&fields);
|
||||
if (ret != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set advertising data (err: %d)", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// GAP - device connectivity definition
|
||||
struct ble_gap_adv_params adv_params;
|
||||
memset(&adv_params, 0, sizeof(adv_params));
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; // connectable or non-connectable
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable
|
||||
ret = ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Advertising failed to start (err %d)", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Configure Scan Response Data (SCAN_RSP) ---
|
||||
struct ble_hs_adv_fields scan_rsp_fields;
|
||||
memset(&scan_rsp_fields, 0, sizeof(scan_rsp_fields));
|
||||
|
||||
// Get the device name
|
||||
const char *device_name;
|
||||
device_name = ble_svc_gap_device_name();
|
||||
scan_rsp_fields.name = (uint8_t *)device_name;
|
||||
scan_rsp_fields.name_len = strlen(device_name);
|
||||
scan_rsp_fields.name_is_complete = 1;
|
||||
|
||||
// Optionally, add TX power level to scan response
|
||||
scan_rsp_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
scan_rsp_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
ret = ble_gap_adv_rsp_set_fields(&scan_rsp_fields);
|
||||
if (ret != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Error setting scan response data; rc=%d", ret);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_stack_reset(int reason)
|
||||
{
|
||||
/* On reset, print reset reason to console */
|
||||
ESP_LOGI(TAG, "nimble stack reset, reset reason: %d", reason);
|
||||
}
|
||||
|
||||
static void on_stack_sync(void)
|
||||
{
|
||||
remote_control_start_advertising();
|
||||
}
|
||||
|
||||
void remote_control_start_advertising(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
uint8_t ble_addr[6] = {0};
|
||||
|
||||
ret = ble_hs_id_infer_auto(0, &ble_addr_type);
|
||||
if (ret != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "error determining address type; rc=%d", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = ble_hs_id_copy_addr(ble_addr_type, ble_addr, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to get BLE MAC address (err: %d)", ret);
|
||||
}
|
||||
|
||||
char formatted_name[32];
|
||||
snprintf(formatted_name, sizeof(formatted_name), "Lighthouse %02X%02X", ble_addr[4], ble_addr[5]);
|
||||
ble_svc_gap_device_name_set(formatted_name);
|
||||
|
||||
ble_app_advertise();
|
||||
}
|
||||
|
||||
void remote_control_stop_advertising(void)
|
||||
{
|
||||
ble_gap_adv_stop();
|
||||
}
|
||||
|
||||
static esp_err_t gatt_svc_init(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
ble_svc_gatt_init();
|
||||
|
||||
ret = ble_gatts_count_cfg(gatt_svcs);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = ble_gatts_add_svcs(gatt_svcs);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t gap_init(void)
|
||||
{
|
||||
ble_svc_gap_init();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// The infinite task
|
||||
static void host_task(void *param)
|
||||
{
|
||||
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
|
||||
}
|
||||
|
||||
static void nimble_host_config_init(void)
|
||||
{
|
||||
// callbacks
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
|
||||
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_ONLY;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
|
||||
// Initialize BLE store configuration
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
void remote_control_register_gatt(void)
|
||||
{
|
||||
init_connection_pool();
|
||||
gap_init();
|
||||
gatt_svc_init();
|
||||
|
||||
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_ONLY;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
ble_store_config_init();
|
||||
|
||||
xTaskCreate(uart_tx_task, "uart_tx", 2048, NULL, 1, NULL);
|
||||
}
|
||||
|
||||
void remote_control_init(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to initialize nimble stack (err: %s)", esp_err_to_name(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
remote_control_register_gatt();
|
||||
nimble_host_config_init();
|
||||
nimble_port_freertos_init(host_task);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
#include "uart_service.h"
|
||||
#include "remote_control.h"
|
||||
#include <esp_log.h>
|
||||
#include <sdkconfig.h>
|
||||
|
||||
static const char *TAG = "uart_service";
|
||||
|
||||
// Service UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
const ble_uuid128_t gatt_svr_svc_uart_uuid =
|
||||
BLE_UUID128_INIT(0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E);
|
||||
|
||||
// RX Characteristic UUID: 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
const ble_uuid128_t gatt_svr_chr_uart_rx_uuid =
|
||||
BLE_UUID128_INIT(0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E);
|
||||
|
||||
// TX Characteristic UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
|
||||
const ble_uuid128_t gatt_svr_chr_uart_tx_uuid =
|
||||
BLE_UUID128_INIT(0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E);
|
||||
|
||||
uint16_t tx_chr_val_handle;
|
||||
|
||||
// Callback function for GATT events (read/write on characteristics)
|
||||
int gatt_svr_chr_uart_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
switch (ctxt->op)
|
||||
{
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
// Check if the RX characteristic is being written to
|
||||
// if (ble_uuid_cmp((const ble_uuid_t *)&ctxt->chr->uuid, (const ble_uuid_t *)&gatt_svr_chr_uart_rx_uuid) == 0)
|
||||
{
|
||||
// Get data from the buffer
|
||||
uint16_t data_len = OS_MBUF_PKTLEN(ctxt->om);
|
||||
char *data = (char *)malloc(data_len + 1);
|
||||
if (data)
|
||||
{
|
||||
int rc = ble_hs_mbuf_to_flat(ctxt->om, data, data_len, NULL);
|
||||
if (rc == 0)
|
||||
{
|
||||
data[data_len] = '\0';
|
||||
ESP_LOGI(TAG, "Received data: %s", data);
|
||||
}
|
||||
free(data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
// Function to send data via the TX characteristic
|
||||
void send_ble_data(const char *data)
|
||||
{
|
||||
ESP_LOGI(TAG, "Preparing to send data: %s", data);
|
||||
|
||||
struct os_mbuf *om;
|
||||
|
||||
for (int i = 0; i < CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++)
|
||||
{
|
||||
if (g_connections[i].is_connected)
|
||||
{
|
||||
om = ble_hs_mbuf_from_flat(data, strlen(data));
|
||||
if (om)
|
||||
{
|
||||
int rc = ble_gatts_notify_custom(g_connections[i].conn_handle, tx_chr_val_handle, om);
|
||||
if (rc == 0)
|
||||
{
|
||||
ESP_LOGI(TAG, "Sent data to conn_handle %d: %s", g_connections[i].conn_handle, data);
|
||||
}
|
||||
else if (rc != BLE_HS_ENOTCONN) // Ignore "not connected" errors if a device just disconnected
|
||||
{
|
||||
ESP_LOGE(TAG, "Error sending data to conn_handle %d: %d", g_connections[i].conn_handle, rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void uart_tx_task(void *param)
|
||||
{
|
||||
char buffer[50];
|
||||
int count = 0;
|
||||
while (1)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
if (is_any_device_connected())
|
||||
{
|
||||
ESP_LOGI(TAG, "Sending data over BLE UART TX");
|
||||
sprintf(buffer, "Hello World #%d", count++);
|
||||
send_ble_data(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
idf_component_register(SRCS
|
||||
"src/thread_control.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
openthread
|
||||
PRIV_REQUIRES
|
||||
nvs_flash
|
||||
light
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
menu "Thread Configuration"
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_openthread_types.h"
|
||||
|
||||
// ESP32-H2 has a native IEEE 802.15.4 radio.
|
||||
#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG() \
|
||||
{ \
|
||||
.radio_mode = RADIO_MODE_NATIVE, \
|
||||
}
|
||||
|
||||
// No OpenThread CLI — the application uses CoAP directly.
|
||||
#define ESP_OPENTHREAD_DEFAULT_HOST_CONFIG() \
|
||||
{ \
|
||||
.host_connection_mode = HOST_CONNECTION_MODE_NONE, \
|
||||
}
|
||||
|
||||
#define ESP_OPENTHREAD_DEFAULT_PORT_CONFIG() \
|
||||
{ \
|
||||
.storage_partition_name = "nvs", \
|
||||
.netif_queue_size = 32, \
|
||||
.task_queue_size = 32, \
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Starts the OpenThread stack and begins network commissioning.
|
||||
*
|
||||
* Spawns the `thread_task` FreeRTOS task which:
|
||||
* - initialises the OpenThread platform (radio, host, port)
|
||||
* - configures the device as a Full Thread Device (FTD)
|
||||
* - starts the Joiner if no dataset is commissioned yet, or enables the
|
||||
* Thread interface directly when a dataset is already stored
|
||||
*
|
||||
* The Joiner retries commissioning up to `JOINER_MAX_RETRIES` times.
|
||||
* After all attempts fail the status LED turns solid red and the joiner
|
||||
* stops permanently until the next reboot.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK: Task created successfully.
|
||||
* - ESP_ERR_NO_MEM: FreeRTOS task creation failed.
|
||||
*/
|
||||
esp_err_t thread_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,774 @@
|
||||
#include "thread_control.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_openthread.h"
|
||||
#include "esp_openthread_lock.h"
|
||||
#include "esp_openthread_netif_glue.h"
|
||||
#include "esp_openthread_types.h"
|
||||
#include "esp_ot_config.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "nvs.h"
|
||||
#include "openthread/coap.h"
|
||||
#include "openthread/dataset.h"
|
||||
#include "openthread/instance.h"
|
||||
#include "openthread/ip6.h"
|
||||
#include "openthread/link.h"
|
||||
#include "openthread/logging.h"
|
||||
#include "openthread/thread.h"
|
||||
#include "openthread/thread_ftd.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "beacon.h"
|
||||
#include "outdoor.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
static const char *TAG = "thread";
|
||||
|
||||
static char s_device_name[32];
|
||||
static otCoapResource s_wellknown_resource;
|
||||
static otCoapResource s_beacon_resource;
|
||||
static otCoapResource s_outdoor_resource;
|
||||
static otCoapResource s_flicker_resource;
|
||||
static otCoapResource s_group_resource;
|
||||
static bool s_coap_started = false;
|
||||
static bool s_announced = false;
|
||||
static bool s_groups_restored = false;
|
||||
|
||||
#define THREAD_NVS_NAMESPACE "thread"
|
||||
#define THREAD_NVS_KEY_NAME "name"
|
||||
#define THREAD_NVS_KEY_GROUPS "groups"
|
||||
#define THREAD_GROUPS_MAX_LEN 384
|
||||
|
||||
static const char *WELLKNOWN_CORE_PAYLOAD =
|
||||
"</beacon>;rt=\"maerklin.switch\";title=\"Blinken\";sim;obs,"
|
||||
// "</outdoor>;rt=\"maerklin.switch\";title=\"Aussenlicht\";sim;obs,"
|
||||
// "</flicker>;rt=\"maerklin.value\";min=0;max=100;step=5;title=\"Flackern in %\";obs,"
|
||||
"</group>;rt=\"maerklin.group\";title=\"Group\"";
|
||||
|
||||
// ─── CoAP Observe (RFC 7641) ──────────────────────────────────────────────────
|
||||
|
||||
#define MAX_OBSERVERS 4
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool active;
|
||||
otIp6Address peer_addr;
|
||||
uint16_t peer_port;
|
||||
uint8_t token[OT_COAP_MAX_TOKEN_LENGTH];
|
||||
uint8_t token_len;
|
||||
uint32_t obs_seq; // 24-bit monotonic counter (RFC 7641 §4.4)
|
||||
} coap_observer_t;
|
||||
|
||||
static coap_observer_t s_beacon_observers[MAX_OBSERVERS];
|
||||
static coap_observer_t s_outdoor_observers[MAX_OBSERVERS];
|
||||
static coap_observer_t s_flicker_observers[MAX_OBSERVERS];
|
||||
|
||||
static bool get_observe_option(const otMessage *msg, uint64_t *out)
|
||||
{
|
||||
otCoapOptionIterator it;
|
||||
if (otCoapOptionIteratorInit(&it, msg) != OT_ERROR_NONE)
|
||||
return false;
|
||||
if (!otCoapOptionIteratorGetFirstOptionMatching(&it, OT_COAP_OPTION_OBSERVE))
|
||||
return false;
|
||||
return otCoapOptionIteratorGetOptionUintValue(&it, out) == OT_ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool observer_matches(const coap_observer_t *obs, const otMessageInfo *info, const otMessage *msg)
|
||||
{
|
||||
if (!obs->active)
|
||||
return false;
|
||||
if (memcmp(&obs->peer_addr, &info->mPeerAddr, sizeof(otIp6Address)) != 0)
|
||||
return false;
|
||||
if (obs->peer_port != info->mPeerPort)
|
||||
return false;
|
||||
uint8_t tlen = otCoapMessageGetTokenLength(msg);
|
||||
return obs->token_len == tlen && memcmp(obs->token, otCoapMessageGetToken(msg), tlen) == 0;
|
||||
}
|
||||
|
||||
static void observer_add(coap_observer_t *observers, const otMessageInfo *info, const otMessage *msg)
|
||||
{
|
||||
// exact match (same addr+port+token) — already registered
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
if (observer_matches(&observers[i], info, msg))
|
||||
return;
|
||||
|
||||
// same peer, new token — update slot in place (handles observer restart)
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
{
|
||||
if (!observers[i].active)
|
||||
continue;
|
||||
if (memcmp(&observers[i].peer_addr, &info->mPeerAddr, sizeof(otIp6Address)) != 0)
|
||||
continue;
|
||||
if (observers[i].peer_port != info->mPeerPort)
|
||||
continue;
|
||||
observers[i].token_len = otCoapMessageGetTokenLength(msg);
|
||||
memcpy(observers[i].token, otCoapMessageGetToken(msg), observers[i].token_len);
|
||||
observers[i].obs_seq = 0;
|
||||
|
||||
char addr[OT_IP6_ADDRESS_STRING_SIZE];
|
||||
otIp6AddressToString(&info->mPeerAddr, addr, sizeof(addr));
|
||||
ESP_LOGI(TAG, "Observer re-registered (new token): %s", addr);
|
||||
return;
|
||||
}
|
||||
|
||||
// new peer — find empty slot
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
{
|
||||
if (observers[i].active)
|
||||
continue;
|
||||
observers[i].active = true;
|
||||
observers[i].peer_addr = info->mPeerAddr;
|
||||
observers[i].peer_port = info->mPeerPort;
|
||||
observers[i].token_len = otCoapMessageGetTokenLength(msg);
|
||||
memcpy(observers[i].token, otCoapMessageGetToken(msg), observers[i].token_len);
|
||||
observers[i].obs_seq = 0;
|
||||
|
||||
char addr[OT_IP6_ADDRESS_STRING_SIZE];
|
||||
otIp6AddressToString(&info->mPeerAddr, addr, sizeof(addr));
|
||||
ESP_LOGI(TAG, "Observer registered: %s", addr);
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Observer list full — registration ignored");
|
||||
}
|
||||
|
||||
static void observer_remove(coap_observer_t *observers, const otMessageInfo *info, const otMessage *msg)
|
||||
{
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
{
|
||||
if (!observer_matches(&observers[i], info, msg))
|
||||
continue;
|
||||
observers[i].active = false;
|
||||
char addr[OT_IP6_ADDRESS_STRING_SIZE];
|
||||
otIp6AddressToString(&info->mPeerAddr, addr, sizeof(addr));
|
||||
ESP_LOGI(TAG, "Observer deregistered: %s", addr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void notify_observers(otInstance *instance, coap_observer_t *observers, const char *payload)
|
||||
{
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
{
|
||||
if (!observers[i].active)
|
||||
continue;
|
||||
|
||||
otMessage *msg = otCoapNewMessage(instance, NULL);
|
||||
if (!msg)
|
||||
continue;
|
||||
|
||||
otCoapMessageInit(msg, OT_COAP_TYPE_NON_CONFIRMABLE, OT_COAP_CODE_CONTENT);
|
||||
otCoapMessageSetToken(msg, observers[i].token, observers[i].token_len);
|
||||
|
||||
observers[i].obs_seq = (observers[i].obs_seq + 1) & 0xFFFFFF;
|
||||
otCoapMessageAppendObserveOption(msg, observers[i].obs_seq);
|
||||
otCoapMessageAppendContentFormatOption(msg, OT_COAP_OPTION_CONTENT_FORMAT_TEXT_PLAIN);
|
||||
otCoapMessageSetPayloadMarker(msg);
|
||||
otMessageAppend(msg, payload, (uint16_t)strlen(payload));
|
||||
|
||||
otMessageInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.mPeerAddr = observers[i].peer_addr;
|
||||
info.mPeerPort = observers[i].peer_port;
|
||||
|
||||
if (otCoapSendRequest(instance, msg, &info, NULL, NULL) != OT_ERROR_NONE)
|
||||
{
|
||||
otMessageFree(msg);
|
||||
ESP_LOGW(TAG, "Failed to send Observe notification to slot %d", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void coap_send_observe_response(otInstance *instance, otMessage *request, const otMessageInfo *msg_info,
|
||||
coap_observer_t *observers, const char *payload)
|
||||
{
|
||||
otCoapType type = (otCoapMessageGetType(request) == OT_COAP_TYPE_CONFIRMABLE) ? OT_COAP_TYPE_ACKNOWLEDGMENT
|
||||
: OT_COAP_TYPE_NON_CONFIRMABLE;
|
||||
otMessage *response = otCoapNewMessage(instance, NULL);
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
otCoapMessageInitResponse(response, request, type, OT_COAP_CODE_CONTENT);
|
||||
|
||||
uint32_t seq = 0;
|
||||
for (int i = 0; i < MAX_OBSERVERS; i++)
|
||||
{
|
||||
if (!observers[i].active)
|
||||
continue;
|
||||
uint8_t tlen = otCoapMessageGetTokenLength(request);
|
||||
if (observers[i].token_len == tlen && memcmp(observers[i].token, otCoapMessageGetToken(request), tlen) == 0 &&
|
||||
memcmp(&observers[i].peer_addr, &msg_info->mPeerAddr, sizeof(otIp6Address)) == 0)
|
||||
{
|
||||
seq = observers[i].obs_seq;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
otCoapMessageAppendObserveOption(response, seq);
|
||||
otCoapMessageAppendContentFormatOption(response, OT_COAP_OPTION_CONTENT_FORMAT_TEXT_PLAIN);
|
||||
otCoapMessageSetPayloadMarker(response);
|
||||
otMessageAppend(response, payload, (uint16_t)strlen(payload));
|
||||
|
||||
if (otCoapSendResponse(instance, response, msg_info) != OT_ERROR_NONE)
|
||||
otMessageFree(response);
|
||||
}
|
||||
|
||||
// ─── NVS storage ────────────────────────────────────────────────────────────
|
||||
|
||||
static bool load_device_name(char *out, size_t max_len)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
if (nvs_open(THREAD_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
|
||||
size_t len = max_len;
|
||||
bool ok = (nvs_get_str(handle, THREAD_NVS_KEY_NAME, out, &len) == ESP_OK && len > 1);
|
||||
nvs_close(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool load_group_memberships(char *out, size_t max_len)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
if (nvs_open(THREAD_NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
|
||||
size_t len = max_len;
|
||||
esp_err_t err = nvs_get_str(handle, THREAD_NVS_KEY_GROUPS, out, &len);
|
||||
nvs_close(handle);
|
||||
|
||||
return err == ESP_OK && len > 1;
|
||||
}
|
||||
|
||||
static bool save_group_memberships(const char *groups)
|
||||
{
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(THREAD_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groups && groups[0] != '\0')
|
||||
{
|
||||
err = nvs_set_str(handle, THREAD_NVS_KEY_GROUPS, groups);
|
||||
}
|
||||
else
|
||||
{
|
||||
err = nvs_erase_key(handle, THREAD_NVS_KEY_GROUPS);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND)
|
||||
err = ESP_OK;
|
||||
}
|
||||
|
||||
if (err == ESP_OK)
|
||||
err = nvs_commit(handle);
|
||||
|
||||
nvs_close(handle);
|
||||
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to persist groups: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool group_list_contains(const char *list, const char *addr)
|
||||
{
|
||||
char tmp[THREAD_GROUPS_MAX_LEN] = {};
|
||||
strlcpy(tmp, list, sizeof(tmp));
|
||||
|
||||
char *saveptr = NULL;
|
||||
char *token = strtok_r(tmp, ";", &saveptr);
|
||||
while (token)
|
||||
{
|
||||
if (strcmp(token, addr) == 0)
|
||||
return true;
|
||||
token = strtok_r(NULL, ";", &saveptr);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool group_list_add(char *list, size_t list_size, const char *addr)
|
||||
{
|
||||
if (group_list_contains(list, addr))
|
||||
return false;
|
||||
|
||||
size_t curr_len = strlen(list);
|
||||
size_t addr_len = strlen(addr);
|
||||
size_t needed = curr_len + (curr_len > 0 ? 1 : 0) + addr_len + 1;
|
||||
if (needed > list_size)
|
||||
return false;
|
||||
|
||||
if (curr_len > 0)
|
||||
strcat(list, ";");
|
||||
strcat(list, addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool group_list_remove(char *list, size_t list_size, const char *addr)
|
||||
{
|
||||
char src[THREAD_GROUPS_MAX_LEN] = {};
|
||||
char dst[THREAD_GROUPS_MAX_LEN] = {};
|
||||
bool removed = false;
|
||||
|
||||
strlcpy(src, list, sizeof(src));
|
||||
|
||||
char *saveptr = NULL;
|
||||
char *token = strtok_r(src, ";", &saveptr);
|
||||
while (token)
|
||||
{
|
||||
if (strcmp(token, addr) == 0)
|
||||
{
|
||||
removed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dst[0] != '\0')
|
||||
strlcat(dst, ";", sizeof(dst));
|
||||
strlcat(dst, token, sizeof(dst));
|
||||
}
|
||||
token = strtok_r(NULL, ";", &saveptr);
|
||||
}
|
||||
|
||||
strlcpy(list, dst, list_size);
|
||||
return removed;
|
||||
}
|
||||
|
||||
static void persist_group_membership(const char *addr, bool join)
|
||||
{
|
||||
char groups[THREAD_GROUPS_MAX_LEN] = {};
|
||||
bool have_groups = load_group_memberships(groups, sizeof(groups));
|
||||
if (!have_groups)
|
||||
groups[0] = '\0';
|
||||
|
||||
bool changed =
|
||||
join ? group_list_add(groups, sizeof(groups), addr) : group_list_remove(groups, sizeof(groups), addr);
|
||||
|
||||
if (changed && !save_group_memberships(groups))
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to update persisted groups");
|
||||
}
|
||||
}
|
||||
|
||||
static void restore_group_memberships(otInstance *instance)
|
||||
{
|
||||
if (s_groups_restored)
|
||||
return;
|
||||
|
||||
s_groups_restored = true;
|
||||
|
||||
char groups[THREAD_GROUPS_MAX_LEN] = {};
|
||||
if (!load_group_memberships(groups, sizeof(groups)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char *saveptr = NULL;
|
||||
char *token = strtok_r(groups, ";", &saveptr);
|
||||
while (token)
|
||||
{
|
||||
otIp6Address addr;
|
||||
if (otIp6AddressFromString(token, &addr) == OT_ERROR_NONE)
|
||||
{
|
||||
otIp6SubscribeMulticastAddress(instance, &addr);
|
||||
}
|
||||
token = strtok_r(NULL, ";", &saveptr);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Fixed dataset from sdkconfig ────────────────────────────────────────────
|
||||
|
||||
static uint8_t hex_nibble(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
return (uint8_t)(c - '0');
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return (uint8_t)(c - 'a' + 10);
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return (uint8_t)(c - 'A' + 10);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void hex_str_to_bytes(const char *hex, uint8_t *bytes, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; i++)
|
||||
bytes[i] = (uint8_t)((hex_nibble(hex[2 * i]) << 4) | hex_nibble(hex[2 * i + 1]));
|
||||
}
|
||||
|
||||
static void apply_fixed_dataset(otInstance *instance)
|
||||
{
|
||||
otOperationalDataset dataset;
|
||||
memset(&dataset, 0, sizeof(dataset));
|
||||
|
||||
dataset.mActiveTimestamp.mSeconds = 1;
|
||||
dataset.mComponents.mIsActiveTimestampPresent = true;
|
||||
|
||||
dataset.mChannel = CONFIG_OPENTHREAD_NETWORK_CHANNEL;
|
||||
dataset.mComponents.mIsChannelPresent = true;
|
||||
|
||||
dataset.mPanId = (otPanId)CONFIG_OPENTHREAD_NETWORK_PANID;
|
||||
dataset.mComponents.mIsPanIdPresent = true;
|
||||
|
||||
strlcpy(dataset.mNetworkName.m8, CONFIG_OPENTHREAD_NETWORK_NAME, OT_NETWORK_NAME_MAX_SIZE + 1);
|
||||
dataset.mComponents.mIsNetworkNamePresent = true;
|
||||
|
||||
hex_str_to_bytes(CONFIG_OPENTHREAD_NETWORK_EXTPANID, dataset.mExtendedPanId.m8, OT_EXT_PAN_ID_SIZE);
|
||||
dataset.mComponents.mIsExtendedPanIdPresent = true;
|
||||
|
||||
hex_str_to_bytes(CONFIG_OPENTHREAD_NETWORK_MASTERKEY, dataset.mNetworkKey.m8, OT_NETWORK_KEY_SIZE);
|
||||
dataset.mComponents.mIsNetworkKeyPresent = true;
|
||||
|
||||
if (otDatasetSetActive(instance, &dataset) != OT_ERROR_NONE)
|
||||
ESP_LOGE(TAG, "Failed to apply fixed Thread dataset");
|
||||
else
|
||||
ESP_LOGI(TAG, "Fixed Thread dataset applied (net=%s ch=%d)", CONFIG_OPENTHREAD_NETWORK_NAME,
|
||||
CONFIG_OPENTHREAD_NETWORK_CHANNEL);
|
||||
}
|
||||
|
||||
// ─── CoAP handlers ───────────────────────────────────────────────────────────
|
||||
|
||||
static void coap_send_response(otInstance *instance, otMessage *request, const otMessageInfo *msg_info, otCoapCode code,
|
||||
const char *payload)
|
||||
{
|
||||
otCoapType type = (otCoapMessageGetType(request) == OT_COAP_TYPE_CONFIRMABLE) ? OT_COAP_TYPE_ACKNOWLEDGMENT
|
||||
: OT_COAP_TYPE_NON_CONFIRMABLE;
|
||||
otMessage *response = otCoapNewMessage(instance, NULL);
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
otCoapMessageInitResponse(response, request, type, code);
|
||||
|
||||
if (payload && strlen(payload) > 0)
|
||||
{
|
||||
(void)otCoapMessageSetPayloadMarker(response);
|
||||
otMessageAppend(response, payload, (uint16_t)strlen(payload));
|
||||
}
|
||||
|
||||
if (otCoapSendResponse(instance, response, msg_info) != OT_ERROR_NONE)
|
||||
otMessageFree(response);
|
||||
}
|
||||
|
||||
static void wellknown_core_handler(void *context, otMessage *message, const otMessageInfo *msg_info)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
|
||||
if (otCoapMessageGetCode(message) != OT_COAP_CODE_GET)
|
||||
{
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_METHOD_NOT_ALLOWED, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
otCoapType type = (otCoapMessageGetType(message) == OT_COAP_TYPE_CONFIRMABLE) ? OT_COAP_TYPE_ACKNOWLEDGMENT
|
||||
: OT_COAP_TYPE_NON_CONFIRMABLE;
|
||||
|
||||
otMessage *response = otCoapNewMessage(instance, NULL);
|
||||
if (!response)
|
||||
return;
|
||||
|
||||
otCoapMessageInitResponse(response, message, type, OT_COAP_CODE_CONTENT);
|
||||
otCoapMessageAppendContentFormatOption(response, OT_COAP_OPTION_CONTENT_FORMAT_LINK_FORMAT);
|
||||
otCoapMessageSetPayloadMarker(response);
|
||||
otMessageAppend(response, WELLKNOWN_CORE_PAYLOAD, (uint16_t)strlen(WELLKNOWN_CORE_PAYLOAD));
|
||||
|
||||
if (otCoapSendResponse(instance, response, msg_info) != OT_ERROR_NONE)
|
||||
otMessageFree(response);
|
||||
}
|
||||
|
||||
static void beacon_coap_handler(void *context, otMessage *message, const otMessageInfo *msg_info)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
otCoapCode code = otCoapMessageGetCode(message);
|
||||
|
||||
if (code == OT_LOG_LEVEL_NONE) // dummy check to keep code
|
||||
{
|
||||
}
|
||||
|
||||
if (code == OT_COAP_CODE_GET)
|
||||
{
|
||||
uint64_t obs_val;
|
||||
bool has_observe = get_observe_option(message, &obs_val);
|
||||
|
||||
if (has_observe && obs_val == 0)
|
||||
observer_add(s_beacon_observers, msg_info, message);
|
||||
else if (has_observe && obs_val == 1)
|
||||
observer_remove(s_beacon_observers, msg_info, message);
|
||||
|
||||
const char *state = beacon_is_running() ? "1" : "0";
|
||||
if (has_observe && obs_val == 0)
|
||||
coap_send_observe_response(instance, message, msg_info, s_beacon_observers, state);
|
||||
else
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_CONTENT, state);
|
||||
}
|
||||
else if (code == OT_COAP_CODE_PUT)
|
||||
{
|
||||
char buf[8] = {};
|
||||
uint16_t len = otMessageRead(message, otMessageGetOffset(message), buf, sizeof(buf) - 1);
|
||||
buf[len] = '\0';
|
||||
|
||||
esp_err_t ret = (strcmp(buf, "1") == 0) ? beacon_start() : beacon_stop();
|
||||
coap_send_response(instance, message, msg_info,
|
||||
ret == ESP_OK ? OT_COAP_CODE_CHANGED : OT_COAP_CODE_INTERNAL_ERROR, NULL);
|
||||
if (ret == ESP_OK)
|
||||
notify_observers(instance, s_beacon_observers, beacon_is_running() ? "1" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
static void outdoor_coap_handler(void *context, otMessage *message, const otMessageInfo *msg_info)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
otCoapCode code = otCoapMessageGetCode(message);
|
||||
|
||||
if (code == OT_COAP_CODE_GET)
|
||||
{
|
||||
uint64_t obs_val;
|
||||
bool has_observe = get_observe_option(message, &obs_val);
|
||||
|
||||
if (has_observe && obs_val == 0)
|
||||
observer_add(s_outdoor_observers, msg_info, message);
|
||||
else if (has_observe && obs_val == 1)
|
||||
observer_remove(s_outdoor_observers, msg_info, message);
|
||||
|
||||
const char *state = outdoor_is_running() ? "1" : "0";
|
||||
if (has_observe && obs_val == 0)
|
||||
coap_send_observe_response(instance, message, msg_info, s_outdoor_observers, state);
|
||||
else
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_CONTENT, state);
|
||||
}
|
||||
else if (code == OT_COAP_CODE_PUT)
|
||||
{
|
||||
char buf[8] = {};
|
||||
uint16_t len = otMessageRead(message, otMessageGetOffset(message), buf, sizeof(buf) - 1);
|
||||
buf[len] = '\0';
|
||||
|
||||
esp_err_t ret = (strcmp(buf, "1") == 0) ? outdoor_start() : outdoor_stop();
|
||||
coap_send_response(instance, message, msg_info,
|
||||
ret == ESP_OK ? OT_COAP_CODE_CHANGED : OT_COAP_CODE_INTERNAL_ERROR, NULL);
|
||||
if (ret == ESP_OK)
|
||||
notify_observers(instance, s_outdoor_observers, outdoor_is_running() ? "1" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
static void flicker_coap_handler(void *context, otMessage *message, const otMessageInfo *msg_info)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
otCoapCode code = otCoapMessageGetCode(message);
|
||||
|
||||
if (code == OT_COAP_CODE_GET)
|
||||
{
|
||||
uint64_t obs_val;
|
||||
bool has_observe = get_observe_option(message, &obs_val);
|
||||
|
||||
if (has_observe && obs_val == 0)
|
||||
observer_add(s_flicker_observers, msg_info, message);
|
||||
else if (has_observe && obs_val == 1)
|
||||
observer_remove(s_flicker_observers, msg_info, message);
|
||||
|
||||
char payload[8];
|
||||
snprintf(payload, sizeof(payload), "%d", outdoor_get_flicker());
|
||||
if (has_observe && obs_val == 0)
|
||||
coap_send_observe_response(instance, message, msg_info, s_flicker_observers, payload);
|
||||
else
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_CONTENT, payload);
|
||||
}
|
||||
else if (code == OT_COAP_CODE_PUT)
|
||||
{
|
||||
char buf[8] = {};
|
||||
uint16_t len = otMessageRead(message, otMessageGetOffset(message), buf, sizeof(buf) - 1);
|
||||
buf[len] = '\0';
|
||||
|
||||
uint8_t val = (uint8_t)atoi(buf);
|
||||
if (val > 100)
|
||||
val = 100;
|
||||
|
||||
esp_err_t ret = outdoor_set_flicker(val);
|
||||
coap_send_response(instance, message, msg_info,
|
||||
ret == ESP_OK ? OT_COAP_CODE_CHANGED : OT_COAP_CODE_INTERNAL_ERROR, NULL);
|
||||
if (ret == ESP_OK)
|
||||
{
|
||||
char payload[8];
|
||||
snprintf(payload, sizeof(payload), "%d", val);
|
||||
notify_observers(instance, s_flicker_observers, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void group_coap_handler(void *context, otMessage *message, const otMessageInfo *msg_info)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
otCoapCode code = otCoapMessageGetCode(message);
|
||||
|
||||
if (code != OT_COAP_CODE_PUT)
|
||||
{
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_METHOD_NOT_ALLOWED, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[48] = {};
|
||||
uint16_t len = otMessageRead(message, otMessageGetOffset(message), buf, sizeof(buf) - 1);
|
||||
buf[len] = '\0';
|
||||
|
||||
char *comma = strrchr(buf, ',');
|
||||
if (!comma)
|
||||
return;
|
||||
*comma = '\0';
|
||||
bool join = (*(comma + 1) == '1');
|
||||
|
||||
otIp6Address addr;
|
||||
if (otIp6AddressFromString(buf, &addr) == OT_ERROR_NONE)
|
||||
{
|
||||
if (join)
|
||||
otIp6SubscribeMulticastAddress(instance, &addr);
|
||||
else
|
||||
otIp6UnsubscribeMulticastAddress(instance, &addr);
|
||||
persist_group_membership(buf, join);
|
||||
}
|
||||
coap_send_response(instance, message, msg_info, OT_COAP_CODE_CHANGED, NULL);
|
||||
}
|
||||
|
||||
static void send_announce(otInstance *instance)
|
||||
{
|
||||
otMessage *msg = otCoapNewMessage(instance, NULL);
|
||||
if (!msg)
|
||||
return;
|
||||
|
||||
otCoapMessageInit(msg, OT_COAP_TYPE_NON_CONFIRMABLE, OT_COAP_CODE_POST);
|
||||
otCoapMessageAppendUriPathOptions(msg, "announce");
|
||||
otCoapMessageSetPayloadMarker(msg);
|
||||
otMessageAppend(msg, s_device_name, (uint16_t)strlen(s_device_name));
|
||||
|
||||
otMessageInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
uint8_t mc_addr[16] = {0xff, 0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01};
|
||||
memcpy(info.mPeerAddr.mFields.m8, mc_addr, sizeof(mc_addr));
|
||||
info.mPeerPort = OT_DEFAULT_COAP_PORT;
|
||||
|
||||
otCoapSendRequest(instance, msg, &info, NULL, NULL);
|
||||
}
|
||||
|
||||
static void setup_coap(otInstance *instance)
|
||||
{
|
||||
if (s_coap_started)
|
||||
return;
|
||||
s_coap_started = true;
|
||||
otCoapStart(instance, OT_DEFAULT_COAP_PORT);
|
||||
|
||||
s_wellknown_resource =
|
||||
(otCoapResource){.mUriPath = ".well-known/core", .mHandler = wellknown_core_handler, .mContext = instance};
|
||||
otCoapAddResource(instance, &s_wellknown_resource);
|
||||
|
||||
s_beacon_resource = (otCoapResource){.mUriPath = "beacon", .mHandler = beacon_coap_handler, .mContext = instance};
|
||||
otCoapAddResource(instance, &s_beacon_resource);
|
||||
|
||||
s_outdoor_resource =
|
||||
(otCoapResource){.mUriPath = "outdoor", .mHandler = outdoor_coap_handler, .mContext = instance};
|
||||
otCoapAddResource(instance, &s_outdoor_resource);
|
||||
|
||||
s_flicker_resource =
|
||||
(otCoapResource){.mUriPath = "flicker", .mHandler = flicker_coap_handler, .mContext = instance};
|
||||
otCoapAddResource(instance, &s_flicker_resource);
|
||||
|
||||
s_group_resource = (otCoapResource){.mUriPath = "group", .mHandler = group_coap_handler, .mContext = instance};
|
||||
otCoapAddResource(instance, &s_group_resource);
|
||||
}
|
||||
|
||||
static void state_changed_cb(otChangedFlags flags, void *context)
|
||||
{
|
||||
otInstance *instance = (otInstance *)context;
|
||||
|
||||
if (!(flags & OT_CHANGED_THREAD_ROLE))
|
||||
return;
|
||||
|
||||
otDeviceRole role = otThreadGetDeviceRole(instance);
|
||||
ESP_LOGI(TAG, "Thread role: %s", otThreadDeviceRoleToString(role));
|
||||
|
||||
if (role == OT_DEVICE_ROLE_CHILD || role == OT_DEVICE_ROLE_ROUTER)
|
||||
{
|
||||
beacon_set_status(BEACON_STATUS_CONNECTED);
|
||||
setup_coap(instance);
|
||||
restore_group_memberships(instance);
|
||||
if (!s_announced)
|
||||
{
|
||||
s_announced = true;
|
||||
send_announce(instance);
|
||||
}
|
||||
}
|
||||
else if (role == OT_DEVICE_ROLE_DETACHED)
|
||||
{
|
||||
beacon_set_status(BEACON_STATUS_SEARCHING);
|
||||
}
|
||||
}
|
||||
|
||||
static void thread_task(void *arg)
|
||||
{
|
||||
esp_openthread_platform_config_t config = {
|
||||
.radio_config = ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG(),
|
||||
.host_config = ESP_OPENTHREAD_DEFAULT_HOST_CONFIG(),
|
||||
.port_config = ESP_OPENTHREAD_DEFAULT_PORT_CONFIG(),
|
||||
};
|
||||
|
||||
// Initialize the ESP-OpenThread stack
|
||||
ESP_ERROR_CHECK(esp_openthread_init(&config));
|
||||
|
||||
otInstance *instance = esp_openthread_get_instance();
|
||||
|
||||
// Initialize the esp_netif for OpenThread as seen in IDF examples
|
||||
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_OPENTHREAD();
|
||||
esp_netif_t *openthread_netif = esp_netif_new(&cfg);
|
||||
assert(openthread_netif != NULL);
|
||||
|
||||
// Initialize the glue and attach it to the netif
|
||||
void *glue_handle = esp_openthread_netif_glue_init(&config);
|
||||
assert(glue_handle != NULL);
|
||||
ESP_ERROR_CHECK(esp_netif_attach(openthread_netif, glue_handle));
|
||||
|
||||
otSetStateChangedCallback(instance, state_changed_cb, instance);
|
||||
|
||||
esp_openthread_lock_acquire(portMAX_DELAY);
|
||||
|
||||
#if CONFIG_OPENTHREAD_FTD
|
||||
// Allow router role for mesh range extension, but never win leader election
|
||||
otThreadSetLocalLeaderWeight(instance, 1);
|
||||
#endif
|
||||
|
||||
ESP_ERROR_CHECK(otIp6SetEnabled(instance, true));
|
||||
|
||||
if (!load_device_name(s_device_name, sizeof(s_device_name)))
|
||||
{
|
||||
strlcpy(s_device_name, "Lighthouse", sizeof(s_device_name));
|
||||
}
|
||||
|
||||
if (!otDatasetIsCommissioned(instance))
|
||||
{
|
||||
apply_fixed_dataset(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(TAG, "Using stored Thread dataset");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Enabling Thread");
|
||||
otThreadSetEnabled(instance, true);
|
||||
|
||||
esp_openthread_lock_release();
|
||||
|
||||
// Run the main loop (this blocks until deinit)
|
||||
esp_openthread_launch_mainloop();
|
||||
|
||||
// Cleanup
|
||||
esp_openthread_netif_glue_deinit();
|
||||
esp_netif_destroy(openthread_netif);
|
||||
esp_openthread_deinit();
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t thread_init(void)
|
||||
{
|
||||
BaseType_t ret = xTaskCreate(thread_task, "thread_task", 10240, NULL, 5, NULL);
|
||||
return (ret == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @brief Initializes the touch/button input on GPIO_NUM_2.
|
||||
*
|
||||
* Configures the pin as a pull-up input with edge-triggered interrupts.
|
||||
* A software debounce timer (250 ms) filters out noise; a confirmed falling
|
||||
* edge calls beacon_toggle() to start or stop the rotating beacon light.
|
||||
*
|
||||
* Internally spawns a FreeRTOS task and a one-shot timer; both run for the
|
||||
* lifetime of the application.
|
||||
*/
|
||||
void init_touch(void);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
menu "Warnemuende Lighthouse"
|
||||
config WLED_DIN_PIN
|
||||
int "WLED Data In Pin"
|
||||
default 8
|
||||
default 10
|
||||
help
|
||||
The number of the WLED data in pin.
|
||||
|
||||
@@ -24,20 +24,14 @@ menu "Warnemuende Lighthouse"
|
||||
|
||||
config LED_PIN_LEFT
|
||||
int "LED Left Pin"
|
||||
default 11
|
||||
default 13
|
||||
help
|
||||
The pin of the LED for the left side.
|
||||
|
||||
config LED_PIN_RIGHT
|
||||
int "LED Right Pin"
|
||||
default 12
|
||||
default 14
|
||||
help
|
||||
The pin of the LED for the right side.
|
||||
|
||||
config BONDING_PASSPHRASE
|
||||
int "Bonding Passphrase"
|
||||
default 123456
|
||||
help
|
||||
The passphrase for the BLE bonding.
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
dependencies:
|
||||
idf:
|
||||
version: '>=5.4.0'
|
||||
espressif/esp_matter: ^1.4.0
|
||||
# IDF 5.5.x: esp_netif input_fn return-type changed (void → esp_netif_recv_ret_t),
|
||||
# openthread/src/esp_openthread_lwip_netif.c not updated → compile error.
|
||||
# IDF 6.0.x: mbedTLS 4.x removed EC-JPAKE (mbedtls_ssl_set_hs_ecjpake_password),
|
||||
# Thread Joiner/Commissioner DTLS handshake broken.
|
||||
# Tested working: 5.4.x
|
||||
version: ">=5.4.0,<5.5.0"
|
||||
|
||||
+29
-18
@@ -1,43 +1,54 @@
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_vfs_eventfd.h"
|
||||
#include "esp_mac.h"
|
||||
|
||||
#include "beacon.h"
|
||||
#include "light.h"
|
||||
#include "matter.h"
|
||||
#include "thread_control.h"
|
||||
#include "persistence.h"
|
||||
#include "touch.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/// init persistence
|
||||
uint8_t base_mac[6];
|
||||
esp_read_mac(base_mac, ESP_MAC_EFUSE_FACTORY);
|
||||
ESP_LOGI("H2", "Factory MAC: %02X:%02X:%02X:%02X:%02X:%02X",
|
||||
base_mac[0], base_mac[1], base_mac[2], base_mac[3], base_mac[4], base_mac[5]);
|
||||
|
||||
ESP_LOGI("H2", "Reset Reason: %d", esp_reset_reason());
|
||||
|
||||
/// init persistence (calls nvs_flash_init internally)
|
||||
persistence_init("lighthouse");
|
||||
outdoor_init();
|
||||
|
||||
init_touch();
|
||||
// Required by OpenThread: event loop, netif subsystem, and eventfd VFS
|
||||
// (OpenThread task queue uses eventfd internally)
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
esp_vfs_eventfd_config_t eventfd_config = {
|
||||
.max_fds = 5, // Increased for OT task queue, radio driver and potentially others
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
|
||||
|
||||
// init_touch(); // Temporarily disabled for diagnostics
|
||||
|
||||
/// init WLED
|
||||
if (wled_init() != ESP_OK)
|
||||
{
|
||||
printf("Failed to initialize WLED task");
|
||||
printf("Failed to initialize WLED");
|
||||
return;
|
||||
}
|
||||
|
||||
/// start beacon service
|
||||
if (beacon_init() != ESP_OK)
|
||||
{
|
||||
printf("Failed to initialize beacon task");
|
||||
return;
|
||||
}
|
||||
|
||||
/// start outdoor light service
|
||||
if (outdoor_start() != ESP_OK)
|
||||
if (thread_init() != ESP_OK)
|
||||
{
|
||||
printf("Failed to start outdoor task");
|
||||
printf("Failed to initialize thread task");
|
||||
return;
|
||||
}
|
||||
|
||||
beacon_start();
|
||||
/*
|
||||
if (matter_init() != ESP_OK)
|
||||
{
|
||||
printf("Failed to initialize matter task");
|
||||
return;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Writes a device name into the NVS partition of a connected ESP32-H2.
|
||||
# Required because the device uses USB Serial JTAG (no hardware UART),
|
||||
# so runtime UART input during boot is not available.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/set_device_name.sh <device-name> [port]
|
||||
# make set-name NAME="Leuchtturm West"
|
||||
# make set-name NAME="Leuchtturm West" PORT=/dev/cu.usbmodem1101
|
||||
#
|
||||
# The name is stored in NVS namespace "thread", key "name".
|
||||
# It is broadcast in a CoAP multicast announcement (ff03::1/announce)
|
||||
# when the device first attaches to the Thread network so the
|
||||
# Master (ESP32-C6) can identify the device by name.
|
||||
#
|
||||
# Requires:
|
||||
# - IDF_PATH set (run: . $IDF_PATH/export.sh)
|
||||
# - esptool.py in PATH (provided by export.sh)
|
||||
# - Device connected via USB
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${1:-}"
|
||||
PORT="${2:-}"
|
||||
|
||||
# ─── Usage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "Usage: $0 <device-name> [port]"
|
||||
echo ""
|
||||
echo " device-name Human-readable name broadcast on Thread attach (CoAP announce)."
|
||||
echo " Stored in NVS. Do not use commas in the name."
|
||||
echo " port Serial port, e.g. /dev/cu.usbmodem1101"
|
||||
echo " Omit to auto-detect the first /dev/cu.usbmodem* device."
|
||||
echo ""
|
||||
echo " make set-name NAME=\"Leuchtturm West\""
|
||||
echo " make set-name NAME=\"Leuchtturm West\" PORT=/dev/cu.usbmodem1101"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Sanity checks ───────────────────────────────────────────────────────────
|
||||
|
||||
if [ -z "${IDF_PATH:-}" ]; then
|
||||
echo "Error: IDF_PATH is not set."
|
||||
echo "Source the IDF environment first:"
|
||||
echo " . \$IDF_PATH/export.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NVS_GEN="${IDF_PATH}/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py"
|
||||
|
||||
if [ ! -f "$NVS_GEN" ]; then
|
||||
echo "Error: nvs_partition_gen.py not found: $NVS_GEN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v esptool.py &>/dev/null; then
|
||||
echo "Error: esptool.py not found in PATH."
|
||||
echo "Source the IDF environment first:"
|
||||
echo " . \$IDF_PATH/export.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Port detection ──────────────────────────────────────────────────────────
|
||||
|
||||
if [ -z "$PORT" ]; then
|
||||
PORT=$(ls /dev/cu.usbmodem* 2>/dev/null | head -1 || true)
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "Error: No USB Serial JTAG device found (/dev/cu.usbmodem*)."
|
||||
echo "Connect the ESP32-H2 via USB or specify PORT explicitly."
|
||||
exit 1
|
||||
fi
|
||||
echo "Port: $PORT (auto-detected)"
|
||||
else
|
||||
echo "Port: $PORT"
|
||||
fi
|
||||
|
||||
# ─── NVS partition parameters (must match partitions.csv) ────────────────────
|
||||
|
||||
NVS_OFFSET="0x9000"
|
||||
NVS_SIZE="0x6000" # 24 KiB
|
||||
|
||||
# ─── Generate NVS binary ─────────────────────────────────────────────────────
|
||||
|
||||
TMP_CSV=$(mktemp /tmp/nvs_name_XXXXXX.csv)
|
||||
TMP_BIN=$(mktemp /tmp/nvs_name_XXXXXX.bin)
|
||||
trap 'rm -f "$TMP_CSV" "$TMP_BIN"' EXIT
|
||||
|
||||
# CSV format expected by nvs_partition_gen.py
|
||||
printf 'key,type,encoding,value\nthread,namespace,,\nname,data,string,%s\n' "$NAME" > "$TMP_CSV"
|
||||
|
||||
echo "Name: \"$NAME\""
|
||||
echo "Offset: $NVS_OFFSET Size: $NVS_SIZE"
|
||||
echo ""
|
||||
echo "Generating NVS partition..."
|
||||
python3 "$NVS_GEN" generate "$TMP_CSV" "$TMP_BIN" "$NVS_SIZE"
|
||||
|
||||
# ─── Flash ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "Flashing to $PORT..."
|
||||
esptool.py --chip esp32h2 --port "$PORT" write_flash "$NVS_OFFSET" "$TMP_BIN"
|
||||
|
||||
echo ""
|
||||
echo "Done. Device name \"$NAME\" written to NVS."
|
||||
echo "Power-cycle the ESP32-H2 to apply."
|
||||
+14
-14
@@ -1,17 +1,3 @@
|
||||
# mbedTLS: required by Matter (CHIPCryptoPALmbedTLS)
|
||||
CONFIG_MBEDTLS_HKDF_C=y
|
||||
|
||||
# activate Bluetooth Low Energy (BLE)
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
|
||||
# NimBLE Options
|
||||
CONFIG_BT_NIMBLE_SECURITY_ENABLE=y
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
CONFIG_BT_NIMBLE_SMP_ID_RESET=y
|
||||
CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y
|
||||
CONFIG_BT_NIMBLE_DYNAMIC_SERVICE_ENABLE=y
|
||||
|
||||
# Flash Size
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||
@@ -19,3 +5,17 @@ CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||
# Partitions
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
|
||||
# Thread (OpenThread FTD — Router-eligible, minimal leader priority)
|
||||
CONFIG_OPENTHREAD_ENABLED=y
|
||||
CONFIG_OPENTHREAD_FTD=y
|
||||
|
||||
# Thread network credentials — must match system-control (C6)
|
||||
CONFIG_OPENTHREAD_NETWORK_NAME="MaerklinNet"
|
||||
CONFIG_OPENTHREAD_NETWORK_PANID=0xBEEF
|
||||
CONFIG_OPENTHREAD_NETWORK_EXTPANID="dead00beef00cafe"
|
||||
CONFIG_OPENTHREAD_NETWORK_MASTERKEY="00112233445566778899aabbccddeeff"
|
||||
|
||||
# esp_openthread_lwip_netif.c uses void input_fn — keep recv_ret_t as void.
|
||||
# With RECEIVE_REPORT_ERRORS=y the typedef becomes esp_err_t which breaks the build.
|
||||
CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS=n
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32"
|
||||
@@ -1,2 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32c3"
|
||||
@@ -1,2 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32c5"
|
||||
@@ -1,2 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32c6"
|
||||
@@ -8,3 +8,14 @@ CONFIG_ENABLE_WIFI_TELEMETRY=n
|
||||
|
||||
# ESP System Settings
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_48=y
|
||||
|
||||
# OpenThread FTD (Full Thread Device)
|
||||
# Allows the device to act as a Router and extend the mesh network.
|
||||
CONFIG_OPENTHREAD_FTD=y
|
||||
CONFIG_OPENTHREAD_MTD=n
|
||||
|
||||
# Thread channel — must match system-control (C6)
|
||||
CONFIG_OPENTHREAD_NETWORK_CHANNEL=18
|
||||
|
||||
# OpenThread log level — WARN keeps the serial output manageable
|
||||
CONFIG_OPENTHREAD_LOG_LEVEL_WARN=y
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32p4"
|
||||
@@ -1,3 +0,0 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_WLED_USE_DMA=y
|
||||
Reference in New Issue
Block a user