implement openthread

Signed-off-by: Peter Siegmund <mars3142@users.noreply.github.com>
This commit is contained in:
2026-05-31 07:58:49 +02:00
parent ed4af3d1b1
commit 5e4f3faf40
46 changed files with 1677 additions and 1161 deletions
+1
View File
@@ -12,3 +12,4 @@ firmware/.vscode/c_cpp_properties.json
hardware/mcu_board/.history/
*.lck
wiki/
.cache/
+10
View File
@@ -0,0 +1,10 @@
{
"permissions": {
"additionalDirectories": [
"/Volumes/Coding/git.mars3142.dev/model-railway/system-control"
],
"allow": [
"Bash(xargs ls -la)"
]
}
}
+10 -1
View File
@@ -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
+343
View File
@@ -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 `"<0100>"` |
| GET | `0` (register) | — | 2.05 Content + Observe seq + current value |
| GET | `1` (cancel) | — | 2.05 Content |
| PUT | — | `"<0100>"` (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
+2
View File
@@ -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);
+4 -12
View File
@@ -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.
+7 -1
View File
@@ -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);
+101 -12
View File
@@ -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;
}
+2 -2
View File
@@ -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);
+68 -34
View File
@@ -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
-82
View File
@@ -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, &params);
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
)
+3
View File
@@ -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;
}
+10
View File
@@ -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);
+3 -9
View File
@@ -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
+6 -2
View File
@@ -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
View File
@@ -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;
}
*/
}
+106
View File
@@ -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
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32"
-2
View File
@@ -1,2 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32c3"
-2
View File
@@ -1,2 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32c5"
-2
View File
@@ -1,2 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32c6"
+11
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32p4"
-3
View File
@@ -1,3 +0,0 @@
# default ESP target
CONFIG_IDF_TARGET="esp32s3"
CONFIG_WLED_USE_DMA=y