Compare commits
42 Commits
e81fc62645
...
feature/sv
| Author | SHA1 | Date | |
|---|---|---|---|
|
99678087cb
|
|||
|
fe4bd11a21
|
|||
|
684ce36270
|
|||
|
98b5df1ff2
|
|||
|
8128b958cb
|
|||
|
955b4bef04
|
|||
|
81141d8859
|
|||
|
e01006cd49
|
|||
|
c28d7d08df
|
|||
|
df50aaedda
|
|||
|
1f02d35a97
|
|||
|
501c2de874
|
|||
|
b39a3be956
|
|||
|
3ec7bf7acb
|
|||
|
a12dfe7760
|
|||
|
dc40acfd06
|
|||
|
3d7de05614
|
|||
|
3f32b791b7
|
|||
|
ccdc2bb63f
|
|||
|
7d12d98ec9
|
|||
|
cdac9cbfb8
|
|||
|
1fade06bdb
|
|||
|
f7cedf24e8
|
|||
|
1c52f7d679
|
|||
|
7a73fc4b7b
|
|||
|
1fbc28a628
|
|||
|
bccfb80791
|
|||
|
b77fdee21d
|
|||
|
ef0cda1d67
|
|||
|
cfca3f1535
|
|||
|
d18c9bfea1
|
|||
|
28e991cf58
|
|||
|
ebf0dc6556
|
|||
|
29785a96bc
|
|||
|
ee587f1381
|
|||
|
a66c48e713
|
|||
|
b0e93d613c
|
|||
|
52f6c2acab
|
|||
|
dfad7cfb76
|
|||
|
5d78572481
|
|||
|
9e9fb15f86
|
|||
|
e7af663bc3
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@
|
||||
**/*_front.png
|
||||
**/*_schematic*.png
|
||||
**/wiki/*
|
||||
*.FCBak
|
||||
firmware/**/node_modules
|
||||
|
||||
2
firmware/.clangd
Normal file
2
firmware/.clangd
Normal file
@@ -0,0 +1,2 @@
|
||||
CompileFlags:
|
||||
Remove: [-f*, -m*]
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG DOCKER_TAG=latest
|
||||
ARG DOCKER_TAG=release-v5.4
|
||||
FROM espressif/idf:${DOCKER_TAG}
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
idf_build_set_property(BOOTLOADER_EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/bootloader_components_extra/" APPEND)
|
||||
|
||||
project(system_control)
|
||||
|
||||
target_add_binary_data(${PROJECT_NAME}.elf "main/isrgrootx1.pem" TEXT)
|
||||
859
firmware/README-API.md
Normal file
859
firmware/README-API.md
Normal file
@@ -0,0 +1,859 @@
|
||||
# System Control - API Documentation
|
||||
|
||||
This document describes all REST API endpoints and WebSocket messages required for the ESP32 firmware implementation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [REST API Endpoints](#rest-api-endpoints)
|
||||
- [Capabilities](#capabilities)
|
||||
- [WiFi](#wifi)
|
||||
- [Light Control](#light-control)
|
||||
- [LED Configuration](#led-configuration)
|
||||
- [Schema](#schema)
|
||||
- [Devices](#devices)
|
||||
- [Scenes](#scenes)
|
||||
- [WebSocket](#websocket)
|
||||
- [Connection](#connection)
|
||||
- [Client to Server Messages](#client-to-server-messages)
|
||||
- [Server to Client Messages](#server-to-client-messages)
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Capabilities
|
||||
|
||||
#### Get Device Capabilities
|
||||
|
||||
Returns the device capabilities. Used to determine which features are available.
|
||||
|
||||
- **URL:** `/api/capabilities`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"thread": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------|---------|-------------------------------------------------------|
|
||||
| thread | boolean | Whether Thread/Matter features are enabled |
|
||||
|
||||
**Notes:**
|
||||
- If `thread` is `true`, the UI shows Matter device management and Scenes
|
||||
- If `thread` is `false` or the endpoint is unavailable, these features are hidden
|
||||
- The client can also force-enable features via URL parameter `?thread=true`
|
||||
|
||||
---
|
||||
|
||||
### WiFi
|
||||
|
||||
#### Scan Networks
|
||||
|
||||
Scans for available WiFi networks.
|
||||
|
||||
- **URL:** `/api/wifi/scan`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ssid": "NetworkName",
|
||||
"rssi": -45
|
||||
},
|
||||
{
|
||||
"ssid": "AnotherNetwork",
|
||||
"rssi": -72
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|------------------------------------|
|
||||
| ssid | string | Network name (SSID) |
|
||||
| rssi | number | Signal strength in dBm |
|
||||
|
||||
---
|
||||
|
||||
#### Save WiFi Configuration
|
||||
|
||||
Saves WiFi credentials and initiates connection.
|
||||
|
||||
- **URL:** `/api/wifi/config`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ssid": "NetworkName",
|
||||
"password": "SecretPassword123"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|----------|--------|----------|-------------------|
|
||||
| ssid | string | Yes | Network name |
|
||||
| password | string | No | Network password |
|
||||
|
||||
- **Response:** `200 OK` on success, `400 Bad Request` on error
|
||||
|
||||
---
|
||||
|
||||
#### Get WiFi Status
|
||||
|
||||
Returns current WiFi connection status.
|
||||
|
||||
- **URL:** `/api/wifi/status`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"ssid": "NetworkName",
|
||||
"ip": "192.168.1.100",
|
||||
"rssi": -45
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|---------|------------------------------------------|
|
||||
| connected | boolean | Whether connected to a network |
|
||||
| ssid | string | Connected network name (if connected) |
|
||||
| ip | string | Assigned IP address (if connected) |
|
||||
| rssi | number | Signal strength in dBm (if connected) |
|
||||
|
||||
---
|
||||
|
||||
### Light Control
|
||||
|
||||
#### Set Light Power
|
||||
|
||||
Turns the main light on or off.
|
||||
|
||||
- **URL:** `/api/light/power`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"on": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|---------|----------|--------------------------|
|
||||
| on | boolean | Yes | `true` = on, `false` = off |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Set Thunder Effect
|
||||
|
||||
Turns the thunder/lightning effect on or off.
|
||||
|
||||
- **URL:** `/api/light/thunder`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"on": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|---------|----------|--------------------------------|
|
||||
| on | boolean | Yes | `true` = on, `false` = off |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
**Notes:**
|
||||
- When enabled, random lightning flashes are triggered
|
||||
- Can be combined with any light mode
|
||||
- Thunder effect stops automatically when light is turned off
|
||||
|
||||
---
|
||||
|
||||
#### Set Light Mode
|
||||
|
||||
Sets the lighting mode.
|
||||
|
||||
- **URL:** `/api/light/mode`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "simulation"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|------------------------------------------------|
|
||||
| mode | string | Yes | One of: `day`, `night`, `simulation` |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Set Active Schema
|
||||
|
||||
Sets the active schema for simulation mode.
|
||||
|
||||
- **URL:** `/api/light/schema`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "schema_01.csv"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|--------|--------|----------|-------------------------------------------------------|
|
||||
| schema | string | Yes | Schema filename: `schema_01.csv`, `schema_02.csv`, etc. |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Get Light Status
|
||||
|
||||
Returns current light status (alternative to WebSocket).
|
||||
|
||||
- **URL:** `/api/light/status`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"on": true,
|
||||
"thunder": false,
|
||||
"mode": "simulation",
|
||||
"schema": "schema_01.csv",
|
||||
"color": {
|
||||
"r": 255,
|
||||
"g": 240,
|
||||
"b": 220
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------|---------|--------------------------------------|
|
||||
| on | boolean | Current power state |
|
||||
| thunder | boolean | Current thunder effect state |
|
||||
| mode | string | Current mode (day/night/simulation) |
|
||||
| schema | string | Active schema filename |
|
||||
| color | object | Current RGB color being displayed |
|
||||
|
||||
---
|
||||
|
||||
### LED Configuration
|
||||
|
||||
#### Get LED Configuration
|
||||
|
||||
Returns the current LED segment configuration.
|
||||
|
||||
- **URL:** `/api/wled/config`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"name": "Main Light",
|
||||
"start": 0,
|
||||
"leds": 60
|
||||
},
|
||||
{
|
||||
"name": "Accent Light",
|
||||
"start": 60,
|
||||
"leds": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|--------|------------------------------------------|
|
||||
| segments | array | List of LED segments |
|
||||
| segments[].name | string | Optional segment name |
|
||||
| segments[].start | number | Start LED index (0-based) |
|
||||
| segments[].leds | number | Number of LEDs in this segment |
|
||||
|
||||
---
|
||||
|
||||
#### Save LED Configuration
|
||||
|
||||
Saves the LED segment configuration.
|
||||
|
||||
- **URL:** `/api/wled/config`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"name": "Main Light",
|
||||
"start": 0,
|
||||
"leds": 60
|
||||
},
|
||||
{
|
||||
"name": "Accent Light",
|
||||
"start": 60,
|
||||
"leds": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|--------------------|--------|----------|------------------------------------------|
|
||||
| segments | array | Yes | List of LED segments (can be empty) |
|
||||
| segments[].name | string | No | Optional segment name |
|
||||
| segments[].start | number | Yes | Start LED index (0-based) |
|
||||
| segments[].leds | number | Yes | Number of LEDs in this segment |
|
||||
|
||||
- **Response:** `200 OK` on success, `400 Bad Request` on validation error
|
||||
|
||||
**Notes:**
|
||||
- Segments define how the LED strip is divided into logical groups
|
||||
- Changes are persisted to NVS (non-volatile storage)
|
||||
- Each segment can be controlled independently in the light schema
|
||||
|
||||
---
|
||||
|
||||
### Schema
|
||||
|
||||
#### Load Schema
|
||||
|
||||
Loads a schema file.
|
||||
|
||||
- **URL:** `/api/schema/{filename}`
|
||||
- **Method:** `GET`
|
||||
- **URL Parameters:**
|
||||
- `filename`: Schema file name (e.g., `schema_01.csv`)
|
||||
- **Response:** CSV text data
|
||||
|
||||
```
|
||||
255,240,220,0,100,250
|
||||
255,230,200,0,120,250
|
||||
...
|
||||
```
|
||||
|
||||
The CSV format has 48 rows (one per 30-minute interval) with 6 values per row:
|
||||
|
||||
| Column | Description | Range |
|
||||
|--------|--------------------------------|---------|
|
||||
| 1 | Red (R) | 0-255 |
|
||||
| 2 | Green (G) | 0-255 |
|
||||
| 3 | Blue (B) | 0-255 |
|
||||
| 4 | Value 1 (V1) - custom value | 0-255 |
|
||||
| 5 | Value 2 (V2) - custom value | 0-255 |
|
||||
| 6 | Value 3 (V3) - custom value | 0-255 |
|
||||
|
||||
---
|
||||
|
||||
#### Save Schema
|
||||
|
||||
Saves a schema file.
|
||||
|
||||
- **URL:** `/api/schema/{filename}`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `text/csv`
|
||||
- **URL Parameters:**
|
||||
- `filename`: Schema file name (e.g., `schema_01.csv`)
|
||||
- **Request Body:** CSV text data (same format as above)
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
### Devices
|
||||
|
||||
#### Scan for Devices
|
||||
|
||||
Scans for available Matter devices to pair.
|
||||
|
||||
- **URL:** `/api/devices/scan`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "matter-001",
|
||||
"type": "light",
|
||||
"name": "Matter Lamp"
|
||||
},
|
||||
{
|
||||
"id": "matter-002",
|
||||
"type": "sensor",
|
||||
"name": "Temperature Sensor"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|-----------------------------------------------|
|
||||
| id | string | Unique device identifier |
|
||||
| type | string | Device type: `light`, `sensor`, `unknown` |
|
||||
| name | string | Device name (can be empty) |
|
||||
|
||||
---
|
||||
|
||||
#### Pair Device
|
||||
|
||||
Pairs a discovered device.
|
||||
|
||||
- **URL:** `/api/devices/pair`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "matter-001",
|
||||
"name": "Living Room Lamp"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|------------------------------|
|
||||
| id | string | Yes | Device ID from scan |
|
||||
| name | string | Yes | User-defined device name |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Get Paired Devices
|
||||
|
||||
Returns list of all paired devices.
|
||||
|
||||
- **URL:** `/api/devices/paired`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "matter-001",
|
||||
"type": "light",
|
||||
"name": "Living Room Lamp"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|-------------------------------------------|
|
||||
| id | string | Unique device identifier |
|
||||
| type | string | Device type: `light`, `sensor`, `unknown` |
|
||||
| name | string | User-defined device name |
|
||||
|
||||
---
|
||||
|
||||
#### Update Device Name
|
||||
|
||||
Updates the name of a paired device.
|
||||
|
||||
- **URL:** `/api/devices/update`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "matter-001",
|
||||
"name": "New Device Name"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|------------------------|
|
||||
| id | string | Yes | Device ID |
|
||||
| name | string | Yes | New device name |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Unpair Device
|
||||
|
||||
Removes a paired device.
|
||||
|
||||
- **URL:** `/api/devices/unpair`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "matter-001"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|---------------|
|
||||
| id | string | Yes | Device ID |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Toggle Device
|
||||
|
||||
Toggles a device (e.g., light on/off).
|
||||
|
||||
- **URL:** `/api/devices/toggle`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "matter-001"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|---------------|
|
||||
| id | string | Yes | Device ID |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
### Scenes
|
||||
|
||||
#### Get All Scenes
|
||||
|
||||
Returns all configured scenes.
|
||||
|
||||
- **URL:** `/api/scenes`
|
||||
- **Method:** `GET`
|
||||
- **Response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "scene-1",
|
||||
"name": "Evening Mood",
|
||||
"icon": "🌅",
|
||||
"actions": {
|
||||
"light": "on",
|
||||
"mode": "simulation",
|
||||
"schema": "schema_02.csv",
|
||||
"devices": [
|
||||
{
|
||||
"id": "matter-001",
|
||||
"state": "on"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "scene-2",
|
||||
"name": "Night Mode",
|
||||
"icon": "🌙",
|
||||
"actions": {
|
||||
"light": "on",
|
||||
"mode": "night"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Scene Object:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------|--------|------------------------------------|
|
||||
| id | string | Unique scene identifier |
|
||||
| name | string | Scene name |
|
||||
| icon | string | Emoji icon for the scene |
|
||||
| actions | object | Actions to execute (see below) |
|
||||
|
||||
**Actions Object:**
|
||||
|
||||
| Field | Type | Optional | Description |
|
||||
|---------|--------|----------|------------------------------------------|
|
||||
| light | string | Yes | `"on"` or `"off"` |
|
||||
| mode | string | Yes | `"day"`, `"night"`, or `"simulation"` |
|
||||
| schema | string | Yes | Schema filename (e.g., `schema_01.csv`) |
|
||||
| devices | array | Yes | Array of device actions (see below) |
|
||||
|
||||
**Device Action Object:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|----------------------------------|
|
||||
| id | string | Device ID |
|
||||
| state | string | `"on"` or `"off"` |
|
||||
|
||||
---
|
||||
|
||||
#### Create/Update Scene
|
||||
|
||||
Creates a new scene or updates an existing one.
|
||||
|
||||
- **URL:** `/api/scenes`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "scene-1",
|
||||
"name": "Evening Mood",
|
||||
"icon": "🌅",
|
||||
"actions": {
|
||||
"light": "on",
|
||||
"mode": "simulation",
|
||||
"schema": "schema_02.csv",
|
||||
"devices": [
|
||||
{
|
||||
"id": "matter-001",
|
||||
"state": "on"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Delete Scene
|
||||
|
||||
Deletes a scene.
|
||||
|
||||
- **URL:** `/api/scenes`
|
||||
- **Method:** `DELETE`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "scene-1"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|---------------|
|
||||
| id | string | Yes | Scene ID |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
#### Activate Scene
|
||||
|
||||
Executes all actions of a scene.
|
||||
|
||||
- **URL:** `/api/scenes/activate`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "scene-1"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|--------|----------|---------------|
|
||||
| id | string | Yes | Scene ID |
|
||||
|
||||
- **Response:** `200 OK` on success
|
||||
|
||||
---
|
||||
|
||||
## WebSocket
|
||||
|
||||
### Connection
|
||||
|
||||
- **URL:** `ws://{host}/ws` or `wss://{host}/ws`
|
||||
- **Protocol:** JSON messages
|
||||
|
||||
The WebSocket connection is used for real-time status updates. The client should reconnect automatically if the connection is lost (recommended: 3 second delay).
|
||||
|
||||
---
|
||||
|
||||
### Client to Server Messages
|
||||
|
||||
#### Request Status
|
||||
|
||||
Requests the current system status.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "getStatus"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Server to Client Messages
|
||||
|
||||
#### Status Update
|
||||
|
||||
Sent in response to `getStatus` or when status changes.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "status",
|
||||
"on": true,
|
||||
"mode": "simulation",
|
||||
"schema": "schema_01.csv",
|
||||
"color": {
|
||||
"r": 255,
|
||||
"g": 240,
|
||||
"b": 220
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------|---------|--------------------------------------|
|
||||
| type | string | Always `"status"` |
|
||||
| on | boolean | Light power state |
|
||||
| mode | string | Current mode (day/night/simulation) |
|
||||
| schema | string | Active schema filename |
|
||||
| color | object | Current RGB color (optional) |
|
||||
|
||||
---
|
||||
|
||||
#### Color Update
|
||||
|
||||
Sent when the current color changes (during simulation).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "color",
|
||||
"r": 255,
|
||||
"g": 200,
|
||||
"b": 150
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|--------|--------------------------|
|
||||
| type | string | Always `"color"` |
|
||||
| r | number | Red value (0-255) |
|
||||
| g | number | Green value (0-255) |
|
||||
| b | number | Blue value (0-255) |
|
||||
|
||||
---
|
||||
|
||||
#### WiFi Status Update
|
||||
|
||||
Sent when WiFi connection status changes.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "wifi",
|
||||
"connected": true,
|
||||
"ip": "192.168.1.100",
|
||||
"rssi": -45
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------|---------|---------------------------------------|
|
||||
| type | string | Always `"wifi"` |
|
||||
| connected | boolean | Whether connected to a network |
|
||||
| ip | string | Assigned IP address (if connected) |
|
||||
| rssi | number | Signal strength in dBm (if connected) |
|
||||
|
||||
---
|
||||
|
||||
## Static Files
|
||||
|
||||
The web interface files should be served from the `/spiffs/www/` directory:
|
||||
|
||||
| Path | Description |
|
||||
|----------------------|------------------------------------------------|
|
||||
| `/` | Serves `index.html` (or `captive.html` in AP mode) |
|
||||
| `/index.html` | Main HTML file (full interface) |
|
||||
| `/captive.html` | Captive portal (WiFi setup only) |
|
||||
| `/css/shared.css` | Shared styles for all pages |
|
||||
| `/css/index.css` | Styles for main interface |
|
||||
| `/css/captive.css` | Styles for captive portal |
|
||||
| `/js/wifi-shared.js` | Shared WiFi configuration logic |
|
||||
| `/js/*.js` | JavaScript modules |
|
||||
|
||||
---
|
||||
|
||||
## Captive Portal
|
||||
|
||||
When the ESP32 is in Access Point (AP) mode (no WiFi configured or connection failed), it should serve the captive portal:
|
||||
|
||||
### Behavior
|
||||
|
||||
1. **AP Mode Activation:**
|
||||
- ESP32 creates an access point (e.g., "marklin-setup")
|
||||
- DNS server redirects all requests to the ESP32's IP (captive portal detection)
|
||||
|
||||
2. **Captive Portal Detection:**
|
||||
- Respond to common captive portal detection URLs:
|
||||
- `/generate_204` (Android)
|
||||
- `/hotspot-detect.html` (Apple)
|
||||
- `/connecttest.txt` (Windows)
|
||||
- Return redirect or serve `captive.html`
|
||||
|
||||
3. **Serving Files in AP Mode:**
|
||||
- `/` → `captive.html`
|
||||
- `/captive.html` → Captive portal page
|
||||
- `/js/wifi-shared.js` → WiFi functions
|
||||
- API endpoints remain the same (`/api/wifi/*`)
|
||||
|
||||
4. **After Successful Configuration:**
|
||||
- ESP32 attempts to connect to the configured network
|
||||
- If successful, switch to Station mode and serve `index.html`
|
||||
- If failed, remain in AP mode
|
||||
|
||||
### Recommended AP Settings
|
||||
|
||||
| Setting | Value |
|
||||
|---------------|--------------------------|
|
||||
| SSID | `marklin-setup` |
|
||||
| Password | None (open) or `marklin` |
|
||||
| IP Address | `192.168.4.1` |
|
||||
| Gateway | `192.168.4.1` |
|
||||
| Subnet | `255.255.255.0` |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
All endpoints should return appropriate HTTP status codes:
|
||||
|
||||
| Code | Description |
|
||||
|------|---------------------------------------|
|
||||
| 200 | Success |
|
||||
| 400 | Bad Request (invalid input) |
|
||||
| 404 | Not Found (resource doesn't exist) |
|
||||
| 500 | Internal Server Error |
|
||||
|
||||
Error responses should include a JSON body with an error message:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Description of what went wrong"
|
||||
}
|
||||
```
|
||||
209
firmware/README-captive.md
Normal file
209
firmware/README-captive.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Captive Portal Implementation Guide
|
||||
|
||||
This document describes how to implement the captive portal functionality on the ESP32 side to work with `captive.html`.
|
||||
|
||||
## Overview
|
||||
|
||||
When the ESP32 has no WiFi credentials stored (or connection fails), it should start in Access Point (AP) mode and serve a captive portal that allows users to configure WiFi settings.
|
||||
|
||||
## How Captive Portal Detection Works
|
||||
|
||||
Operating systems automatically send HTTP requests to known URLs to check for internet connectivity:
|
||||
|
||||
| OS | Detection URL | Expected Response |
|
||||
|---|---|---|
|
||||
| **iOS/macOS** | `http://captive.apple.com/hotspot-detect.html` | `<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>` |
|
||||
| **Android** | `http://connectivitycheck.gstatic.com/generate_204` | HTTP 204 No Content |
|
||||
| **Windows** | `http://www.msftconnecttest.com/connecttest.txt` | `Microsoft Connect Test` |
|
||||
|
||||
If the response doesn't match, the OS assumes there's a captive portal and opens a browser.
|
||||
|
||||
## ESP32 Implementation Steps
|
||||
|
||||
### 1. Start Access Point Mode
|
||||
|
||||
```c
|
||||
wifi_config_t ap_config = {
|
||||
.ap = {
|
||||
.ssid = "SystemControl-Setup",
|
||||
.ssid_len = 0,
|
||||
.password = "", // Open network for easy access
|
||||
.max_connection = 4,
|
||||
.authmode = WIFI_AUTH_OPEN
|
||||
}
|
||||
};
|
||||
esp_wifi_set_mode(WIFI_MODE_AP);
|
||||
esp_wifi_set_config(WIFI_IF_AP, &ap_config);
|
||||
esp_wifi_start();
|
||||
```
|
||||
|
||||
### 2. Start DNS Server (DNS Hijacking)
|
||||
|
||||
Redirect ALL DNS queries to the ESP32's IP address:
|
||||
|
||||
```c
|
||||
// Simplified example - use a proper DNS server component
|
||||
void dns_server_task(void *pvParameters) {
|
||||
// Listen on UDP port 53
|
||||
// For any DNS query, respond with ESP32's AP IP (e.g., 192.168.4.1)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Configure HTTP Server with Redirects
|
||||
|
||||
```c
|
||||
// Handler for captive portal detection URLs
|
||||
esp_err_t captive_redirect_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_status(req, "302 Found");
|
||||
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/captive.html");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Register handlers for detection URLs
|
||||
httpd_uri_t apple_detect = {
|
||||
.uri = "/hotspot-detect.html",
|
||||
.method = HTTP_GET,
|
||||
.handler = captive_redirect_handler
|
||||
};
|
||||
|
||||
httpd_uri_t android_detect = {
|
||||
.uri = "/generate_204",
|
||||
.method = HTTP_GET,
|
||||
.handler = captive_redirect_handler
|
||||
};
|
||||
|
||||
// Catch-all for any unknown paths
|
||||
httpd_uri_t catch_all = {
|
||||
.uri = "/*",
|
||||
.method = HTTP_GET,
|
||||
.handler = captive_redirect_handler
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Serve Static Files
|
||||
|
||||
Serve the captive portal files from SPIFFS/LittleFS:
|
||||
|
||||
- `/captive.html` - Main captive portal page
|
||||
- `/favicon.svg` - Favicon
|
||||
- `/css/shared.css` - Shared styles
|
||||
- `/css/captive.css` - Captive-specific styles
|
||||
- `/js/i18n.js` - Internationalization
|
||||
- `/js/wifi-shared.js` - WiFi configuration logic
|
||||
|
||||
### 5. Implement WiFi Configuration API
|
||||
|
||||
```c
|
||||
// POST /api/wifi/config
|
||||
// Body: { "ssid": "NetworkName", "password": "SecretPassword" }
|
||||
esp_err_t wifi_config_handler(httpd_req_t *req) {
|
||||
// 1. Parse JSON body
|
||||
// 2. Store credentials in NVS
|
||||
// 3. Send success response
|
||||
// 4. Schedule restart/reconnect
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// GET /api/wifi/scan
|
||||
// Returns: [{ "ssid": "Network1", "rssi": -45 }, ...]
|
||||
esp_err_t wifi_scan_handler(httpd_req_t *req) {
|
||||
// 1. Perform WiFi scan
|
||||
// 2. Return JSON array of networks
|
||||
return ESP_OK;
|
||||
}
|
||||
```
|
||||
|
||||
## Flow After User Submits WiFi Credentials
|
||||
|
||||
```
|
||||
1. User enters SSID + Password, clicks "Connect"
|
||||
↓
|
||||
2. Frontend sends POST /api/wifi/config
|
||||
↓
|
||||
3. ESP32 stores credentials in NVS (Non-Volatile Storage)
|
||||
↓
|
||||
4. ESP32 sends HTTP 200 OK response
|
||||
↓
|
||||
5. Frontend shows countdown (10 seconds)
|
||||
↓
|
||||
6. ESP32 stops AP mode
|
||||
↓
|
||||
7. ESP32 connects to configured WiFi
|
||||
↓
|
||||
8. ESP32 gets new IP from router (e.g., 192.168.1.42)
|
||||
↓
|
||||
9. User connects phone/PC to normal WiFi
|
||||
↓
|
||||
10. User accesses ESP32 via new IP or mDNS (e.g., http://system-control.local)
|
||||
```
|
||||
|
||||
## Recommended: mDNS Support
|
||||
|
||||
Register an mDNS hostname so users can access the device without knowing the IP:
|
||||
|
||||
```c
|
||||
mdns_init();
|
||||
mdns_hostname_set("system-control");
|
||||
mdns_instance_name_set("System Control");
|
||||
```
|
||||
|
||||
Then the device is accessible at: `http://system-control.local`
|
||||
|
||||
## Error Handling / Fallback
|
||||
|
||||
If WiFi connection fails after credentials are saved:
|
||||
|
||||
1. Wait for connection timeout (e.g., 30 seconds)
|
||||
2. If connection fails, restart in AP mode
|
||||
3. Show error message on captive portal
|
||||
4. Allow user to re-enter credentials
|
||||
|
||||
```c
|
||||
// Pseudo-code
|
||||
if (wifi_connect_timeout()) {
|
||||
nvs_erase_key("wifi_ssid");
|
||||
nvs_erase_key("wifi_password");
|
||||
esp_restart(); // Will boot into AP mode again
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints Summary
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/wifi/scan` | Scan for available networks |
|
||||
| POST | `/api/wifi/config` | Save WiFi credentials |
|
||||
| GET | `/api/wifi/status` | Get current connection status |
|
||||
|
||||
### Request/Response Examples
|
||||
|
||||
**GET /api/wifi/scan**
|
||||
```json
|
||||
[
|
||||
{ "ssid": "HomeNetwork", "rssi": -45, "secure": true },
|
||||
{ "ssid": "GuestWiFi", "rssi": -67, "secure": false }
|
||||
]
|
||||
```
|
||||
|
||||
**POST /api/wifi/config**
|
||||
```json
|
||||
{ "ssid": "HomeNetwork", "password": "MySecretPassword" }
|
||||
```
|
||||
|
||||
**GET /api/wifi/status**
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"ssid": "HomeNetwork",
|
||||
"ip": "192.168.1.42",
|
||||
"rssi": -52
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Open AP**: The setup AP is intentionally open for easy access. Keep setup time short.
|
||||
2. **HTTPS**: Consider using HTTPS for the main interface (after WiFi setup).
|
||||
3. **Timeout**: Auto-disable AP mode after successful connection.
|
||||
4. **Button Reset**: Implement a physical button to reset WiFi credentials and re-enter AP mode.
|
||||
@@ -0,0 +1,9 @@
|
||||
idf_component_register(SRCS "hooks.c"
|
||||
REQUIRES extra_component)
|
||||
|
||||
# We need to force GCC to integrate this static library into the
|
||||
# bootloader link. Indeed, by default, as the hooks in the bootloader are weak,
|
||||
# the linker would just ignore the symbols in the extra. (i.e. not strictly
|
||||
# required)
|
||||
# To do so, we need to define the symbol (function) `bootloader_hooks_include`
|
||||
# within hooks.c source file.
|
||||
21
firmware/bootloader_components/my_boot_hooks/hooks.c
Normal file
21
firmware/bootloader_components/my_boot_hooks/hooks.c
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "esp_log.h"
|
||||
|
||||
/* Function used to tell the linker to include this file
|
||||
* with all its symbols.
|
||||
*/
|
||||
void bootloader_hooks_include(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* Keep in my mind that a lot of functions cannot be called from here
|
||||
* as system initialization has not been performed yet, including
|
||||
* BSS, SPI flash, or memory protection. */
|
||||
void bootloader_before_init(void)
|
||||
{
|
||||
ESP_LOGI("HOOK", "This hook is called BEFORE bootloader initialization");
|
||||
}
|
||||
|
||||
void bootloader_after_init(void)
|
||||
{
|
||||
ESP_LOGI("HOOK", "This hook is called AFTER bootloader initialization");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
idf_component_register(SRCS "extra_component.c")
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include "esp_log.h"
|
||||
|
||||
void bootloader_extra_dir_function(void)
|
||||
{
|
||||
ESP_LOGI("EXTRA", "This function is called from an extra component");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ void analytics_init(void)
|
||||
.log_type = ESP_DIAG_LOG_TYPE_ERROR | ESP_DIAG_LOG_TYPE_EVENT | ESP_DIAG_LOG_TYPE_WARNING,
|
||||
.node_id = NULL,
|
||||
.auth_key = insights_auth_key_start,
|
||||
.alloc_ext_ram = false,
|
||||
.alloc_ext_ram = true,
|
||||
};
|
||||
|
||||
esp_insights_init(&config);
|
||||
|
||||
19
firmware/components/api-server/CMakeLists.txt
Normal file
19
firmware/components/api-server/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
idf_component_register(SRCS
|
||||
src/api_server.c
|
||||
src/common.c
|
||||
src/api_handlers.c
|
||||
src/websocket_handler.c
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
esp_http_server
|
||||
mdns
|
||||
esp_wifi
|
||||
esp_netif
|
||||
esp_event
|
||||
json
|
||||
led-manager
|
||||
simulator
|
||||
persistence-manager
|
||||
message-manager
|
||||
simulator
|
||||
)
|
||||
38
firmware/components/api-server/Kconfig
Normal file
38
firmware/components/api-server/Kconfig
Normal file
@@ -0,0 +1,38 @@
|
||||
menu "API Server Configuration"
|
||||
|
||||
config API_SERVER_HOSTNAME
|
||||
string "mDNS Hostname"
|
||||
default "system-control"
|
||||
help
|
||||
The mDNS hostname for the API server.
|
||||
The device will be accessible at <hostname>.local
|
||||
|
||||
config API_SERVER_PORT
|
||||
int "HTTP Server Port"
|
||||
default 80
|
||||
range 1 65535
|
||||
help
|
||||
The port number for the HTTP server.
|
||||
|
||||
config API_SERVER_MAX_WS_CLIENTS
|
||||
int "Maximum WebSocket Clients"
|
||||
default 4
|
||||
range 1 8
|
||||
help
|
||||
Maximum number of concurrent WebSocket connections.
|
||||
|
||||
config API_SERVER_ENABLE_CORS
|
||||
bool "Enable CORS"
|
||||
default y
|
||||
help
|
||||
Enable Cross-Origin Resource Sharing (CORS) headers.
|
||||
This is required for web interfaces served from different origins.
|
||||
|
||||
config API_SERVER_STATIC_FILES_PATH
|
||||
string "Static Files Path"
|
||||
default "/spiffs/www"
|
||||
help
|
||||
Base path for serving static web files.
|
||||
|
||||
endmenu
|
||||
|
||||
5
firmware/components/api-server/idf_component.yml
Normal file
5
firmware/components/api-server/idf_component.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
idf:
|
||||
version: '>=5.0.0'
|
||||
espressif/mdns:
|
||||
version: '*'
|
||||
63
firmware/components/api-server/include/api_handlers.h
Normal file
63
firmware/components/api-server/include/api_handlers.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Register all API handlers with the HTTP server
|
||||
*
|
||||
* @param server HTTP server handle
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_handlers_register(httpd_handle_t server);
|
||||
|
||||
// Capabilities API
|
||||
esp_err_t api_capabilities_get_handler(httpd_req_t *req);
|
||||
|
||||
// WiFi API
|
||||
esp_err_t api_wifi_scan_handler(httpd_req_t *req);
|
||||
esp_err_t api_wifi_config_handler(httpd_req_t *req);
|
||||
esp_err_t api_wifi_status_handler(httpd_req_t *req);
|
||||
|
||||
// Light Control API
|
||||
esp_err_t api_light_power_handler(httpd_req_t *req);
|
||||
esp_err_t api_light_thunder_handler(httpd_req_t *req);
|
||||
esp_err_t api_light_mode_handler(httpd_req_t *req);
|
||||
esp_err_t api_light_schema_handler(httpd_req_t *req);
|
||||
esp_err_t api_light_status_handler(httpd_req_t *req);
|
||||
|
||||
// LED Configuration API
|
||||
esp_err_t api_wled_config_get_handler(httpd_req_t *req);
|
||||
esp_err_t api_wled_config_post_handler(httpd_req_t *req);
|
||||
|
||||
// Schema API
|
||||
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);
|
||||
|
||||
// Scenes API
|
||||
esp_err_t api_scenes_get_handler(httpd_req_t *req);
|
||||
esp_err_t api_scenes_post_handler(httpd_req_t *req);
|
||||
esp_err_t api_scenes_delete_handler(httpd_req_t *req);
|
||||
esp_err_t api_scenes_activate_handler(httpd_req_t *req);
|
||||
|
||||
// Static file serving
|
||||
esp_err_t api_static_file_handler(httpd_req_t *req);
|
||||
|
||||
// Captive portal detection
|
||||
esp_err_t api_captive_portal_handler(httpd_req_t *req);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
119
firmware/components/api-server/include/api_server.h
Normal file
119
firmware/components/api-server/include/api_server.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Configuration for the API server
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
const char *hostname; ///< mDNS hostname (default: "system-control")
|
||||
uint16_t port; ///< HTTP server port (default: 80)
|
||||
const char *base_path; ///< Base path for static files (default: "/storage/www")
|
||||
bool enable_cors; ///< Enable CORS headers (default: true)
|
||||
} api_server_config_t;
|
||||
|
||||
#ifdef CONFIG_API_SERVER_ENABLE_CORS
|
||||
#define API_SERVER_ENABLE_CORS true
|
||||
#else
|
||||
#define API_SERVER_ENABLE_CORS false
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Default configuration for the API server
|
||||
*/
|
||||
#define API_SERVER_CONFIG_DEFAULT() \
|
||||
{ \
|
||||
.hostname = CONFIG_API_SERVER_HOSTNAME, \
|
||||
.port = CONFIG_API_SERVER_PORT, \
|
||||
.base_path = CONFIG_API_SERVER_STATIC_FILES_PATH, \
|
||||
.enable_cors = API_SERVER_ENABLE_CORS, \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize and start the API server with mDNS
|
||||
*
|
||||
* This function starts an HTTP server with:
|
||||
* - REST API endpoints
|
||||
* - WebSocket endpoint at /ws
|
||||
* - mDNS registration (system-control.local)
|
||||
* - Static file serving from SPIFFS
|
||||
*
|
||||
* @param config Pointer to server configuration, or NULL for defaults
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_start(const api_server_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Stop the API server and mDNS
|
||||
*
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Check if the API server is running
|
||||
*
|
||||
* @return true if server is running
|
||||
*/
|
||||
bool api_server_is_running(void);
|
||||
|
||||
/**
|
||||
* @brief Get the HTTP server handle
|
||||
*
|
||||
* @return httpd_handle_t Server handle, or NULL if not running
|
||||
*/
|
||||
httpd_handle_t api_server_get_handle(void);
|
||||
|
||||
/**
|
||||
* @brief Broadcast a message to all connected WebSocket clients
|
||||
*
|
||||
* @param message JSON message to broadcast
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_ws_broadcast(const char *message);
|
||||
|
||||
/**
|
||||
* @brief Broadcast a status update to all WebSocket clients
|
||||
*
|
||||
* @param on Light power state
|
||||
* @param mode Current mode (day/night/simulation)
|
||||
* @param schema Active schema filename
|
||||
* @param r Red value (0-255)
|
||||
* @param g Green value (0-255)
|
||||
* @param b Blue value (0-255)
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g,
|
||||
uint8_t b);
|
||||
|
||||
/**
|
||||
* @brief Broadcast a color update to all WebSocket clients
|
||||
*
|
||||
* @param r Red value (0-255)
|
||||
* @param g Green value (0-255)
|
||||
* @param b Blue value (0-255)
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/**
|
||||
* @brief Broadcast a WiFi status update to all WebSocket clients
|
||||
*
|
||||
* @param connected Connection state
|
||||
* @param ip IP address (can be NULL if not connected)
|
||||
* @param rssi Signal strength
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
9
firmware/components/api-server/include/common.h
Normal file
9
firmware/components/api-server/include/common.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
#include <cJSON.h>
|
||||
|
||||
void common_init(void);
|
||||
cJSON *create_light_status_json(void);
|
||||
|
||||
#endif // COMMON_H
|
||||
60
firmware/components/api-server/include/websocket_handler.h
Normal file
60
firmware/components/api-server/include/websocket_handler.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Maximum number of concurrent WebSocket connections
|
||||
*/
|
||||
#define WS_MAX_CLIENTS CONFIG_API_SERVER_MAX_WS_CLIENTS
|
||||
|
||||
/**
|
||||
* @brief Initialize WebSocket handler
|
||||
*
|
||||
* @param server HTTP server handle
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t websocket_handler_init(httpd_handle_t server);
|
||||
|
||||
/**
|
||||
* @brief WebSocket request handler
|
||||
*
|
||||
* @param req HTTP request
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t websocket_handler(httpd_req_t *req);
|
||||
|
||||
/**
|
||||
* @brief Send message to a specific WebSocket client
|
||||
*
|
||||
* @param server HTTP server handle
|
||||
* @param fd Socket file descriptor
|
||||
* @param message Message to send
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message);
|
||||
|
||||
/**
|
||||
* @brief Broadcast message to all connected WebSocket clients
|
||||
*
|
||||
* @param server HTTP server handle
|
||||
* @param message Message to broadcast
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t websocket_broadcast(httpd_handle_t server, const char *message);
|
||||
|
||||
/**
|
||||
* @brief Get number of connected WebSocket clients
|
||||
*
|
||||
* @return int Number of connected clients
|
||||
*/
|
||||
int websocket_get_client_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1177
firmware/components/api-server/src/api_handlers.c
Normal file
1177
firmware/components/api-server/src/api_handlers.c
Normal file
File diff suppressed because it is too large
Load Diff
200
firmware/components/api-server/src/api_server.c
Normal file
200
firmware/components/api-server/src/api_server.c
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "api_server.h"
|
||||
#include "api_handlers.h"
|
||||
#include "websocket_handler.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "storage.h"
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <mdns.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "api_server";
|
||||
|
||||
static httpd_handle_t s_server = NULL;
|
||||
static api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT();
|
||||
|
||||
static esp_err_t init_mdns(const char *hostname)
|
||||
{
|
||||
esp_err_t err = mdns_init();
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to initialize mDNS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = mdns_hostname_set(hostname);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set mDNS hostname: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = mdns_instance_name_set("System Control");
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set mDNS instance name: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Add HTTP service
|
||||
err = mdns_service_add("System Control Web Server", "_http", "_tcp", s_config.port, NULL, 0);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to add mDNS HTTP service: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "mDNS initialized: %s.local", hostname);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
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 = 7;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
|
||||
|
||||
esp_err_t err = httpd_start(&s_server, &config);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// WebSocket-Handler explizit vor allen API-Handlern
|
||||
err = websocket_handler_init(s_server);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to initialize WebSocket handler: %s", esp_err_to_name(err));
|
||||
httpd_stop(s_server);
|
||||
s_server = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
// Register API handlers
|
||||
err = api_handlers_register(s_server);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to register API handlers: %s", esp_err_to_name(err));
|
||||
httpd_stop(s_server);
|
||||
s_server = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
// Common initialization
|
||||
common_init();
|
||||
|
||||
ESP_LOGI(TAG, "HTTP server started successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t api_server_start(const api_server_config_t *config)
|
||||
{
|
||||
if (s_server != NULL)
|
||||
{
|
||||
ESP_LOGW(TAG, "Server already running");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (config != NULL)
|
||||
{
|
||||
s_config = *config;
|
||||
}
|
||||
|
||||
initialize_storage();
|
||||
|
||||
// Initialize mDNS
|
||||
esp_err_t err = init_mdns(s_config.hostname);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
// Start web server
|
||||
err = start_webserver();
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
mdns_free();
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t api_server_stop(void)
|
||||
{
|
||||
if (s_server == NULL)
|
||||
{
|
||||
ESP_LOGW(TAG, "Server not running");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t err = httpd_stop(s_server);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to stop HTTP server: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
s_server = NULL;
|
||||
mdns_free();
|
||||
|
||||
ESP_LOGI(TAG, "Server stopped");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool api_server_is_running(void)
|
||||
{
|
||||
return s_server != NULL;
|
||||
}
|
||||
|
||||
httpd_handle_t api_server_get_handle(void)
|
||||
{
|
||||
return s_server;
|
||||
}
|
||||
|
||||
esp_err_t api_server_ws_broadcast(const char *message)
|
||||
{
|
||||
if (s_server == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return websocket_broadcast(s_server, message);
|
||||
}
|
||||
|
||||
esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
char buffer[256];
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"{\"type\":\"status\",\"on\":%s,\"mode\":\"%s\",\"schema\":\"%s\","
|
||||
"\"color\":{\"r\":%d,\"g\":%d,\"b\":%d}}",
|
||||
on ? "true" : "false", mode, schema, r, g, b);
|
||||
return api_server_ws_broadcast(buffer);
|
||||
}
|
||||
|
||||
esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "{\"type\":\"color\",\"r\":%d,\"g\":%d,\"b\":%d}", r, g, b);
|
||||
return api_server_ws_broadcast(buffer);
|
||||
}
|
||||
|
||||
esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi)
|
||||
{
|
||||
char buffer[128];
|
||||
if (connected && ip != NULL)
|
||||
{
|
||||
snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":true,\"ip\":\"%s\",\"rssi\":%d}", ip, rssi);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":false}");
|
||||
}
|
||||
return api_server_ws_broadcast(buffer);
|
||||
}
|
||||
79
firmware/components/api-server/src/common.c
Normal file
79
firmware/components/api-server/src/common.c
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "common.h"
|
||||
#include <cJSON.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "api_server.h"
|
||||
#include "color.h"
|
||||
#include "message_manager.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "simulator.h"
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
const char *system_time = NULL;
|
||||
rgb_t color = {0, 0, 0};
|
||||
|
||||
static void on_message_received(const message_t *msg)
|
||||
{
|
||||
if (msg->type == MESSAGE_TYPE_SIMULATION)
|
||||
{
|
||||
system_time = msg->data.simulation.time;
|
||||
color.red = msg->data.simulation.red;
|
||||
color.green = msg->data.simulation.green;
|
||||
color.blue = msg->data.simulation.blue;
|
||||
|
||||
cJSON *json = create_light_status_json();
|
||||
cJSON_AddStringToObject(json, "type", "status");
|
||||
char *response = cJSON_PrintUnformatted(json);
|
||||
cJSON_Delete(json);
|
||||
api_server_ws_broadcast(response);
|
||||
free(response);
|
||||
}
|
||||
}
|
||||
|
||||
void common_init(void)
|
||||
{
|
||||
message_manager_register_listener(on_message_received);
|
||||
}
|
||||
|
||||
// Gibt ein cJSON-Objekt with dem aktuellen Lichtstatus zurück
|
||||
cJSON *create_light_status_json(void)
|
||||
{
|
||||
persistence_manager_t pm;
|
||||
persistence_manager_init(&pm, "config");
|
||||
cJSON *json = cJSON_CreateObject();
|
||||
|
||||
bool light_active = persistence_manager_get_bool(&pm, "light_active", false);
|
||||
cJSON_AddBoolToObject(json, "on", light_active);
|
||||
|
||||
cJSON_AddBoolToObject(json, "thunder", false);
|
||||
|
||||
int mode = persistence_manager_get_int(&pm, "light_mode", 0);
|
||||
const char *mode_str = "simulation";
|
||||
if (mode == 1)
|
||||
{
|
||||
mode_str = "day";
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
mode_str = "night";
|
||||
}
|
||||
cJSON_AddStringToObject(json, "mode", mode_str);
|
||||
|
||||
int variant = persistence_manager_get_int(&pm, "light_variant", 3);
|
||||
char schema_filename[20];
|
||||
snprintf(schema_filename, sizeof(schema_filename), "schema_%02d.csv", variant);
|
||||
cJSON_AddStringToObject(json, "schema", schema_filename);
|
||||
|
||||
persistence_manager_deinit(&pm);
|
||||
|
||||
cJSON *c = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(c, "r", color.red);
|
||||
cJSON_AddNumberToObject(c, "g", color.green);
|
||||
cJSON_AddNumberToObject(c, "b", color.blue);
|
||||
cJSON_AddItemToObject(json, "color", c);
|
||||
|
||||
cJSON_AddStringToObject(json, "clock", system_time);
|
||||
|
||||
return json;
|
||||
}
|
||||
276
firmware/components/api-server/src/websocket_handler.c
Normal file
276
firmware/components/api-server/src/websocket_handler.c
Normal file
@@ -0,0 +1,276 @@
|
||||
#include "websocket_handler.h"
|
||||
#include "api_server.h"
|
||||
#include "common.h"
|
||||
|
||||
#include "message_manager.h"
|
||||
#include <esp_http_server.h>
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "websocket_handler";
|
||||
|
||||
// Store connected WebSocket client file descriptors
|
||||
static int ws_clients[WS_MAX_CLIENTS];
|
||||
static int ws_client_count = 0;
|
||||
|
||||
static void on_message_received(const message_t *msg)
|
||||
{
|
||||
cJSON *json = create_light_status_json();
|
||||
cJSON_AddStringToObject(json, "type", "status");
|
||||
char *response = cJSON_PrintUnformatted(json);
|
||||
cJSON_Delete(json);
|
||||
api_server_ws_broadcast(response);
|
||||
free(response);
|
||||
}
|
||||
|
||||
static void ws_clients_init(void)
|
||||
{
|
||||
for (int i = 0; i < WS_MAX_CLIENTS; i++)
|
||||
ws_clients[i] = -1;
|
||||
}
|
||||
|
||||
// Add a client to the list
|
||||
static bool add_client(int fd)
|
||||
{
|
||||
for (int i = 0; i < WS_MAX_CLIENTS; i++)
|
||||
{
|
||||
if (ws_clients[i] == -1)
|
||||
{
|
||||
ws_clients[i] = fd;
|
||||
ws_client_count++;
|
||||
ESP_LOGI(TAG, "WebSocket client connected: fd=%d (total: %d)", fd, ws_client_count);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Max WebSocket clients reached, cannot add fd=%d", fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove a client from the list
|
||||
static void remove_client(int fd)
|
||||
{
|
||||
for (int i = 0; i < WS_MAX_CLIENTS; i++)
|
||||
{
|
||||
if (ws_clients[i] == fd)
|
||||
{
|
||||
ws_clients[i] = -1;
|
||||
ws_client_count--;
|
||||
ESP_LOGI(TAG, "WebSocket client disconnected: fd=%d (total: %d)", fd, ws_client_count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket message
|
||||
static esp_err_t handle_ws_message(httpd_req_t *req, httpd_ws_frame_t *ws_pkt)
|
||||
{
|
||||
ESP_LOGI(TAG, "Received WS message: %s", (char *)ws_pkt->payload);
|
||||
|
||||
// Parse the message and handle different types
|
||||
// For now, we just check if it's a status request
|
||||
if (ws_pkt->payload != NULL && strstr((char *)ws_pkt->payload, "getStatus") != NULL)
|
||||
{
|
||||
// Status-JSON generieren
|
||||
cJSON *json = create_light_status_json();
|
||||
cJSON_AddStringToObject(json, "type", "status");
|
||||
char *response = cJSON_PrintUnformatted(json);
|
||||
cJSON_Delete(json);
|
||||
|
||||
httpd_ws_frame_t ws_resp = {.final = true,
|
||||
.fragmented = false,
|
||||
.type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t *)response,
|
||||
.len = strlen(response)};
|
||||
|
||||
esp_err_t ret = httpd_ws_send_frame(req, &ws_resp);
|
||||
free(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t websocket_handler(httpd_req_t *req)
|
||||
{
|
||||
if (req->method == HTTP_GET)
|
||||
{
|
||||
// This is the handshake
|
||||
ESP_LOGI(TAG, "WebSocket handshake");
|
||||
int fd = httpd_req_to_sockfd(req);
|
||||
if (!add_client(fd))
|
||||
{
|
||||
// Zu viele Clients, Verbindung schließen
|
||||
httpd_sess_trigger_close(req->handle, fd);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Receive the frame
|
||||
httpd_ws_frame_t ws_pkt;
|
||||
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
|
||||
|
||||
// Get frame length first
|
||||
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (ws_pkt.len > 0)
|
||||
{
|
||||
// Allocate buffer for payload
|
||||
ws_pkt.payload = malloc(ws_pkt.len + 1);
|
||||
if (ws_pkt.payload == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for WS payload");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// Receive the payload
|
||||
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "httpd_ws_recv_frame failed: %s", esp_err_to_name(ret));
|
||||
free(ws_pkt.payload);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Null-terminate the payload
|
||||
ws_pkt.payload[ws_pkt.len] = '\0';
|
||||
|
||||
// Handle the message
|
||||
ret = handle_ws_message(req, &ws_pkt);
|
||||
|
||||
free(ws_pkt.payload);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Handle close frame
|
||||
if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE)
|
||||
{
|
||||
int fd = httpd_req_to_sockfd(req);
|
||||
remove_client(fd);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Async send structure
|
||||
typedef struct
|
||||
{
|
||||
httpd_handle_t hd;
|
||||
int fd;
|
||||
char *message;
|
||||
} ws_async_arg_t;
|
||||
|
||||
// Async send work function
|
||||
static void ws_async_send(void *arg)
|
||||
{
|
||||
ws_async_arg_t *async_arg = (ws_async_arg_t *)arg;
|
||||
|
||||
httpd_ws_frame_t ws_pkt = {.final = true,
|
||||
.fragmented = false,
|
||||
.type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t *)async_arg->message,
|
||||
.len = strlen(async_arg->message)};
|
||||
|
||||
esp_err_t ret = httpd_ws_send_frame_async(async_arg->hd, async_arg->fd, &ws_pkt);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to send WS frame to fd=%d: %s", async_arg->fd, esp_err_to_name(ret));
|
||||
// Remove client on error
|
||||
remove_client(async_arg->fd);
|
||||
}
|
||||
|
||||
free(async_arg->message);
|
||||
free(async_arg);
|
||||
}
|
||||
|
||||
esp_err_t websocket_handler_init(httpd_handle_t server)
|
||||
{
|
||||
message_manager_register_listener(on_message_received);
|
||||
|
||||
ws_clients_init();
|
||||
// Register WebSocket URI handler
|
||||
httpd_uri_t ws_uri = {.uri = "/ws",
|
||||
.method = HTTP_GET,
|
||||
.handler = websocket_handler,
|
||||
.is_websocket = true,
|
||||
.handle_ws_control_frames = true,
|
||||
.user_ctx = NULL};
|
||||
|
||||
esp_err_t ret = httpd_register_uri_handler(server, &ws_uri);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to register WebSocket handler: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "WebSocket handler initialized at /ws");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message)
|
||||
{
|
||||
if (server == NULL || message == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ws_async_arg_t *arg = malloc(sizeof(ws_async_arg_t));
|
||||
if (arg == NULL)
|
||||
{
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
arg->hd = server;
|
||||
arg->fd = fd;
|
||||
arg->message = strdup(message);
|
||||
if (arg->message == NULL)
|
||||
{
|
||||
free(arg);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
esp_err_t ret = httpd_queue_work(server, ws_async_send, arg);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
free(arg->message);
|
||||
free(arg);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t websocket_broadcast(httpd_handle_t server, const char *message)
|
||||
{
|
||||
if (server == NULL || message == NULL)
|
||||
{
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
for (int i = 0; i < WS_MAX_CLIENTS; i++)
|
||||
{
|
||||
if (ws_clients[i] != -1)
|
||||
{
|
||||
esp_err_t send_ret = websocket_send(server, ws_clients[i], message);
|
||||
if (send_ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to queue WS message for fd=%d", ws_clients[i]);
|
||||
ret = send_ret; // Return last error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int websocket_get_client_count(void)
|
||||
{
|
||||
return ws_client_count;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ idf_component_register(SRCS
|
||||
src/ble/ble_connection.c
|
||||
src/ble/ble_scanner.c
|
||||
src/ble_manager.c
|
||||
src/dns_hijack.c
|
||||
src/wifi_manager.c
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
@@ -9,5 +10,7 @@ idf_component_register(SRCS
|
||||
driver
|
||||
nvs_flash
|
||||
esp_insights
|
||||
analytics
|
||||
led-manager
|
||||
api-server
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
void dns_server_start(const char *ap_ip);
|
||||
void dns_set_ap_ip(const char *ip);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
79
firmware/components/connectivity-manager/src/dns_hijack.c
Normal file
79
firmware/components/connectivity-manager/src/dns_hijack.c
Normal file
@@ -0,0 +1,79 @@
|
||||
// Minimaler DNS-Server für Captive Portal (alle Anfragen auf AP-IP)
|
||||
// Quelle: https://github.com/espressif/esp-idf/blob/master/examples/protocols/sntp/main/dns_server.c (angepasst)
|
||||
#include <arpa/inet.h>
|
||||
#include <esp_log.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <lwip/inet.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <netinet/in.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#define DNS_PORT 53
|
||||
#define DNS_MAX_LEN 512
|
||||
static const char *TAG = "dns_hijack";
|
||||
|
||||
static char s_ap_ip[16] = "192.168.4.1"; // Default AP-IP, ggf. dynamisch setzen
|
||||
|
||||
void dns_set_ap_ip(const char *ip)
|
||||
{
|
||||
strncpy(s_ap_ip, ip, sizeof(s_ap_ip) - 1);
|
||||
s_ap_ip[sizeof(s_ap_ip) - 1] = 0;
|
||||
}
|
||||
|
||||
static void dns_server_task(void *pvParameters)
|
||||
{
|
||||
int sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sock < 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to create socket");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
struct sockaddr_in server_addr = {0};
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_port = htons(DNS_PORT);
|
||||
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
|
||||
|
||||
uint8_t buf[DNS_MAX_LEN];
|
||||
while (1)
|
||||
{
|
||||
struct sockaddr_in from;
|
||||
socklen_t fromlen = sizeof(from);
|
||||
int len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&from, &fromlen);
|
||||
if (len < 0)
|
||||
continue;
|
||||
// DNS Header: 12 bytes, Antwort-Flag setzen
|
||||
buf[2] |= 0x80; // QR=1 (Antwort)
|
||||
buf[3] = 0x80; // RA=1, RCODE=0
|
||||
// Fragen: 1, Antworten: 1
|
||||
buf[7] = 1;
|
||||
// Antwort anhängen (Name Pointer auf Frage)
|
||||
int pos = len;
|
||||
buf[pos++] = 0xC0;
|
||||
buf[pos++] = 0x0C; // Name pointer
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x01; // Type A
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x01; // Class IN
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x3C; // TTL 60s
|
||||
buf[pos++] = 0x00;
|
||||
buf[pos++] = 0x04; // Data length
|
||||
inet_pton(AF_INET, s_ap_ip, &buf[pos]);
|
||||
pos += 4;
|
||||
sendto(sock, buf, pos, 0, (struct sockaddr *)&from, fromlen);
|
||||
}
|
||||
close(sock);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void dns_server_start(const char *ap_ip)
|
||||
{
|
||||
dns_set_ap_ip(ap_ip);
|
||||
xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 3, NULL);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "wifi_manager.h"
|
||||
#include "dns_hijack.h"
|
||||
|
||||
#include "analytics.h"
|
||||
#include "api_server.h"
|
||||
#include <esp_event.h>
|
||||
#include <esp_insights.h>
|
||||
#include <esp_log.h>
|
||||
@@ -11,219 +14,166 @@
|
||||
#include <led_status.h>
|
||||
#include <lwip/err.h>
|
||||
#include <lwip/sys.h>
|
||||
#include <mdns.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <persistence_manager.h>
|
||||
#include <sdkconfig.h>
|
||||
#include <string.h>
|
||||
|
||||
// Event group to signal when we are connected
|
||||
// Event group to signal WiFi connection status
|
||||
static EventGroupHandle_t s_wifi_event_group;
|
||||
|
||||
// The bits for the event group
|
||||
// Event group bits
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
static const char *TAG = "wifi_manager";
|
||||
|
||||
static int s_retry_num = 0;
|
||||
static int s_current_network_index = 0;
|
||||
|
||||
// WiFi network configuration structure
|
||||
typedef struct
|
||||
static void led_status_reconnect()
|
||||
{
|
||||
const char *ssid;
|
||||
const char *password;
|
||||
} wifi_network_config_t;
|
||||
|
||||
// Array of configured WiFi networks
|
||||
static const wifi_network_config_t s_wifi_networks[] = {
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
{CONFIG_WIFI_SSID_1, CONFIG_WIFI_PASSWORD_1},
|
||||
#if CONFIG_WIFI_NETWORK_COUNT >= 2
|
||||
{CONFIG_WIFI_SSID_2, CONFIG_WIFI_PASSWORD_2},
|
||||
#endif
|
||||
#if CONFIG_WIFI_NETWORK_COUNT >= 3
|
||||
{CONFIG_WIFI_SSID_3, CONFIG_WIFI_PASSWORD_3},
|
||||
#endif
|
||||
#if CONFIG_WIFI_NETWORK_COUNT >= 4
|
||||
{CONFIG_WIFI_SSID_4, CONFIG_WIFI_PASSWORD_4},
|
||||
#endif
|
||||
#if CONFIG_WIFI_NETWORK_COUNT >= 5
|
||||
{CONFIG_WIFI_SSID_5, CONFIG_WIFI_PASSWORD_5},
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
static const int s_wifi_network_count = sizeof(s_wifi_networks) / sizeof(s_wifi_networks[0]);
|
||||
|
||||
static void try_next_network(void);
|
||||
|
||||
static void connect_to_network(int index)
|
||||
{
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
if (index >= s_wifi_network_count)
|
||||
{
|
||||
ESP_LOGE(TAG, "No more networks to try");
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
return;
|
||||
}
|
||||
|
||||
const wifi_network_config_t *network = &s_wifi_networks[index];
|
||||
|
||||
// Skip empty SSIDs
|
||||
if (network->ssid == NULL || strlen(network->ssid) == 0)
|
||||
{
|
||||
ESP_LOGW(TAG, "Skipping empty SSID at index %d", index);
|
||||
s_current_network_index++;
|
||||
s_retry_num = 0;
|
||||
try_next_network();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_DIAG_EVENT(TAG, "Trying to connect to network %d: %s", index + 1, network->ssid);
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta =
|
||||
{
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
|
||||
strncpy((char *)wifi_config.sta.ssid, network->ssid, sizeof(wifi_config.sta.ssid) - 1);
|
||||
strncpy((char *)wifi_config.sta.password, network->password, sizeof(wifi_config.sta.password) - 1);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
esp_wifi_connect();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void try_next_network(void)
|
||||
{
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
s_current_network_index++;
|
||||
s_retry_num = 0;
|
||||
|
||||
if (s_current_network_index < s_wifi_network_count)
|
||||
{
|
||||
connect_to_network(s_current_network_index);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to any configured network");
|
||||
led_behavior_t led0_behavior = {
|
||||
led_behavior_t led_behavior = {
|
||||
.on_time_ms = 250,
|
||||
.off_time_ms = 100,
|
||||
.color = {.red = 50, .green = 50, .blue = 0},
|
||||
.index = 0,
|
||||
.mode = LED_MODE_BLINK,
|
||||
.color = {.red = 50, .green = 0, .blue = 0},
|
||||
.on_time_ms = 1000,
|
||||
.off_time_ms = 500,
|
||||
};
|
||||
led_status_set_behavior(led0_behavior);
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
}
|
||||
#endif
|
||||
led_status_set_behavior(led_behavior);
|
||||
}
|
||||
|
||||
static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
||||
{
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
|
||||
{
|
||||
led_behavior_t led0_behavior = {
|
||||
.index = 0,
|
||||
.mode = LED_MODE_BLINK,
|
||||
.color = {.red = 50, .green = 50, .blue = 0},
|
||||
.on_time_ms = 200,
|
||||
.off_time_ms = 200,
|
||||
};
|
||||
led_status_set_behavior(led0_behavior);
|
||||
|
||||
connect_to_network(s_current_network_index);
|
||||
ESP_LOGI(TAG, "WIFI_EVENT_STA_START: Connecting to AP...");
|
||||
esp_wifi_connect();
|
||||
}
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
|
||||
{
|
||||
if (s_retry_num < CONFIG_WIFI_CONNECT_RETRIES)
|
||||
{
|
||||
led_behavior_t led0_behavior = {
|
||||
.index = 0,
|
||||
.mode = LED_MODE_BLINK,
|
||||
.color = {.red = 50, .green = 50, .blue = 0},
|
||||
.on_time_ms = 200,
|
||||
.off_time_ms = 200,
|
||||
};
|
||||
led_status_set_behavior(led0_behavior);
|
||||
|
||||
s_retry_num++;
|
||||
ESP_DIAG_EVENT(TAG, "Retrying network %d (%d/%d)", s_current_network_index + 1, s_retry_num,
|
||||
CONFIG_WIFI_CONNECT_RETRIES);
|
||||
ESP_LOGW(TAG, "WIFI_EVENT_STA_DISCONNECTED: Verbindung verloren, versuche erneut...");
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
led_status_reconnect();
|
||||
esp_wifi_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retries exhausted for current network, try next one
|
||||
ESP_LOGW(TAG, "Failed to connect to network %d after %d retries, trying next...", s_current_network_index + 1,
|
||||
CONFIG_WIFI_CONNECT_RETRIES);
|
||||
try_next_network();
|
||||
}
|
||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
|
||||
{
|
||||
led_behavior_t led0_behavior = {
|
||||
.index = 0,
|
||||
.mode = LED_MODE_SOLID,
|
||||
.color = {.red = 0, .green = 50, .blue = 0},
|
||||
};
|
||||
led_status_set_behavior(led0_behavior);
|
||||
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_DIAG_EVENT(TAG, "Got IP address:" IPSTR " (network %d: %s)", IP2STR(&event->ip_info.ip),
|
||||
s_current_network_index + 1, s_wifi_networks[s_current_network_index].ssid);
|
||||
s_retry_num = 0;
|
||||
ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP: IP-Adresse erhalten: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
analytics_init();
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
#endif
|
||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP)
|
||||
{
|
||||
ESP_LOGW(TAG, "IP_EVENT_STA_LOST_IP: IP-Adresse verloren! Versuche Reconnect...");
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
led_status_reconnect();
|
||||
esp_wifi_connect();
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_create_ap()
|
||||
{
|
||||
ESP_ERROR_CHECK(esp_wifi_stop());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
wifi_config_t ap_config = {.ap = {.ssid = "system-control",
|
||||
.ssid_len = strlen("system-control"),
|
||||
.password = "",
|
||||
.max_connection = 4,
|
||||
.authmode = WIFI_AUTH_OPEN}};
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_LOGI(TAG, "Access Point 'system-control' started");
|
||||
dns_server_start("192.168.4.1");
|
||||
|
||||
led_behavior_t led_behavior = {
|
||||
.color = {.red = 50, .green = 0, .blue = 0},
|
||||
.index = 0,
|
||||
.mode = LED_MODE_SOLID,
|
||||
};
|
||||
led_status_set_behavior(led_behavior);
|
||||
}
|
||||
|
||||
void wifi_manager_init()
|
||||
{
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
s_current_network_index = 0;
|
||||
s_retry_num = 0;
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Default WiFi Station
|
||||
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Event Handler registrieren
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
|
||||
|
||||
// Try to load stored WiFi configuration
|
||||
persistence_manager_t pm;
|
||||
char ssid[33] = {0};
|
||||
char password[65] = {0};
|
||||
bool have_ssid = false, have_password = false;
|
||||
if (persistence_manager_init(&pm, "wifi_config") == ESP_OK)
|
||||
{
|
||||
persistence_manager_get_string(&pm, "ssid", ssid, sizeof(ssid), "");
|
||||
persistence_manager_get_string(&pm, "password", password, sizeof(password), "");
|
||||
have_ssid = strlen(ssid) > 0;
|
||||
have_password = strlen(password) > 0;
|
||||
}
|
||||
|
||||
if (have_ssid && have_password)
|
||||
{
|
||||
led_status_reconnect();
|
||||
|
||||
ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid);
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
|
||||
ESP_ERROR_CHECK(
|
||||
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
wifi_config_t wifi_config = {0};
|
||||
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
|
||||
strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_DIAG_EVENT(TAG, "WiFi manager initialized with %d network(s), waiting for connection...", s_wifi_network_count);
|
||||
|
||||
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or
|
||||
connection failed for all networks (WIFI_FAIL_BIT). The bits are set by event_handler() */
|
||||
EventBits_t bits =
|
||||
xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
|
||||
|
||||
int retries = 0;
|
||||
EventBits_t bits;
|
||||
do
|
||||
{
|
||||
ESP_LOGI(TAG, "Warte auf IP-Adresse (DHCP)...");
|
||||
bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
|
||||
10000 / portTICK_PERIOD_MS);
|
||||
if (bits & WIFI_CONNECTED_BIT)
|
||||
{
|
||||
ESP_DIAG_EVENT(TAG, "Connected to AP SSID:%s", s_wifi_networks[s_current_network_index].ssid);
|
||||
led_behavior_t led_behavior = {
|
||||
.index = 0,
|
||||
.color = {.red = 0, .green = 50, .blue = 0},
|
||||
.mode = LED_MODE_SOLID,
|
||||
};
|
||||
led_status_set_behavior(led_behavior);
|
||||
ESP_LOGI(TAG, "WiFi connection established successfully (mit IP)");
|
||||
break;
|
||||
}
|
||||
else if (bits & WIFI_FAIL_BIT)
|
||||
retries++;
|
||||
} while (!(bits & WIFI_CONNECTED_BIT) && retries < CONFIG_WIFI_CONNECT_RETRIES);
|
||||
|
||||
if (!(bits & WIFI_CONNECTED_BIT))
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to any configured WiFi network");
|
||||
ESP_LOGW(TAG, "WiFi connection failed (keine IP?), switching to Access Point mode");
|
||||
// AP-Netzwerkschnittstelle initialisieren, falls noch nicht geschehen
|
||||
esp_netif_create_default_wifi_ap();
|
||||
wifi_create_ap();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Unexpected event");
|
||||
// Create Access Point
|
||||
esp_netif_create_default_wifi_ap();
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
wifi_create_ap();
|
||||
}
|
||||
#endif
|
||||
|
||||
// API server start
|
||||
api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(api_server_start(&s_config));
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ idf_component_register(SRCS
|
||||
led-manager
|
||||
persistence-manager
|
||||
simulator
|
||||
message-manager
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
// Project-specific headers
|
||||
#include "common/Widget.h"
|
||||
#include "IPersistenceManager.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "u8g2.h"
|
||||
|
||||
class MenuItem;
|
||||
@@ -61,14 +61,8 @@ typedef struct
|
||||
std::function<void(ButtonType button)> onButtonClicked;
|
||||
|
||||
/**
|
||||
* @brief Shared pointer to platform-independent persistence manager
|
||||
* @details This provides access to persistent key-value storage across different
|
||||
* platforms. The actual implementation (SDL3 or ESP32/NVS) is determined
|
||||
* at compile time based on the target platform.
|
||||
*
|
||||
* @note The persistence manager is shared across all menu widgets and maintains
|
||||
* its state throughout the application lifecycle.
|
||||
* @brief Zeiger auf C-Persistence-Manager-Instanz
|
||||
*/
|
||||
std::shared_ptr<IPersistenceManager> persistenceManager;
|
||||
persistence_manager_t *persistenceManager;
|
||||
|
||||
} menu_options_t;
|
||||
@@ -59,5 +59,37 @@ class MenuItem;
|
||||
*/
|
||||
typedef std::function<void(MenuItem, ButtonType)> ButtonCallback;
|
||||
|
||||
/**
|
||||
* @def IMPLEMENT_GET_NAME
|
||||
* @brief Macro to implement getName() method using __FILE__
|
||||
* @details Extracts the class name from the source filename automatically.
|
||||
* Use this macro in .cpp files to implement Widget::getName().
|
||||
* @param ClassName The class name for the method scope (e.g., MainMenu)
|
||||
*/
|
||||
#define IMPLEMENT_GET_NAME(ClassName) \
|
||||
const char *ClassName::getName() const \
|
||||
{ \
|
||||
static const char *cachedName = nullptr; \
|
||||
if (!cachedName) \
|
||||
{ \
|
||||
const char *file = __FILE__; \
|
||||
const char *lastSlash = file; \
|
||||
for (const char *p = file; *p; ++p) \
|
||||
{ \
|
||||
if (*p == '/' || *p == '\\') \
|
||||
lastSlash = p + 1; \
|
||||
} \
|
||||
static char buffer[64]; \
|
||||
size_t i = 0; \
|
||||
for (; lastSlash[i] && lastSlash[i] != '.' && i < sizeof(buffer) - 1; ++i) \
|
||||
{ \
|
||||
buffer[i] = lastSlash[i]; \
|
||||
} \
|
||||
buffer[i] = '\0'; \
|
||||
cachedName = buffer; \
|
||||
} \
|
||||
return cachedName; \
|
||||
}
|
||||
|
||||
// Include MenuItem.h after the typedef to avoid circular dependency
|
||||
#include "data/MenuItem.h"
|
||||
@@ -175,6 +175,21 @@ class Menu : public Widget
|
||||
*/
|
||||
void toggle(const MenuItem &menuItem);
|
||||
|
||||
/**
|
||||
* @brief Setzt den Zustand eines Toggle-Menüeintrags explizit
|
||||
* @param menuItem Der zu ändernde Toggle-Menüeintrag
|
||||
* @param state Neuer Zustand (true = aktiviert, false = deaktiviert)
|
||||
*
|
||||
* @pre menuItem muss vom Typ TOGGLE sein
|
||||
* @post Der Wert des Menüeintrags wird auf den angegebenen Zustand gesetzt
|
||||
*
|
||||
* @details Diese Methode setzt den Wert eines Toggle-Menüeintrags gezielt auf den gewünschten Zustand.
|
||||
* Der geänderte Eintrag ersetzt das Original in der internen Menüstruktur.
|
||||
*
|
||||
* @note Diese Methode verändert direkt den internen Zustand des Menüs.
|
||||
*/
|
||||
void setToggle(const MenuItem &menuItem, const bool state);
|
||||
|
||||
/**
|
||||
* @brief Changes the selected value of a selection menu item based on button input
|
||||
* @param menuItem The selection menu item to modify
|
||||
@@ -191,6 +206,8 @@ class Menu : public Widget
|
||||
*/
|
||||
MenuItem switchValue(const MenuItem &menuItem, ButtonType button);
|
||||
|
||||
void setSelectionIndex(const MenuItem &menuItem, int index);
|
||||
|
||||
private:
|
||||
MenuItem replaceItem(int index, const MenuItem &item);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "u8g2.h"
|
||||
|
||||
#include "common/Common.h"
|
||||
#include "message_manager.h"
|
||||
|
||||
/**
|
||||
* @class Widget
|
||||
@@ -49,7 +50,9 @@ class Widget
|
||||
* @details Ensures that derived class destructors are called correctly when
|
||||
* a widget is destroyed through a base class pointer.
|
||||
*/
|
||||
virtual ~Widget() = default;
|
||||
virtual ~Widget();
|
||||
|
||||
virtual void onMessageReceived(const message_t *msg);
|
||||
|
||||
/**
|
||||
* @brief Called when the widget becomes active or enters the foreground
|
||||
@@ -150,6 +153,19 @@ class Widget
|
||||
*/
|
||||
virtual void OnButtonClicked(ButtonType button);
|
||||
|
||||
/**
|
||||
* @brief Returns the name of this widget for diagnostic purposes
|
||||
* @return A string identifying the widget type
|
||||
*
|
||||
* @details This method returns a human-readable name for the widget which
|
||||
* is used for logging and diagnostic events. Derived classes should
|
||||
* override this method to return their specific screen/widget name.
|
||||
*
|
||||
* @note The base implementation returns "Widget". Override in derived classes
|
||||
* to provide meaningful screen names for diagnostics.
|
||||
*/
|
||||
virtual const char *getName() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Pointer to the u8g2 display context used for rendering operations
|
||||
@@ -165,4 +181,8 @@ class Widget
|
||||
* the u8g2 context and assumes it is managed externally.
|
||||
*/
|
||||
u8g2_t *u8g2;
|
||||
|
||||
private:
|
||||
static std::vector<Widget *> s_instances;
|
||||
static void globalMessageCallback(const message_t *msg);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ class ClockScreenSaver final : public Widget
|
||||
void Update(uint64_t dt) override;
|
||||
void Render() override;
|
||||
void OnButtonClicked(ButtonType button) override;
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
static constexpr int MOVE_INTERVAL = 50; // milliseconds between movements
|
||||
|
||||
@@ -7,6 +7,8 @@ class ExternalDevices final : public Menu
|
||||
public:
|
||||
explicit ExternalDevices(menu_options_t *options);
|
||||
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
void onButtonPressed(const MenuItem &menuItem, ButtonType button) override;
|
||||
menu_options_t *m_options;
|
||||
|
||||
@@ -80,6 +80,8 @@ class LightMenu final : public Menu
|
||||
*/
|
||||
explicit LightMenu(menu_options_t *options);
|
||||
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Handles button press events specific to light control menu items
|
||||
@@ -118,6 +120,8 @@ class LightMenu final : public Menu
|
||||
*/
|
||||
void onButtonPressed(const MenuItem &menuItem, ButtonType button) override;
|
||||
|
||||
void onMessageReceived(const message_t *msg);
|
||||
|
||||
/**
|
||||
* @brief Pointer to menu options configuration structure
|
||||
* @details Stores a reference to the menu configuration passed during construction.
|
||||
|
||||
@@ -57,6 +57,8 @@ class MainMenu final : public Menu
|
||||
*/
|
||||
explicit MainMenu(menu_options_t *options);
|
||||
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Handles button press events specific to main menu items
|
||||
|
||||
@@ -26,6 +26,7 @@ class ScreenSaver final : public Widget
|
||||
void Update(uint64_t dt) override;
|
||||
void Render() override;
|
||||
void OnButtonClicked(ButtonType button) override;
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -74,4 +74,6 @@ public:
|
||||
* @see Menu::Menu for base class construction details
|
||||
*/
|
||||
explicit SettingsMenu(menu_options_t *options);
|
||||
|
||||
const char *getName() const override;
|
||||
};
|
||||
@@ -151,6 +151,8 @@ class SplashScreen final : public Widget
|
||||
*/
|
||||
void Render() override;
|
||||
|
||||
const char *getName() const override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Pointer to menu options configuration structure
|
||||
|
||||
@@ -28,7 +28,7 @@ constexpr int BOTTOM_OFFSET = 10;
|
||||
|
||||
Menu::Menu(menu_options_t *options) : Widget(options->u8g2), m_options(options)
|
||||
{
|
||||
// Set up button callback using lambda to forward to member function
|
||||
// Set up button callback using a lambda to forward to the member function
|
||||
m_options->onButtonClicked = [this](const ButtonType button) { OnButtonClicked(button); };
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ void Menu::setItemSize(const size_t size, int8_t startIndex)
|
||||
constexpr int key_length = 20;
|
||||
char key[key_length] = "";
|
||||
snprintf(key, key_length, "section_%zu", i + 1 - startIndex);
|
||||
index = m_options->persistenceManager->GetValue(key, index);
|
||||
index = persistence_manager_get_int(m_options->persistenceManager, key, index);
|
||||
}
|
||||
addSelection(i + 1, caption, m_items.at(0).getValues(), index);
|
||||
}
|
||||
@@ -82,6 +82,12 @@ void Menu::toggle(const MenuItem &menuItem)
|
||||
replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
|
||||
void Menu::setToggle(const MenuItem &menuItem, const bool state)
|
||||
{
|
||||
const auto item = menuItem.copyWith(state ? std::to_string(true) : std::to_string(false));
|
||||
replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
|
||||
MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button)
|
||||
{
|
||||
MenuItem result = menuItem;
|
||||
@@ -120,6 +126,15 @@ MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button)
|
||||
return result;
|
||||
}
|
||||
|
||||
void Menu::setSelectionIndex(const MenuItem &menuItem, int index)
|
||||
{
|
||||
if (index >= 0 && index < menuItem.getItemCount())
|
||||
{
|
||||
auto item = menuItem.copyWith(index);
|
||||
replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem Menu::replaceItem(const int index, const MenuItem &item)
|
||||
{
|
||||
m_items.at(index) = item;
|
||||
@@ -134,13 +149,13 @@ void Menu::Render()
|
||||
m_selected_item = 0;
|
||||
}
|
||||
|
||||
// Early return if no items to render
|
||||
// Early return if there are no items to render
|
||||
if (m_items.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear screen with black background
|
||||
// Clear the screen with a black background
|
||||
u8g2_SetDrawColor(u8g2, 0);
|
||||
u8g2_DrawBox(u8g2, 0, 0, u8g2->width, u8g2->height);
|
||||
|
||||
@@ -151,7 +166,7 @@ void Menu::Render()
|
||||
drawScrollBar();
|
||||
drawSelectionBox();
|
||||
|
||||
// Calculate center position for main item
|
||||
// Calculate center position for the main item
|
||||
const int centerY = u8g2->height / 2 + 3;
|
||||
|
||||
// Render the currently selected item (main/center item)
|
||||
@@ -176,7 +191,7 @@ void Menu::Render()
|
||||
|
||||
void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x, const int y) const
|
||||
{
|
||||
// Set font and draw main text
|
||||
// Set font and draw the main text
|
||||
u8g2_SetFont(u8g2, font);
|
||||
u8g2_DrawStr(u8g2, x, y, item->getText().c_str());
|
||||
|
||||
@@ -206,7 +221,7 @@ void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x,
|
||||
}
|
||||
|
||||
case MenuItemTypes::TOGGLE: {
|
||||
// Draw checkbox frame
|
||||
// Draw the checkbox frame
|
||||
const int frameX = u8g2->width - UIConstants::FRAME_BOX_SIZE - UIConstants::SELECTION_MARGIN;
|
||||
const int frameY = y - UIConstants::FRAME_OFFSET;
|
||||
u8g2_DrawFrame(u8g2, frameX, frameY, UIConstants::FRAME_BOX_SIZE, UIConstants::FRAME_BOX_SIZE);
|
||||
@@ -272,7 +287,7 @@ void Menu::onPressedDown()
|
||||
if (m_items.empty())
|
||||
return;
|
||||
|
||||
// Wrap around to first item when at the end
|
||||
// Wrap around to the first item when at the end
|
||||
m_selected_item = (m_selected_item + 1) % m_items.size();
|
||||
}
|
||||
|
||||
@@ -281,7 +296,7 @@ void Menu::onPressedUp()
|
||||
if (m_items.empty())
|
||||
return;
|
||||
|
||||
// Wrap around to last item when at the beginning
|
||||
// Wrap around to the last item when at the beginning
|
||||
m_selected_item = (m_selected_item == 0) ? m_items.size() - 1 : m_selected_item - 1;
|
||||
}
|
||||
|
||||
@@ -314,7 +329,7 @@ void Menu::onPressedSelect() const
|
||||
|
||||
void Menu::onPressedBack() const
|
||||
{
|
||||
// Navigate back to previous screen if callback is available
|
||||
// Navigate back to the previous screen if callback is available
|
||||
if (m_options && m_options->popScreen)
|
||||
{
|
||||
m_options->popScreen();
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#include "common/Widget.h"
|
||||
#include <algorithm>
|
||||
|
||||
std::vector<Widget *> Widget::s_instances;
|
||||
|
||||
Widget::Widget(u8g2_t *u8g2) : u8g2(u8g2)
|
||||
{
|
||||
s_instances.push_back(this);
|
||||
if (s_instances.size() == 1)
|
||||
{
|
||||
message_manager_register_listener(globalMessageCallback);
|
||||
}
|
||||
}
|
||||
|
||||
Widget::~Widget()
|
||||
{
|
||||
s_instances.erase(std::remove(s_instances.begin(), s_instances.end(), this), s_instances.end());
|
||||
}
|
||||
|
||||
void Widget::onEnter()
|
||||
@@ -31,3 +44,20 @@ void Widget::Render()
|
||||
void Widget::OnButtonClicked(ButtonType button)
|
||||
{
|
||||
}
|
||||
|
||||
const char *Widget::getName() const
|
||||
{
|
||||
return "Widget";
|
||||
}
|
||||
|
||||
void Widget::onMessageReceived(const message_t *msg)
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::globalMessageCallback(const message_t *msg)
|
||||
{
|
||||
for (auto *w : s_instances)
|
||||
{
|
||||
w->onMessageReceived(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "ui/ClockScreenSaver.h"
|
||||
#include "hal_esp32/PersistenceManager.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "simulator.h"
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
@@ -38,13 +38,14 @@ void ClockScreenSaver::updateTextDimensions()
|
||||
|
||||
void ClockScreenSaver::getCurrentTimeString(char *buffer, size_t bufferSize) const
|
||||
{
|
||||
if (m_options && m_options->persistenceManager->GetValue("light_active", false) &&
|
||||
m_options->persistenceManager->GetValue("light_mode", 0) == 0)
|
||||
if (m_options && m_options->persistenceManager &&
|
||||
persistence_manager_get_bool(m_options->persistenceManager, "light_active", false) &&
|
||||
persistence_manager_get_int(m_options->persistenceManager, "light_mode", 0) == 0)
|
||||
{
|
||||
char *simulated_time = get_time();
|
||||
if (simulated_time != nullptr)
|
||||
{
|
||||
strncpy(buffer, simulated_time, bufferSize);
|
||||
snprintf(buffer, bufferSize, "%s Uhr", simulated_time);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -132,3 +133,5 @@ void ClockScreenSaver::OnButtonClicked(ButtonType button)
|
||||
m_options->popScreen();
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(ClockScreenSaver)
|
||||
|
||||
@@ -23,3 +23,5 @@ void ExternalDevices::onButtonPressed(const MenuItem &menuItem, const ButtonType
|
||||
ble_connect_to_device(menuItem.getId());
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(ExternalDevices)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "ui/LightMenu.h"
|
||||
|
||||
#include "led_strip_ws2812.h"
|
||||
#include "message_manager.h"
|
||||
#include "simulator.h"
|
||||
#include <cstring>
|
||||
|
||||
/**
|
||||
* @namespace LightMenuItem
|
||||
@@ -27,7 +28,7 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
|
||||
bool active = false;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
active = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_ACTIVE, active);
|
||||
active = persistence_manager_get_bool(m_options->persistenceManager, LightMenuOptions::LIGHT_ACTIVE, active);
|
||||
}
|
||||
addToggle(LightMenuItem::ACTIVATE, "Einschalten", active);
|
||||
|
||||
@@ -39,7 +40,8 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
|
||||
int mode_value = 0;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
mode_value = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_MODE, mode_value);
|
||||
mode_value =
|
||||
persistence_manager_get_int(m_options->persistenceManager, LightMenuOptions::LIGHT_MODE, mode_value);
|
||||
}
|
||||
addSelection(LightMenuItem::MODE, "Modus", items, mode_value);
|
||||
|
||||
@@ -47,10 +49,12 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
|
||||
variants.emplace_back("1");
|
||||
variants.emplace_back("2");
|
||||
variants.emplace_back("3");
|
||||
int variant_value = 2;
|
||||
int variant_value = 3;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
variant_value = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_VARIANT, variant_value);
|
||||
variant_value =
|
||||
persistence_manager_get_int(m_options->persistenceManager, LightMenuOptions::LIGHT_VARIANT, variant_value) -
|
||||
1;
|
||||
}
|
||||
addSelection(LightMenuItem::VARIANT, "Variante", variants, variant_value);
|
||||
}
|
||||
@@ -68,12 +72,13 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
|
||||
{
|
||||
toggle(menuItem);
|
||||
const auto value = getItem(menuItem.getId()).getValue() == "1";
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_ACTIVE, value);
|
||||
}
|
||||
|
||||
start_simulation();
|
||||
// Post change via message_manager
|
||||
message_t msg = {};
|
||||
msg.type = MESSAGE_TYPE_SETTINGS;
|
||||
msg.data.settings.type = SETTINGS_TYPE_BOOL;
|
||||
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_ACTIVE, sizeof(msg.data.settings.key) - 1);
|
||||
msg.data.settings.value.bool_value = value;
|
||||
message_manager_post(&msg, pdMS_TO_TICKS(100));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -86,11 +91,14 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
|
||||
const auto value = getItem(item.getId()).getIndex();
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_MODE, value);
|
||||
m_options->persistenceManager->Save();
|
||||
// Post change via message_manager
|
||||
message_t msg = {};
|
||||
msg.type = MESSAGE_TYPE_SETTINGS;
|
||||
msg.data.settings.type = SETTINGS_TYPE_INT;
|
||||
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_MODE, sizeof(msg.data.settings.key) - 1);
|
||||
msg.data.settings.value.int_value = value;
|
||||
message_manager_post(&msg, pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
start_simulation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -103,11 +111,14 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
|
||||
const auto value = getItem(item.getId()).getIndex() + 1;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_VARIANT, value);
|
||||
m_options->persistenceManager->Save();
|
||||
// Post change via message_manager
|
||||
message_t msg = {};
|
||||
msg.type = MESSAGE_TYPE_SETTINGS;
|
||||
msg.data.settings.type = SETTINGS_TYPE_INT;
|
||||
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_VARIANT, sizeof(msg.data.settings.key) - 1);
|
||||
msg.data.settings.value.int_value = value;
|
||||
message_manager_post(&msg, pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
start_simulation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -123,3 +134,26 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
|
||||
m_options->pushScreen(widget);
|
||||
}
|
||||
}
|
||||
|
||||
void LightMenu::onMessageReceived(const message_t *msg)
|
||||
{
|
||||
// Here you can react to messages, e.g. set toggle status
|
||||
// Example: If light_active was changed, synchronize toggle
|
||||
if (msg && msg->type == MESSAGE_TYPE_SETTINGS)
|
||||
{
|
||||
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_ACTIVE) == 0)
|
||||
{
|
||||
setToggle(getItem(LightMenuItem::ACTIVATE), msg->data.settings.value.bool_value);
|
||||
}
|
||||
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_MODE) == 0)
|
||||
{
|
||||
setSelectionIndex(getItem(LightMenuItem::MODE), msg->data.settings.value.int_value);
|
||||
}
|
||||
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_VARIANT) == 0)
|
||||
{
|
||||
setSelectionIndex(getItem(LightMenuItem::VARIANT), msg->data.settings.value.int_value - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(LightMenu)
|
||||
|
||||
@@ -48,3 +48,5 @@ void MainMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(MainMenu)
|
||||
|
||||
@@ -327,3 +327,5 @@ void ScreenSaver::OnButtonClicked(ButtonType button)
|
||||
m_options->popScreen();
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(ScreenSaver)
|
||||
|
||||
@@ -9,3 +9,5 @@ SettingsMenu::SettingsMenu(menu_options_t *options) : Menu(options)
|
||||
{
|
||||
addText(SettingsMenuItem::OTA_UPLOAD, "OTA Einspielen");
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(SettingsMenu)
|
||||
|
||||
@@ -28,3 +28,5 @@ void SplashScreen::Render()
|
||||
u8g2_SetFont(u8g2, u8g2_font_haxrcorp4089_tr);
|
||||
u8g2_DrawStr(u8g2, 35, 50, "Initialisierung...");
|
||||
}
|
||||
|
||||
IMPLEMENT_GET_NAME(SplashScreen)
|
||||
|
||||
@@ -12,9 +12,9 @@ typedef struct
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t h;
|
||||
uint8_t s;
|
||||
uint8_t v;
|
||||
float h;
|
||||
float s;
|
||||
float v;
|
||||
} hsv_t;
|
||||
|
||||
__BEGIN_DECLS
|
||||
|
||||
16
firmware/components/led-manager/include/led_segment.h
Normal file
16
firmware/components/led-manager/include/led_segment.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define LED_SEGMENT_MAX_LEN 15
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char name[32];
|
||||
uint16_t start;
|
||||
uint16_t leds;
|
||||
} led_segment_t;
|
||||
|
||||
led_segment_t segments[LED_SEGMENT_MAX_LEN];
|
||||
size_t segment_count;
|
||||
@@ -45,10 +45,10 @@ rgb_t interpolate_color_hsv(rgb_t start, rgb_t end, float factor)
|
||||
|
||||
// Interpolate HSV values
|
||||
hsv_t interpolated_hsv;
|
||||
interpolated_hsv.h = fmod(h1 + (h2 - h1) * factor, 360.0);
|
||||
if (interpolated_hsv.h < 0)
|
||||
interpolated_hsv.h = fmodf(h1 + (h2 - h1) * factor, 360.0f);
|
||||
if (interpolated_hsv.h < 0.0f)
|
||||
{
|
||||
interpolated_hsv.h += 360.0;
|
||||
interpolated_hsv.h += 360.0f;
|
||||
}
|
||||
interpolated_hsv.s = start_hsv.s + (end_hsv.s - start_hsv.s) * factor;
|
||||
interpolated_hsv.v = start_hsv.v + (end_hsv.v - start_hsv.v) * factor;
|
||||
|
||||
@@ -97,22 +97,21 @@ esp_err_t led_status_init(int gpio_num)
|
||||
.max_leds = STATUS_LED_COUNT,
|
||||
.led_model = LED_MODEL_WS2812,
|
||||
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRBW,
|
||||
.flags =
|
||||
{
|
||||
.invert_out = false,
|
||||
},
|
||||
.flags = {.invert_out = 0},
|
||||
};
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||
.mem_block_symbols = 0,
|
||||
.flags =
|
||||
{
|
||||
.with_dma = false,
|
||||
},
|
||||
.flags = {.with_dma = 0},
|
||||
};
|
||||
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
ESP_LOGI(TAG, "LED strip initialized.");
|
||||
esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to init status LED: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
ESP_LOGI(TAG, "Status LED initialized.");
|
||||
|
||||
// Create mutex
|
||||
mutex = xSemaphoreCreateMutex();
|
||||
|
||||
@@ -72,17 +72,21 @@ esp_err_t led_strip_init(void)
|
||||
.max_leds = MAX_LEDS,
|
||||
.led_model = LED_MODEL_WS2812,
|
||||
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
|
||||
.flags = {.invert_out = false},
|
||||
.flags = {.invert_out = 0},
|
||||
};
|
||||
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 0,
|
||||
.mem_block_symbols = 0,
|
||||
.flags = {.with_dma = true},
|
||||
.flags = {.with_dma = 0},
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to init main LED strip: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
led_command_queue = xQueueCreate(5, sizeof(led_command_t));
|
||||
if (led_command_queue == NULL)
|
||||
|
||||
8
firmware/components/message-manager/CMakeLists.txt
Normal file
8
firmware/components/message-manager/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
idf_component_register(
|
||||
SRCS "src/message_manager.c"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
persistence-manager
|
||||
my_mqtt_client
|
||||
app_update
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
typedef enum
|
||||
{
|
||||
MESSAGE_TYPE_SETTINGS,
|
||||
MESSAGE_TYPE_BUTTON,
|
||||
MESSAGE_TYPE_SIMULATION
|
||||
} message_type_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
BUTTON_EVENT_PRESS,
|
||||
BUTTON_EVENT_RELEASE
|
||||
} button_event_type_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
button_event_type_t event_type;
|
||||
uint8_t button_id;
|
||||
} button_message_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SETTINGS_TYPE_BOOL,
|
||||
SETTINGS_TYPE_INT,
|
||||
SETTINGS_TYPE_FLOAT,
|
||||
SETTINGS_TYPE_STRING
|
||||
} settings_type_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
settings_type_t type;
|
||||
char key[32];
|
||||
union {
|
||||
bool bool_value;
|
||||
int32_t int_value;
|
||||
float float_value;
|
||||
char string_value[64];
|
||||
} value;
|
||||
} settings_message_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char time[6];
|
||||
uint8_t red;
|
||||
uint8_t green;
|
||||
uint8_t blue;
|
||||
} simulation_message_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
message_type_t type;
|
||||
union {
|
||||
settings_message_t settings;
|
||||
button_message_t button;
|
||||
simulation_message_t simulation;
|
||||
} data;
|
||||
} message_t;
|
||||
|
||||
// Observer Pattern: Listener-Typ und Registrierungsfunktionen
|
||||
typedef void (*message_listener_t)(const message_t *msg);
|
||||
void message_manager_register_listener(message_listener_t listener);
|
||||
void message_manager_unregister_listener(message_listener_t listener);
|
||||
void message_manager_init(void);
|
||||
bool message_manager_post(const message_t *msg, TickType_t timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
134
firmware/components/message-manager/src/message_manager.c
Normal file
134
firmware/components/message-manager/src/message_manager.c
Normal file
@@ -0,0 +1,134 @@
|
||||
#include "message_manager.h"
|
||||
#include "my_mqtt_client.h"
|
||||
#include <esp_app_desc.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_system.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/task.h>
|
||||
#include <persistence_manager.h>
|
||||
#include <sdkconfig.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MESSAGE_QUEUE_LENGTH 16
|
||||
#define MESSAGE_QUEUE_ITEM_SIZE sizeof(message_t)
|
||||
|
||||
static const char *TAG = "message_manager";
|
||||
static QueueHandle_t message_queue = NULL;
|
||||
|
||||
// Observer Pattern: Listener-Liste
|
||||
#define MAX_MESSAGE_LISTENERS 8
|
||||
static message_listener_t message_listeners[MAX_MESSAGE_LISTENERS] = {0};
|
||||
static size_t message_listener_count = 0;
|
||||
|
||||
void message_manager_register_listener(message_listener_t listener)
|
||||
{
|
||||
if (listener && message_listener_count < MAX_MESSAGE_LISTENERS)
|
||||
{
|
||||
// Doppelte Registrierung vermeiden
|
||||
for (size_t i = 0; i < message_listener_count; ++i)
|
||||
{
|
||||
if (message_listeners[i] == listener)
|
||||
return;
|
||||
}
|
||||
message_listeners[message_listener_count++] = listener;
|
||||
}
|
||||
}
|
||||
|
||||
void message_manager_unregister_listener(message_listener_t listener)
|
||||
{
|
||||
for (size_t i = 0; i < message_listener_count; ++i)
|
||||
{
|
||||
if (message_listeners[i] == listener)
|
||||
{
|
||||
// Nachfolgende Listener nach vorne schieben
|
||||
for (size_t j = i; j < message_listener_count - 1; ++j)
|
||||
{
|
||||
message_listeners[j] = message_listeners[j + 1];
|
||||
}
|
||||
message_listeners[--message_listener_count] = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void message_manager_task(void *param)
|
||||
{
|
||||
message_t msg;
|
||||
persistence_manager_t pm;
|
||||
while (1)
|
||||
{
|
||||
if (xQueueReceive(message_queue, &msg, portMAX_DELAY) == pdTRUE)
|
||||
{
|
||||
switch (msg.type)
|
||||
{
|
||||
case MESSAGE_TYPE_SETTINGS:
|
||||
if (persistence_manager_init(&pm, "config") == ESP_OK)
|
||||
{
|
||||
switch (msg.data.settings.type)
|
||||
{
|
||||
case SETTINGS_TYPE_BOOL:
|
||||
persistence_manager_set_bool(&pm, msg.data.settings.key, msg.data.settings.value.bool_value);
|
||||
break;
|
||||
case SETTINGS_TYPE_INT:
|
||||
persistence_manager_set_int(&pm, msg.data.settings.key, msg.data.settings.value.int_value);
|
||||
break;
|
||||
case SETTINGS_TYPE_FLOAT:
|
||||
persistence_manager_set_float(&pm, msg.data.settings.key, msg.data.settings.value.float_value);
|
||||
break;
|
||||
case SETTINGS_TYPE_STRING:
|
||||
persistence_manager_set_string(&pm, msg.data.settings.key,
|
||||
msg.data.settings.value.string_value);
|
||||
break;
|
||||
}
|
||||
persistence_manager_deinit(&pm);
|
||||
ESP_LOGD(TAG, "Setting written: %s", msg.data.settings.key);
|
||||
}
|
||||
break;
|
||||
case MESSAGE_TYPE_BUTTON:
|
||||
ESP_LOGD(TAG, "Button event: id=%d, type=%d", msg.data.button.button_id, msg.data.button.event_type);
|
||||
break;
|
||||
case MESSAGE_TYPE_SIMULATION:
|
||||
/// just logging
|
||||
ESP_LOGD(TAG, "Simulation event: time=%s, color=(%d,%d,%d)", msg.data.simulation.time,
|
||||
msg.data.simulation.red, msg.data.simulation.green, msg.data.simulation.blue);
|
||||
break;
|
||||
}
|
||||
// Observer Pattern: Listener benachrichtigen
|
||||
for (size_t i = 0; i < message_listener_count; ++i)
|
||||
{
|
||||
if (message_listeners[i])
|
||||
{
|
||||
message_listeners[i](&msg);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
const esp_app_desc_t *app_desc = esp_app_get_description();
|
||||
char topic[60];
|
||||
snprintf(topic, sizeof(topic), "device/%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
|
||||
|
||||
char *data = "{\"key\":\"value\"}";
|
||||
mqtt_client_publish(topic, data, strlen(data), 0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void message_manager_init(void)
|
||||
{
|
||||
if (!message_queue)
|
||||
{
|
||||
message_queue = xQueueCreate(MESSAGE_QUEUE_LENGTH, MESSAGE_QUEUE_ITEM_SIZE);
|
||||
xTaskCreate(message_manager_task, "message_manager_task", 4096, NULL, 5, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool message_manager_post(const message_t *msg, TickType_t timeout)
|
||||
{
|
||||
if (!message_queue)
|
||||
return false;
|
||||
ESP_LOGD(TAG, "Post: type=%d", msg->type);
|
||||
return xQueueSend(message_queue, msg, timeout) == pdTRUE;
|
||||
}
|
||||
7
firmware/components/my_mqtt_client/CMakeLists.txt
Normal file
7
firmware/components/my_mqtt_client/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
idf_component_register(
|
||||
SRCS "src/my_mqtt_client.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
mqtt
|
||||
app_update
|
||||
)
|
||||
21
firmware/components/my_mqtt_client/Kconfig
Normal file
21
firmware/components/my_mqtt_client/Kconfig
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
18
firmware/components/my_mqtt_client/README.md
Normal file
18
firmware/components/my_mqtt_client/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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`.
|
||||
2
firmware/components/my_mqtt_client/idf_component.yml
Normal file
2
firmware/components/my_mqtt_client/idf_component.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/mqtt: ^1.0.0
|
||||
14
firmware/components/my_mqtt_client/include/my_mqtt_client.h
Normal file
14
firmware/components/my_mqtt_client/include/my_mqtt_client.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void mqtt_client_start(void);
|
||||
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
122
firmware/components/my_mqtt_client/src/my_mqtt_client.c
Normal file
122
firmware/components/my_mqtt_client/src/my_mqtt_client.c
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "my_mqtt_client.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 "mqtt_client.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
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 mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain)
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
int msg_id = esp_mqtt_client_publish(client, topic, data, len, qos, retain);
|
||||
ESP_LOGI(TAG, "Publish: topic=%s, msg_id=%d, qos=%d, retain=%d, len=%d", topic, msg_id, qos, retain, (int)len);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "Publish aufgerufen, aber Client ist nicht initialisiert!");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
idf_component_register(SRCS
|
||||
src/PersistenceManager.cpp
|
||||
src/persistence_manager.c
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES
|
||||
nvs_flash
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @interface IPersistenceManager
|
||||
* @brief Abstract interface for platform-independent persistence management
|
||||
* @details This interface defines the contract for key-value storage and retrieval
|
||||
* systems across different platforms (Desktop/SDL3 and ESP32).
|
||||
*/
|
||||
class IPersistenceManager
|
||||
{
|
||||
public:
|
||||
virtual ~IPersistenceManager() = default;
|
||||
|
||||
/**
|
||||
* @brief Template methods for type-safe setting and retrieving of values
|
||||
* @tparam T The type of value to set (must be one of: bool, int, float, double, std::string)
|
||||
* @param key The key to associate with the value
|
||||
* @param value The value to store
|
||||
*/
|
||||
template<typename T>
|
||||
void SetValue(const std::string& key, const T& value) {
|
||||
static_assert(std::is_same_v<T, bool> ||
|
||||
std::is_same_v<T, int> ||
|
||||
std::is_same_v<T, float> ||
|
||||
std::is_same_v<T, double> ||
|
||||
std::is_same_v<T, std::string>,
|
||||
"Unsupported type for IPersistenceManager");
|
||||
SetValueImpl(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Template method for type-safe retrieval of values
|
||||
* @tparam T The type of value to retrieve
|
||||
* @param key The key to look up
|
||||
* @param defaultValue The default value to return if the key is not found
|
||||
* @return The stored value or default value if the key doesn't exist
|
||||
*/
|
||||
template<typename T>
|
||||
[[nodiscard]] T GetValue(const std::string& key, const T& defaultValue = T{}) const {
|
||||
return GetValueImpl<T>(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Utility methods for key management
|
||||
*/
|
||||
[[nodiscard]] virtual bool HasKey(const std::string& key) const = 0; ///< Check if a key exists
|
||||
virtual void RemoveKey(const std::string& key) = 0; ///< Remove a key-value pair
|
||||
virtual void Clear() = 0; ///< Clear all stored data
|
||||
[[nodiscard]] virtual size_t GetKeyCount() const = 0; ///< Get the number of stored keys
|
||||
|
||||
/**
|
||||
* @brief Persistence operations
|
||||
*/
|
||||
virtual bool Save() = 0; ///< Save data to persistent storage
|
||||
virtual bool Load() = 0; ///< Load data from persistent storage
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Template-specific implementations that must be overridden by derived classes
|
||||
* @details These methods handle the actual storage and retrieval of different data types
|
||||
*/
|
||||
virtual void SetValueImpl(const std::string& key, bool value) = 0;
|
||||
virtual void SetValueImpl(const std::string& key, int value) = 0;
|
||||
virtual void SetValueImpl(const std::string& key, float value) = 0;
|
||||
virtual void SetValueImpl(const std::string& key, double value) = 0;
|
||||
virtual void SetValueImpl(const std::string& key, const std::string& value) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool GetValueImpl(const std::string& key, bool defaultValue) const = 0;
|
||||
[[nodiscard]] virtual int GetValueImpl(const std::string& key, int defaultValue) const = 0;
|
||||
[[nodiscard]] virtual float GetValueImpl(const std::string& key, float defaultValue) const = 0;
|
||||
[[nodiscard]] virtual double GetValueImpl(const std::string& key, double defaultValue) const = 0;
|
||||
[[nodiscard]] virtual std::string GetValueImpl(const std::string& key, const std::string& defaultValue) const = 0;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Template dispatch methods for type-safe value retrieval
|
||||
* @tparam T The type to retrieve
|
||||
* @param key The key to look up
|
||||
* @param defaultValue The default value to return
|
||||
* @return The retrieved value or default if not found
|
||||
*/
|
||||
template<typename T>
|
||||
[[nodiscard]] T GetValueImpl(const std::string& key, const T& defaultValue) const
|
||||
{
|
||||
if constexpr (std::is_same_v<T, bool>) {
|
||||
return GetValueImpl(key, static_cast<bool>(defaultValue));
|
||||
} else if constexpr (std::is_same_v<T, int>) {
|
||||
return GetValueImpl(key, static_cast<int>(defaultValue));
|
||||
} else if constexpr (std::is_same_v<T, float>) {
|
||||
return GetValueImpl(key, static_cast<float>(defaultValue));
|
||||
} else if constexpr (std::is_same_v<T, double>) {
|
||||
return GetValueImpl(key, static_cast<double>(defaultValue));
|
||||
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||
return GetValueImpl(key, static_cast<const std::string&>(defaultValue));
|
||||
} else {
|
||||
static_assert(std::is_same_v<T, bool> ||
|
||||
std::is_same_v<T, int> ||
|
||||
std::is_same_v<T, float> ||
|
||||
std::is_same_v<T, double> ||
|
||||
std::is_same_v<T, std::string>,
|
||||
"Unsupported type for IPersistenceManager");
|
||||
return defaultValue; // This line will never be reached, but satisfies the compiler
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "IPersistenceManager.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
|
||||
/**
|
||||
* @class PersistenceManager
|
||||
* @brief ESP32-specific implementation using NVS (Non-Volatile Storage)
|
||||
* @details This implementation uses ESP32's NVS API for persistent storage
|
||||
* in flash memory, providing a platform-optimized solution for
|
||||
* embedded systems.
|
||||
*/
|
||||
class PersistenceManager final : public IPersistenceManager
|
||||
{
|
||||
private:
|
||||
nvs_handle_t nvs_handle_;
|
||||
std::string namespace_;
|
||||
bool initialized_;
|
||||
|
||||
public:
|
||||
explicit PersistenceManager(const std::string &nvs_namespace = "config");
|
||||
~PersistenceManager() override;
|
||||
|
||||
bool HasKey(const std::string &key) const override;
|
||||
void RemoveKey(const std::string &key) override;
|
||||
void Clear() override;
|
||||
size_t GetKeyCount() const override;
|
||||
|
||||
bool Save() override;
|
||||
bool Load() override;
|
||||
|
||||
bool Initialize();
|
||||
void Deinitialize();
|
||||
bool IsInitialized() const
|
||||
{
|
||||
return initialized_;
|
||||
}
|
||||
|
||||
protected:
|
||||
void SetValueImpl(const std::string &key, bool value) override;
|
||||
void SetValueImpl(const std::string &key, int value) override;
|
||||
void SetValueImpl(const std::string &key, float value) override;
|
||||
void SetValueImpl(const std::string &key, double value) override;
|
||||
void SetValueImpl(const std::string &key, const std::string &value) override;
|
||||
|
||||
bool GetValueImpl(const std::string &key, bool defaultValue) const override;
|
||||
int GetValueImpl(const std::string &key, int defaultValue) const override;
|
||||
float GetValueImpl(const std::string &key, float defaultValue) const override;
|
||||
double GetValueImpl(const std::string &key, double defaultValue) const override;
|
||||
std::string GetValueImpl(const std::string &key, const std::string &defaultValue) const override;
|
||||
|
||||
private:
|
||||
bool EnsureInitialized() const;
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Structure to manage persistent storage using NVS.
|
||||
*
|
||||
* This struct holds the NVS handle, namespace, and initialization state
|
||||
* for managing persistent key-value storage on the device.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
/** Handle to the NVS storage. */
|
||||
nvs_handle_t nvs_handle;
|
||||
/** Namespace used for NVS operations (max 15 chars + null terminator). */
|
||||
char nvs_namespace[16];
|
||||
/** Indicates if the persistence manager is initialized. */
|
||||
bool initialized;
|
||||
} persistence_manager_t;
|
||||
|
||||
/**
|
||||
* @brief Erases the entire NVS flash (factory reset).
|
||||
*
|
||||
* Warning: This will remove all stored data and namespaces!
|
||||
*
|
||||
* @return esp_err_t ESP_OK on success, otherwise error code.
|
||||
*/
|
||||
esp_err_t persistence_manager_factory_reset(void);
|
||||
|
||||
/**
|
||||
* @brief Initialize the persistence manager with a given NVS namespace.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param nvs_namespace Namespace to use for NVS operations.
|
||||
*/
|
||||
esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_namespace);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize the persistence manager and release resources.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
*/
|
||||
esp_err_t persistence_manager_deinit(persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Check if the persistence manager is initialized.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @return true if initialized, false otherwise.
|
||||
*/
|
||||
bool persistence_manager_is_initialized(const persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Check if a key exists in the NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to check for existence.
|
||||
* @return true if the key exists, false otherwise.
|
||||
*/
|
||||
bool persistence_manager_has_key(const persistence_manager_t *pm, const char *key);
|
||||
|
||||
/**
|
||||
* @brief Remove a key from the NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to remove.
|
||||
*/
|
||||
void persistence_manager_remove_key(persistence_manager_t *pm, const char *key);
|
||||
|
||||
/**
|
||||
* @brief Clear all keys from the NVS storage in the current namespace.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
*/
|
||||
void persistence_manager_clear(persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Get the number of keys stored in the current namespace.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @return Number of keys.
|
||||
*/
|
||||
size_t persistence_manager_get_key_count(const persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Save all pending changes to NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @return true if successful, false otherwise.
|
||||
*/
|
||||
bool persistence_manager_save(persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Load all data from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @return true if successful, false otherwise.
|
||||
*/
|
||||
bool persistence_manager_load(persistence_manager_t *pm);
|
||||
|
||||
/**
|
||||
* @brief Set a boolean value for a key in NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value Boolean value to store.
|
||||
*/
|
||||
void persistence_manager_set_bool(persistence_manager_t *pm, const char *key, bool value);
|
||||
|
||||
/**
|
||||
* @brief Set an integer value for a key in NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value Integer value to store.
|
||||
*/
|
||||
void persistence_manager_set_int(persistence_manager_t *pm, const char *key, int32_t value);
|
||||
|
||||
/**
|
||||
* @brief Set a float value for a key in NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value Float value to store.
|
||||
*/
|
||||
void persistence_manager_set_float(persistence_manager_t *pm, const char *key, float value);
|
||||
|
||||
/**
|
||||
* @brief Set a double value for a key in NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value Double value to store.
|
||||
*/
|
||||
void persistence_manager_set_double(persistence_manager_t *pm, const char *key, double value);
|
||||
|
||||
/**
|
||||
* @brief Set a string value for a key in NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value String value to store.
|
||||
*/
|
||||
void persistence_manager_set_string(persistence_manager_t *pm, const char *key, const char *value);
|
||||
|
||||
/**
|
||||
* @brief Get a boolean value for a key from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param default_value Value to return if key does not exist.
|
||||
* @return Boolean value.
|
||||
*/
|
||||
bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value);
|
||||
|
||||
/**
|
||||
* @brief Get an integer value for a key from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param default_value Value to return if key does not exist.
|
||||
* @return Integer value.
|
||||
*/
|
||||
int32_t persistence_manager_get_int(const persistence_manager_t *pm, const char *key, int32_t default_value);
|
||||
|
||||
/**
|
||||
* @brief Get a float value for a key from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param default_value Value to return if key does not exist.
|
||||
* @return Float value.
|
||||
*/
|
||||
float persistence_manager_get_float(const persistence_manager_t *pm, const char *key, float default_value);
|
||||
|
||||
/**
|
||||
* @brief Get a double value for a key from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param default_value Value to return if key does not exist.
|
||||
* @return Double value.
|
||||
*/
|
||||
double persistence_manager_get_double(const persistence_manager_t *pm, const char *key, double default_value);
|
||||
|
||||
/**
|
||||
* @brief Get a string value for a key from NVS storage.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param out_value Buffer to store the retrieved string.
|
||||
* @param max_len Maximum length of the output buffer.
|
||||
* @param default_value Value to use if key does not exist.
|
||||
*/
|
||||
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_value,
|
||||
size_t max_len, const char *default_value);
|
||||
|
||||
/**
|
||||
* @brief Set a blob (binary data) value for a key in NVS storage.
|
||||
*
|
||||
* This function stores arbitrary binary data under the given key.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to set.
|
||||
* @param value Pointer to the data to store.
|
||||
* @param length Length of the data in bytes.
|
||||
*/
|
||||
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length);
|
||||
|
||||
/**
|
||||
* @brief Get a blob (binary data) value for a key from NVS storage.
|
||||
*
|
||||
* This function retrieves binary data previously stored under the given key.
|
||||
*
|
||||
* @param pm Pointer to the persistence manager structure.
|
||||
* @param key Key to retrieve.
|
||||
* @param out_value Buffer to store the retrieved data.
|
||||
* @param max_length Maximum length of the output buffer in bytes.
|
||||
* @param out_length Pointer to variable to receive the actual data length.
|
||||
* @return true if the blob was found and read successfully, false otherwise.
|
||||
*/
|
||||
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value,
|
||||
size_t max_length, size_t *out_length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,279 +0,0 @@
|
||||
#include "hal_esp32/PersistenceManager.h"
|
||||
#include <cstring>
|
||||
#include <esp_log.h>
|
||||
|
||||
static const char *TAG = "PersistenceManager";
|
||||
|
||||
PersistenceManager::PersistenceManager(const std::string &nvs_namespace)
|
||||
: namespace_(nvs_namespace), initialized_(false)
|
||||
{
|
||||
Initialize();
|
||||
Load();
|
||||
}
|
||||
|
||||
PersistenceManager::~PersistenceManager()
|
||||
{
|
||||
Deinitialize();
|
||||
}
|
||||
|
||||
bool PersistenceManager::Initialize()
|
||||
{
|
||||
if (initialized_)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Open NVS handle
|
||||
esp_err_t err = nvs_open(namespace_.c_str(), NVS_READWRITE, &nvs_handle_);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
ESP_LOGI(TAG, "PersistenceManager initialized with namespace: %s", namespace_.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void PersistenceManager::Deinitialize()
|
||||
{
|
||||
if (initialized_)
|
||||
{
|
||||
nvs_close(nvs_handle_);
|
||||
initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool PersistenceManager::EnsureInitialized() const
|
||||
{
|
||||
if (!initialized_)
|
||||
{
|
||||
ESP_LOGE(TAG, "PersistenceManager not initialized");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PersistenceManager::HasKey(const std::string &key) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return false;
|
||||
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), nullptr, &required_size);
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
void PersistenceManager::RemoveKey(const std::string &key)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_erase_key(nvs_handle_, key.c_str());
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to remove key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void PersistenceManager::Clear()
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_erase_all(nvs_handle_);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to clear all keys: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
size_t PersistenceManager::GetKeyCount() const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return 0;
|
||||
|
||||
nvs_iterator_t it = nullptr;
|
||||
esp_err_t err = nvs_entry_find(NVS_DEFAULT_PART_NAME, namespace_.c_str(), NVS_TYPE_ANY, &it);
|
||||
|
||||
if (err != ESP_OK || it == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
|
||||
while (it != nullptr)
|
||||
{
|
||||
count++;
|
||||
err = nvs_entry_next(&it);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nvs_release_iterator(it);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool PersistenceManager::Save()
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return false;
|
||||
|
||||
esp_err_t err = nvs_commit(nvs_handle_);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PersistenceManager::Load()
|
||||
{
|
||||
return EnsureInitialized();
|
||||
}
|
||||
|
||||
void PersistenceManager::SetValueImpl(const std::string &key, bool value)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
uint8_t val = value ? 1 : 0;
|
||||
esp_err_t err = nvs_set_u8(nvs_handle_, key.c_str(), val);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set bool key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void PersistenceManager::SetValueImpl(const std::string &key, int value)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_i32(nvs_handle_, key.c_str(), value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set int key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void PersistenceManager::SetValueImpl(const std::string &key, float value)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_blob(nvs_handle_, key.c_str(), &value, sizeof(float));
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set float key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void PersistenceManager::SetValueImpl(const std::string &key, double value)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_blob(nvs_handle_, key.c_str(), &value, sizeof(double));
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set double key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void PersistenceManager::SetValueImpl(const std::string &key, const std::string &value)
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_set_str(nvs_handle_, key.c_str(), value.c_str());
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set string key '%s': %s", key.c_str(), esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
bool PersistenceManager::GetValueImpl(const std::string &key, bool defaultValue) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return defaultValue;
|
||||
|
||||
uint8_t value;
|
||||
esp_err_t err = nvs_get_u8(nvs_handle_, key.c_str(), &value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
int PersistenceManager::GetValueImpl(const std::string &key, int defaultValue) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return defaultValue;
|
||||
|
||||
int32_t value;
|
||||
esp_err_t err = nvs_get_i32(nvs_handle_, key.c_str(), &value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
|
||||
float PersistenceManager::GetValueImpl(const std::string &key, float defaultValue) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return defaultValue;
|
||||
|
||||
float value;
|
||||
size_t required_size = sizeof(float);
|
||||
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), &value, &required_size);
|
||||
if (err != ESP_OK || required_size != sizeof(float))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double PersistenceManager::GetValueImpl(const std::string &key, double defaultValue) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return defaultValue;
|
||||
|
||||
double value;
|
||||
size_t required_size = sizeof(double);
|
||||
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), &value, &required_size);
|
||||
if (err != ESP_OK || required_size != sizeof(double))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string PersistenceManager::GetValueImpl(const std::string &key, const std::string &defaultValue) const
|
||||
{
|
||||
if (!EnsureInitialized())
|
||||
return defaultValue;
|
||||
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_str(nvs_handle_, key.c_str(), nullptr, &required_size);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
std::string value(required_size - 1, '\0'); // -1 for null terminator
|
||||
err = nvs_get_str(nvs_handle_, key.c_str(), value.data(), &required_size);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "persistence_manager.h"
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "persistence_manager"
|
||||
|
||||
esp_err_t persistence_manager_factory_reset(void)
|
||||
{
|
||||
// Erase the entire NVS flash (factory reset)
|
||||
esp_err_t err = nvs_flash_erase();
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Factory reset failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_namespace)
|
||||
{
|
||||
if (!pm)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
strncpy(pm->nvs_namespace, nvs_namespace ? nvs_namespace : "config", sizeof(pm->nvs_namespace) - 1);
|
||||
pm->nvs_namespace[sizeof(pm->nvs_namespace) - 1] = '\0';
|
||||
pm->initialized = false;
|
||||
esp_err_t err = nvs_open(pm->nvs_namespace, NVS_READWRITE, &pm->nvs_handle);
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
pm->initialized = true;
|
||||
ESP_LOGD(TAG, "Initialized with namespace: %s", pm->nvs_namespace);
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t persistence_manager_deinit(persistence_manager_t *pm)
|
||||
{
|
||||
if (pm && pm->initialized)
|
||||
{
|
||||
nvs_close(pm->nvs_handle);
|
||||
pm->initialized = false;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool persistence_manager_is_initialized(const persistence_manager_t *pm)
|
||||
{
|
||||
return pm && pm->initialized;
|
||||
}
|
||||
|
||||
bool persistence_manager_has_key(const persistence_manager_t *pm, const char *key)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return false;
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, NULL, &required_size);
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
void persistence_manager_remove_key(persistence_manager_t *pm, const char *key)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_erase_key(pm->nvs_handle, key);
|
||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to remove key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_clear(persistence_manager_t *pm)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_erase_all(pm->nvs_handle);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to clear all keys: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
size_t persistence_manager_get_key_count(const persistence_manager_t *pm)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return 0;
|
||||
nvs_iterator_t it = NULL;
|
||||
esp_err_t err = nvs_entry_find(NVS_DEFAULT_PART_NAME, pm->nvs_namespace, NVS_TYPE_ANY, &it);
|
||||
if (err != ESP_OK || it == NULL)
|
||||
return 0;
|
||||
size_t count = 0;
|
||||
while (it != NULL)
|
||||
{
|
||||
count++;
|
||||
err = nvs_entry_next(&it);
|
||||
if (err != ESP_OK)
|
||||
break;
|
||||
}
|
||||
nvs_release_iterator(it);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool persistence_manager_save(persistence_manager_t *pm)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return false;
|
||||
esp_err_t err = nvs_commit(pm->nvs_handle);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool persistence_manager_load(persistence_manager_t *pm)
|
||||
{
|
||||
return persistence_manager_is_initialized(pm);
|
||||
}
|
||||
|
||||
void persistence_manager_set_bool(persistence_manager_t *pm, const char *key, bool value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
uint8_t val = value ? 1 : 0;
|
||||
esp_err_t err = nvs_set_u8(pm->nvs_handle, key, val);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set bool key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_set_int(persistence_manager_t *pm, const char *key, int32_t value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_set_i32(pm->nvs_handle, key, value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set int key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_set_float(persistence_manager_t *pm, const char *key, float value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, &value, sizeof(float));
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set float key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_set_double(persistence_manager_t *pm, const char *key, double value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, &value, sizeof(double));
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set double key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_set_string(persistence_manager_t *pm, const char *key, const char *value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return;
|
||||
esp_err_t err = nvs_set_str(pm->nvs_handle, key, value);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set string key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm) || !value || length == 0)
|
||||
return;
|
||||
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, value, length);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to set blob key '%s': %s", key, esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return default_value;
|
||||
uint8_t value;
|
||||
esp_err_t err = nvs_get_u8(pm->nvs_handle, key, &value);
|
||||
if (err != ESP_OK)
|
||||
return default_value;
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
int32_t persistence_manager_get_int(const persistence_manager_t *pm, const char *key, int32_t default_value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return default_value;
|
||||
int32_t value;
|
||||
esp_err_t err = nvs_get_i32(pm->nvs_handle, key, &value);
|
||||
if (err != ESP_OK)
|
||||
return default_value;
|
||||
return value;
|
||||
}
|
||||
|
||||
float persistence_manager_get_float(const persistence_manager_t *pm, const char *key, float default_value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return default_value;
|
||||
float value;
|
||||
size_t required_size = sizeof(float);
|
||||
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, &value, &required_size);
|
||||
if (err != ESP_OK || required_size != sizeof(float))
|
||||
return default_value;
|
||||
return value;
|
||||
}
|
||||
|
||||
double persistence_manager_get_double(const persistence_manager_t *pm, const char *key, double default_value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
return default_value;
|
||||
double value;
|
||||
size_t required_size = sizeof(double);
|
||||
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, &value, &required_size);
|
||||
if (err != ESP_OK || required_size != sizeof(double))
|
||||
return default_value;
|
||||
return value;
|
||||
}
|
||||
|
||||
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_value, size_t max_len,
|
||||
const char *default_value)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm))
|
||||
{
|
||||
strncpy(out_value, default_value, max_len - 1);
|
||||
out_value[max_len - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_str(pm->nvs_handle, key, NULL, &required_size);
|
||||
if (err != ESP_OK || required_size == 0 || required_size > max_len)
|
||||
{
|
||||
strncpy(out_value, default_value, max_len - 1);
|
||||
out_value[max_len - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
err = nvs_get_str(pm->nvs_handle, key, out_value, &required_size);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
strncpy(out_value, default_value, max_len - 1);
|
||||
out_value[max_len - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value, size_t max_length,
|
||||
size_t *out_length)
|
||||
{
|
||||
if (!persistence_manager_is_initialized(pm) || !out_value || max_length == 0)
|
||||
return false;
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, NULL, &required_size);
|
||||
if (err != ESP_OK || required_size == 0 || required_size > max_length)
|
||||
return false;
|
||||
err = nvs_get_blob(pm->nvs_handle, key, out_value, &required_size);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
if (out_length)
|
||||
*out_length = required_size;
|
||||
return true;
|
||||
}
|
||||
@@ -5,5 +5,6 @@ idf_component_register(SRCS
|
||||
PRIV_REQUIRES
|
||||
led-manager
|
||||
persistence-manager
|
||||
message-manager
|
||||
spiffs
|
||||
)
|
||||
|
||||
@@ -9,6 +9,11 @@ typedef struct
|
||||
int cycle_duration_minutes;
|
||||
} simulation_config_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
char *get_time(void);
|
||||
esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t blue, uint8_t white,
|
||||
uint8_t brightness, uint8_t saturation);
|
||||
@@ -18,3 +23,6 @@ void start_simulate_night(void);
|
||||
void start_simulation_task(void);
|
||||
void stop_simulation_task(void);
|
||||
void start_simulation(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,4 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
void initialize_storage();
|
||||
void load_file(const char *filename);
|
||||
char **read_lines_filtered(const char *filename, int *out_count);
|
||||
void free_lines(char **lines, int count);
|
||||
/**
|
||||
* Write an array of lines to a file (CSV or other text).
|
||||
* @param filename File name (without /spiffs/)
|
||||
* @param lines Array of lines (null-terminated strings)
|
||||
* @param count Number of lines
|
||||
* @return ESP_OK on success, error code otherwise
|
||||
*/
|
||||
esp_err_t write_lines(const char *filename, char **lines, int count);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "simulator.h"
|
||||
|
||||
#include "color.h"
|
||||
#include "hal_esp32/PersistenceManager.h"
|
||||
#include "led_strip_ws2812.h"
|
||||
#include "message_manager.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "storage.h"
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_log.h>
|
||||
@@ -15,12 +16,12 @@
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "simulator";
|
||||
static char *time;
|
||||
static char *time = NULL;
|
||||
|
||||
static char *time_to_string(int hhmm)
|
||||
{
|
||||
static char buffer[20];
|
||||
snprintf(buffer, sizeof(buffer), "%02d:%02d Uhr", hhmm / 100, hhmm % 100);
|
||||
snprintf(buffer, sizeof(buffer), "%02d:%02d", hhmm / 100, hhmm % 100);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -81,10 +82,10 @@ esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t
|
||||
uint8_t brightness, uint8_t saturation)
|
||||
{
|
||||
// Allocate memory for a new node in PSRAM.
|
||||
light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_SPIRAM);
|
||||
light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_DEFAULT);
|
||||
if (new_node == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory in PSRAM for new light_item_node_t.");
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for new light_item_node_t.");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -145,10 +146,12 @@ static void initialize_light_items(void)
|
||||
initialize_storage();
|
||||
|
||||
static char filename[30];
|
||||
auto persistence = PersistenceManager();
|
||||
int variant = persistence.GetValue("light_variant", 1);
|
||||
snprintf(filename, sizeof(filename), "/spiffs/schema_%02d.csv", variant);
|
||||
persistence_manager_t persistence;
|
||||
persistence_manager_init(&persistence, "config");
|
||||
int variant = persistence_manager_get_int(&persistence, "light_variant", 1);
|
||||
snprintf(filename, sizeof(filename), "schema_%02d.csv", variant);
|
||||
load_file(filename);
|
||||
persistence_manager_deinit(&persistence);
|
||||
|
||||
if (head == NULL)
|
||||
{
|
||||
@@ -233,6 +236,17 @@ static light_item_node_t *find_next_light_item_for_time(int hhmm)
|
||||
return next_item;
|
||||
}
|
||||
|
||||
static void send_simulation_message(const char *time, rgb_t color)
|
||||
{
|
||||
message_t msg = {};
|
||||
msg.type = MESSAGE_TYPE_SIMULATION;
|
||||
strncpy(msg.data.simulation.time, time, sizeof(msg.data.simulation.time) - 1);
|
||||
msg.data.simulation.red = color.red;
|
||||
msg.data.simulation.green = color.green;
|
||||
msg.data.simulation.blue = color.blue;
|
||||
message_manager_post(&msg, pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
void start_simulate_day(void)
|
||||
{
|
||||
initialize_light_items();
|
||||
@@ -240,8 +254,9 @@ void start_simulate_day(void)
|
||||
light_item_node_t *current_item = find_best_light_item_for_time(1200);
|
||||
if (current_item != NULL)
|
||||
{
|
||||
led_strip_update(LED_STATE_DAY,
|
||||
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
|
||||
rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
|
||||
led_strip_update(LED_STATE_DAY, color);
|
||||
send_simulation_message("12:00", color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,8 +267,9 @@ void start_simulate_night(void)
|
||||
light_item_node_t *current_item = find_best_light_item_for_time(0);
|
||||
if (current_item != NULL)
|
||||
{
|
||||
led_strip_update(LED_STATE_NIGHT,
|
||||
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
|
||||
rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
|
||||
led_strip_update(LED_STATE_NIGHT, color);
|
||||
send_simulation_message("00:00", color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +310,13 @@ void simulate_cycle(void *args)
|
||||
light_item_node_t *current_item = find_best_light_item_for_time(hhmm);
|
||||
light_item_node_t *next_item = find_next_light_item_for_time(hhmm);
|
||||
|
||||
if (current_item != NULL && next_item != NULL)
|
||||
if (current_item != NULL)
|
||||
{
|
||||
rgb_t color = {0, 0, 0};
|
||||
|
||||
// Use head as fallback if next_item is NULL
|
||||
next_item = next_item ? next_item : head;
|
||||
if (next_item != NULL)
|
||||
{
|
||||
int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
|
||||
int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
|
||||
@@ -323,16 +345,16 @@ void simulate_cycle(void *args)
|
||||
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
|
||||
|
||||
// Use the interpolation function
|
||||
rgb_t final_rgb = interpolate_color(start_rgb, end_rgb, interpolation_factor);
|
||||
|
||||
led_strip_update(LED_STATE_SIMULATION, final_rgb);
|
||||
color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
|
||||
led_strip_update(LED_STATE_SIMULATION, color);
|
||||
}
|
||||
else if (current_item != NULL)
|
||||
else
|
||||
{
|
||||
// No next item, just use current
|
||||
led_strip_update(
|
||||
LED_STATE_SIMULATION,
|
||||
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
|
||||
// No next_item and no head, use only current
|
||||
color = (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
|
||||
led_strip_update(LED_STATE_SIMULATION, color);
|
||||
}
|
||||
send_simulation_message(time, color);
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
||||
@@ -351,7 +373,7 @@ void start_simulation_task(void)
|
||||
stop_simulation_task();
|
||||
|
||||
simulation_config_t *config =
|
||||
(simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_SPIRAM);
|
||||
(simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_DEFAULT);
|
||||
if (config == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for simulation config.");
|
||||
@@ -398,25 +420,22 @@ void start_simulation(void)
|
||||
{
|
||||
stop_simulation_task();
|
||||
|
||||
auto persistence = PersistenceManager();
|
||||
if (persistence.GetValue("light_active", false))
|
||||
persistence_manager_t persistence;
|
||||
persistence_manager_init(&persistence, "config");
|
||||
if (persistence_manager_get_bool(&persistence, "light_active", false))
|
||||
{
|
||||
|
||||
int mode = persistence.GetValue("light_mode", 0);
|
||||
int mode = persistence_manager_get_int(&persistence, "light_mode", 0);
|
||||
switch (mode)
|
||||
{
|
||||
case 0: // Simulation mode
|
||||
start_simulation_task();
|
||||
break;
|
||||
|
||||
case 1: // Day mode
|
||||
start_simulate_day();
|
||||
break;
|
||||
|
||||
case 2: // Night mode
|
||||
start_simulate_night();
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown light mode: %d", mode);
|
||||
break;
|
||||
@@ -426,4 +445,5 @@ void start_simulation(void)
|
||||
{
|
||||
led_strip_update(LED_STATE_OFF, rgb_t{});
|
||||
}
|
||||
persistence_manager_deinit(&persistence);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "simulator.h"
|
||||
#include <cstring>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "storage";
|
||||
|
||||
static bool is_spiffs_mounted = false;
|
||||
|
||||
void initialize_storage()
|
||||
{
|
||||
if (is_spiffs_mounted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = "/spiffs",
|
||||
.partition_label = NULL,
|
||||
@@ -36,64 +44,113 @@ void initialize_storage()
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
is_spiffs_mounted = true;
|
||||
}
|
||||
|
||||
void load_file(const char *filename)
|
||||
{
|
||||
ESP_LOGI(TAG, "Loading file: %s", filename);
|
||||
FILE *f = fopen(filename, "r");
|
||||
if (f == NULL)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open file for reading");
|
||||
return;
|
||||
}
|
||||
|
||||
char line[128];
|
||||
int line_count = 0;
|
||||
char **lines = read_lines_filtered(filename, &line_count);
|
||||
uint8_t line_number = 0;
|
||||
while (fgets(line, sizeof(line), f))
|
||||
for (int i = 0; i < line_count; ++i)
|
||||
{
|
||||
char *pos = strchr(line, '\n');
|
||||
if (pos)
|
||||
{
|
||||
*pos = '\0';
|
||||
}
|
||||
|
||||
if (strlen(line) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char *trimmed = line;
|
||||
while (*trimmed == ' ' || *trimmed == '\t')
|
||||
{
|
||||
trimmed++;
|
||||
}
|
||||
if (*trimmed == '#' || *trimmed == '\0')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char time[10] = {0};
|
||||
int red, green, blue, white, brightness, saturation;
|
||||
|
||||
int items_scanned = sscanf(line, "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation);
|
||||
int items_scanned =
|
||||
sscanf(lines[i], "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation);
|
||||
if (items_scanned == 6)
|
||||
{
|
||||
int total_minutes = line_number * 30;
|
||||
int hours = total_minutes / 60;
|
||||
int minutes = total_minutes % 60;
|
||||
|
||||
snprintf(time, sizeof(time), "%02d%02d", hours, minutes);
|
||||
|
||||
add_light_item(time, red, green, blue, white, brightness, saturation);
|
||||
line_number++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "Could not parse line: %s", line);
|
||||
ESP_LOGW(TAG, "Could not parse line: %s", lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
free_lines(lines, line_count);
|
||||
ESP_LOGI(TAG, "Finished loading file. Loaded %d entries.", line_number);
|
||||
}
|
||||
|
||||
char **read_lines_filtered(const char *filename, int *out_count)
|
||||
{
|
||||
char fullpath[128];
|
||||
snprintf(fullpath, sizeof(fullpath), "/spiffs/%s", filename[0] == '/' ? filename + 1 : filename);
|
||||
FILE *f = fopen(fullpath, "r");
|
||||
if (!f)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open file: %s", fullpath);
|
||||
*out_count = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t capacity = 16;
|
||||
size_t count = 0;
|
||||
char **lines = (char **)malloc(capacity * sizeof(char *));
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f))
|
||||
{
|
||||
// Zeilenumbruch entfernen
|
||||
char *pos = strchr(line, '\n');
|
||||
if (pos)
|
||||
*pos = '\0';
|
||||
// Trim vorne
|
||||
char *trimmed = line;
|
||||
while (*trimmed == ' ' || *trimmed == '\t')
|
||||
trimmed++;
|
||||
// Leere oder Kommentarzeile überspringen
|
||||
if (*trimmed == '\0' || *trimmed == '#')
|
||||
continue;
|
||||
// Trim hinten
|
||||
size_t len = strlen(trimmed);
|
||||
while (len > 0 && (trimmed[len - 1] == ' ' || trimmed[len - 1] == '\t'))
|
||||
trimmed[--len] = '\0';
|
||||
// Kopieren
|
||||
if (count >= capacity)
|
||||
{
|
||||
capacity *= 2;
|
||||
lines = (char **)realloc(lines, capacity * sizeof(char *));
|
||||
}
|
||||
lines[count++] = strdup(trimmed);
|
||||
}
|
||||
fclose(f);
|
||||
*out_count = (int)count;
|
||||
return lines;
|
||||
}
|
||||
|
||||
void free_lines(char **lines, int count)
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
free(lines[i]);
|
||||
free(lines);
|
||||
}
|
||||
|
||||
esp_err_t write_lines(const char *filename, char **lines, int count)
|
||||
{
|
||||
char fullpath[128];
|
||||
snprintf(fullpath, sizeof(fullpath), "/spiffs/%s", filename[0] == '/' ? filename + 1 : filename);
|
||||
FILE *f = fopen(fullpath, "w");
|
||||
if (!f)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to open file for writing: %s", fullpath);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
if (fprintf(f, "%s\n", lines[i]) < 0)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to write line %d", i);
|
||||
fclose(f);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
ESP_LOGI(TAG, "Wrote %d lines to %s", count, fullpath);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
13
firmware/debug-storybook.log
Normal file
13
firmware/debug-storybook.log
Normal file
@@ -0,0 +1,13 @@
|
||||
[17:12:14.017] [INFO] [36mInitializing Storybook[39m
|
||||
[17:12:14.195] [DEBUG] Getting package.json info for /Users/mars3142/Coding/git.mars3142.dev/system-control/firmware/package.json...
|
||||
[17:12:14.195] [DEBUG] Getting CLI versions from NPM for storybook...
|
||||
[17:12:14.195] [DEBUG] Executing command: npm info storybook version
|
||||
[17:12:14.850] [INFO] Adding Storybook version 10.2.8 to your project
|
||||
[17:12:14.851] [ERROR] Unable to initialize Storybook in this directory.
|
||||
|
||||
Storybook couldn't detect a supported framework or configuration for your project. Make sure you're inside a framework project (e.g., React, Vue, Svelte, Angular, Next.js) and that its dependencies are installed.
|
||||
|
||||
Tips:
|
||||
- Run init in an empty directory or create a new framework app first.
|
||||
- If this directory contains unrelated files, try a new directory for Storybook.
|
||||
[17:12:14.853] [INFO] Storybook collects completely anonymous usage telemetry. We use it to shape Storybook's roadmap and prioritize features. You can learn more, including how to opt out, at https://storybook.js.org/telemetry
|
||||
@@ -1,10 +1,10 @@
|
||||
idf_component_register(SRCS
|
||||
main.cpp
|
||||
app_task.cpp
|
||||
button_handling.c
|
||||
i2c_checker.c
|
||||
hal/u8g2_esp32_hal.c
|
||||
INCLUDE_DIRS "."
|
||||
src/main.cpp
|
||||
src/app_task.cpp
|
||||
src/button_handling.c
|
||||
src/i2c_checker.c
|
||||
src/hal/u8g2_esp32_hal.c
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES
|
||||
analytics
|
||||
insa
|
||||
@@ -21,6 +21,7 @@ idf_component_register(SRCS
|
||||
app_update
|
||||
rmaker_common
|
||||
driver
|
||||
my_mqtt_client
|
||||
)
|
||||
|
||||
spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT)
|
||||
|
||||
@@ -6,84 +6,6 @@ menu "System Control"
|
||||
help
|
||||
Enable or disable WiFi connectivity.
|
||||
|
||||
config WIFI_NETWORK_COUNT
|
||||
depends on WIFI_ENABLED
|
||||
int "Number of WiFi Networks"
|
||||
default 1
|
||||
range 1 5
|
||||
help
|
||||
Number of WiFi networks to configure (1-5).
|
||||
|
||||
config WIFI_SSID_1
|
||||
depends on WIFI_ENABLED
|
||||
string "WiFi SSID 1"
|
||||
default "YourSSID1"
|
||||
help
|
||||
The SSID of the first WiFi network.
|
||||
|
||||
config WIFI_PASSWORD_1
|
||||
depends on WIFI_ENABLED
|
||||
string "WiFi Password 1"
|
||||
default "YourPassword1"
|
||||
help
|
||||
The password of the first WiFi network.
|
||||
|
||||
config WIFI_SSID_2
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
|
||||
string "WiFi SSID 2"
|
||||
default ""
|
||||
help
|
||||
The SSID of the second WiFi network.
|
||||
|
||||
config WIFI_PASSWORD_2
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
|
||||
string "WiFi Password 2"
|
||||
default ""
|
||||
help
|
||||
The password of the second WiFi network.
|
||||
|
||||
config WIFI_SSID_3
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
|
||||
string "WiFi SSID 3"
|
||||
default ""
|
||||
help
|
||||
The SSID of the third WiFi network.
|
||||
|
||||
config WIFI_PASSWORD_3
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
|
||||
string "WiFi Password 3"
|
||||
default ""
|
||||
help
|
||||
The password of the third WiFi network.
|
||||
|
||||
config WIFI_SSID_4
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
|
||||
string "WiFi SSID 4"
|
||||
default ""
|
||||
help
|
||||
The SSID of the fourth WiFi network.
|
||||
|
||||
config WIFI_PASSWORD_4
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
|
||||
string "WiFi Password 4"
|
||||
default ""
|
||||
help
|
||||
The password of the fourth WiFi network.
|
||||
|
||||
config WIFI_SSID_5
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
|
||||
string "WiFi SSID 5"
|
||||
default ""
|
||||
help
|
||||
The SSID of the fifth WiFi network.
|
||||
|
||||
config WIFI_PASSWORD_5
|
||||
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
|
||||
string "WiFi Password 5"
|
||||
default ""
|
||||
help
|
||||
The password of the fifth WiFi network.
|
||||
|
||||
config WIFI_CONNECT_RETRIES
|
||||
depends on WIFI_ENABLED
|
||||
int "WiFi Connection Retry Attempts per Network"
|
||||
@@ -105,4 +27,42 @@ menu "System Control"
|
||||
help
|
||||
GPIO pin number for the SCL line of the display.
|
||||
endmenu
|
||||
|
||||
menu "Button Configuration"
|
||||
config BUTTON_UP
|
||||
int "Button UP GPIO Pin"
|
||||
default 1
|
||||
help
|
||||
GPIO pin number for the up button.
|
||||
|
||||
config BUTTON_DOWN
|
||||
int "Button DOWN GPIO Pin"
|
||||
default 6
|
||||
help
|
||||
GPIO pin number for the down button.
|
||||
|
||||
config BUTTON_LEFT
|
||||
int "Button LEFT GPIO Pin"
|
||||
default 3
|
||||
help
|
||||
GPIO pin number for the left button.
|
||||
|
||||
config BUTTON_RIGHT
|
||||
int "Button RIGHT GPIO Pin"
|
||||
default 5
|
||||
help
|
||||
GPIO pin number for the right button.
|
||||
|
||||
config BUTTON_SELECT
|
||||
int "Button SELECT GPIO Pin"
|
||||
default 18
|
||||
help
|
||||
GPIO pin number for the select button.
|
||||
|
||||
config BUTTON_BACK
|
||||
int "Button BACK GPIO Pin"
|
||||
default 16
|
||||
help
|
||||
GPIO pin number for the back button.
|
||||
endmenu
|
||||
endmenu
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define BUTTON_UP GPIO_NUM_1
|
||||
#define BUTTON_DOWN GPIO_NUM_6
|
||||
#define BUTTON_LEFT GPIO_NUM_3
|
||||
#define BUTTON_RIGHT GPIO_NUM_5
|
||||
#define BUTTON_SELECT GPIO_NUM_18
|
||||
#define BUTTON_BACK GPIO_NUM_16
|
||||
@@ -1,262 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "u8g2_esp32_hal.h"
|
||||
|
||||
static const char* TAG = "u8g2_hal";
|
||||
static const unsigned int I2C_TIMEOUT_MS = 1000;
|
||||
|
||||
static spi_device_handle_t handle_spi; // SPI handle.
|
||||
static i2c_cmd_handle_t handle_i2c; // I2C handle.
|
||||
static u8g2_esp32_hal_t u8g2_esp32_hal; // HAL state data.
|
||||
|
||||
#define HOST SPI2_HOST
|
||||
|
||||
#undef ESP_ERROR_CHECK
|
||||
#define ESP_ERROR_CHECK(x) \
|
||||
do { \
|
||||
esp_err_t rc = (x); \
|
||||
if (rc != ESP_OK) { \
|
||||
ESP_LOGE("err", "esp_err_t = %d", rc); \
|
||||
assert(0 && #x); \
|
||||
} \
|
||||
} while (0);
|
||||
|
||||
/*
|
||||
* Initialze the ESP32 HAL.
|
||||
*/
|
||||
void u8g2_esp32_hal_init(u8g2_esp32_hal_t u8g2_esp32_hal_param) {
|
||||
u8g2_esp32_hal = u8g2_esp32_hal_param;
|
||||
} // u8g2_esp32_hal_init
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle SPI communications.
|
||||
*/
|
||||
uint8_t u8g2_esp32_spi_byte_cb(u8x8_t* u8x8,
|
||||
uint8_t msg,
|
||||
uint8_t arg_int,
|
||||
void* arg_ptr) {
|
||||
ESP_LOGD(TAG, "spi_byte_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p",
|
||||
msg, arg_int, arg_ptr);
|
||||
switch (msg) {
|
||||
case U8X8_MSG_BYTE_SET_DC:
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.dc, arg_int);
|
||||
}
|
||||
break;
|
||||
|
||||
case U8X8_MSG_BYTE_INIT: {
|
||||
if (u8g2_esp32_hal.bus.spi.clk == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.spi.mosi == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.spi.cs == U8G2_ESP32_HAL_UNDEFINED) {
|
||||
break;
|
||||
}
|
||||
|
||||
spi_bus_config_t bus_config;
|
||||
memset(&bus_config, 0, sizeof(spi_bus_config_t));
|
||||
bus_config.sclk_io_num = u8g2_esp32_hal.bus.spi.clk; // CLK
|
||||
bus_config.mosi_io_num = u8g2_esp32_hal.bus.spi.mosi; // MOSI
|
||||
bus_config.miso_io_num = GPIO_NUM_NC; // MISO
|
||||
bus_config.quadwp_io_num = GPIO_NUM_NC; // Not used
|
||||
bus_config.quadhd_io_num = GPIO_NUM_NC; // Not used
|
||||
// ESP_LOGI(TAG, "... Initializing bus.");
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(HOST, &bus_config, 1));
|
||||
|
||||
spi_device_interface_config_t dev_config;
|
||||
dev_config.address_bits = 0;
|
||||
dev_config.command_bits = 0;
|
||||
dev_config.dummy_bits = 0;
|
||||
dev_config.mode = 0;
|
||||
dev_config.duty_cycle_pos = 0;
|
||||
dev_config.cs_ena_posttrans = 0;
|
||||
dev_config.cs_ena_pretrans = 0;
|
||||
dev_config.clock_speed_hz = 10000;
|
||||
dev_config.spics_io_num = u8g2_esp32_hal.bus.spi.cs;
|
||||
dev_config.flags = 0;
|
||||
dev_config.queue_size = 200;
|
||||
dev_config.pre_cb = NULL;
|
||||
dev_config.post_cb = NULL;
|
||||
// ESP_LOGI(TAG, "... Adding device bus.");
|
||||
ESP_ERROR_CHECK(spi_bus_add_device(HOST, &dev_config, &handle_spi));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_SEND: {
|
||||
spi_transaction_t trans_desc;
|
||||
trans_desc.addr = 0;
|
||||
trans_desc.cmd = 0;
|
||||
trans_desc.flags = 0;
|
||||
trans_desc.length = 8 * arg_int; // Number of bits NOT number of bytes.
|
||||
trans_desc.rxlength = 0;
|
||||
trans_desc.tx_buffer = arg_ptr;
|
||||
trans_desc.rx_buffer = NULL;
|
||||
|
||||
// ESP_LOGI(TAG, "... Transmitting %d bytes.", arg_int);
|
||||
ESP_ERROR_CHECK(spi_device_transmit(handle_spi, &trans_desc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_spi_byte_cb
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle I2C communications.
|
||||
*/
|
||||
uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t* u8x8,
|
||||
uint8_t msg,
|
||||
uint8_t arg_int,
|
||||
void* arg_ptr) {
|
||||
ESP_LOGD(TAG, "i2c_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p", msg,
|
||||
arg_int, arg_ptr);
|
||||
|
||||
switch (msg) {
|
||||
case U8X8_MSG_BYTE_SET_DC: {
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.dc, arg_int);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_INIT: {
|
||||
if (u8g2_esp32_hal.bus.i2c.sda == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.i2c.scl == U8G2_ESP32_HAL_UNDEFINED) {
|
||||
break;
|
||||
}
|
||||
|
||||
i2c_config_t conf = {0};
|
||||
conf.mode = I2C_MODE_MASTER;
|
||||
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
|
||||
conf.sda_io_num = u8g2_esp32_hal.bus.i2c.sda;
|
||||
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
|
||||
conf.scl_io_num = u8g2_esp32_hal.bus.i2c.scl;
|
||||
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
|
||||
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
|
||||
conf.master.clk_speed = I2C_MASTER_FREQ_HZ;
|
||||
ESP_LOGI(TAG, "i2c_param_config %d", conf.mode);
|
||||
ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf));
|
||||
ESP_LOGI(TAG, "i2c_driver_install %d", I2C_MASTER_NUM);
|
||||
ESP_ERROR_CHECK(i2c_driver_install(I2C_MASTER_NUM, conf.mode,
|
||||
I2C_MASTER_RX_BUF_DISABLE,
|
||||
I2C_MASTER_TX_BUF_DISABLE, 0));
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_SEND: {
|
||||
uint8_t* data_ptr = (uint8_t*)arg_ptr;
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, data_ptr, arg_int, ESP_LOG_VERBOSE);
|
||||
|
||||
while (arg_int > 0) {
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_write_byte(handle_i2c, *data_ptr, ACK_CHECK_EN));
|
||||
data_ptr++;
|
||||
arg_int--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_START_TRANSFER: {
|
||||
uint8_t i2c_address = u8x8_GetI2CAddress(u8x8);
|
||||
handle_i2c = i2c_cmd_link_create();
|
||||
ESP_LOGD(TAG, "Start I2C transfer to %02X.", i2c_address >> 1);
|
||||
ESP_ERROR_CHECK(i2c_master_start(handle_i2c));
|
||||
ESP_ERROR_CHECK(i2c_master_write_byte(
|
||||
handle_i2c, i2c_address | I2C_MASTER_WRITE, ACK_CHECK_EN));
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_END_TRANSFER: {
|
||||
ESP_LOGD(TAG, "End I2C transfer.");
|
||||
ESP_ERROR_CHECK(i2c_master_stop(handle_i2c));
|
||||
ESP_ERROR_CHECK(i2c_master_cmd_begin(I2C_MASTER_NUM, handle_i2c,
|
||||
pdMS_TO_TICKS(I2C_TIMEOUT_MS)));
|
||||
i2c_cmd_link_delete(handle_i2c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_i2c_byte_cb
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle callbacks for GPIO and delay functions.
|
||||
*/
|
||||
uint8_t u8g2_esp32_gpio_and_delay_cb(u8x8_t* u8x8,
|
||||
uint8_t msg,
|
||||
uint8_t arg_int,
|
||||
void* arg_ptr) {
|
||||
ESP_LOGD(TAG,
|
||||
"gpio_and_delay_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p",
|
||||
msg, arg_int, arg_ptr);
|
||||
|
||||
switch (msg) {
|
||||
// Initialize the GPIO and DELAY HAL functions. If the pins for DC and
|
||||
// RESET have been specified then we define those pins as GPIO outputs.
|
||||
case U8X8_MSG_GPIO_AND_DELAY_INIT: {
|
||||
uint64_t bitmask = 0;
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.dc);
|
||||
}
|
||||
if (u8g2_esp32_hal.reset != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.reset);
|
||||
}
|
||||
if (u8g2_esp32_hal.bus.spi.cs != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.bus.spi.cs);
|
||||
}
|
||||
|
||||
if (bitmask == 0) {
|
||||
break;
|
||||
}
|
||||
gpio_config_t gpioConfig;
|
||||
gpioConfig.pin_bit_mask = bitmask;
|
||||
gpioConfig.mode = GPIO_MODE_OUTPUT;
|
||||
gpioConfig.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
gpioConfig.pull_down_en = GPIO_PULLDOWN_ENABLE;
|
||||
gpioConfig.intr_type = GPIO_INTR_DISABLE;
|
||||
gpio_config(&gpioConfig);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the GPIO reset pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_RESET:
|
||||
if (u8g2_esp32_hal.reset != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.reset, arg_int);
|
||||
}
|
||||
break;
|
||||
// Set the GPIO client select pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_CS:
|
||||
if (u8g2_esp32_hal.bus.spi.cs != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.bus.spi.cs, arg_int);
|
||||
}
|
||||
break;
|
||||
// Set the Software I²C pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_I2C_CLOCK:
|
||||
if (u8g2_esp32_hal.bus.i2c.scl != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.bus.i2c.scl, arg_int);
|
||||
// printf("%c",(arg_int==1?'C':'c'));
|
||||
}
|
||||
break;
|
||||
// Set the Software I²C pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_I2C_DATA:
|
||||
if (u8g2_esp32_hal.bus.i2c.sda != U8G2_ESP32_HAL_UNDEFINED) {
|
||||
gpio_set_level(u8g2_esp32_hal.bus.i2c.sda, arg_int);
|
||||
// printf("%c",(arg_int==1?'D':'d'));
|
||||
}
|
||||
break;
|
||||
|
||||
// Delay for the number of milliseconds passed in through arg_int.
|
||||
case U8X8_MSG_DELAY_MILLI:
|
||||
vTaskDelay(arg_int / portTICK_PERIOD_MS);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_gpio_and_delay_cb
|
||||
@@ -1,74 +0,0 @@
|
||||
#include "i2c_checker.h"
|
||||
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_insights.h"
|
||||
#include "esp_log.h"
|
||||
#include "hal/u8g2_esp32_hal.h"
|
||||
|
||||
static const char *TAG = "i2c_checker";
|
||||
|
||||
esp_err_t i2c_device_check(i2c_port_t i2c_port, uint8_t device_address)
|
||||
{
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
// Send the device address with the write bit (LSB = 0)
|
||||
i2c_master_write_byte(cmd, (device_address << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_stop(cmd);
|
||||
|
||||
esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, pdMS_TO_TICKS(100));
|
||||
|
||||
i2c_cmd_link_delete(cmd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t i2c_bus_scan_and_check(void)
|
||||
{
|
||||
// 1. Configure and install I2C bus
|
||||
i2c_config_t conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = I2C_MASTER_SDA_PIN,
|
||||
.scl_io_num = I2C_MASTER_SCL_PIN,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = I2C_MASTER_FREQ_HZ,
|
||||
};
|
||||
esp_err_t err = i2c_param_config(I2C_MASTER_NUM, &conf);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "I2C parameter configuration failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "I2C driver installation failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "I2C driver initialized. Searching for device...");
|
||||
|
||||
// 2. Check if the device is present
|
||||
err = i2c_device_check(I2C_MASTER_NUM, DISPLAY_I2C_ADDRESS);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
|
||||
// Here you could now call e.g. setup_screen()
|
||||
}
|
||||
else if (err == ESP_ERR_TIMEOUT)
|
||||
{
|
||||
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// 3. Uninstall I2C driver if it is no longer needed
|
||||
i2c_driver_delete(I2C_MASTER_NUM);
|
||||
ESP_DIAG_EVENT(TAG, "I2C driver uninstalled.");
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -3,6 +3,5 @@ dependencies:
|
||||
git: https://github.com/olikraus/u8g2.git
|
||||
# u8g2_hal:
|
||||
# git: https://github.com/mkfrey/u8g2-hal-esp-idf.git
|
||||
espressif/button: ^4.1.3
|
||||
espressif/button: ^4.1.4
|
||||
espressif/esp_insights: ^1.2.7
|
||||
espressif/mqtt: ^1.0.0
|
||||
|
||||
10
firmware/main/include/common.h
Normal file
10
firmware/main/include/common.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "driver/gpio.h"
|
||||
|
||||
#define BUTTON_UP ((gpio_num_t)CONFIG_BUTTON_UP)
|
||||
#define BUTTON_DOWN ((gpio_num_t)CONFIG_BUTTON_DOWN)
|
||||
#define BUTTON_LEFT ((gpio_num_t)CONFIG_BUTTON_LEFT)
|
||||
#define BUTTON_RIGHT ((gpio_num_t)CONFIG_BUTTON_RIGHT)
|
||||
#define BUTTON_SELECT ((gpio_num_t)CONFIG_BUTTON_SELECT)
|
||||
#define BUTTON_BACK ((gpio_num_t)CONFIG_BUTTON_BACK)
|
||||
@@ -13,10 +13,11 @@
|
||||
#include "u8g2.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "hal/i2c_types.h"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
#define U8G2_ESP32_HAL_UNDEFINED GPIO_NUM_NC
|
||||
|
||||
#define I2C_MASTER_NUM I2C_NUM_0 // I2C port number for master dev
|
||||
31
firmware/main/isrgrootx1.pem
Normal file
31
firmware/main/isrgrootx1.pem
Normal file
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -2,18 +2,23 @@
|
||||
|
||||
#include "analytics.h"
|
||||
#include "button_handling.h"
|
||||
#include "common.h"
|
||||
#include "common/InactivityTracker.h"
|
||||
#include "hal/u8g2_esp32_hal.h"
|
||||
#include "hal_esp32/PersistenceManager.h"
|
||||
#include "i2c_checker.h"
|
||||
#include "led_status.h"
|
||||
#include "message_manager.h"
|
||||
#include "my_mqtt_client.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "simulator.h"
|
||||
#include "ui/ClockScreenSaver.h"
|
||||
#include "ui/ScreenSaver.h"
|
||||
#include "ui/SplashScreen.h"
|
||||
#include "wifi_manager.h"
|
||||
#include <cstring>
|
||||
#include <driver/i2c.h>
|
||||
#include <esp_diagnostics.h>
|
||||
#include <esp_insights.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
@@ -32,7 +37,8 @@ uint8_t received_signal;
|
||||
std::shared_ptr<Widget> m_widget;
|
||||
std::vector<std::shared_ptr<Widget>> m_history;
|
||||
std::unique_ptr<InactivityTracker> m_inactivityTracker;
|
||||
std::shared_ptr<PersistenceManager> m_persistenceManager;
|
||||
// Persistence Manager for C-API
|
||||
persistence_manager_t g_persistence_manager;
|
||||
|
||||
extern QueueHandle_t buttonQueue;
|
||||
|
||||
@@ -62,6 +68,7 @@ void setScreen(const std::shared_ptr<Widget> &screen)
|
||||
{
|
||||
if (screen != nullptr)
|
||||
{
|
||||
ESP_DIAG_EVENT(TAG, "Screen set: %s", screen->getName());
|
||||
m_widget = screen;
|
||||
m_history.clear();
|
||||
m_history.emplace_back(m_widget);
|
||||
@@ -77,6 +84,7 @@ void pushScreen(const std::shared_ptr<Widget> &screen)
|
||||
{
|
||||
m_widget->onPause();
|
||||
}
|
||||
ESP_DIAG_EVENT(TAG, "Screen pushed: %s", screen->getName());
|
||||
m_widget = screen;
|
||||
m_widget->onEnter();
|
||||
m_history.emplace_back(m_widget);
|
||||
@@ -90,27 +98,25 @@ void popScreen()
|
||||
m_history.pop_back();
|
||||
if (m_widget)
|
||||
{
|
||||
if (m_persistenceManager != nullptr)
|
||||
{
|
||||
m_persistenceManager->Save();
|
||||
}
|
||||
persistence_manager_save(&g_persistence_manager);
|
||||
m_widget->onExit();
|
||||
}
|
||||
m_widget = m_history.back();
|
||||
ESP_DIAG_EVENT(TAG, "Screen popped, now: %s", m_widget->getName());
|
||||
m_widget->onResume();
|
||||
}
|
||||
}
|
||||
|
||||
static void init_ui(void)
|
||||
{
|
||||
m_persistenceManager = std::make_shared<PersistenceManager>();
|
||||
persistence_manager_init(&g_persistence_manager, "config");
|
||||
options = {
|
||||
.u8g2 = &u8g2,
|
||||
.setScreen = [](const std::shared_ptr<Widget> &screen) { setScreen(screen); },
|
||||
.pushScreen = [](const std::shared_ptr<Widget> &screen) { pushScreen(screen); },
|
||||
.popScreen = []() { popScreen(); },
|
||||
.onButtonClicked = nullptr,
|
||||
.persistenceManager = m_persistenceManager,
|
||||
.persistenceManager = &g_persistence_manager,
|
||||
};
|
||||
m_widget = std::make_shared<SplashScreen>(&options);
|
||||
m_inactivityTracker = std::make_unique<InactivityTracker>(60000, []() {
|
||||
@@ -123,6 +129,17 @@ static void init_ui(void)
|
||||
u8g2_SendBuffer(&u8g2);
|
||||
}
|
||||
|
||||
static void on_message_received(const message_t *msg)
|
||||
{
|
||||
if (msg && msg->type == MESSAGE_TYPE_SETTINGS &&
|
||||
(std::strcmp(msg->data.settings.key, "light_active") == 0 ||
|
||||
std::strcmp(msg->data.settings.key, "light_variant") == 0 ||
|
||||
std::strcmp(msg->data.settings.key, "light_mode") == 0))
|
||||
{
|
||||
start_simulation();
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_button(uint8_t button)
|
||||
{
|
||||
m_inactivityTracker->reset();
|
||||
@@ -180,14 +197,65 @@ void app_task(void *args)
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize display so that info can be shown
|
||||
setup_screen();
|
||||
|
||||
// Check BACK button and delete settings if necessary (with countdown)
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_INPUT;
|
||||
io_conf.pin_bit_mask = (1ULL << BUTTON_BACK);
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
if (gpio_get_level(BUTTON_BACK) == 0)
|
||||
{
|
||||
u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr);
|
||||
for (int i = 5; i > 0; --i)
|
||||
{
|
||||
u8g2_ClearBuffer(&u8g2);
|
||||
u8g2_DrawStr(&u8g2, 5, 20, "BACK gedrueckt!");
|
||||
u8g2_DrawStr(&u8g2, 5, 35, "Halte fuer Reset...");
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Loesche in %d s", i);
|
||||
u8g2_DrawStr(&u8g2, 5, 55, buf);
|
||||
u8g2_SendBuffer(&u8g2);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
if (gpio_get_level(BUTTON_BACK) != 0)
|
||||
{
|
||||
// Button released, abort
|
||||
break;
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
// After 5 seconds still pressed: perform factory reset
|
||||
u8g2_ClearBuffer(&u8g2);
|
||||
u8g2_DrawStr(&u8g2, 5, 30, "Alle Einstellungen ");
|
||||
u8g2_DrawStr(&u8g2, 5, 45, "werden geloescht...");
|
||||
u8g2_SendBuffer(&u8g2);
|
||||
persistence_manager_factory_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
u8g2_ClearBuffer(&u8g2);
|
||||
u8g2_DrawStr(&u8g2, 5, 35, "Fertig. Neustart...");
|
||||
u8g2_SendBuffer(&u8g2);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message_manager_init();
|
||||
|
||||
setup_buttons();
|
||||
init_ui();
|
||||
|
||||
#if CONFIG_WIFI_ENABLED
|
||||
wifi_manager_init();
|
||||
analytics_init();
|
||||
#endif
|
||||
|
||||
mqtt_client_start();
|
||||
|
||||
message_manager_register_listener(on_message_received);
|
||||
|
||||
start_simulation();
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
static const char *TAG = "button_handling";
|
||||
|
||||
const uint8_t gpios[] = {BUTTON_DOWN, BUTTON_UP, BUTTON_LEFT, BUTTON_RIGHT, BUTTON_SELECT, BUTTON_BACK};
|
||||
static const char *button_names[] = {"DOWN", "UP", "LEFT", "RIGHT", "SELECT", "BACK"};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t gpio;
|
||||
uint8_t index;
|
||||
} button_user_data_t;
|
||||
|
||||
static button_user_data_t button_data[6];
|
||||
@@ -35,8 +37,9 @@ static void button_event_cb(void *arg, void *usr_data)
|
||||
}
|
||||
button_user_data_t *data = (button_user_data_t *)usr_data;
|
||||
uint8_t gpio_num = data->gpio;
|
||||
const char *button_name = button_names[data->index];
|
||||
|
||||
ESP_LOGI(TAG, "Button pressed on GPIO %d", gpio_num);
|
||||
ESP_DIAG_EVENT(TAG, "Button %s pressed (GPIO %d)", button_name, gpio_num);
|
||||
|
||||
if (xQueueSend(buttonQueue, &gpio_num, 0) != pdTRUE)
|
||||
{
|
||||
@@ -60,6 +63,7 @@ static void init_button(uint8_t gpio, int index)
|
||||
}
|
||||
|
||||
button_data[index].gpio = gpio;
|
||||
button_data[index].index = index;
|
||||
iot_button_register_cb(gpio_btn, BUTTON_SINGLE_CLICK, NULL, button_event_cb, &button_data[index]);
|
||||
}
|
||||
|
||||
316
firmware/main/src/hal/u8g2_esp32_hal.c
Normal file
316
firmware/main/src/hal/u8g2_esp32_hal.c
Normal file
@@ -0,0 +1,316 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "hal/u8g2_esp32_hal.h"
|
||||
|
||||
static const char *TAG = "u8g2_hal";
|
||||
static const unsigned int I2C_TIMEOUT_MS = 1000;
|
||||
|
||||
static spi_device_handle_t handle_spi; // SPI handle.
|
||||
static i2c_master_bus_handle_t i2c_bus; // I2C bus handle (new driver).
|
||||
static i2c_master_dev_handle_t i2c_dev; // I2C device handle (new driver).
|
||||
static uint8_t i2c_tx_buf[256]; // Buffer for one I2C transaction.
|
||||
static size_t i2c_tx_len; // Current length in buffer.
|
||||
static uint8_t current_i2c_addr7; // Current 7-bit device address.
|
||||
static u8g2_esp32_hal_t u8g2_esp32_hal; // HAL state data.
|
||||
static bool i2c_transfer_failed = false; // Flag to track I2C transfer errors
|
||||
|
||||
#define HOST SPI2_HOST
|
||||
|
||||
#undef ESP_ERROR_CHECK
|
||||
#define ESP_ERROR_CHECK(x) \
|
||||
do \
|
||||
{ \
|
||||
esp_err_t rc = (x); \
|
||||
if (rc != ESP_OK) \
|
||||
{ \
|
||||
ESP_LOGE("err", "esp_err_t = %d", rc); \
|
||||
assert(0 && #x); \
|
||||
} \
|
||||
} while (0);
|
||||
|
||||
// Softer error handling for I2C operations that may fail temporarily
|
||||
#define I2C_ERROR_CHECK(x) \
|
||||
do \
|
||||
{ \
|
||||
esp_err_t rc = (x); \
|
||||
if (rc != ESP_OK) \
|
||||
{ \
|
||||
ESP_LOGW(TAG, "I2C error: %s = %d", #x, rc); \
|
||||
i2c_transfer_failed = true; \
|
||||
} \
|
||||
} while (0);
|
||||
|
||||
/*
|
||||
* Initialze the ESP32 HAL.
|
||||
*/
|
||||
void u8g2_esp32_hal_init(u8g2_esp32_hal_t u8g2_esp32_hal_param)
|
||||
{
|
||||
u8g2_esp32_hal = u8g2_esp32_hal_param;
|
||||
} // u8g2_esp32_hal_init
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle SPI communications.
|
||||
*/
|
||||
uint8_t u8g2_esp32_spi_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
|
||||
{
|
||||
ESP_LOGD(TAG, "spi_byte_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p", msg, arg_int, arg_ptr);
|
||||
switch (msg)
|
||||
{
|
||||
case U8X8_MSG_BYTE_SET_DC:
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.dc, arg_int);
|
||||
}
|
||||
break;
|
||||
|
||||
case U8X8_MSG_BYTE_INIT: {
|
||||
if (u8g2_esp32_hal.bus.spi.clk == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.spi.mosi == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.spi.cs == U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
spi_bus_config_t bus_config;
|
||||
memset(&bus_config, 0, sizeof(spi_bus_config_t));
|
||||
bus_config.sclk_io_num = u8g2_esp32_hal.bus.spi.clk; // CLK
|
||||
bus_config.mosi_io_num = u8g2_esp32_hal.bus.spi.mosi; // MOSI
|
||||
bus_config.miso_io_num = GPIO_NUM_NC; // MISO
|
||||
bus_config.quadwp_io_num = GPIO_NUM_NC; // Not used
|
||||
bus_config.quadhd_io_num = GPIO_NUM_NC; // Not used
|
||||
// ESP_LOGI(TAG, "... Initializing bus.");
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(HOST, &bus_config, 1));
|
||||
|
||||
spi_device_interface_config_t dev_config;
|
||||
dev_config.address_bits = 0;
|
||||
dev_config.command_bits = 0;
|
||||
dev_config.dummy_bits = 0;
|
||||
dev_config.mode = 0;
|
||||
dev_config.duty_cycle_pos = 0;
|
||||
dev_config.cs_ena_posttrans = 0;
|
||||
dev_config.cs_ena_pretrans = 0;
|
||||
dev_config.clock_speed_hz = 10000;
|
||||
dev_config.spics_io_num = u8g2_esp32_hal.bus.spi.cs;
|
||||
dev_config.flags = 0;
|
||||
dev_config.queue_size = 200;
|
||||
dev_config.pre_cb = NULL;
|
||||
dev_config.post_cb = NULL;
|
||||
// ESP_LOGI(TAG, "... Adding device bus.");
|
||||
ESP_ERROR_CHECK(spi_bus_add_device(HOST, &dev_config, &handle_spi));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_SEND: {
|
||||
spi_transaction_t trans_desc;
|
||||
trans_desc.addr = 0;
|
||||
trans_desc.cmd = 0;
|
||||
trans_desc.flags = 0;
|
||||
trans_desc.length = 8 * arg_int; // Number of bits NOT number of bytes.
|
||||
trans_desc.rxlength = 0;
|
||||
trans_desc.tx_buffer = arg_ptr;
|
||||
trans_desc.rx_buffer = NULL;
|
||||
|
||||
// ESP_LOGI(TAG, "... Transmitting %d bytes.", arg_int);
|
||||
ESP_ERROR_CHECK(spi_device_transmit(handle_spi, &trans_desc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_spi_byte_cb
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle I2C communications.
|
||||
*/
|
||||
uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
|
||||
{
|
||||
ESP_LOGD(TAG, "i2c_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p", msg, arg_int, arg_ptr);
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case U8X8_MSG_BYTE_SET_DC: {
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.dc, arg_int);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_INIT: {
|
||||
if (u8g2_esp32_hal.bus.i2c.sda == U8G2_ESP32_HAL_UNDEFINED ||
|
||||
u8g2_esp32_hal.bus.i2c.scl == U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Neue I2C-Master-API: Bus einmalig anlegen
|
||||
if (i2c_bus == NULL)
|
||||
{
|
||||
i2c_master_bus_config_t bus_cfg = {
|
||||
.i2c_port = I2C_MASTER_NUM,
|
||||
.scl_io_num = u8g2_esp32_hal.bus.i2c.scl,
|
||||
.sda_io_num = u8g2_esp32_hal.bus.i2c.sda,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.flags = {.enable_internal_pullup = true},
|
||||
};
|
||||
|
||||
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
|
||||
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
|
||||
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
|
||||
ESP_LOGI(TAG, "i2c_new_master_bus %d", I2C_MASTER_NUM);
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &i2c_bus));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_SEND: {
|
||||
if (i2c_transfer_failed)
|
||||
{
|
||||
break; // Skip sending if transfer already failed
|
||||
}
|
||||
uint8_t *data_ptr = (uint8_t *)arg_ptr;
|
||||
ESP_LOG_BUFFER_HEXDUMP(TAG, data_ptr, arg_int, ESP_LOG_VERBOSE);
|
||||
|
||||
// Bytes in lokalen Puffer sammeln, tatsächliche Übertragung bei END_TRANSFER
|
||||
if (i2c_tx_len + (size_t)arg_int > sizeof(i2c_tx_buf))
|
||||
{
|
||||
ESP_LOGW(TAG, "I2C tx buffer overflow (%zu + %d)", i2c_tx_len, arg_int);
|
||||
i2c_transfer_failed = true;
|
||||
break;
|
||||
}
|
||||
memcpy(&i2c_tx_buf[i2c_tx_len], data_ptr, arg_int);
|
||||
i2c_tx_len += (size_t)arg_int;
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_START_TRANSFER: {
|
||||
uint8_t i2c_address = u8x8_GetI2CAddress(u8x8);
|
||||
ESP_LOGD(TAG, "Start I2C transfer to %02X.", i2c_address >> 1);
|
||||
i2c_transfer_failed = false; // Reset error flag at start of transfer
|
||||
|
||||
// Für neuen Treiber: Device-Handle für diese 7-Bit-Adresse anlegen (oder wiederverwenden)
|
||||
uint8_t addr7 = i2c_address >> 1;
|
||||
if (i2c_dev == NULL || addr7 != current_i2c_addr7)
|
||||
{
|
||||
if (i2c_dev)
|
||||
{
|
||||
i2c_master_bus_rm_device(i2c_dev);
|
||||
i2c_dev = NULL;
|
||||
}
|
||||
|
||||
i2c_device_config_t dev_cfg = {
|
||||
.device_address = addr7,
|
||||
.scl_speed_hz = I2C_MASTER_FREQ_HZ,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &dev_cfg, &i2c_dev));
|
||||
current_i2c_addr7 = addr7;
|
||||
}
|
||||
i2c_tx_len = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case U8X8_MSG_BYTE_END_TRANSFER: {
|
||||
ESP_LOGD(TAG, "End I2C transfer.");
|
||||
if (!i2c_transfer_failed && i2c_dev != NULL && i2c_tx_len > 0)
|
||||
{
|
||||
esp_err_t rc = i2c_master_transmit(i2c_dev, i2c_tx_buf, i2c_tx_len, I2C_TIMEOUT_MS);
|
||||
if (rc != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "I2C error: i2c_master_transmit = %d", rc);
|
||||
i2c_transfer_failed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_i2c_byte_cb
|
||||
|
||||
/*
|
||||
* HAL callback function as prescribed by the U8G2 library. This callback is
|
||||
* invoked to handle callbacks for GPIO and delay functions.
|
||||
*/
|
||||
uint8_t u8g2_esp32_gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
|
||||
{
|
||||
ESP_LOGD(TAG, "gpio_and_delay_cb: Received a msg: %d, arg_int: %d, arg_ptr: %p", msg, arg_int, arg_ptr);
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
// Initialize the GPIO and DELAY HAL functions. If the pins for DC and
|
||||
// RESET have been specified then we define those pins as GPIO outputs.
|
||||
case U8X8_MSG_GPIO_AND_DELAY_INIT: {
|
||||
uint64_t bitmask = 0;
|
||||
if (u8g2_esp32_hal.dc != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.dc);
|
||||
}
|
||||
if (u8g2_esp32_hal.reset != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.reset);
|
||||
}
|
||||
if (u8g2_esp32_hal.bus.spi.cs != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
bitmask = bitmask | (1ull << u8g2_esp32_hal.bus.spi.cs);
|
||||
}
|
||||
|
||||
if (bitmask == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
gpio_config_t gpioConfig;
|
||||
gpioConfig.pin_bit_mask = bitmask;
|
||||
gpioConfig.mode = GPIO_MODE_OUTPUT;
|
||||
gpioConfig.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
gpioConfig.pull_down_en = GPIO_PULLDOWN_ENABLE;
|
||||
gpioConfig.intr_type = GPIO_INTR_DISABLE;
|
||||
gpio_config(&gpioConfig);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the GPIO reset pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_RESET:
|
||||
if (u8g2_esp32_hal.reset != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.reset, arg_int);
|
||||
}
|
||||
break;
|
||||
// Set the GPIO client select pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_CS:
|
||||
if (u8g2_esp32_hal.bus.spi.cs != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.bus.spi.cs, arg_int);
|
||||
}
|
||||
break;
|
||||
// Set the Software I²C pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_I2C_CLOCK:
|
||||
if (u8g2_esp32_hal.bus.i2c.scl != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.bus.i2c.scl, arg_int);
|
||||
// printf("%c",(arg_int==1?'C':'c'));
|
||||
}
|
||||
break;
|
||||
// Set the Software I²C pin to the value passed in through arg_int.
|
||||
case U8X8_MSG_GPIO_I2C_DATA:
|
||||
if (u8g2_esp32_hal.bus.i2c.sda != U8G2_ESP32_HAL_UNDEFINED)
|
||||
{
|
||||
gpio_set_level(u8g2_esp32_hal.bus.i2c.sda, arg_int);
|
||||
// printf("%c",(arg_int==1?'D':'d'));
|
||||
}
|
||||
break;
|
||||
|
||||
// Delay for the number of milliseconds passed in through arg_int.
|
||||
case U8X8_MSG_DELAY_MILLI:
|
||||
vTaskDelay(arg_int / portTICK_PERIOD_MS);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
} // u8g2_esp32_gpio_and_delay_cb
|
||||
63
firmware/main/src/i2c_checker.c
Normal file
63
firmware/main/src/i2c_checker.c
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "i2c_checker.h"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_insights.h"
|
||||
#include "esp_log.h"
|
||||
#include "hal/u8g2_esp32_hal.h"
|
||||
|
||||
static const char *TAG = "i2c_checker";
|
||||
|
||||
static esp_err_t i2c_device_check(i2c_master_bus_handle_t i2c_bus, uint8_t device_address)
|
||||
{
|
||||
// Use the new I2C master driver to probe for the device.
|
||||
return i2c_master_probe(i2c_bus, device_address, 100);
|
||||
}
|
||||
|
||||
esp_err_t i2c_bus_scan_and_check(void)
|
||||
{
|
||||
// 1. Configure and create I2C master bus using the new driver API
|
||||
i2c_master_bus_handle_t i2c_bus = NULL;
|
||||
i2c_master_bus_config_t bus_cfg = {
|
||||
.i2c_port = I2C_MASTER_NUM,
|
||||
.scl_io_num = I2C_MASTER_SCL_PIN,
|
||||
.sda_io_num = I2C_MASTER_SDA_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.flags = {.enable_internal_pullup = true},
|
||||
};
|
||||
|
||||
esp_err_t err = i2c_new_master_bus(&bus_cfg, &i2c_bus);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "I2C bus creation failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "I2C master bus initialized. Searching for device...");
|
||||
|
||||
// 2. Check if the device is present using the new API
|
||||
err = i2c_device_check(i2c_bus, DISPLAY_I2C_ADDRESS);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
|
||||
// Here you could now call e.g. setup_screen()
|
||||
}
|
||||
else if (err == ESP_ERR_TIMEOUT)
|
||||
{
|
||||
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// 3. Delete I2C master bus if it is no longer needed
|
||||
esp_err_t del_err = i2c_del_master_bus(i2c_bus);
|
||||
if (del_err != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "Failed to delete I2C master bus: %s", esp_err_to_name(del_err));
|
||||
}
|
||||
ESP_DIAG_EVENT(TAG, "I2C master bus deleted.");
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "app_task.h"
|
||||
#include "color.h"
|
||||
#include "hal_esp32/PersistenceManager.h"
|
||||
#include "led_status.h"
|
||||
#include "led_strip_ws2812.h"
|
||||
#include "persistence_manager.h"
|
||||
#include "wifi_manager.h"
|
||||
#include <ble_manager.h>
|
||||
#include <esp_event.h>
|
||||
@@ -24,8 +24,9 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
}
|
||||
|
||||
auto persistence = PersistenceManager();
|
||||
persistence.Load();
|
||||
persistence_manager_t persistence;
|
||||
persistence_manager_init(&persistence, "config");
|
||||
persistence_manager_load(&persistence);
|
||||
|
||||
led_status_init(CONFIG_STATUS_WLED_PIN);
|
||||
|
||||
@@ -42,3 +42,11 @@ CONFIG_SPIRAM=y
|
||||
# SPI RAM config
|
||||
CONFIG_SPIRAM_SPEED=80
|
||||
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
|
||||
|
||||
# HTTP Server WebSocket Support
|
||||
CONFIG_HTTPD_WS_SUPPORT=y
|
||||
|
||||
# MQTT
|
||||
CONFIG_MQTT_CLIENT_BROKER_URL="mqtts://mqtt.mars3142.dev:8883"
|
||||
CONFIG_MQTT_CLIENT_USERNAME="mars3142"
|
||||
CONFIG_MQTT_CLIENT_PASSWORD="KPkEyzs9aur3Y7LfEybnd8PsxWd94ouQZGNGJ24y"
|
||||
|
||||
@@ -1,2 +1,20 @@
|
||||
# default ESP target
|
||||
CONFIG_IDF_TARGET="esp32c6"
|
||||
|
||||
#
|
||||
# Display Settings
|
||||
#
|
||||
CONFIG_DISPLAY_SDA_PIN=9
|
||||
CONFIG_DISPLAY_SCL_PIN=8
|
||||
# end of Display Settings
|
||||
|
||||
#
|
||||
# Button Configuration
|
||||
#
|
||||
CONFIG_BUTTON_UP=7
|
||||
CONFIG_BUTTON_DOWN=4
|
||||
CONFIG_BUTTON_LEFT=6
|
||||
CONFIG_BUTTON_RIGHT=5
|
||||
CONFIG_BUTTON_SELECT=19
|
||||
CONFIG_BUTTON_BACK=20
|
||||
# end of Button Configuration
|
||||
|
||||
120
firmware/storage/www/captive.html
Normal file
120
firmware/storage/www/captive.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<title data-i18n="captive.title">System Control - WLAN Setup</title>
|
||||
<link rel="stylesheet" href="css/shared.css">
|
||||
<link rel="stylesheet" href="css/captive.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-controls">
|
||||
<button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln">
|
||||
<span class="lang-flag" id="lang-flag">🇩🇪</span>
|
||||
<span class="lang-label" id="lang-label">DE</span>
|
||||
</button>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Theme wechseln">
|
||||
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
|
||||
<span class="theme-toggle-label" id="theme-label">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1>🚂 System Control</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-group">
|
||||
<label for="ssid" data-i18n="wifi.ssid">WLAN-Name (SSID)</label>
|
||||
<input type="text" id="ssid" data-i18n-placeholder="wifi.ssid.placeholder"
|
||||
placeholder="Netzwerkname eingeben">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" data-i18n="wifi.password.short">Passwort</label>
|
||||
<div class="password-toggle">
|
||||
<input type="password" id="password" data-i18n-placeholder="captive.password.placeholder"
|
||||
placeholder="WLAN-Passwort">
|
||||
<button type="button" onclick="togglePassword()" id="password-btn">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="connect-btn" onclick="saveWifi()" data-i18n="captive.connect"
|
||||
disabled>
|
||||
💾 Verbinden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="wifi-status" class="status"></div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>ℹ️ <span data-i18n="captive.note.title">Hinweis:</span></strong>
|
||||
<span data-i18n="captive.note.text">Nach dem Speichern verbindet sich das Gerät mit dem gewählten
|
||||
Netzwerk.
|
||||
Diese Seite wird dann nicht mehr erreichbar sein. Verbinden Sie sich mit Ihrem normalen WLAN,
|
||||
um auf das Gerät zuzugreifen.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/i18n.js"></script>
|
||||
<script src="js/wifi-shared.js"></script>
|
||||
<script>
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
const icon = document.getElementById('theme-icon');
|
||||
const label = document.getElementById('theme-label');
|
||||
const metaTheme = document.querySelector('meta[name="theme-color"]');
|
||||
|
||||
if (theme === 'light') {
|
||||
icon.textContent = '☀️';
|
||||
label.textContent = 'Light';
|
||||
if (metaTheme) metaTheme.content = '#faf8f5';
|
||||
} else {
|
||||
icon.textContent = '🌙';
|
||||
label.textContent = 'Dark';
|
||||
if (metaTheme) metaTheme.content = '#1a1a2e';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
setTheme(current === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
// Button aktivieren/deaktivieren
|
||||
function updateConnectBtn() {
|
||||
const ssid = document.getElementById('ssid').value;
|
||||
const pw = document.getElementById('password').value;
|
||||
const btn = document.getElementById('connect-btn');
|
||||
btn.disabled = !(ssid.length > 0 && pw.length > 0);
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTheme();
|
||||
initI18n();
|
||||
document.getElementById('ssid').addEventListener('input', updateConnectBtn);
|
||||
document.getElementById('password').addEventListener('input', updateConnectBtn);
|
||||
updateConnectBtn();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
217
firmware/storage/www/css/captive.css
Normal file
217
firmware/storage/www/css/captive.css
Normal file
@@ -0,0 +1,217 @@
|
||||
@media (max-width: 600px) {
|
||||
.header {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
flex: 1 1 100%;
|
||||
text-align: center;
|
||||
order: 2;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
order: 1;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Captive Portal CSS - WiFi setup specific styles */
|
||||
/* Base styles are in shared.css */
|
||||
|
||||
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.header p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Theme Toggle - Absolute positioned */
|
||||
/* Handled by .captive-header in shared.css */
|
||||
|
||||
/* Card - Larger padding for captive */
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 20px var(--shadow);
|
||||
}
|
||||
|
||||
#connect-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
box-shadow: none;
|
||||
background-color: #888 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Group - More spacing */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Inputs - Thicker border for captive */
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select {
|
||||
padding: 14px 16px;
|
||||
border-width: 2px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Buttons - Full width for captive */
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Status - Centered text */
|
||||
.status {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.divider span {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Network List */
|
||||
.network-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.network-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.network-item:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.network-item.selected {
|
||||
background: var(--accent);
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.network-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.network-signal {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.signal-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
|
||||
/* Info Box */
|
||||
.info-box {
|
||||
background: var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
margin-top: 20px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Spinner - Smaller for captive */
|
||||
.spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px;
|
||||
}
|
||||
1334
firmware/storage/www/css/index.css
Normal file
1334
firmware/storage/www/css/index.css
Normal file
File diff suppressed because it is too large
Load Diff
357
firmware/storage/www/css/shared.css
Normal file
357
firmware/storage/www/css/shared.css
Normal file
@@ -0,0 +1,357 @@
|
||||
/* Passwortfeld Toggle (zentral für alle Seiten) */
|
||||
.password-toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.password-toggle input {
|
||||
padding-right: 50px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.password-toggle button {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.password-toggle button:active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Passwortfeld Toggle */
|
||||
.password-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.password-toggle input[type="password"],
|
||||
.password-toggle input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.password-toggle button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
padding: 0 6px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.password-toggle button:active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Shared CSS - Base styles for all pages */
|
||||
|
||||
/* CSS Variables - Dark Mode (default) */
|
||||
:root {
|
||||
--bg-color: #1a1a2e;
|
||||
--card-bg: #16213e;
|
||||
--accent: #0f3460;
|
||||
--text: #eaeaea;
|
||||
--text-muted: #a0a0a0;
|
||||
--success: #00d26a;
|
||||
--error: #ff6b6b;
|
||||
--border: #2a2a4a;
|
||||
--input-bg: #1a1a2e;
|
||||
--shadow: rgba(0, 0, 0, 0.3);
|
||||
--primary: #c41e3a;
|
||||
}
|
||||
|
||||
/* CSS Variables - Light Mode */
|
||||
[data-theme="light"] {
|
||||
--bg-color: #faf8f5;
|
||||
--card-bg: #ffffff;
|
||||
--accent: #fef2f2;
|
||||
--text: #1a1a2e;
|
||||
--text-muted: #6b7280;
|
||||
--success: #c41e3a;
|
||||
--error: #dc2626;
|
||||
--border: #e5d9d0;
|
||||
--input-bg: #ffffff;
|
||||
--shadow: rgba(196, 30, 58, 0.1);
|
||||
--primary: #c41e3a;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg-color);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h2::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: var(--success);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-primary:hover {
|
||||
background: #a31830;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Status Messages */
|
||||
.status {
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
display: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
display: block;
|
||||
background: rgba(0, 210, 106, 0.15);
|
||||
border: 1px solid var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
display: block;
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
border: 1px solid var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.status.info {
|
||||
display: block;
|
||||
background: rgba(15, 52, 96, 0.5);
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--card-bg);
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
font-size: 1.2rem;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.theme-toggle-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Language Toggle */
|
||||
.lang-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--card-bg);
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lang-toggle:hover {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.lang-flag {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Header Controls */
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.captive-header {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
[data-theme="light"] .header h1 {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--success);
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 12px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Safe area for notched phones */
|
||||
@supports (padding: max(0px)) {
|
||||
body {
|
||||
padding-left: max(12px, env(safe-area-inset-left));
|
||||
padding-right: max(12px, env(safe-area-inset-right));
|
||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
1574
firmware/storage/www/css/style.css
Normal file
1574
firmware/storage/www/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
1
firmware/storage/www/favicon.svg
Normal file
1
firmware/storage/www/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🚂</text></svg>
|
||||
|
After Width: | Height: | Size: 109 B |
475
firmware/storage/www/index.html
Normal file
475
firmware/storage/www/index.html
Normal file
@@ -0,0 +1,475 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<title data-i18n="page.title">System Control</title>
|
||||
<link rel="stylesheet" href="css/shared.css">
|
||||
<link rel="stylesheet" href="css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-controls">
|
||||
<button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln">
|
||||
<span class="lang-flag" id="lang-flag">🇩🇪</span>
|
||||
<span class="lang-label" id="lang-label">DE</span>
|
||||
</button>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Theme wechseln">
|
||||
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
|
||||
<span class="theme-toggle-label" id="theme-label">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1>🚂 System Control</h1>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('control')" data-i18n="tab.control">🎛️ Bedienung</button>
|
||||
<button class="tab" onclick="switchTab('config')" data-i18n="tab.config">⚙️ Konfiguration</button>
|
||||
</div>
|
||||
|
||||
<!-- Bedienung Tab -->
|
||||
<div id="tab-control" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2 data-i18n="control.light.title">Lichtsteuerung</h2>
|
||||
|
||||
<div class="control-section">
|
||||
<div class="control-group">
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label" data-i18n="control.light.light">Licht</span>
|
||||
<button class="toggle-switch" id="light-toggle" onclick="toggleLight()">
|
||||
<span class="toggle-state" id="light-state" data-i18n="common.off">AUS</span>
|
||||
<span class="toggle-icon" id="light-icon">💡</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label" data-i18n="control.light.thunder">Gewitter</span>
|
||||
<button class="toggle-switch" id="thunder-toggle" onclick="toggleThunder()">
|
||||
<span class="toggle-state" id="thunder-state" data-i18n="common.off">AUS</span>
|
||||
<span class="toggle-icon" id="thunder-icon">⚡</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="light-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3 data-i18n="control.mode.title">Betriebsmodus</h3>
|
||||
<div class="mode-selector">
|
||||
<button class="mode-btn" id="mode-day" onclick="setMode('day')">
|
||||
<span class="mode-icon">☀️</span>
|
||||
<span class="mode-name" data-i18n="mode.day">Tag</span>
|
||||
</button>
|
||||
<button class="mode-btn" id="mode-night" onclick="setMode('night')">
|
||||
<span class="mode-icon">🌙</span>
|
||||
<span class="mode-name" data-i18n="mode.night">Nacht</span>
|
||||
</button>
|
||||
<button class="mode-btn" id="mode-simulation" onclick="setMode('simulation')">
|
||||
<span class="mode-icon">🔄</span>
|
||||
<span class="mode-name" data-i18n="mode.simulation">Simulation</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="simulation-options" class="simulation-options">
|
||||
<div class="form-group">
|
||||
<label for="active-schema" data-i18n="control.schema.active">Aktives Schema</label>
|
||||
<select id="active-schema" onchange="setActiveSchema()">
|
||||
<option value="schema_01.csv" data-i18n="schema.name.1">Schema 1 (Standard)</option>
|
||||
<option value="schema_02.csv" data-i18n="schema.name.2">Schema 2 (Warm)</option>
|
||||
<option value="schema_03.csv" data-i18n="schema.name.3">Schema 3 (Natur)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mode-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="control-section">
|
||||
<h3 data-i18n="control.status.title">Aktueller Status</h3>
|
||||
<div class="status-display">
|
||||
<div class="status-item visible">
|
||||
<span class="status-label" data-i18n="control.status.mode">Modus</span>
|
||||
<span class="status-value" id="current-mode" data-i18n="mode.simulation">Simulation</span>
|
||||
</div>
|
||||
<div class="status-item visible">
|
||||
<span class="status-label" data-i18n="control.status.color">Aktuelle Farbe</span>
|
||||
<div class="current-color-preview" id="current-color"></div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label" data-i18n="control.status.clock">Uhrzeit</span>
|
||||
<span class="status-value" id="current-clock">--:-- Uhr</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Szenen Card -->
|
||||
<div class="card" id="scenes-control-card" style="display: none;">
|
||||
<h2 data-i18n="scenes.title">Szenen</h2>
|
||||
<div id="scenes-control-list" class="scenes-grid">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-scenes-control">
|
||||
<span class="empty-icon">🎬</span>
|
||||
<p data-i18n="scenes.empty">Keine Szenen definiert</p>
|
||||
<p class="empty-hint" data-i18n="scenes.empty.hint">Erstelle Szenen unter Konfiguration</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scenes-control-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Externe Geräte Card -->
|
||||
<div class="card" id="devices-control-card" style="display: none;">
|
||||
<h2 data-i18n="devices.external">Externe Geräte</h2>
|
||||
<div id="devices-control-list" class="devices-control-grid">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-devices-control">
|
||||
<span class="empty-icon">🔗</span>
|
||||
<p data-i18n="devices.control.empty">Keine Geräte hinzugefügt</p>
|
||||
<p class="empty-hint" data-i18n="devices.control.empty.hint">Füge Geräte unter Konfiguration
|
||||
hinzu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Konfiguration Tab -->
|
||||
<div id="tab-config" class="tab-content">
|
||||
<div class="sub-tabs">
|
||||
<button class="sub-tab active" onclick="switchSubTab('wifi')" data-i18n="subtab.wifi">📶 WLAN</button>
|
||||
<button class="sub-tab" onclick="switchSubTab('schema')" data-i18n="subtab.light">💡
|
||||
Lichtsteuerung</button>
|
||||
<button class="sub-tab" id="subtab-btn-devices" onclick="switchSubTab('devices')"
|
||||
data-i18n="subtab.devices">🔗 Geräte</button>
|
||||
<button class="sub-tab" id="subtab-btn-scenes" onclick="switchSubTab('scenes')"
|
||||
data-i18n="subtab.scenes">🎬 Szenen</button>
|
||||
</div>
|
||||
|
||||
<!-- WLAN Sub-Tab -->
|
||||
<div id="subtab-wifi" class="sub-tab-content active">
|
||||
<div class="card">
|
||||
<h2 data-i18n="wifi.config.title">WLAN Konfiguration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ssid" data-i18n="wifi.ssid">WLAN Name (SSID)</label>
|
||||
<input type="text" id="ssid" data-i18n-placeholder="wifi.ssid.placeholder"
|
||||
placeholder="Netzwerkname eingeben" autocomplete="off" autocapitalize="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" data-i18n="wifi.password">WLAN Passwort</label>
|
||||
<div class="password-toggle">
|
||||
<input type="password" id="password" data-i18n-placeholder="wifi.password.placeholder"
|
||||
placeholder="Passwort eingeben" autocomplete="off">
|
||||
<button type="button" onclick="togglePassword()" id="password-btn">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label data-i18n="wifi.available">Verfügbare Netzwerke</label>
|
||||
<select id="available-networks"
|
||||
onchange="if(this.value) document.getElementById('ssid').value = this.value">
|
||||
<option value="" data-i18n="wifi.scan.hint">Nach Netzwerken suchen...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="scanNetworks()" data-i18n="btn.scan">🔍
|
||||
Suchen</button>
|
||||
<button class="btn btn-primary" onclick="saveWifi()" data-i18n="btn.save">💾 Speichern</button>
|
||||
</div>
|
||||
|
||||
<div id="wifi-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 data-i18n="wifi.status.title">Verbindungsstatus</h2>
|
||||
<div id="connection-info">
|
||||
<p><strong data-i18n="wifi.status.status">Status:</strong> <span id="conn-status"
|
||||
data-i18n="common.loading">Wird geladen...</span></p>
|
||||
<p><strong data-i18n="wifi.status.ip">IP-Adresse:</strong> <span id="conn-ip">-</span></p>
|
||||
<p><strong data-i18n="wifi.status.signal">Signal:</strong> <span id="conn-rssi">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schema Sub-Tab (Lichtsteuerung) -->
|
||||
<div id="subtab-schema" class="sub-tab-content">
|
||||
<!-- LED Konfiguration -->
|
||||
<div class="card">
|
||||
<h2 data-i18n="wled.config.title">LED Konfiguration</h2>
|
||||
<p class="card-description" data-i18n="wled.config.desc">Konfiguriere die LED-Segmente und Anzahl
|
||||
LEDs pro
|
||||
Segment</p>
|
||||
|
||||
<div class="segment-header">
|
||||
<h3 data-i18n="wled.segments.title">Segmente</h3>
|
||||
<button class="btn btn-secondary btn-small" onclick="addWledSegment()"
|
||||
data-i18n="wled.segment.add">➕ Segment hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<div id="wled-segments-list" class="wled-segments-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-wled-segments">
|
||||
<span class="empty-icon">💡</span>
|
||||
<p data-i18n="wled.segments.empty">Keine Segmente konfiguriert</p>
|
||||
<p class="empty-hint" data-i18n="wled.segments.empty.hint">Klicke auf "Segment hinzufügen"
|
||||
um ein Segment zu erstellen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="saveWledConfig()" data-i18n="btn.save">💾
|
||||
Speichern</button>
|
||||
</div>
|
||||
|
||||
<div id="wled-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Schema Editor -->
|
||||
<div class="card">
|
||||
<h2 data-i18n="schema.editor.title">Licht-Schema Editor</h2>
|
||||
|
||||
<div class="schema-controls">
|
||||
<div class="form-group">
|
||||
<label for="schema-select" data-i18n="schema.file">Schema-Datei</label>
|
||||
<select id="schema-select" onchange="loadSchema()">
|
||||
<option value="schema_01.csv" data-i18n="schema.name.1">Schema 1 (Standard)</option>
|
||||
<option value="schema_02.csv" data-i18n="schema.name.2">Schema 2 (Warm)</option>
|
||||
<option value="schema_03.csv" data-i18n="schema.name.3">Schema 3 (Natur)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="loadSchema()" data-i18n="btn.load">🔄 Laden</button>
|
||||
<button class="btn btn-primary" onclick="saveSchema()" data-i18n="btn.save">💾
|
||||
Speichern</button>
|
||||
</div>
|
||||
|
||||
<div id="schema-status" class="status"></div>
|
||||
|
||||
<div id="schema-loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p data-i18n="schema.loading">Schema wird geladen...</p>
|
||||
</div>
|
||||
|
||||
<div class="value-header">
|
||||
<span data-i18n="schema.header.time">Zeit</span>
|
||||
<span data-i18n="schema.header.color">Farbe</span>
|
||||
<span>R</span>
|
||||
<span>G</span>
|
||||
<span>B</span>
|
||||
<span>V1</span>
|
||||
<span>V2</span>
|
||||
<span>V3</span>
|
||||
</div>
|
||||
|
||||
<div id="schema-grid" class="time-grid">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Szenen Sub-Tab -->
|
||||
<div id="subtab-scenes" class="sub-tab-content">
|
||||
<div class="card">
|
||||
<h2 data-i18n="scenes.manage.title">Szenen verwalten</h2>
|
||||
<p class="card-description" data-i18n="scenes.manage.desc">Erstelle und bearbeite Szenen für
|
||||
schnellen Zugriff</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="openSceneModal()" data-i18n="btn.new.scene">➕ Neue
|
||||
Szene</button>
|
||||
</div>
|
||||
|
||||
<div id="scenes-config-list" class="scenes-config-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-scenes-config">
|
||||
<span class="empty-icon">🎬</span>
|
||||
<p data-i18n="scenes.config.empty">Keine Szenen erstellt</p>
|
||||
<p class="empty-hint" data-i18n="scenes.config.empty.hint">Klicke auf "Neue Szene" um eine
|
||||
Szene zu erstellen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scenes-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Geräte Sub-Tab -->
|
||||
<div id="subtab-devices" class="sub-tab-content">
|
||||
<div class="card">
|
||||
<h2 data-i18n="devices.new.title">Neue Geräte</h2>
|
||||
<p class="card-description" data-i18n="devices.new.desc">Unprovisionierte Matter-Geräte in der Nähe
|
||||
</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" onclick="scanDevices()" data-i18n="btn.scan.devices">🔍 Geräte
|
||||
suchen</button>
|
||||
</div>
|
||||
|
||||
<div id="devices-loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p data-i18n="devices.searching">Suche nach Geräten...</p>
|
||||
</div>
|
||||
|
||||
<div id="unpaired-devices" class="device-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-unpaired-devices">
|
||||
<span class="empty-icon">📡</span>
|
||||
<p data-i18n="devices.unpaired.empty">Keine neuen Geräte gefunden</p>
|
||||
<p class="empty-hint" data-i18n="devices.unpaired.empty.hint">Drücke "Geräte suchen" um nach
|
||||
Matter-Geräten zu suchen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="devices-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 data-i18n="devices.paired.title">Zugeordnete Geräte</h2>
|
||||
<p class="card-description" data-i18n="devices.paired.desc">Bereits hinzugefügte externe Geräte</p>
|
||||
|
||||
<div id="paired-devices" class="device-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state" id="no-paired-devices">
|
||||
<span class="empty-icon">📦</span>
|
||||
<p data-i18n="devices.paired.empty">Keine Geräte hinzugefügt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Picker Modal -->
|
||||
<div id="color-modal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<h3><span data-i18n="modal.color.title">Farbe wählen</span> - <span id="modal-time"></span></h3>
|
||||
|
||||
<div class="color-picker-container">
|
||||
<div class="color-slider">
|
||||
<label style="color: #f66;">R</label>
|
||||
<input type="range" id="rangeR" min="0" max="255" value="255" oninput="updateModalColor()">
|
||||
<span id="valR" class="value">255</span>
|
||||
</div>
|
||||
<div class="color-slider">
|
||||
<label style="color: #6f6;">G</label>
|
||||
<input type="range" id="rangeG" min="0" max="255" value="255" oninput="updateModalColor()">
|
||||
<span id="valG" class="value">255</span>
|
||||
</div>
|
||||
<div class="color-slider">
|
||||
<label style="color: #66f;">B</label>
|
||||
<input type="range" id="rangeB" min="0" max="255" value="255" oninput="updateModalColor()">
|
||||
<span id="valB" class="value">255</span>
|
||||
</div>
|
||||
|
||||
<div id="preview-large" class="preview-large"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button class="btn btn-secondary" onclick="closeColorModal()" data-i18n="btn.cancel">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="applyColor()" data-i18n="btn.apply">Übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scene Modal -->
|
||||
<div id="scene-modal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<h3 id="scene-modal-title" data-i18n="modal.scene.new">Neue Szene erstellen</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="scene-name" data-i18n="scene.name">Name</label>
|
||||
<input type="text" id="scene-name" data-i18n-placeholder="scene.name.placeholder"
|
||||
placeholder="z.B. Abendstimmung" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label data-i18n="scene.icon">Icon auswählen</label>
|
||||
<div class="icon-selector">
|
||||
<button type="button" class="icon-btn active" data-icon="🌅"
|
||||
onclick="selectSceneIcon('🌅')">🌅</button>
|
||||
<button type="button" class="icon-btn" data-icon="🌙" onclick="selectSceneIcon('🌙')">🌙</button>
|
||||
<button type="button" class="icon-btn" data-icon="☀️" onclick="selectSceneIcon('☀️')">☀️</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎬" onclick="selectSceneIcon('🎬')">🎬</button>
|
||||
<button type="button" class="icon-btn" data-icon="💤" onclick="selectSceneIcon('💤')">💤</button>
|
||||
<button type="button" class="icon-btn" data-icon="🎉" onclick="selectSceneIcon('🎉')">🎉</button>
|
||||
<button type="button" class="icon-btn" data-icon="🍿" onclick="selectSceneIcon('🍿')">🍿</button>
|
||||
<button type="button" class="icon-btn" data-icon="📚" onclick="selectSceneIcon('📚')">📚</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label data-i18n="scene.actions">Aktionen</label>
|
||||
<div class="scene-actions-editor">
|
||||
<div class="scene-action-row">
|
||||
<label>
|
||||
<input type="checkbox" id="scene-action-light" checked>
|
||||
<span data-i18n="scene.action.light">Licht Ein/Aus</span>
|
||||
</label>
|
||||
<select id="scene-light-state">
|
||||
<option value="on" data-i18n="scene.light.on">Einschalten</option>
|
||||
<option value="off" data-i18n="scene.light.off">Ausschalten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="scene-action-row">
|
||||
<label>
|
||||
<input type="checkbox" id="scene-action-mode">
|
||||
<span data-i18n="scene.action.mode">Modus setzen</span>
|
||||
</label>
|
||||
<select id="scene-mode-value">
|
||||
<option value="day" data-i18n="mode.day">Tag</option>
|
||||
<option value="night" data-i18n="mode.night">Nacht</option>
|
||||
<option value="simulation" data-i18n="mode.simulation">Simulation</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="scene-action-row">
|
||||
<label>
|
||||
<input type="checkbox" id="scene-action-schema">
|
||||
<span data-i18n="scene.action.schema">Schema wählen</span>
|
||||
</label>
|
||||
<select id="scene-schema-value">
|
||||
<option value="schema_01.csv">Schema 1</option>
|
||||
<option value="schema_02.csv">Schema 2</option>
|
||||
<option value="schema_03.csv">Schema 3</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label data-i18n="devices.external">Externe Geräte</label>
|
||||
<div id="scene-devices-list" class="scene-devices-list">
|
||||
<!-- Wird dynamisch gefüllt -->
|
||||
<div class="empty-state small" id="no-scene-devices">
|
||||
<span class="empty-icon">🔗</span>
|
||||
<p data-i18n="devices.none.available">Keine Geräte verfügbar</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button class="btn btn-secondary" onclick="closeSceneModal()" data-i18n="btn.cancel">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="saveScene()" data-i18n="btn.save">💾 Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript Modules -->
|
||||
<script src="js/i18n.js"></script>
|
||||
<script src="js/capabilities.js"></script>
|
||||
<script src="js/wifi-shared.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/websocket.js"></script>
|
||||
<script src="js/light.js"></script>
|
||||
<script src="js/scenes.js"></script>
|
||||
<script src="js/devices.js"></script>
|
||||
<script src="js/schema.js"></script>
|
||||
<script src="js/wled.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
65
firmware/storage/www/js/app.js
Normal file
65
firmware/storage/www/js/app.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// Global variables
|
||||
let schemaData = [];
|
||||
let currentEditRow = null;
|
||||
let lightOn = false;
|
||||
let currentMode = 'simulation';
|
||||
let ws = null;
|
||||
let wsReconnectTimer = null;
|
||||
let pairedDevices = [];
|
||||
let scenes = [];
|
||||
let currentEditScene = null;
|
||||
let selectedSceneIcon = '🌅';
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeColorModal();
|
||||
closeSceneModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('color-modal').addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-overlay')) {
|
||||
closeColorModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('scene-modal').addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-overlay')) {
|
||||
closeSceneModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent zoom on double-tap for iOS
|
||||
let lastTouchEnd = 0;
|
||||
document.addEventListener('touchend', (e) => {
|
||||
const now = Date.now();
|
||||
if (now - lastTouchEnd <= 300) {
|
||||
e.preventDefault();
|
||||
}
|
||||
lastTouchEnd = now;
|
||||
}, false);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initI18n();
|
||||
initTheme();
|
||||
await initCapabilities();
|
||||
initWebSocket();
|
||||
updateConnectionStatus();
|
||||
loadLightStatus();
|
||||
|
||||
// Only load scenes and devices if thread is enabled
|
||||
if (isThreadEnabled()) {
|
||||
loadScenes();
|
||||
loadPairedDevices();
|
||||
}
|
||||
// WiFi status polling (less frequent)
|
||||
setInterval(updateConnectionStatus, 30000);
|
||||
});
|
||||
|
||||
// Close WebSocket on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user