testing thread

- remove BLE
- remove MQTT

Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
2026-05-08 16:58:59 +02:00
parent 4dfd881efc
commit d8386e18de
43 changed files with 2560 additions and 1500 deletions
+39
View File
@@ -0,0 +1,39 @@
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
# Rolle: Senior Software Architect & Developer
Du agierst ab sofort als erfahrener Senior Software Developer und Architekt. Dein primärer Fokus liegt stets auf sauberer Architektur, Wartbarkeit, Skalierbarkeit und Best Practices, bevor auch nur eine Zeile Code geschrieben wird.
## Deine Prinzipien:
1. **Denke in Systemen, nicht in Snippets:** Bevor du ein Problem löst oder Code generierst, analysiere, wie sich die Änderung in die bestehende Architektur (z. B. SvelteKit, Backend-Services) einfügt.
2. **Hinterfrage Anforderungen (Push Back):** Wenn eine vom Nutzer vorgeschlagene Lösung architektonisch unsauber ist (z. B. "Quick & Dirty" Workarounds, enge Kopplung, Verletzung von SOLID-Prinzipien), weise darauf hin und schlage eine bessere, nachhaltigere Alternative vor.
3. **Trennung von Verantwortlichkeiten (SoC):** Achte peinlich genau darauf, dass UI-Logik, State-Management und Business-Logik sauber getrennt bleiben.
4. **Zukunftssicherheit:** Schreibe Code, der auch in 6 Monaten von anderen Entwicklern noch leicht verstanden und erweitert werden kann.
5. **Erkläre das "Warum":** Wenn du Code refactorst oder vorschlägst, erkläre immer die architektonische Entscheidung dahinter (z.B. "Ich habe dies in einen eigenen Service ausgelagert, um eine zirkuläre Abhängigkeit zu vermeiden...").
## Workflow bei neuen Features:
- Skizziere zuerst kurz den architektonischen Ansatz (z.B. Datenfluss, State-Management-Strategie).
- Schreibe erst danach den eigentlichen Code.
- Berücksichtige Aspekte wie Performance, Fehlerbehandlung (Error Handling) und Edge Cases.
+342 -69
View File
@@ -10,13 +10,15 @@ This document describes all REST API endpoints and WebSocket messages required f
- [Light Control](#light-control)
- [LED Configuration](#led-configuration)
- [Schema](#schema)
- [Devices](#devices)
- [Thread Devices](#thread-devices)
- [Thread Groups](#thread-groups)
- [Scenes](#scenes)
- [Input](#input)
- [WebSocket](#websocket)
- [Connection](#connection)
- [Client to Server Messages](#client-to-server-messages)
- [Server to Client Messages](#server-to-client-messages)
- [Thread: Resource State Change (RFC 7641 Observe)](#thread-resource-state-change-rfc-7641-observe)
---
@@ -384,158 +386,285 @@ Saves a schema file.
---
### Devices
### Thread Devices
#### Scan for Devices
Manages OpenThread devices (e.g. ESP32-H2 lighthouses). Devices join the Thread network automatically and announce themselves via CoAP. They can also be added manually by IPv6 address.
Scans for available Matter devices to pair.
---
- **URL:** `/api/devices/scan`
#### List Thread Devices
Returns all known Thread devices (auto-discovered via CoAP announce + manually added). Persisted in NVS.
- **URL:** `/api/thread/devices`
- **Method:** `GET`
- **Response:**
```json
[
{
"id": "matter-001",
"type": "light",
"name": "Matter Lamp"
},
{
"id": "matter-002",
"type": "sensor",
"name": "Temperature Sensor"
"name": "Leuchtturm West",
"addr": "fd12:3456:789a::1",
"has_beacon": true,
"has_outdoor": true,
"reachable": true,
"beacon_on": false,
"outdoor_on": true
}
]
```
| Field | Type | Description |
|-------|--------|-----------------------------------------------|
| id | string | Unique device identifier |
| type | string | Device type: `light`, `sensor`, `unknown` |
| name | string | Device name (can be empty) |
| Field | Type | Description |
|-------------|---------|--------------------------------------------------------------------------------|
| name | string | Device name (from announce or manually set) |
| addr | string | Mesh-local IPv6 address |
| has_beacon | boolean | Device exposes `/beacon` CoAP resource |
| has_outdoor | boolean | Device exposes `/outdoor` CoAP resource |
| reachable | boolean | True after last successful CoAP response (resets on boot) |
| beacon_on | boolean | Current beacon state — updated via CoAP Observe (RFC 7641), `false` until first notification |
| outdoor_on | boolean | Current outdoor lamp state — updated via CoAP Observe (RFC 7641), `false` until first notification |
**Notes:**
- After the C6 registers as a CoAP observer (`GET /beacon` with `Observe: 0`), the H2 pushes a `thread_state` WebSocket event on every state change — no polling needed.
- `beacon_on` / `outdoor_on` start as `false` on boot and are set to the actual device state on the first observer notification, which arrives within seconds of the Observe registration.
- Observe registrations survive normal device reboots: when the H2 re-announces after a reboot, the C6 re-registers automatically.
---
#### Pair Device
#### Add Thread Device
Pairs a discovered device.
Manually registers a Thread device by name and IPv6 address. Capabilities are discovered asynchronously via CoAP and arrive as a `thread_capabilities` WebSocket event. Idempotent: if `addr` already exists, the name is updated.
- **URL:** `/api/devices/pair`
- **URL:** `/api/thread/devices`
- **Method:** `POST`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"id": "matter-001",
"name": "Living Room Lamp"
"name": "Leuchtturm Ost",
"addr": "fd12:3456:789a::2"
}
```
| Field | Type | Required | Description |
|-------|--------|----------|------------------------------|
| id | string | Yes | Device ID from scan |
| name | string | Yes | User-defined device name |
| Field | Type | Required | Description |
|-------|--------|----------|-------------------------|
| name | string | Yes | Human-readable label |
| addr | string | Yes | Mesh-local IPv6 address |
- **Response:** `200 OK` on success
- **Response:** `{"ok":true}` on success, `507` if device limit (16) reached
---
#### Get Paired Devices
#### Remove Thread Device
Returns list of all paired devices.
Removes a device from NVS. The device is also removed from all groups and CoAP group-leave messages are sent.
- **URL:** `/api/devices/paired`
- **Method:** `GET`
- **Response:**
- **URL:** `/api/thread/devices`
- **Method:** `DELETE`
- **Content-Type:** `application/json`
- **Request Body:**
```json
[
{
"id": "matter-001",
"type": "light",
"name": "Living Room Lamp"
}
]
{
"addr": "fd12:3456:789a::2"
}
```
| Field | Type | Description |
|-------|--------|-------------------------------------------|
| id | string | Unique device identifier |
| type | string | Device type: `light`, `sensor`, `unknown` |
| name | string | User-defined device name |
| Field | Type | Required | Description |
|-------|--------|----------|-------------------------|
| addr | string | Yes | Mesh-local IPv6 address |
- **Response:** `{"ok":true}` on success, `404` if not found
---
#### Update Device Name
#### Control Device Resource
Updates the name of a paired device.
Sends a CoAP PUT to a device resource. Fire-and-forget.
- **URL:** `/api/devices/update`
- **URL:** `/api/thread/devices/set`
- **Method:** `POST`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"id": "matter-001",
"name": "New Device Name"
"addr": "fd12:3456:789a::1",
"resource": "beacon",
"on": true
}
```
| Field | Type | Required | Description |
|----------|---------|----------|--------------------------------------|
| addr | string | Yes | Target device IPv6 address |
| resource | string | Yes | `"beacon"` or `"outdoor"` |
| on | boolean | Yes | `true` = on, `false` = off |
- **Response:** `{"ok":true}` on success
---
### Thread Groups
Groups use IPv6 multicast addresses (`ff03::/16` mesh-local scope). Devices subscribed to a group respond to CoAP commands sent to the multicast address.
Suggested multicast addresses:
- `ff03::10` — All outdoor lamps
- `ff03::11` — All beacons
- `ff03::20` — All devices (harbour scene)
---
#### List Groups
Returns all groups with their members.
- **URL:** `/api/thread/groups`
- **Method:** `GET`
- **Response:**
```json
[
{
"name": "Alle Aussenlampen",
"addr": "ff03::10",
"members": ["fd12:3456:789a::1", "fd12:3456:789a::2"]
}
]
```
| Field | Type | Description |
|---------|--------|------------------------------------------|
| name | string | Group label |
| addr | string | IPv6 multicast address |
| members | array | List of member device IPv6 addresses |
---
#### Create Group
Creates a new group. The multicast address must be unique.
- **URL:** `/api/thread/groups`
- **Method:** `POST`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"name": "Alle Aussenlampen",
"addr": "ff03::10"
}
```
| Field | Type | Required | Description |
|-------|--------|----------|-------------------------|
| name | string | Yes | Group label |
| addr | string | Yes | IPv6 multicast address |
- **Response:** `{"ok":true}` on success, `409` if address already in use, `507` if group limit (8) reached
---
#### Delete Group
Removes a group. CoAP group-leave messages are sent to all current members.
- **URL:** `/api/thread/groups`
- **Method:** `DELETE`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"addr": "ff03::10"
}
```
| Field | Type | Required | Description |
|-------|--------|----------|------------------------|
| id | string | Yes | Device ID |
| name | string | Yes | New device name |
| addr | string | Yes | Group multicast address |
- **Response:** `200 OK` on success
- **Response:** `{"ok":true}` on success, `404` if not found
---
#### Unpair Device
#### Assign Device to Group
Removes a paired device.
Adds a device to a group. The device receives a CoAP PUT `/group` with payload `"ff03::10,1"` to subscribe to the multicast address. Idempotent.
- **URL:** `/api/devices/unpair`
- **URL:** `/api/thread/groups/assign`
- **Method:** `POST`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"id": "matter-001"
"device_addr": "fd12:3456:789a::1",
"group_addr": "ff03::10"
}
```
| Field | Type | Required | Description |
|-------|--------|----------|---------------|
| id | string | Yes | Device ID |
| Field | Type | Required | Description |
|-------------|--------|----------|--------------------------|
| device_addr | string | Yes | Device IPv6 address |
| group_addr | string | Yes | Group multicast address |
- **Response:** `200 OK` on success
- **Response:** `{"ok":true}` on success, `404` if group not found, `507` if member limit (8) reached
---
#### Toggle Device
#### Remove Device from Group
Toggles a device (e.g., light on/off).
Removes a device from a group. The device receives a CoAP PUT `/group` with payload `"ff03::10,0"` to unsubscribe.
- **URL:** `/api/devices/toggle`
- **URL:** `/api/thread/groups/assign`
- **Method:** `DELETE`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"device_addr": "fd12:3456:789a::1",
"group_addr": "ff03::10"
}
```
| Field | Type | Required | Description |
|-------------|--------|----------|--------------------------|
| device_addr | string | Yes | Device IPv6 address |
| group_addr | string | Yes | Group multicast address |
- **Response:** `{"ok":true}` on success, `404` if group or member not found
---
#### Send Group Command
Sends a CoAP PUT to a multicast address. All subscribed devices respond. Fire-and-forget.
- **URL:** `/api/thread/groups/command`
- **Method:** `POST`
- **Content-Type:** `application/json`
- **Request Body:**
```json
{
"id": "matter-001"
"addr": "ff03::10",
"resource": "outdoor",
"on": false
}
```
| Field | Type | Required | Description |
|-------|--------|----------|---------------|
| id | string | Yes | Device ID |
| Field | Type | Required | Description |
|----------|---------|----------|--------------------------------------|
| addr | string | Yes | Group multicast address |
| resource | string | Yes | `"beacon"` or `"outdoor"` |
| on | boolean | Yes | `true` = on, `false` = off |
- **Response:** `200 OK` on success
- **Response:** `{"ok":true}` on success
---
@@ -804,6 +933,150 @@ Sent when the current color changes (during simulation).
---
#### Thread: Resource State Change (RFC 7641 Observe)
Sent whenever a device's `/beacon` or `/outdoor` state changes. Fired by the C6 after receiving a CoAP Observe notification (CON 2.05 Content) from the H2.
```json
{
"type": "thread_state",
"addr": "fd12:3456:789a::1",
"resource": "beacon",
"on": true
}
```
| Field | Type | Description |
|----------|---------|---------------------------------------|
| type | string | Always `"thread_state"` |
| addr | string | Device IPv6 address |
| resource | string | `"beacon"` or `"outdoor"` |
| on | boolean | New resource state |
**Notes:**
- Also fires once when the Observe subscription is first established (initial state).
- The first notification after `GET /api/thread/devices` or a new device announcement reflects the actual current hardware state.
- When a group command (multicast PUT) is issued, each device sends an individual `thread_state` event after switching — the number of events received is a confirmation count for the broadcast.
---
#### Thread: Device Announced
Sent when a Thread device sends a CoAP `/announce` (on network join) or when one is added manually.
```json
{
"type": "thread_device",
"name": "Leuchtturm West",
"addr": "fd12:3456:789a::1"
}
```
---
#### Thread: Device Removed
Sent when a device is deleted via the REST API.
```json
{
"type": "thread_device_removed",
"addr": "fd12:3456:789a::1"
}
```
---
#### Thread: Capabilities Discovered
Sent after a successful CoAP `/.well-known/core` query (triggered automatically on announce, manual add, or when a device comes back online after being unreachable). Also serves as the **reachable-again** signal after a `thread_unreachable` event.
```json
{
"type": "thread_capabilities",
"addr": "fd12:3456:789a::1",
"beacon": true,
"outdoor": true
}
```
---
#### Thread: Device Unreachable
Sent when CoAP Observe times out for a device — i.e. the device stopped sending notifications and OpenThread gave up retransmitting. Emitted at most once per outage regardless of how many resources (beacon, outdoor) the device had registered observers for.
```json
{
"type": "thread_unreachable",
"addr": "fd12:3456:789a::1"
}
```
| Field | Type | Description |
| ----- | ------ | ---------------------------------- |
| type | string | Always `"thread_unreachable"` |
| addr | string | IPv6 address of the offline device |
The next `thread_capabilities` event for the same `addr` signals that the device is reachable again. At that point CoAP Observe is automatically re-registered.
---
#### Thread: Group Added
Sent when a group is created.
```json
{
"type": "thread_group_added",
"name": "Alle Aussenlampen",
"addr": "ff03::10"
}
```
---
#### Thread: Group Removed
Sent when a group is deleted.
```json
{
"type": "thread_group_removed",
"addr": "ff03::10"
}
```
---
#### Thread: Device Assigned to Group
Sent when a device is added to a group.
```json
{
"type": "thread_group_assigned",
"device": "fd12:3456:789a::1",
"group": "ff03::10"
}
```
---
#### Thread: Device Removed from Group
Sent when a device is removed from a group.
```json
{
"type": "thread_group_unassigned",
"device": "fd12:3456:789a::1",
"group": "ff03::10"
}
```
---
#### WiFi Status Update
Sent when WiFi connection status changes.
+459
View File
@@ -0,0 +1,459 @@
# Thread Integration — ESP32-C6 Coordinator
This document describes how the ESP32-C6 system-control unit forms and manages a
Thread network, discovers and controls ESP32-H2 devices (lighthouses), and exposes
the full device lifecycle via REST API and WebSocket.
For the **H2 device perspective** (Joiner, CoAP server, Observe server) see
`README-THREAD.md` in the `warnemuende-lighthouses/firmware` repository.
---
## 1 — Architecture
```
ESP32-C6 (system-control)
├── WiFi ──────────────────── Web UI / REST API / WebSocket
└── Thread / IEEE 802.15.4
├── Role: Leader (preferred) or Router
├── Commissioner: accepts Joiners with PSKd MAERKLN
├── CoAP server: listens for /announce
└── CoAP client: controls H2 devices via unicast + multicast
└── ESP32-H2 (Joiner / Full Thread Device)
CoAP server: /beacon /outdoor /group
CoAP Observe server: /beacon /outdoor (RFC 7641)
```
The C6 is the **only** Commissioner on the network. H2 devices never form a network
themselves — they always join as Children or Routers with minimal leader priority.
---
## 2 — Thread network formation
On first boot (no stored dataset) the C6 calls `otDatasetCreateNewNetwork` to create
a random Thread network with a fresh Mesh-Local Prefix. The dataset is stored inside
OpenThread's own NVS partition and survives reboots.
On subsequent boots the C6 resumes the same network (`otDatasetIsCommissioned`
returns true). The Mesh-Local Prefix therefore remains **the same across reboots**,
which keeps all H2 device addresses stable (see section 7).
The C6 tries to become **Leader** (highest router priority). Once it holds the Leader
role it starts the Commissioner automatically.
---
## 3 — Commissioner
The C6 starts a wildcard Joiner entry as soon as it becomes Leader:
```c
otCommissionerStart(instance, NULL, NULL, NULL);
otCommissionerAddJoiner(instance, NULL, "MAERKLN", 120);
// ^^^^ NULL = any EUI-64
```
| Parameter | Value |
| --------- | ------------------------------------ |
| PSKd | `MAERKLN` |
| Timeout | 120 s (restarted on every role gain) |
| Vendor | Maerklin / Lighthouse / 1.0 |
A new H2 that has no stored dataset starts the Joiner role on boot, discovers the
Commissioner via CoAP, performs EC-JPAKE (DTLS + mbedTLS J-PAKE), receives the
dataset and joins. After that, the H2 keeps its dataset through reboots and never
needs to re-commission.
---
## 4 — Device discovery
After joining the network the H2 sends a single **NON-CONFIRMABLE CoAP POST**:
```
Destination : coap://[ff03::1]:5683/announce
Payload : <device-name> e.g. "Leuchtturm West"
```
The C6's CoAP server listens on `/announce` and records:
- **Source IPv6 address** → used for all subsequent unicast CoAP
- **Device name** → stored in NVS, shown in API and web UI
If the C6 restarts, it must wait for the H2 to re-announce (which happens on H2
reboot) or the user can manually add a device via `POST /api/thread/devices`.
---
## 5 — Capability discovery
Immediately after recording an announce, the C6 sends:
```
GET coap://[<H2-addr>]:5683/.well-known/core
```
Expected response (RFC 6690 link-format):
```
</beacon>;rt="maerklin.switch";title="Beacon";obs,
</outdoor>;rt="maerklin.switch";title="Outdoor";obs,
</group>;rt="maerklin.group";title="Group"
```
The C6 parses this to set `has_beacon` and `has_outdoor` on the device entry.
The `obs` attribute (RFC 7641) signals that the resource supports CoAP Observe —
the C6 immediately registers as an observer on each `obs`-flagged resource
(see section 6).
Capabilities are **persisted in NVS** so the web UI knows which controls to show
even after a C6 reboot (before the H2 re-announces).
---
## 6 — CoAP Observe (RFC 7641)
After capabilities are discovered, the C6 registers as a CoAP observer on every
resource that carries the `obs` attribute (`/beacon` and `/outdoor`).
### Why
- CoAP Observe gives the C6 real-time state without polling.
- After a **multicast group command** (which cannot carry individual ACKs), each
device sends a CON Observe notification back to the C6. The C6's ACK is proof of
delivery — a device that does not send a notification (or does not ACK the C6's
cancel) failed to execute the command.
### Registration
The C6 sends a **CONFIRMABLE GET** with the `Observe: 0` option:
```
GET coap://[<H2-addr>]:5683/beacon
Observe: 0
Token: <8-byte random token>
```
The H2 responds with the current resource state and an Observe sequence number.
The same callback receives all subsequent **CON 2.05 Content** notifications from
the H2:
```c
// observe_notification_cb — called for initial response AND every notification
if (result != OT_ERROR_NONE) {
// mark device unreachable (only on first error to avoid duplicate events)
if (dev->reachable) {
dev->reachable = false;
emit("{\"type\":\"thread_unreachable\",\"addr\":\"...\"}");
}
free(ctx); // observation ended (timeout, RST, cancel)
return;
}
bool is_on = (payload[0] == '1');
// → update device state, emit "thread_state" WebSocket event
```
OpenThread ACKs incoming CON messages automatically.
### Cancellation
When a device is **removed via the API** while an observation is active, the next
incoming notification triggers an automatic deregister:
```c
send_observe_cancel(instance, notification_msg, msg_info, resource);
// sends: GET /beacon Observe: 1 Token: <same as registration>
free(ctx);
```
This frees the observer slot on the H2 (`MAX_OBSERVERS = 4` per resource).
### Re-registration after C6 reboot
On reboot, the C6 loads all devices from NVS. When it becomes Leader or Router
(`state_changed_cb`), `register_all_observers` re-sends Observe registrations for
every device that has known capabilities. The initial response brings the current
hardware state.
### Notification flow
```
C6 reboots
└─ loads devices from NVS
└─ becomes Leader
└─ register_all_observers()
└─ GET /beacon Observe:0 ──► H2
◄────────────────────── 2.05 Content (current state)
observe_notification_cb fires
→ beacon_on = true/false
→ WS event "thread_state"
H2 changes state (e.g. user presses button on H2)
└─ H2 sends CON 2.05 to all observers
└─ observe_notification_cb fires
→ beacon_on updated
→ WS event "thread_state"
```
---
## 7 — Reachability monitoring
The C6 tracks each device's reachability without polling or dedicated ping commands.
### Detecting unreachable
When a device goes offline, incoming CoAP Observe notifications stop. OpenThread
eventually calls `observe_notification_cb` with `result != OT_ERROR_NONE` (timeout
or RST). At that point:
1. `dev->reachable` is set to `false`
2. A `thread_unreachable` WebSocket event is emitted (only on the first error —
devices with both `beacon` and `outdoor` observers do not emit duplicates)
3. The dead observe context is freed
The detection latency depends on the CoAP retransmit timeout (~45 s by default).
### Detecting reachable again
The C6 registers a `otThreadRegisterNeighborTableCallback`. When any neighbor
joins the Thread network (`OT_NEIGHBOR_TABLE_EVENT_CHILD_ADDED` or
`ROUTER_ADDED`), the callback:
1. Snapshots all devices with `reachable == false`
2. Sends a `.well-known/core` CoAP query to each of them
If a device responds, `caps_response_cb` fires:
- `dev->reachable` is set to `true`
- CoAP Observe is re-registered for `beacon` and `outdoor`
- A `thread_capabilities` WebSocket event is emitted
The client therefore uses `thread_unreachable` as the "gone" signal and
`thread_capabilities` as the "back" signal.
### Scenario overview
| Event | Mechanism | WS event |
| ----- | --------- | -------- |
| Device powers off / RF loss | Observe timeout → `observe_notification_cb` error | `thread_unreachable` |
| Device reboots | H2 sends `/announce` → caps query | `thread_capabilities` |
| Device rejoins without reboot | Neighbor table `ADDED` → caps query to all unreachable | `thread_capabilities` |
| C6 reboots, device was already in mesh | `register_all_observers()` on Leader role → initial Observe response | `thread_state` (not `thread_capabilities`) |
---
## 8 — IPv6 address stability
The C6 stores each H2's **Mesh-Local EID** (e.g. `fd12:3456:789a::eui64`).
This address has two components:
```
fd12:3456:789a:0000 : XX:XX:XX:ff:fe:XX:XX:XX
└── Mesh-Local Prefix ┘ └── IID from EUI-64 ────┘
from Thread dataset hardware identifier
```
| Component | Origin | Changes? |
| --------- | ------ | -------- |
| Mesh-Local Prefix | Thread dataset on C6 | Only on factory reset (new network) |
| Interface Identifier | H2's EUI-64 (802.15.4 chip, fixed) | Never |
**Conclusion:** under normal operation (reboots of C6 or H2 without factory reset)
the address is **the same every time**. The NVS device list remains valid across
power cycles.
The only scenario that invalidates stored addresses is a **C6 factory reset** (new
Thread network → new Mesh-Local Prefix). Since `persistence_manager_factory_reset()`
erases all NVS, the device list is wiped at the same time — the C6 never holds stale
addresses.
> Note: the **RLOC** (`fd..::ff:fe00:xxxx`) changes after every reboot and is not
> used for application-level CoAP communication.
---
## 9 — Device control
All unicast control uses **NON-CONFIRMABLE CoAP PUT** (fire-and-forget). The HTTP
handler acquires the OpenThread API lock (`esp_openthread_lock_acquire`) to call OT
APIs directly, then releases it immediately after `otCoapSendRequest`:
```
PUT coap://[<H2-addr>]:5683/beacon
Payload: "1" (on)
"0" (off)
```
```
PUT coap://[<H2-addr>]:5683/outdoor
Payload: "1" / "0"
```
REST: `POST /api/thread/devices/set`
---
## 10 — Group management
Groups use **IPv6 multicast** in the mesh-local scope `ff03::/16`. The C6 manages
group membership by telling each H2 which multicast address to subscribe to.
### Joining a group
```
PUT coap://[<H2-addr>]:5683/group
Payload: "ff03::10,1" ← <multicast-addr>,1
```
### Leaving a group
```
PUT coap://[<H2-addr>]:5683/group
Payload: "ff03::10,0" ← <multicast-addr>,0
```
### Sending a group command
```
PUT coap://[ff03::10]:5683/outdoor
Payload: "1"
```
All H2 devices subscribed to `ff03::10` receive this simultaneously.
After switching, each device sends an individual Observe notification back to
the C6 — the C6 collects these as confirmation that every member executed the
command.
### Recommended multicast addresses
| Address | Suggested use |
| ---------- | ----------------------- |
| `ff03::10` | All outdoor lamps |
| `ff03::11` | All beacons |
| `ff03::20` | Scene: harbour lights |
### Persistence
Groups (name, multicast address, member list) are stored in NVS namespace
`thread_mgr` key `groups`. Group membership on the H2 is also persisted on the H2
itself — after H2 reboot the device rejoins its multicast subscriptions automatically.
On `DELETE /api/thread/groups` the C6 sends CoAP group-leave to every member before
deleting the local record. On `DELETE /api/thread/devices` the C6 removes the device
from all groups first.
---
## 11 — NVS layout
All Thread-manager state lives in the `thread_mgr` namespace:
| Key | Type | Content |
| --------- | ---- | ------- |
| `devices` | blob | `thread_device_t[]` — name, IPv6, has\_beacon, has\_outdoor |
| `groups` | blob | `thread_group_t[]` — name, multicast addr, member addr list |
`reachable`, `beacon_on`, `outdoor_on` are runtime-only and not persisted
(refreshed via Observe on next network join).
Blob size encodes the element count: `count = blob_size / sizeof(element)`.
---
## 12 — Limits
| Parameter | Value | Defined in |
| --------- | ----- | ---------- |
| Max tracked devices | 16 | `THREAD_MAX_DEVICES` |
| Max groups | 8 | `THREAD_MAX_GROUPS` |
| Max devices per group | 8 | `THREAD_GROUP_MAX_MEMBERS` |
| Max Observe observers per resource (H2-side) | 4 | `MAX_OBSERVERS` in lighthouse firmware |
| Commissioner timeout | 120 s | `COMMISSIONER_TIMEOUT_S` |
| Joiner PSKd | `MAERKLN` | `COMMISSIONER_PSK` |
---
## 13 — REST API quick reference
| Method | URL | Action |
| ------ | --- | ------ |
| `GET` | `/api/thread/devices` | List all devices (name, addr, state) |
| `POST` | `/api/thread/devices` | Add device manually `{name, addr}` |
| `DELETE` | `/api/thread/devices` | Remove device `{addr}` |
| `POST` | `/api/thread/devices/set` | Control resource `{addr, resource, on}` |
| `GET` | `/api/thread/groups` | List all groups with members |
| `POST` | `/api/thread/groups` | Create group `{name, addr}` |
| `DELETE` | `/api/thread/groups` | Delete group `{addr}` |
| `POST` | `/api/thread/groups/assign` | Add device to group `{device_addr, group_addr}` |
| `DELETE` | `/api/thread/groups/assign` | Remove device from group |
| `POST` | `/api/thread/groups/command` | Group command `{addr, resource, on}` |
Full request/response schemas: `README-API.md` → sections *Thread Devices* and
*Thread Groups*.
---
## 14 — WebSocket events
| Type | Trigger |
| ---- | ------- |
| `thread_device` | H2 announced (or manually added) |
| `thread_device_removed` | Device deleted via API |
| `thread_capabilities` | `/.well-known/core` query completed — also signals device is **reachable again** |
| `thread_state` | CoAP Observe notification received (RFC 7641) |
| `thread_unreachable` | CoAP Observe timed out / device went offline |
| `thread_group_added` | Group created |
| `thread_group_removed` | Group deleted |
| `thread_group_assigned` | Device added to group |
| `thread_group_unassigned` | Device removed from group |
Full event schemas: `README-API.md` → section *WebSocket*.
---
## 15 — Lifecycle summary
```
C6 boot
├─ load devices + groups from NVS
├─ start Thread stack (thread_task)
│ ├─ resume existing dataset (or create new network)
│ ├─ become Leader → start Commissioner (PSKd MAERKLN)
│ └─ register_all_observers() → GET /beacon + /outdoor for all known devices
├─ H2 joins (commissioning via EC-JPAKE)
│ └─ H2 sends announce → C6 records addr + name
│ └─ C6 queries /.well-known/core
│ └─ capabilities stored → Observe registered for /beacon + /outdoor
├─ H2 state changes
│ └─ H2 sends CON Observe notification → C6 ACKs → updates beacon_on/outdoor_on
│ └─ WS event "thread_state" broadcast to web clients
├─ H2 goes offline (power loss / RF)
│ └─ Observe notifications stop → timeout → observe_notification_cb error
│ └─ reachable = false → WS event "thread_unreachable"
├─ H2 comes back (reboot)
│ └─ H2 sends /announce → caps query → reachable = true + Observe re-registered
│ └─ WS event "thread_capabilities"
├─ H2 comes back (rejoin without reboot)
│ └─ neighbor_table_cb CHILD_ADDED → caps query to all unreachable devices
│ └─ H2 responds → reachable = true + Observe re-registered
│ └─ WS event "thread_capabilities"
├─ Web client: POST /api/thread/devices/set
│ └─ NON-CONF CoAP PUT (OT lock held) → H2 → switches → Observe notification → WS event
├─ Web client: POST /api/thread/groups/command
│ └─ NON-CONF CoAP PUT to ff03::10 → all members switch
│ └─ each member sends individual Observe notification → WS events
└─ C6 factory reset
├─ persistence_manager_factory_reset() → NVS erased (devices + groups cleared)
└─ Thread dataset erased → new network on next boot → new Mesh-Local Prefix
```
+1 -1
View File
@@ -20,6 +20,6 @@ idf_component_register(SRCS
simulator
persistence-manager
message-manager
my_mqtt_client
heimdall
connectivity-manager
)
@@ -38,13 +38,19 @@ extern "C"
esp_err_t api_schema_get_handler(httpd_req_t *req);
esp_err_t api_schema_post_handler(httpd_req_t *req);
// Devices API (Matter)
esp_err_t api_devices_scan_handler(httpd_req_t *req);
esp_err_t api_devices_pair_handler(httpd_req_t *req);
esp_err_t api_devices_paired_handler(httpd_req_t *req);
esp_err_t api_devices_update_handler(httpd_req_t *req);
esp_err_t api_devices_unpair_handler(httpd_req_t *req);
esp_err_t api_devices_toggle_handler(httpd_req_t *req);
// Thread Devices API
esp_err_t api_thread_devices_get_handler(httpd_req_t *req);
esp_err_t api_thread_devices_add_handler(httpd_req_t *req);
esp_err_t api_thread_devices_delete_handler(httpd_req_t *req);
esp_err_t api_thread_devices_set_handler(httpd_req_t *req);
// Thread Groups API
esp_err_t api_thread_groups_get_handler(httpd_req_t *req);
esp_err_t api_thread_groups_add_handler(httpd_req_t *req);
esp_err_t api_thread_groups_delete_handler(httpd_req_t *req);
esp_err_t api_thread_groups_assign_handler(httpd_req_t *req);
esp_err_t api_thread_groups_unassign_handler(httpd_req_t *req);
esp_err_t api_thread_groups_command_handler(httpd_req_t *req);
// Scenes API
esp_err_t api_scenes_get_handler(httpd_req_t *req);
+44 -17
View File
@@ -124,38 +124,65 @@ esp_err_t api_handlers_register(httpd_handle_t server)
if (err != ESP_OK)
return err;
// Devices endpoints
httpd_uri_t devices_scan = {.uri = "/api/devices/scan", .method = HTTP_GET, .handler = api_devices_scan_handler};
err = httpd_register_uri_handler(server, &devices_scan);
// Thread device endpoints
httpd_uri_t thread_devices_get = {
.uri = "/api/thread/devices", .method = HTTP_GET, .handler = api_thread_devices_get_handler};
err = httpd_register_uri_handler(server, &thread_devices_get);
if (err != ESP_OK)
return err;
httpd_uri_t devices_pair = {.uri = "/api/devices/pair", .method = HTTP_POST, .handler = api_devices_pair_handler};
err = httpd_register_uri_handler(server, &devices_pair);
httpd_uri_t thread_devices_add = {
.uri = "/api/thread/devices", .method = HTTP_POST, .handler = api_thread_devices_add_handler};
err = httpd_register_uri_handler(server, &thread_devices_add);
if (err != ESP_OK)
return err;
httpd_uri_t devices_paired = {
.uri = "/api/devices/paired", .method = HTTP_GET, .handler = api_devices_paired_handler};
err = httpd_register_uri_handler(server, &devices_paired);
httpd_uri_t thread_devices_delete = {
.uri = "/api/thread/devices", .method = HTTP_DELETE, .handler = api_thread_devices_delete_handler};
err = httpd_register_uri_handler(server, &thread_devices_delete);
if (err != ESP_OK)
return err;
httpd_uri_t devices_update = {
.uri = "/api/devices/update", .method = HTTP_POST, .handler = api_devices_update_handler};
err = httpd_register_uri_handler(server, &devices_update);
httpd_uri_t thread_devices_set = {
.uri = "/api/thread/devices/set", .method = HTTP_POST, .handler = api_thread_devices_set_handler};
err = httpd_register_uri_handler(server, &thread_devices_set);
if (err != ESP_OK)
return err;
httpd_uri_t devices_unpair = {
.uri = "/api/devices/unpair", .method = HTTP_POST, .handler = api_devices_unpair_handler};
err = httpd_register_uri_handler(server, &devices_unpair);
// Thread group endpoints
httpd_uri_t thread_groups_get = {
.uri = "/api/thread/groups", .method = HTTP_GET, .handler = api_thread_groups_get_handler};
err = httpd_register_uri_handler(server, &thread_groups_get);
if (err != ESP_OK)
return err;
httpd_uri_t devices_toggle = {
.uri = "/api/devices/toggle", .method = HTTP_POST, .handler = api_devices_toggle_handler};
err = httpd_register_uri_handler(server, &devices_toggle);
httpd_uri_t thread_groups_add = {
.uri = "/api/thread/groups", .method = HTTP_POST, .handler = api_thread_groups_add_handler};
err = httpd_register_uri_handler(server, &thread_groups_add);
if (err != ESP_OK)
return err;
httpd_uri_t thread_groups_delete = {
.uri = "/api/thread/groups", .method = HTTP_DELETE, .handler = api_thread_groups_delete_handler};
err = httpd_register_uri_handler(server, &thread_groups_delete);
if (err != ESP_OK)
return err;
httpd_uri_t thread_groups_assign = {
.uri = "/api/thread/groups/assign", .method = HTTP_POST, .handler = api_thread_groups_assign_handler};
err = httpd_register_uri_handler(server, &thread_groups_assign);
if (err != ESP_OK)
return err;
httpd_uri_t thread_groups_unassign = {
.uri = "/api/thread/groups/assign", .method = HTTP_DELETE, .handler = api_thread_groups_unassign_handler};
err = httpd_register_uri_handler(server, &thread_groups_unassign);
if (err != ESP_OK)
return err;
httpd_uri_t thread_groups_command = {
.uri = "/api/thread/groups/command", .method = HTTP_POST, .handler = api_thread_groups_command_handler};
err = httpd_register_uri_handler(server, &thread_groups_command);
if (err != ESP_OK)
return err;
@@ -1,117 +1,350 @@
#include "bifrost/api_handlers.h"
#include "bifrost/api_handlers_util.h"
#include "thread_manager.h"
#include <esp_heap_caps.h>
#include <cJSON.h>
#include <esp_log.h>
#include <string.h>
static const char *TAG = "api_devices";
static const char *TAG = "api_thread";
// ============================================================================
// Devices API (Matter)
// Thread Devices API
// ============================================================================
esp_err_t api_devices_scan_handler(httpd_req_t *req)
esp_err_t api_thread_devices_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/devices/scan");
ESP_LOGI(TAG, "GET /api/thread/devices");
// TODO: Implement Matter device scanning
const char *response = "["
"{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Matter Lamp\"},"
"{\"id\":\"matter-002\",\"type\":\"sensor\",\"name\":\"Temperature Sensor\"}"
"]";
return send_json_response(req, response);
}
size_t count;
const thread_device_t *devs = thread_manager_get_devices(&count);
esp_err_t api_devices_pair_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/pair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
cJSON *arr = cJSON_CreateArray();
for (size_t i = 0; i < count; ++i)
{
return send_error_response(req, 400, "Failed to receive request body");
cJSON *obj = cJSON_CreateObject();
cJSON_AddStringToObject(obj, "name", devs[i].name);
cJSON_AddStringToObject(obj, "addr", devs[i].ipv6_addr);
cJSON_AddBoolToObject(obj, "has_beacon", devs[i].has_beacon);
cJSON_AddBoolToObject(obj, "has_outdoor", devs[i].has_outdoor);
cJSON_AddBoolToObject(obj, "reachable", devs[i].reachable);
cJSON_AddBoolToObject(obj, "beacon_on", devs[i].beacon_on);
cJSON_AddBoolToObject(obj, "outdoor_on", devs[i].outdoor_on);
cJSON_AddItemToArray(arr, obj);
}
buf[ret] = '\0';
ESP_LOGI(TAG, "Pairing device: %s", buf);
char *json = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!json)
return send_error_response(req, 500, "Out of memory");
// TODO: Implement Matter device pairing
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
esp_err_t err = send_json_response(req, json);
free(json);
return err;
}
esp_err_t api_devices_paired_handler(httpd_req_t *req)
esp_err_t api_thread_devices_add_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/devices/paired");
ESP_LOGI(TAG, "POST /api/thread/devices");
// TODO: Get list of paired devices
const char *response = "["
"{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Living Room Lamp\"}"
"]";
return send_json_response(req, response);
}
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
esp_err_t api_devices_update_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/update");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
const cJSON *jname = cJSON_GetObjectItem(json, "name");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
if (!is_valid(jname) || !is_valid(jaddr))
{
return send_error_response(req, 400, "Failed to receive request body");
cJSON_Delete(json);
return send_error_response(req, 400, "Missing name or addr");
}
buf[ret] = '\0';
ESP_LOGI(TAG, "Updating device: %s", buf);
esp_err_t err = thread_manager_add_device(jname->valuestring, jaddr->valuestring);
cJSON_Delete(json);
// TODO: Update device name
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
if (err == ESP_ERR_NO_MEM)
return send_error_response(req, 507, "Device limit reached");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to add device");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_devices_unpair_handler(httpd_req_t *req)
esp_err_t api_thread_devices_delete_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/unpair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
ESP_LOGI(TAG, "DELETE /api/thread/devices");
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
char body[128];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
if (!is_valid(jaddr))
{
return send_error_response(req, 400, "Failed to receive request body");
cJSON_Delete(json);
return send_error_response(req, 400, "Missing addr");
}
buf[ret] = '\0';
ESP_LOGI(TAG, "Unpairing device: %s", buf);
esp_err_t err = thread_manager_remove_device(jaddr->valuestring);
cJSON_Delete(json);
// TODO: Unpair device
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
if (err == ESP_ERR_NOT_FOUND)
return send_error_response(req, 404, "Device not found");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to remove device");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_devices_toggle_handler(httpd_req_t *req)
esp_err_t api_thread_devices_set_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/devices/toggle");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
ESP_LOGI(TAG, "POST /api/thread/devices/set");
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
const cJSON *jresource = cJSON_GetObjectItem(json, "resource");
const cJSON *jon = cJSON_GetObjectItem(json, "on");
if (!is_valid(jaddr) || !is_valid(jresource) || !cJSON_IsBool(jon))
{
return send_error_response(req, 400, "Failed to receive request body");
cJSON_Delete(json);
return send_error_response(req, 400, "Missing addr, resource or on");
}
buf[ret] = '\0';
ESP_LOGI(TAG, "Toggling device: %s", buf);
esp_err_t err = thread_manager_set_resource(jaddr->valuestring, jresource->valuestring,
cJSON_IsTrue(jon));
cJSON_Delete(json);
// TODO: Toggle device
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
return (err == ESP_OK) ? send_json_response(req, "{\"ok\":true}")
: send_error_response(req, 500, "Failed to send command");
}
// ============================================================================
// Thread Groups API
// ============================================================================
esp_err_t api_thread_groups_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/thread/groups");
size_t count;
const thread_group_t *groups = thread_manager_get_groups(&count);
cJSON *arr = cJSON_CreateArray();
for (size_t i = 0; i < count; ++i)
{
cJSON *obj = cJSON_CreateObject();
cJSON_AddStringToObject(obj, "name", groups[i].name);
cJSON_AddStringToObject(obj, "addr", groups[i].multicast_addr);
cJSON *members = cJSON_CreateArray();
for (uint8_t m = 0; m < groups[i].member_count; ++m)
cJSON_AddItemToArray(members, cJSON_CreateString(groups[i].member_addrs[m]));
cJSON_AddItemToObject(obj, "members", members);
cJSON_AddItemToArray(arr, obj);
}
char *json = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!json)
return send_error_response(req, 500, "Out of memory");
esp_err_t err = send_json_response(req, json);
free(json);
return err;
}
esp_err_t api_thread_groups_add_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/thread/groups");
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jname = cJSON_GetObjectItem(json, "name");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
if (!is_valid(jname) || !is_valid(jaddr))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing name or addr");
}
esp_err_t err = thread_manager_add_group(jname->valuestring, jaddr->valuestring);
cJSON_Delete(json);
if (err == ESP_ERR_INVALID_STATE)
return send_error_response(req, 409, "Group already exists");
if (err == ESP_ERR_NO_MEM)
return send_error_response(req, 507, "Group limit reached");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to add group");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_thread_groups_delete_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "DELETE /api/thread/groups");
char body[128];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
if (!is_valid(jaddr))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing addr");
}
esp_err_t err = thread_manager_delete_group(jaddr->valuestring);
cJSON_Delete(json);
if (err == ESP_ERR_NOT_FOUND)
return send_error_response(req, 404, "Group not found");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to delete group");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_thread_groups_assign_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/thread/groups/assign");
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jdevice = cJSON_GetObjectItem(json, "device_addr");
const cJSON *jgroup = cJSON_GetObjectItem(json, "group_addr");
if (!is_valid(jdevice) || !is_valid(jgroup))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing device_addr or group_addr");
}
esp_err_t err = thread_manager_assign_device(jdevice->valuestring, jgroup->valuestring);
cJSON_Delete(json);
if (err == ESP_ERR_NOT_FOUND)
return send_error_response(req, 404, "Group not found");
if (err == ESP_ERR_NO_MEM)
return send_error_response(req, 507, "Group member limit reached");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to assign device");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_thread_groups_unassign_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "DELETE /api/thread/groups/assign");
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jdevice = cJSON_GetObjectItem(json, "device_addr");
const cJSON *jgroup = cJSON_GetObjectItem(json, "group_addr");
if (!is_valid(jdevice) || !is_valid(jgroup))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing device_addr or group_addr");
}
esp_err_t err = thread_manager_unassign_device(jdevice->valuestring, jgroup->valuestring);
cJSON_Delete(json);
if (err == ESP_ERR_NOT_FOUND)
return send_error_response(req, 404, "Group or member not found");
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to unassign device");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_thread_groups_command_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/thread/groups/command");
char body[256];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
cJSON *json = cJSON_Parse(body);
if (!json)
return send_error_response(req, 400, "Invalid JSON");
const cJSON *jaddr = cJSON_GetObjectItem(json, "addr");
const cJSON *jresource = cJSON_GetObjectItem(json, "resource");
const cJSON *jon = cJSON_GetObjectItem(json, "on");
if (!is_valid(jaddr) || !is_valid(jresource) || !cJSON_IsBool(jon))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing addr, resource or on");
}
esp_err_t err = thread_manager_group_command(jaddr->valuestring, jresource->valuestring,
cJSON_IsTrue(jon));
cJSON_Delete(json);
return (err == ESP_OK) ? send_json_response(req, "{\"ok\":true}")
: send_error_response(req, 500, "Failed to send group command");
}
// ============================================================================
@@ -121,87 +354,44 @@ esp_err_t api_devices_toggle_handler(httpd_req_t *req)
esp_err_t api_scenes_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/scenes");
// TODO: Get scenes from storage
const char *response = "["
"{"
"\"id\":\"scene-1\","
"\"name\":\"Evening Mood\","
"\"icon\":\"🌅\","
"\"actions\":{"
"\"light\":\"on\","
"\"mode\":\"simulation\","
"\"schema\":\"schema_02.csv\""
"}"
"},"
"{"
"\"id\":\"scene-2\","
"\"name\":\"Night Mode\","
"\"icon\":\"🌙\","
"\"actions\":{"
"\"light\":\"on\","
"\"mode\":\"night\""
"}"
"}"
"]";
return send_json_response(req, response);
return send_json_response(req, "[]");
}
esp_err_t api_scenes_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/scenes");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[512];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
{
return send_error_response(req, 400, "Failed to receive request body");
}
buf[ret] = '\0';
char body[512];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
ESP_LOGI(TAG, "Creating/updating scene: %s", buf);
// TODO: Save scene to storage
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_scenes_delete_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "DELETE /api/scenes");
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
{
return send_error_response(req, 400, "Failed to receive request body");
}
buf[ret] = '\0';
char body[128];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
ESP_LOGI(TAG, "Deleting scene: %s", buf);
// TODO: Delete scene from storage
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
return send_json_response(req, "{\"ok\":true}");
}
esp_err_t api_scenes_activate_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/scenes/activate");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret <= 0)
{
return send_error_response(req, 400, "Failed to receive request body");
}
buf[ret] = '\0';
char body[128];
int n = httpd_req_recv(req, body, sizeof(body) - 1);
if (n <= 0)
return send_error_response(req, 400, "Empty body");
body[n] = '\0';
ESP_LOGI(TAG, "Activating scene: %s", buf);
// TODO: Activate scene
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
return send_json_response(req, "{\"ok\":true}");
}
+2 -2
View File
@@ -55,8 +55,8 @@ static esp_err_t start_webserver(void)
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = s_config.port;
config.lru_purge_enable = true;
config.max_uri_handlers = 32;
config.max_open_sockets = (CONFIG_LWIP_MAX_SOCKETS - 3);
config.max_uri_handlers = 40;
config.max_open_sockets = 5;
config.uri_match_fn = httpd_uri_match_wildcard;
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
@@ -2,7 +2,6 @@
#include "bifrost/api_server.h"
#include "bifrost/common.h"
#include "message_manager.h"
#include "my_mqtt_client.h"
#include <esp_http_server.h>
#include <esp_log.h>
@@ -268,8 +267,6 @@ esp_err_t websocket_broadcast(httpd_handle_t server, const char *message)
}
}
mqtt_publish(message);
return ret;
}
@@ -1,14 +1,13 @@
idf_component_register(SRCS
src/ble/ble_connection.c
src/ble/ble_scanner.c
src/ble_manager.c
src/dns_hijack.c
src/thread_manager.c
src/wifi_manager.c
INCLUDE_DIRS "include"
REQUIRES
bt
driver
nvs_flash
openthread
vfs
skuld
led-manager
bifrost
@@ -0,0 +1,10 @@
menu "Thread Configuration"
config THREAD_JOINER_PSK
string "Thread Joiner Pre-Shared Key (PSKd)"
default "MAERKLN"
help
Password used during Thread commissioning (6-32 uppercase alphanumeric chars,
no 0/O/I/Q). Must match the PSKd configured on the Commissioner (C6).
endmenu
@@ -1,4 +1,3 @@
dependencies:
idf:
version: '>=5.0.0'
espressif/ble_conn_mgr: '^0.1.6'
version: ">=5.4.0"
@@ -1,15 +0,0 @@
#pragma once
#include "ble_device.h"
#ifdef __cplusplus
extern "C"
{
#endif
void ble_connect(device_info_t *device);
void ble_clear_bonds(void);
void ble_clear_bond(const ble_addr_t *addr);
void read_characteristic(uint16_t char_val_handle);
#ifdef __cplusplus
}
#endif
@@ -1,24 +0,0 @@
#pragma once
#include <host/ble_hs.h>
#include <host/util/util.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <stdint.h>
// Structure to cache device data
typedef struct
{
ble_addr_t addr;
uint16_t manufacturer_id;
uint8_t manufacturer_data[31]; // Max. length of manufacturer data
uint8_t manufacturer_data_len;
char name[32];
uint16_t service_uuids_16[10]; // Up to 10 16-bit Service UUIDs
uint8_t service_uuids_16_count;
ble_uuid128_t service_uuids_128[5]; // Up to 5 128-bit Service UUIDs
uint8_t service_uuids_128_count;
bool has_manufacturer;
bool has_name;
int8_t rssi;
} device_info_t;
@@ -1,14 +0,0 @@
#pragma once
#include "ble_device.h"
#ifdef __cplusplus
extern "C"
{
#endif
void start_scan(void);
int get_device_count(void);
device_info_t *get_device(int index);
#ifdef __cplusplus
}
#endif
@@ -1,11 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
void ble_manager_task(void *pvParameter);
void ble_connect_to_device(int index);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,22 @@
#pragma once
#include "esp_openthread_types.h"
// ESP32-C6 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,62 @@
#pragma once
#include <esp_err.h>
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C"
{
#endif
#define THREAD_MAX_DEVICES 16
#define THREAD_MAX_GROUPS 8
#define THREAD_GROUP_MAX_MEMBERS 8
typedef struct
{
char name[32];
char ipv6_addr[46];
bool has_beacon;
bool has_outdoor;
bool reachable;
bool beacon_on; // current state, updated via CoAP Observe (RFC 7641)
bool outdoor_on; // current state, updated via CoAP Observe (RFC 7641)
} thread_device_t;
typedef struct
{
char name[32];
char multicast_addr[46];
char member_addrs[THREAD_GROUP_MAX_MEMBERS][46];
uint8_t member_count;
} thread_group_t;
// Callback fired on async events; json_event is valid only for the duration of the call.
typedef void (*thread_event_cb_t)(const char *json_event);
// Initialize Thread coordinator: forms network as Leader, starts Commissioner,
// and listens for CoAP /announce messages from H2 devices.
esp_err_t thread_manager_init(thread_event_cb_t event_cb);
// Device management
const thread_device_t *thread_manager_get_devices(size_t *count);
esp_err_t thread_manager_add_device(const char *name, const char *ipv6_addr);
esp_err_t thread_manager_remove_device(const char *ipv6_addr);
// Send CoAP PUT to a device. resource = "beacon" | "outdoor". Fire-and-forget.
esp_err_t thread_manager_set_resource(const char *ipv6_addr, const char *resource, bool on);
// Group management
const thread_group_t *thread_manager_get_groups(size_t *count);
esp_err_t thread_manager_add_group(const char *name, const char *multicast_addr);
esp_err_t thread_manager_delete_group(const char *multicast_addr);
esp_err_t thread_manager_assign_device(const char *device_addr, const char *multicast_addr);
esp_err_t thread_manager_unassign_device(const char *device_addr, const char *multicast_addr);
// Send CoAP PUT to a multicast address (group command). Fire-and-forget.
esp_err_t thread_manager_group_command(const char *multicast_addr, const char *resource, bool on);
#ifdef __cplusplus
}
#endif
@@ -1,364 +0,0 @@
#include "ble/ble_connection.h"
#include <esp_log.h>
#include <host/ble_gap.h>
#include <host/ble_hs.h>
#include <host/ble_sm.h>
#include <host/ble_store.h>
#include <string.h>
static const char *TAG = "ble_connection";
static uint16_t g_conn_handle;
static uint16_t g_char_val_handle; // Handle der Characteristic, die du lesen willst
static bool g_bonding_in_progress = false;
const char *ble_error_to_string(int status)
{
switch (status)
{
case 0:
return "Success";
case BLE_HS_EDONE:
return "Operation complete";
case BLE_HS_EALREADY:
return "Operation already in progress";
case BLE_HS_EINVAL:
return "Invalid argument";
case BLE_HS_EMSGSIZE:
return "Message too large";
case BLE_HS_ENOENT:
return "No entry found";
case BLE_HS_ENOMEM:
return "Out of memory";
case BLE_HS_ENOTCONN:
return "Not connected";
case BLE_HS_ENOTSUP:
return "Not supported";
case BLE_HS_EAPP:
return "Application error";
case BLE_HS_EBADDATA:
return "Bad data";
case BLE_HS_EOS:
return "OS error";
case BLE_HS_ECONTROLLER:
return "Controller error";
case BLE_HS_ETIMEOUT:
return "Timeout";
case BLE_HS_EBUSY:
return "Busy";
case BLE_HS_EREJECT:
return "Rejected";
case BLE_HS_EUNKNOWN:
return "Unknown error";
case BLE_HS_EROLE:
return "Role error";
case BLE_HS_ETIMEOUT_HCI:
return "HCI timeout";
case BLE_HS_ENOMEM_EVT:
return "No memory for event";
case BLE_HS_ENOADDR:
return "No address";
case BLE_HS_ENOTSYNCED:
return "Not synchronized";
case BLE_HS_EAUTHEN:
return "Authentication failed";
case BLE_HS_EAUTHOR:
return "Authorization failed";
case BLE_HS_EENCRYPT:
return "Encryption failed";
case BLE_HS_EENCRYPT_KEY_SZ:
return "Encryption key size";
case BLE_HS_ESTORE_CAP:
return "Storage capacity exceeded";
case BLE_HS_ESTORE_FAIL:
return "Storage failure";
default:
// ATT-Fehler prüfen
if ((status & 0x100) == 0x100)
{
return "ATT error";
}
return "Unknown error";
}
}
static void ble_sm_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type)
{
case BLE_GAP_EVENT_PASSKEY_ACTION:
ESP_LOGI(TAG, "Passkey action required");
// Hier können Sie Passkey-Aktionen implementieren
// z.B. Display passkey, Input passkey, etc.
break;
case BLE_GAP_EVENT_ENC_CHANGE:
ESP_LOGI(TAG, "Encryption change: status=%d", event->enc_change.status);
if (event->enc_change.status == 0)
{
ESP_LOGI(TAG, "Encryption established successfully");
g_bonding_in_progress = false;
}
break;
case BLE_GAP_EVENT_REPEAT_PAIRING:
ESP_LOGI(TAG, "Repeat pairing");
break;
default:
break;
}
}
static bool is_device_bonded(const ble_addr_t *addr)
{
struct ble_store_value_sec sec_value;
struct ble_store_key_sec sec_key = {0};
sec_key.peer_addr = *addr;
sec_key.idx = 0;
int rc = ble_store_read_peer_sec(&sec_key, &sec_value);
return (rc == 0);
}
static void initiate_bonding(uint16_t conn_handle)
{
if (!g_bonding_in_progress)
{
g_bonding_in_progress = true;
ESP_LOGI(TAG, "Initiating bonding for connection %d", conn_handle);
// Starte Security/Bonding Prozess
int rc = ble_gap_security_initiate(conn_handle);
if (rc != 0)
{
ESP_LOGE(TAG, "Failed to initiate security: %s", ble_error_to_string(rc));
g_bonding_in_progress = false;
}
}
}
static int gattc_svcs_callback(uint16_t conn_handle, const struct ble_gatt_error *error,
const struct ble_gatt_svc *service, void *arg)
{
if (error->status != 0)
{
ESP_LOGE(TAG, "Error discovering service: %s", ble_error_to_string(error->status));
return 0;
}
char uuid_str[37]; // Maximale Länge für 128-bit UUID
ble_uuid_to_str(&service->uuid.u, uuid_str);
ESP_LOGI(TAG, "Discovered service: %s", uuid_str);
return 0;
}
static int gattc_char_callback(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_chr *chr,
void *arg)
{
if (error->status != 0)
{
ESP_LOGE(TAG, "Error discovering characteristic: %d", error->status);
return 0;
}
g_char_val_handle = chr->val_handle;
read_characteristic(chr->val_handle);
return 0;
}
// Callback für GATT-Events
static int gattc_event_callback(uint16_t conn_handle, const struct ble_gatt_error *error,
const struct ble_gatt_svc *service, void *arg)
{
if (error->status != 0)
{
ESP_LOGE(TAG, "Error discovering service: %d", error->status);
return 0;
}
ble_gattc_disc_all_svcs(conn_handle, gattc_svcs_callback, NULL);
// ble_gattc_disc_all_chrs(conn_handle, service->start_handle, service->end_handle, gattc_char_callback, NULL);
return 0;
}
static int gattc_read_callback(uint16_t conn_handle, const struct ble_gatt_error *error, struct ble_gatt_attr *attr,
void *arg)
{
if (error->status == 0)
{
ESP_LOGI(TAG, "Wert gelesen %d, Länge: %d", attr->handle, attr->om->om_len);
ESP_LOG_BUFFER_HEX("READ_DATA", attr->om->om_data, attr->om->om_len);
}
else
{
ESP_LOGE(TAG, "Lesefehler, Status: %d", error->status);
}
return 0;
}
static int ble_gap_event_handler(struct ble_gap_event *event, void *arg)
{
device_info_t *device = (device_info_t *)arg;
switch (event->type)
{
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0)
{
g_conn_handle = event->connect.conn_handle;
ESP_LOGI(TAG, "Connected; conn_handle=%d", g_conn_handle);
// Prüfe ob Device bereits gebondet ist
if (is_device_bonded(&device->addr))
{
ESP_LOGI(TAG, "Device already bonded, using existing bond");
// Bei gebondetem Device kann direkt mit Service Discovery begonnen werden
ble_gattc_disc_all_svcs(g_conn_handle, gattc_event_callback, NULL);
}
else
{
ESP_LOGI(TAG, "Device not bonded, initiating bonding");
// Starte Bonding-Prozess
initiate_bonding(g_conn_handle);
// Service Discovery wird nach erfolgreichem Bonding gestartet
}
}
else
{
ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status);
}
break;
case BLE_GAP_EVENT_DISCONNECT:
g_conn_handle = 0;
g_bonding_in_progress = false;
ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason);
break;
case BLE_GAP_EVENT_CONN_UPDATE:
ESP_LOGI(TAG, "Connection updated; status=%d", event->conn_update.status);
break;
case BLE_GAP_EVENT_ENC_CHANGE:
ESP_LOGI(TAG, "Encryption change: status=%d", event->enc_change.status);
if (event->enc_change.status == 0)
{
ESP_LOGI(TAG, "Encryption established, bonding complete");
g_bonding_in_progress = false;
// Nach erfolgreichem Bonding: Service Discovery starten
ble_gattc_disc_all_svcs(g_conn_handle, gattc_event_callback, NULL);
}
else
{
ESP_LOGE(TAG, "Encryption failed: %s", ble_error_to_string(event->enc_change.status));
g_bonding_in_progress = false;
}
break;
case BLE_GAP_EVENT_PASSKEY_ACTION:
ESP_LOGI(TAG, "Passkey action event");
// Implementieren Sie hier die Passkey-Behandlung
// z.B. einen festen Passkey eingeben:
struct ble_sm_io pkey = {0};
pkey.action = BLE_SM_IOACT_INPUT;
pkey.passkey = 100779;
ble_sm_inject_io(event->passkey.conn_handle, &pkey);
break;
case BLE_GAP_EVENT_REPEAT_PAIRING:
ESP_LOGI(TAG, "Device requests repeat pairing");
// Hole die Peer-Adresse aus der Verbindung
struct ble_gap_conn_desc conn_desc;
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &conn_desc);
if (rc == 0)
{
// Lösche alte Bonding-Info
ble_clear_bond(&conn_desc.peer_ota_addr);
}
// Erlaube erneutes Pairing
return BLE_GAP_REPEAT_PAIRING_RETRY;
default:
break;
}
return 0;
}
void ble_connect(device_info_t *device)
{
struct ble_gap_conn_params conn_params = {
.scan_itvl = 0x0010,
.scan_window = 0x0010,
.itvl_min = BLE_GAP_INITIAL_CONN_ITVL_MIN,
.itvl_max = BLE_GAP_INITIAL_CONN_ITVL_MAX,
.latency = BLE_GAP_INITIAL_CONN_LATENCY,
.supervision_timeout = BLE_GAP_INITIAL_SUPERVISION_TIMEOUT,
.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN,
.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN,
};
// Prüfe ob Device bereits gebondet ist
if (is_device_bonded(&device->addr))
{
ESP_LOGI(TAG, "Connecting to bonded device");
}
else
{
ESP_LOGI(TAG, "Connecting to new device (will bond after connection)");
}
int rc = ble_gap_connect(BLE_OWN_ADDR_PUBLIC, &device->addr, 30000, &conn_params, ble_gap_event_handler, device);
if (rc != 0)
{
ESP_LOGE(TAG, "Error initiating connection: %s", ble_error_to_string(rc));
}
}
void ble_clear_bonds(void)
{
ESP_LOGI(TAG, "Clearing all bonds");
int rc = ble_store_clear();
if (rc != 0)
{
ESP_LOGE(TAG, "Failed to clear bond storage: %s", ble_error_to_string(rc));
}
else
{
ESP_LOGI(TAG, "All bonds cleared successfully");
}
}
// Funktion zum Löschen der Bonding-Info eines bestimmten Devices
void ble_clear_bond(const ble_addr_t *addr)
{
ESP_LOGI(TAG, "Clearing bond for specific device");
int rc = ble_store_util_delete_peer(addr);
if (rc != 0)
{
ESP_LOGE(TAG, "Failed to delete peer: %s", ble_error_to_string(rc));
}
else
{
ESP_LOGI(TAG, "Peer deleted successfully");
}
}
void read_characteristic(uint16_t char_val_handle)
{
if (char_val_handle != 0 && g_conn_handle != 0)
{
int rc = ble_gattc_read(g_conn_handle, char_val_handle, gattc_read_callback, NULL);
if (rc != 0)
{
ESP_LOGE(TAG, "Error reading characteristic: %d", rc);
}
}
}
@@ -1,324 +0,0 @@
#include "ble/ble_scanner.h"
#include "ble/ble_device.h"
#include "led_status.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <host/ble_hs.h>
#include <host/util/util.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <services/gap/ble_svc_gap.h>
#include <stdint.h>
static const char *TAG = "ble_scanner";
// List of allowed manufacturer IDs
static const uint16_t ALLOWED_MANUFACTURERS[] = {
0xC0DE, // mars3142
};
static const size_t NUM_MANUFACTURERS = sizeof(ALLOWED_MANUFACTURERS) / sizeof(uint16_t);
static bool scanning = false;
static bool is_manufacturer_allowed(uint16_t company_id)
{
for (size_t i = 0; i < NUM_MANUFACTURERS; i++)
{
if (ALLOWED_MANUFACTURERS[i] == company_id)
{
return true;
}
}
return false;
}
static int ble_central_gap_event(struct ble_gap_event *event, void *arg);
/**
* Starts the BLE scan process.
*/
void start_scan(void)
{
led_behavior_t led_behavior = {
.on_time_ms = 200,
.off_time_ms = 200,
.color = {.red = 0, .green = 0, .blue = 50},
.index = 1,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
struct ble_gap_disc_params disc_params = {
.filter_policy = 0,
.limited = 0,
.passive = 0,
.filter_duplicates = 1,
};
int32_t duration_ms = 10000; // 10 seconds
int rc = ble_gap_disc(BLE_OWN_ADDR_PUBLIC, duration_ms, &disc_params, ble_central_gap_event, NULL);
if (rc != 0)
{
ESP_LOGE(TAG, "Error starting scan; rc=%d", rc);
}
scanning = true;
}
#define MAX_DEVICES 40
static device_info_t devices[MAX_DEVICES];
static int device_count = 0;
// Helper function to find or create a device entry
static device_info_t *find_or_create_device(const ble_addr_t *addr)
{
// Search for existing device
for (int i = 0; i < device_count; i++)
{
if (memcmp(&devices[i].addr, addr, sizeof(ble_addr_t)) == 0)
{
return &devices[i];
}
}
// Add new device
if (device_count < MAX_DEVICES)
{
memset(&devices[device_count], 0, sizeof(device_info_t));
memcpy(&devices[device_count].addr, addr, sizeof(ble_addr_t));
devices[device_count].has_manufacturer = false;
devices[device_count].has_name = false;
strcpy(devices[device_count].name, "Unknown");
return &devices[device_count++];
}
return NULL;
}
static int ble_central_gap_event(struct ble_gap_event *event, void *arg)
{
struct ble_gap_disc_desc *disc;
struct ble_hs_adv_fields fields;
switch (event->type)
{
case BLE_GAP_EVENT_DISC: {
disc = &event->disc;
// Find or create device
device_info_t *device = find_or_create_device(&disc->addr);
if (device == NULL)
{
return 0;
}
// Update RSSI
device->rssi = disc->rssi;
// Parse advertising data
memset(&fields, 0, sizeof(fields));
ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
// Process manufacturer data
if (fields.mfg_data != NULL && fields.mfg_data_len >= 2)
{
uint16_t company_id = fields.mfg_data[0] | (fields.mfg_data[1] << 8);
device->manufacturer_id = company_id;
device->has_manufacturer = true;
// Store complete manufacturer data (incl. Company ID)
device->manufacturer_data_len = fields.mfg_data_len;
memcpy(device->manufacturer_data, fields.mfg_data, fields.mfg_data_len);
}
// Process name
if (fields.name != NULL && fields.name_len > 0)
{
size_t copy_len = fields.name_len < sizeof(device->name) - 1 ? fields.name_len : sizeof(device->name) - 1;
memcpy(device->name, fields.name, copy_len);
device->name[copy_len] = '\0';
device->has_name = true;
}
// Process 16-bit Service UUIDs
if (fields.uuids16 != NULL && fields.num_uuids16 > 0)
{
for (int i = 0; i < fields.num_uuids16 && device->service_uuids_16_count < 10; i++)
{
// Check if UUID already exists
bool exists = false;
for (int j = 0; j < device->service_uuids_16_count; j++)
{
if (device->service_uuids_16[j] == fields.uuids16[i].value)
{
exists = true;
break;
}
}
if (!exists)
{
device->service_uuids_16[device->service_uuids_16_count++] = fields.uuids16[i].value;
}
}
}
// Process 128-bit Service UUIDs
if (fields.uuids128 != NULL && fields.num_uuids128 > 0)
{
for (int i = 0; i < fields.num_uuids128 && device->service_uuids_128_count < 5; i++)
{
// Check if UUID already exists
bool exists = false;
for (int j = 0; j < device->service_uuids_128_count; j++)
{
if (memcmp(&device->service_uuids_128[j], &fields.uuids128[i], sizeof(ble_uuid128_t)) == 0)
{
exists = true;
break;
}
}
if (!exists)
{
memcpy(&device->service_uuids_128[device->service_uuids_128_count++], &fields.uuids128[i],
sizeof(ble_uuid128_t));
}
}
}
// Check if we have all data and the device is allowed
if (device->has_name && device->has_manufacturer && is_manufacturer_allowed(device->manufacturer_id))
{
ESP_LOGI(TAG, "*** Allowed device found ***");
ESP_LOGI(TAG, " Name: %s", device->name);
ESP_LOGI(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X", device->addr.val[5], device->addr.val[4],
device->addr.val[3], device->addr.val[2], device->addr.val[1], device->addr.val[0]);
ESP_LOGI(TAG, " Manufacturer ID: 0x%04X", device->manufacturer_id);
ESP_LOGI(TAG, " RSSI: %d dBm", device->rssi);
// Print Service UUIDs
if (device->service_uuids_16_count > 0)
{
ESP_LOGI(TAG, " 16-bit Service UUIDs (%d):", device->service_uuids_16_count);
for (int i = 0; i < device->service_uuids_16_count; i++)
{
const char *name = "";
// Known Service UUIDs
switch (device->service_uuids_16[i])
{
case 0x180A:
name = " (Device Information)";
break;
case 0x180F:
name = " (Battery Service)";
break;
case 0x1801:
name = " (Generic Attribute)";
break;
case 0x1800:
name = " (Generic Access)";
break;
case 0x181A:
name = " (Environmental Sensing)";
break;
default:
if (device->service_uuids_16[i] >= 0xA000)
{
name = " (Custom)";
}
break;
}
ESP_LOGI(TAG, " - 0x%04X%s", device->service_uuids_16[i], name);
}
}
if (device->service_uuids_128_count > 0)
{
ESP_LOGI(TAG, " 128-bit Service UUIDs (%d):", device->service_uuids_128_count);
for (int i = 0; i < device->service_uuids_128_count; i++)
{
char uuid_str[37]; // UUID string format
snprintf(uuid_str, sizeof(uuid_str),
"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
device->service_uuids_128[i].value[15], device->service_uuids_128[i].value[14],
device->service_uuids_128[i].value[13], device->service_uuids_128[i].value[12],
device->service_uuids_128[i].value[11], device->service_uuids_128[i].value[10],
device->service_uuids_128[i].value[9], device->service_uuids_128[i].value[8],
device->service_uuids_128[i].value[7], device->service_uuids_128[i].value[6],
device->service_uuids_128[i].value[5], device->service_uuids_128[i].value[4],
device->service_uuids_128[i].value[3], device->service_uuids_128[i].value[2],
device->service_uuids_128[i].value[1], device->service_uuids_128[i].value[0]);
ESP_LOGI(TAG, " - %s", uuid_str);
}
}
// Print manufacturer data (without Company ID, i.e., from byte 2)
if (device->manufacturer_data_len > 2)
{
int payload_len = device->manufacturer_data_len - 2;
ESP_LOGI(TAG, " Manufacturer Data (%d bytes):", payload_len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, &device->manufacturer_data[2], payload_len, ESP_LOG_INFO);
// Print data byte by byte for better readability
ESP_LOGI(TAG, " Data interpretation:");
for (int i = 0; i < payload_len; i++)
{
ESP_LOGI(TAG, " - Byte %d: 0x%02X (%d)", i, device->manufacturer_data[i + 2],
device->manufacturer_data[i + 2]);
}
// Optional: Interpret data as 16-bit values (if the number of bytes is even)
if (payload_len >= 2 && (payload_len % 2 == 0))
{
ESP_LOGI(TAG, " As 16-bit values:");
for (int i = 0; i < payload_len; i += 2)
{
uint16_t value = device->manufacturer_data[i + 2] | (device->manufacturer_data[i + 3] << 8);
ESP_LOGI(TAG, " - Word %d: 0x%04X (%d)", i / 2, value, value);
}
}
}
else
{
ESP_LOGI(TAG, " No manufacturer payload data");
}
}
return 0;
}
case BLE_GAP_EVENT_DISC_COMPLETE: {
led_behavior_t led_behavior = {
.index = 1,
.mode = LED_MODE_OFF,
};
led_status_set_behavior(led_behavior);
ESP_LOGI(TAG, "Discovery complete");
scanning = false;
return 0;
}
default:
break;
}
return 0;
}
device_info_t *get_device(int index)
{
if (index < 0 || index >= device_count || !is_manufacturer_allowed(devices[index].manufacturer_id))
{
return NULL;
}
return &devices[index];
}
int get_device_count(void)
{
if (!scanning)
{
return device_count;
}
return 0;
}
@@ -1,60 +0,0 @@
#include "ble_manager.h"
#include "ble/ble_connection.h"
#include "ble/ble_scanner.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <host/ble_hs.h>
#include <host/util/util.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <services/gap/ble_svc_gap.h>
static const char *TAG = "ble_manager";
/**
* Callback that is called when the NimBLE stack is synchronized and ready.
*/
static void on_sync(void)
{
start_scan();
}
static void ble_host_task(void *param)
{
ESP_LOGI(TAG, "BLE Host Task Started");
nimble_port_run(); // This blocks until the stack is stopped
nimble_port_freertos_deinit();
}
void ble_manager_task(void *pvParameter)
{
// Initialize and start the NimBLE stack
nimble_port_init();
// Host configuration with our sync callback
ble_hs_cfg.sync_cb = on_sync;
// Configure GAP service for central mode
ble_svc_gap_init();
// Start the NimBLE host task
nimble_port_freertos_init(ble_host_task); // Not a separate task, can run in the app task
vTaskDelete(NULL);
}
bool ble_found_devices(void)
{
return get_device_count() > 0;
}
void ble_connect_to_device(int index)
{
device_info_t *device = get_device(index);
if (device != NULL)
{
ble_connect(device);
}
}
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,6 @@ idf_component_register(SRCS
src/led_strip_ws2812.c
INCLUDE_DIRS "include"
PRIV_REQUIRES
insa
u8g2
esp_event
esp_timer
@@ -12,15 +12,17 @@ typedef enum
{
LED_MODE_OFF,
LED_MODE_SOLID,
LED_MODE_BLINK
LED_MODE_BLINK,
LED_MODE_BLINK_ALT // alternates between color and alt_color
} led_mode_t;
// This is the structure you pass from the outside to define a behavior
typedef struct
{
uint32_t on_time_ms; // Only relevant for BLINK
uint32_t off_time_ms; // Only relevant for BLINK
uint32_t on_time_ms; // Only relevant for BLINK / BLINK_ALT
uint32_t off_time_ms; // Only relevant for BLINK / BLINK_ALT
rgb_t color;
rgb_t alt_color; // Only relevant for BLINK_ALT
uint8_t index;
led_mode_t mode;
} led_behavior_t;
@@ -55,19 +55,29 @@ static void led_status_task(void *pvParameters)
control->is_on_in_blink ? control->behavior.on_time_ms : control->behavior.off_time_ms;
if ((now_us - control->last_toggle_time_us) / 1000 >= duration_ms)
{
control->is_on_in_blink = !control->is_on_in_blink; // Toggle state
control->last_toggle_time_us = now_us; // Update timestamp
control->is_on_in_blink = !control->is_on_in_blink;
control->last_toggle_time_us = now_us;
}
if (control->is_on_in_blink)
{
led_strip_set_pixel(led_strip, i, control->behavior.color.red, control->behavior.color.green,
control->behavior.color.blue);
}
else
{
led_strip_set_pixel(led_strip, i, 0, 0, 0);
}
break;
case LED_MODE_BLINK_ALT: {
uint32_t duration_ms =
control->is_on_in_blink ? control->behavior.on_time_ms : control->behavior.off_time_ms;
if ((now_us - control->last_toggle_time_us) / 1000 >= duration_ms)
{
control->is_on_in_blink = !control->is_on_in_blink;
control->last_toggle_time_us = now_us;
}
rgb_t *c = control->is_on_in_blink ? &control->behavior.color : &control->behavior.alt_color;
led_strip_set_pixel(led_strip, i, c->red, c->green, c->blue);
}
break;
}
@@ -1,8 +1,7 @@
idf_component_register(
SRCS "src/message_manager.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES
PRIV_REQUIRES
persistence-manager
my_mqtt_client
app_update
)
@@ -1,5 +1,4 @@
#include "message_manager.h"
#include "my_mqtt_client.h"
#include "persistence_manager.h"
#include <esp_app_desc.h>
@@ -1,7 +0,0 @@
idf_component_register(
SRCS "src/my_mqtt_client.c"
INCLUDE_DIRS "include"
REQUIRES
mqtt
app_update
)
@@ -1,21 +0,0 @@
menu "MQTT Client Settings"
config MQTT_CLIENT_BROKER_URL
string "MQTT Broker URL (TLS)"
default "mqtts://example.com:8883"
help
Die Adresse des MQTT-Brokers (z.B. mqtts://broker.example.com:8883)
config MQTT_CLIENT_USERNAME
string "MQTT Username"
default "user"
help
Benutzername für die Authentifizierung (optional)
config MQTT_CLIENT_PASSWORD
string "MQTT Password"
default "password"
help
Passwort für die Authentifizierung (optional)
endmenu
@@ -1,18 +0,0 @@
# MQTT Client Component for ESP-IDF
Diese Komponente stellt eine einfache MQTT-Client-Implementierung bereit, die Daten an einen TLS-gesicherten MQTT-Broker sendet.
## Dateien
- mqtt_client.c: Implementierung des MQTT-Clients
- mqtt_client.h: Header-Datei
- CMakeLists.txt: Build-Konfiguration
- Kconfig: Konfiguration für die Komponente
## Abhängigkeiten
- ESP-IDF (empfohlen: >= v4.0)
- Komponenten: esp-mqtt, esp-tls
## Nutzung
1. Füge die Komponente in dein Projekt ein.
2. Passe die Konfiguration in `Kconfig` an.
3. Binde die Komponente in deinem Code ein und nutze die API aus `mqtt_client.h`.
@@ -1,5 +0,0 @@
dependencies:
idf:
version: '>=5.0.0'
espressif/mqtt: ^1.0.0
espressif/cjson: "*"
@@ -1,16 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C"
{
#endif
void mqtt_client_start(void);
void mqtt_publish(const char *message);
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain);
#ifdef __cplusplus
}
#endif
@@ -1,239 +0,0 @@
#include "my_mqtt_client.h"
#include <cJSON.h>
#include <esp_app_desc.h>
#include <esp_err.h>
#include <esp_interface.h>
#include <esp_log.h>
#include <esp_mac.h>
#include <esp_system.h>
#include <esp_timer.h>
#include <mqtt_client.h>
#include <sdkconfig.h>
#include <sys/time.h>
#define DEVICE_TOPIC_MAX_LEN 60
static const char *TAG = "mqtt_client";
static esp_mqtt_client_handle_t client = NULL;
extern const uint8_t isrgrootx1_pem_start[] asm("_binary_isrgrootx1_pem_start");
extern const uint8_t isrgrootx1_pem_end[] asm("_binary_isrgrootx1_pem_end");
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id = esp_mqtt_client_subscribe(client, "topic/qos0", 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "topic/qos1", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_unsubscribe(client, "topic/qos1");
ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d, return code=0x%02x ", event->msg_id, (uint8_t)*event->data);
msg_id = esp_mqtt_client_publish(client, "topic/qos0", "data", 0, 0, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA:");
ESP_LOGI(TAG, "TOPIC=%.*s\r\n", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle)
{
ESP_LOGE(TAG, "error_type: %d", event->error_handle->error_type);
ESP_LOGE(TAG, "esp-tls error code: 0x%x", event->error_handle->esp_tls_last_esp_err);
ESP_LOGE(TAG, "tls_stack_err: 0x%x", event->error_handle->esp_tls_stack_err);
ESP_LOGE(TAG, "transport_sock_errno: %d", event->error_handle->esp_transport_sock_errno);
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
void mqtt_client_start(void)
{
ESP_LOGI(TAG, "Starte MQTT-Client mit URI: %s", CONFIG_MQTT_CLIENT_BROKER_URL);
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
char client_id[60];
snprintf(client_id, sizeof(client_id), "%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
const esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = CONFIG_MQTT_CLIENT_BROKER_URL,
.broker.verification.certificate = (const char *)isrgrootx1_pem_start,
.broker.verification.certificate_len = isrgrootx1_pem_end - isrgrootx1_pem_start,
.credentials.username = CONFIG_MQTT_CLIENT_USERNAME,
.credentials.client_id = client_id,
.credentials.authentication.password = CONFIG_MQTT_CLIENT_PASSWORD,
};
client = esp_mqtt_client_init(&mqtt_cfg);
if (client == NULL)
{
ESP_LOGE(TAG, "Fehler bei esp_mqtt_client_init!");
return;
}
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_err_t err = esp_mqtt_client_start(client);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_mqtt_client_start fehlgeschlagen: %s", esp_err_to_name(err));
}
else
{
ESP_LOGI(TAG, "MQTT-Client gestartet");
}
}
void get_device_topic(char *topic, size_t topic_len)
{
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
snprintf(topic, topic_len, "device/%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
}
void mqtt_publish(const char *message)
{
// Uptime in ms
int64_t uptime_ms = esp_timer_get_time() / 1000;
// UTC time as ISO8601
struct timeval tv;
gettimeofday(&tv, NULL);
struct tm tm_utc;
gmtime_r(&tv.tv_sec, &tm_utc);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm_utc);
// Firmware version
const esp_app_desc_t *app_desc = esp_app_get_description();
const char *firmware = app_desc->version;
// Reset reason
esp_reset_reason_t reset_reason = esp_reset_reason();
const char *reset_reason_str = "UNKNOWN";
switch (reset_reason)
{
case ESP_RST_POWERON:
reset_reason_str = "POWERON";
break;
case ESP_RST_EXT:
reset_reason_str = "EXT";
break;
case ESP_RST_SW:
reset_reason_str = "SW";
break;
case ESP_RST_PANIC:
reset_reason_str = "PANIC";
break;
case ESP_RST_INT_WDT:
reset_reason_str = "INT_WDT";
break;
case ESP_RST_TASK_WDT:
reset_reason_str = "TASK_WDT";
break;
case ESP_RST_WDT:
reset_reason_str = "WDT";
break;
case ESP_RST_DEEPSLEEP:
reset_reason_str = "DEEPSLEEP";
break;
case ESP_RST_BROWNOUT:
reset_reason_str = "BROWNOUT";
break;
case ESP_RST_SDIO:
reset_reason_str = "SDIO";
break;
default:
break;
}
// Create JSON object
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
cJSON *root = cJSON_CreateObject();
char mac_str[18];
snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
cJSON_AddStringToObject(root, "device_id", mac_str);
cJSON_AddNumberToObject(root, "uptime", uptime_ms);
cJSON_AddStringToObject(root, "timestamp", timestamp);
cJSON_AddStringToObject(root, "firmware", firmware);
cJSON_AddStringToObject(root, "reset_reason", reset_reason_str);
// Insert message as JSON object if possible
char topic_with_type[128];
strncpy(topic_with_type, "", sizeof(topic_with_type));
topic_with_type[sizeof(topic_with_type) - 1] = '\0';
cJSON *msg_obj = cJSON_Parse(message);
if (msg_obj)
{
cJSON *type_item = cJSON_DetachItemFromObject(msg_obj, "type");
if (type_item && cJSON_IsString(type_item))
{
// Extend topic
strncat(topic_with_type, type_item->valuestring, sizeof(topic_with_type) - strlen(topic_with_type) - 1);
}
cJSON_AddItemToObject(root, "message", msg_obj);
cJSON_Delete(type_item); // Free memory
}
else
{
cJSON_AddStringToObject(root, "message", message);
}
// Publish JSON via MQTT
char *json_str = cJSON_PrintUnformatted(root);
mqtt_client_publish(topic_with_type, json_str, strlen(json_str), 0, true);
cJSON_Delete(root);
free(json_str);
}
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain)
{
if (client)
{
char base_topic[DEVICE_TOPIC_MAX_LEN];
get_device_topic(base_topic, sizeof(base_topic));
char full_topic[DEVICE_TOPIC_MAX_LEN + 64];
snprintf(full_topic, sizeof(full_topic), "%s/%s", base_topic, topic);
int msg_id = esp_mqtt_client_publish(client, full_topic, data, len, qos, retain);
ESP_LOGV(TAG, "Publish: topic=%s, msg_id=%d, qos=%d, retain=%d, len=%d", full_topic, msg_id, qos, retain,
(int)len);
}
else
{
ESP_LOGW(TAG, "Publish aufgerufen, aber Client ist nicht initialisiert!");
}
}
@@ -57,12 +57,18 @@ static char *time_to_string(int hhmm)
}
// Helper function: ensures mutex is initialized
static void ensure_mutex_initialized(void)
static bool ensure_mutex_initialized(void)
{
if (simulation_mutex == NULL)
{
simulation_mutex = xSemaphoreCreateMutex();
if (simulation_mutex == NULL)
{
ESP_LOGE(TAG, "Failed to create simulation mutex — out of memory");
return false;
}
}
return true;
}
// Main interpolation function that selects the appropriate method
@@ -391,7 +397,8 @@ void start_simulation_task(void)
void stop_simulation_task(void)
{
ensure_mutex_initialized();
if (!ensure_mutex_initialized())
return;
if (xSemaphoreTake(simulation_mutex, portMAX_DELAY) == pdTRUE)
{
+1 -1
View File
@@ -5,5 +5,5 @@ idf_component_register(SRCS
lwip
esp_http_client
nvs_flash
cjson
json
)
+1 -3
View File
@@ -1,9 +1,8 @@
idf_component_register(SRCS
idf_component_register(SRCS
src/main.cpp
src/app_task.cpp
src/button_handling.c
src/i2c_checker.c
src/u8g2_mqtt.cpp
src/hal/u8g2_esp32_hal.c
INCLUDE_DIRS "include"
PRIV_REQUIRES
@@ -22,7 +21,6 @@ idf_component_register(SRCS
esp_wifi
app_update
driver
my_mqtt_client
)
spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT)
-9
View File
@@ -1,9 +0,0 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
extern QueueHandle_t display_mqtt_queue;
void u8g2_mqtt_task(void *pvParameters);
+11 -21
View File
@@ -1,4 +1,5 @@
#include "app_task.h"
#include "bifrost/api_server.h"
#include "button_handling.h"
#include "common.h"
#include "hal/u8g2_esp32_hal.h"
@@ -8,10 +9,10 @@
#include "led_status.h"
#include "mercedes/mercedes.h"
#include "message_manager.h"
#include "my_mqtt_client.h"
#include "persistence_manager.h"
#include "simulator.h"
#include "u8g2_mqtt.h"
#include "storage.h"
#include "thread_manager.h"
#include "wifi_manager.h"
#include <cstring>
@@ -29,7 +30,6 @@ static const char *TAG = "app_task";
u8g2_t u8g2;
uint8_t received_signal;
uint64_t last_mqtt_sync = 0;
persistence_manager_t g_persistence_manager;
@@ -184,14 +184,15 @@ void app_task(void *args)
.on_time_ms = 1000,
.off_time_ms = 500,
.color = {.red = 50, .green = 0, .blue = 0},
.alt_color = {},
.index = 0,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
ESP_LOGE(TAG, "Display not found on I2C bus");
vTaskDelete(nullptr);
return;
// vTaskDelete(nullptr);
// return;
}
setup_screen();
@@ -249,14 +250,14 @@ void app_task(void *args)
// Initialize Hermes renderer (60s screensaver timeout)
hermes_init(&u8g2, 60000);
// Show splash screen immediately
// Show splash screen for 2 seconds
u8g2_ClearBuffer(&u8g2);
hermes_draw(0);
u8g2_SendBuffer(&u8g2);
vTaskDelay(pdMS_TO_TICKS(2000));
// Start network and services
wifi_manager_init();
mqtt_client_start();
// Start services
thread_manager_init(NULL);
message_manager_register_listener(on_message_received);
start_simulation();
@@ -280,6 +281,7 @@ void app_task(void *args)
// Load dynamic menu from SPIFFS
{
initialize_storage();
FILE *f = fopen("/spiffs/menu.json", "r");
if (f)
{
@@ -304,9 +306,6 @@ void app_task(void *args)
}
}
display_mqtt_queue = xQueueCreate(1, 1024);
xTaskCreatePinnedToCore(u8g2_mqtt_task, "mqtt_disp", 4096, nullptr, 5, nullptr, tskNO_AFFINITY);
xTaskCreatePinnedToCore(display_update_task, "display_update", 4096, nullptr, tskIDLE_PRIORITY + 1,
&display_update_task_handle, CONFIG_FREERTOS_NUMBER_OF_CORES - 1);
@@ -322,15 +321,6 @@ void app_task(void *args)
u8g2_ClearBuffer(&u8g2);
hermes_draw(deltaMs);
// MQTT display sync
auto now = esp_timer_get_time();
if (now - last_mqtt_sync > 1000000)
{
uint8_t *u8g2_buf = u8g2_GetBufferPtr(&u8g2);
xQueueOverwrite(display_mqtt_queue, u8g2_buf);
last_mqtt_sync = now;
}
// Signal display task
if (display_update_task_handle != nullptr)
{
+1 -4
View File
@@ -5,7 +5,6 @@
#include "persistence_manager.h"
#include "wifi_manager.h"
#include <ble_manager.h>
#include <driver/gpio.h>
#include <esp_event.h>
#include <esp_log.h>
@@ -30,7 +29,7 @@ void app_main(void)
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config(&io_conf);
gpio_set_level(WIFI_ENABLE, 0); // LOW
gpio_set_level(WIFI_ENABLE, 1); // HIGH (External/Proper Antenna)
vTaskDelay(pdMS_TO_TICKS(100));
@@ -64,7 +63,5 @@ void app_main(void)
xTaskCreatePinnedToCore(app_task, "app_task", 8192, NULL, tskIDLE_PRIORITY + 5, NULL,
CONFIG_FREERTOS_NUMBER_OF_CORES - 1);
// xTaskCreatePinnedToCore(ble_manager_task, "ble_manager", 4096, NULL, tskIDLE_PRIORITY + 1, NULL,
// CONFIG_FREERTOS_NUMBER_OF_CORES - 1);
}
__END_DECLS
-78
View File
@@ -1,78 +0,0 @@
#include "u8g2_mqtt.h"
#include "my_mqtt_client.h"
#include <esp_timer.h>
#include <stdint.h>
#include <string.h>
#include <u8g2.h>
#define BUFFER_SIZE (128 * 64 / 8)
QueueHandle_t display_mqtt_queue = nullptr;
void u8g2_mqtt_task(void *pvParameters)
{
static uint8_t current_buffer[BUFFER_SIZE];
static uint8_t previous_buffer[BUFFER_SIZE] = {0};
static uint8_t mqtt_payload[BUFFER_SIZE * 2 + 1];
uint64_t last_keyframe_time = 0;
const uint64_t KEYFRAME_INTERVAL_US = 5000000; // 5 seconds in microseconds
while (true)
{
// Blocks without CPU load until app_task provides a frame
if (xQueueReceive(display_mqtt_queue, current_buffer, portMAX_DELAY) == pdTRUE)
{
int payload_size = 0;
uint64_t current_time = esp_timer_get_time();
// Time-based I-frame decision (or on initial start)
bool is_keyframe = (current_time - last_keyframe_time >= KEYFRAME_INTERVAL_US) || (last_keyframe_time == 0);
if (is_keyframe)
{
mqtt_payload[payload_size++] = 0x01; // Header: I-frame
last_keyframe_time = current_time;
for (int i = 0; i < BUFFER_SIZE;)
{
uint8_t count = 1;
while (i + count < BUFFER_SIZE && current_buffer[i] == current_buffer[i + count] && count < 255)
{
count++;
}
mqtt_payload[payload_size++] = count;
mqtt_payload[payload_size++] = current_buffer[i];
i += count;
}
}
else
{
mqtt_payload[payload_size++] = 0x00; // Header: P-frame (Diff)
uint8_t xor_buffer[BUFFER_SIZE];
for (int i = 0; i < BUFFER_SIZE; i++)
{
xor_buffer[i] = current_buffer[i] ^ previous_buffer[i];
}
for (int i = 0; i < BUFFER_SIZE;)
{
uint8_t count = 1;
while (i + count < BUFFER_SIZE && xor_buffer[i] == xor_buffer[i + count] && count < 255)
{
count++;
}
mqtt_payload[payload_size++] = count;
mqtt_payload[payload_size++] = xor_buffer[i];
i += count;
}
}
// --- MQTT SEND ---
mqtt_client_publish("stream", (char *)mqtt_payload, payload_size, 0, false);
memcpy(previous_buffer, current_buffer, BUFFER_SIZE);
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
# Name , Type , SubType , Offset , Size , Flags
nvs , data , nvs , 0x9000 , 16k ,
phy_init , data , phy , , 4k ,
app , app , factory , 0x10000 , 2048K ,
storage , data , spiffs , , 1856K ,
app , app , factory , 0x10000 , 3392K ,
storage , data , spiffs , , 512K ,
fctry , data , nvs , 0x3E0000, 24k ,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 16k
3 phy_init data phy 4k
4 app app factory 0x10000 2048K 3392K
5 storage data spiffs 1856K 512K
6 fctry data nvs 0x3E0000 24k
+37 -11
View File
@@ -1,7 +1,3 @@
# Bluetooth
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
# Logging
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LOG_DEFAULT_LEVEL=3
@@ -27,18 +23,48 @@ CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000
# HTTP Server WebSocket Support
CONFIG_HTTPD_WS_SUPPORT=y
# MQTT
CONFIG_MQTT_CLIENT_BROKER_URL="mqtts://mqtt.mars3142.dev:8883"
CONFIG_MQTT_CLIENT_USERNAME="system-control"
CONFIG_MQTT_CLIENT_PASSWORD="3jHLhNPLcn_dPrukrpMJ"
# System Event Task
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
# FreeRTOS timer service task — needs enough stack for ESP_LOG (printf → newlib lock chain)
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
# Compiler Options
CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y
CONFIG_ESP_SYSTEM_USE_FRAME_POINTER=y
# Certificate Bundle
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
CONFIG_ESP_RMAKER_MQTT_USE_CERT_BUNDLE=n
# OpenThread
CONFIG_OPENTHREAD_ENABLED=y
CONFIG_OPENTHREAD_BORDER_ROUTER=n
CONFIG_OPENTHREAD_COMMISSIONER=y
CONFIG_OPENTHREAD_NETWORK_CHANNEL=18
CONFIG_OPENTHREAD_NUM_MESSAGE_BUFFERS=128
CONFIG_LWIP_IPV6_NUM_ADDRESSES=12
# mbedTLS: required for Thread commissioning (DTLS + J-PAKE + HKDF)
CONFIG_MBEDTLS_SSL_PROTO_DTLS=y
CONFIG_MBEDTLS_SSL_RENEGOTIATION=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECJPAKE=y
CONFIG_MBEDTLS_ECJPAKE_C=y
CONFIG_MBEDTLS_PSK_MODES=y
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
CONFIG_MBEDTLS_HKDF_C=y
# WiFi: reduce DMA buffers to free heap for Thread coexistence
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=8
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=8
# mbedTLS: dynamic buffers (only allocated while connection is active)
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
# WiFi/Thread Coexistence
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
CONFIG_IEEE802154_COEXIST_ENABLE=y
+1 -1
View File
@@ -24,4 +24,4 @@ CONFIG_STATUS_WLED_PIN=16
CONFIG_API_SERVER_HOSTNAME="system-control"
CONFIG_LWIP_MAX_SOCKETS=20
CONFIG_LWIP_MAX_SOCKETS=16