feat: services #2

Merged
master merged 9 commits from feature/wifiscan into main 2025-09-13 13:45:24 +02:00
33 changed files with 3293 additions and 785 deletions

68
.cursor/rules/cpp.mdc Normal file
View File

@@ -0,0 +1,68 @@
---
description: C++ Development Rules
globs:
alwaysApply: true
---
# C++ Development Rules
You are a senior C++ developer with expertise in modern C++ (C++17/20), STL, and system-level programming.
## Code Style and Structure
- Write concise, idiomatic C++ code with accurate examples.
- Follow modern C++ conventions and best practices.
- Use object-oriented, procedural, or functional programming patterns as appropriate.
- Leverage STL and standard algorithms for collection operations.
- Use descriptive variable and method names (e.g., 'isUserSignedIn', 'calculateTotal').
- Structure files into headers (*.hpp) and implementation files (*.cpp) with logical separation of concerns.
## Naming Conventions
- Use PascalCase for class names.
- Use camelCase for variable names and methods.
- Use SCREAMING_SNAKE_CASE for constants and macros.
- Prefix member variables with an underscore or m_ (e.g., `_userId`, `m_userId`).
- Use namespaces to organize code logically.
## C++ Features Usage
- Prefer modern C++ features (e.g., auto, range-based loops, smart pointers).
- Use `std::unique_ptr` and `std::shared_ptr` for memory management.
- Prefer `std::optional`, `std::variant`, and `std::any` for type-safe alternatives.
- Use `constexpr` and `const` to optimize compile-time computations.
- Use `std::string_view` for read-only string operations to avoid unnecessary copies.
## Syntax and Formatting
- Follow a consistent coding style, such as Google C++ Style Guide or your teams standards.
- Place braces on the same line for control structures and methods.
- Use clear and consistent commenting practices.
## Error Handling and Validation
- Use exceptions for error handling (e.g., `std::runtime_error`, `std::invalid_argument`).
- Use RAII for resource management to avoid memory leaks.
- Validate inputs at function boundaries.
- Log errors using a logging library (e.g., spdlog, Boost.Log).
## Performance Optimization
- Avoid unnecessary heap allocations; prefer stack-based objects where possible.
- Use `std::move` to enable move semantics and avoid copies.
- Optimize loops with algorithms from `<algorithm>` (e.g., `std::sort`, `std::for_each`).
- Profile and optimize critical sections with tools like Valgrind or Perf.
## Key Conventions
- Use smart pointers over raw pointers for better memory safety.
- Avoid global variables; use singletons sparingly.
- Use `enum class` for strongly typed enumerations.
- Separate interface from implementation in classes.
- Use templates and metaprogramming judiciously for generic solutions.
## Security
- Use secure coding practices to avoid vulnerabilities (e.g., buffer overflows, dangling pointers).
- Prefer `std::array` or `std::vector` over raw arrays.
- Avoid C-style casts; use `static_cast`, `dynamic_cast`, or `reinterpret_cast` when necessary.
- Enforce const-correctness in functions and member variables.
## Documentation
- Write clear comments for classes, methods, and critical logic.
- Use Doxygen for generating API documentation.
- Document assumptions, constraints, and expected behavior of code.
Follow the official ISO C++ standards and guidelines for best practices in modern C++ development.

View File

@@ -28,6 +28,7 @@ SPORE is a cluster engine for ESP8266 microcontrollers that provides automatic n
- **Event System**: Local and cluster-wide event publishing/subscription - **Event System**: Local and cluster-wide event publishing/subscription
- **Over-The-Air Updates**: Seamless firmware updates across the cluster - **Over-The-Air Updates**: Seamless firmware updates across the cluster
- **REST API**: HTTP-based cluster management and monitoring - **REST API**: HTTP-based cluster management and monitoring
- **Capability Discovery**: Automatic API endpoint and service capability detection
## Supported Hardware ## Supported Hardware
@@ -66,11 +67,15 @@ The system provides a comprehensive RESTful API for monitoring and controlling t
| Endpoint | Method | Description | | Endpoint | Method | Description |
|----------|--------|-------------| |----------|--------|-------------|
| `/api/node/status` | GET | System resources and API endpoint registry | | `/api/node/status` | GET | System resources and API endpoint registry |
| `/api/node/capabilities` | GET | API endpoint capabilities and parameters |
| `/api/cluster/members` | GET | Cluster membership and health status | | `/api/cluster/members` | GET | Cluster membership and health status |
| `/api/node/update` | POST | OTA firmware updates | | `/api/node/update` | POST | OTA firmware updates |
| `/api/node/restart` | POST | System restart | | `/api/node/restart` | POST | System restart |
| `/api/tasks/status` | GET | Task management and monitoring | | `/api/tasks/status` | GET | Task management and monitoring |
| `/api/tasks/control` | POST | Task control operations | | `/api/tasks/control` | POST | Task control operations |
| `/api/network/status` | GET | WiFi and network status information |
| `/api/network/wifi/scan` | GET/POST | WiFi network scanning and discovery |
| `/api/network/wifi/config` | POST | WiFi configuration management |
**Response Format:** All endpoints return JSON with standardized error handling and HTTP status codes. **Response Format:** All endpoints return JSON with standardized error handling and HTTP status codes.
@@ -122,6 +127,7 @@ The project uses PlatformIO with Arduino framework and supports multiple ESP8266
- No persistent storage for configuration - No persistent storage for configuration
- Task monitoring and system health metrics - Task monitoring and system health metrics
- Task execution history and performance analytics not yet implemented - Task execution history and performance analytics not yet implemented
- No authentication or security features implemented
## Troubleshooting ## Troubleshooting

View File

@@ -212,6 +212,9 @@ paths:
sdkVersion: "3.1.2" sdkVersion: "3.1.2"
cpuFreqMHz: 80 cpuFreqMHz: 80
flashChipSize: 1048576 flashChipSize: 1048576
labels:
location: "kitchen"
type: "sensor"
api: api:
- uri: "/api/node/status" - uri: "/api/node/status"
method: 1 method: 1
@@ -220,6 +223,41 @@ paths:
- uri: "/api/tasks/control" - uri: "/api/tasks/control"
method: 3 method: 3
/api/node/capabilities:
get:
summary: Get API endpoint capabilities
description: |
Returns detailed information about all available API endpoints,
including their parameters, types, and validation rules.
tags:
- System Status
responses:
'200':
description: Capabilities retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/CapabilitiesResponse'
examples:
default:
summary: Default response
value:
endpoints:
- uri: "/api/tasks/control"
method: "POST"
params:
- name: "task"
location: "body"
required: true
type: "string"
- name: "action"
location: "body"
required: true
type: "string"
values: ["enable", "disable", "start", "stop", "status"]
/api/cluster/members: /api/cluster/members:
get: get:
summary: Get cluster membership information summary: Get cluster membership information
@@ -325,6 +363,425 @@ paths:
value: value:
status: "restarting" status: "restarting"
/api/network/status:
get:
summary: Get network status information
description: |
Returns comprehensive WiFi and network status information
including connection details, signal strength, and mode.
tags:
- Network Management
responses:
'200':
description: Network status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/NetworkStatusResponse'
examples:
default:
summary: Default response
value:
wifi:
connected: true
mode: "STA"
ssid: "MyNetwork"
ip: "192.168.1.100"
mac: "AA:BB:CC:DD:EE:FF"
hostname: "spore-node-1"
rssi: -45
/api/network/wifi/scan:
get:
summary: Get available WiFi networks
description: |
Returns a list of available WiFi networks discovered
during the last scan.
tags:
- Network Management
responses:
'200':
description: WiFi networks retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/WifiScanResponse'
examples:
default:
summary: Default response
value:
access_points:
- ssid: "MyNetwork"
rssi: -45
channel: 6
encryption_type: 4
hidden: false
bssid: "AA:BB:CC:DD:EE:FF"
post:
summary: Trigger WiFi network scan
description: |
Initiates a new WiFi network scan to discover
available networks.
tags:
- Network Management
responses:
'200':
description: WiFi scan initiated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/WifiScanInitResponse'
examples:
default:
summary: Scan initiated
value:
status: "scanning"
message: "WiFi scan started"
/api/network/wifi/config:
post:
summary: Configure WiFi connection
description: |
Configures WiFi connection with new credentials and
attempts to connect to the specified network.
tags:
- Network Management
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- ssid
- password
properties:
ssid:
type: string
description: Network SSID
example: "MyNetwork"
password:
type: string
description: Network password
example: "mypassword"
connect_timeout_ms:
type: integer
description: Connection timeout in milliseconds
default: 10000
example: 10000
retry_delay_ms:
type: integer
description: Retry delay in milliseconds
default: 500
example: 500
responses:
'200':
description: WiFi configuration updated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/WifiConfigResponse'
examples:
default:
summary: Configuration updated
value:
status: "success"
message: "WiFi configuration updated"
connected: true
ip: "192.168.1.100"
'400':
description: Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/neopixel/status:
get:
summary: Get NeoPixel LED strip status
description: |
Returns current NeoPixel LED strip status and configuration
including pin, count, brightness, and current pattern.
tags:
- Hardware Services
responses:
'200':
description: NeoPixel status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/NeoPixelStatusResponse'
/api/neopixel/patterns:
get:
summary: Get available LED patterns
description: |
Returns a list of available LED patterns for NeoPixel strips.
tags:
- Hardware Services
responses:
'200':
description: Patterns retrieved successfully
content:
application/json:
schema:
type: array
items:
type: string
example: ["off", "color_wipe", "rainbow", "rainbow_cycle", "theater_chase", "theater_chase_rainbow"]
/api/neopixel:
post:
summary: Control NeoPixel LED strip
description: |
Controls NeoPixel LED strip patterns, colors, and brightness.
tags:
- Hardware Services
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
pattern:
type: string
description: Pattern name
example: "rainbow"
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
minimum: 0
maximum: 255
description: Brightness level
example: 50
color:
type: string
description: Primary color (hex value)
example: "0xFF0000"
r:
type: integer
minimum: 0
maximum: 255
description: Red component
g:
type: integer
minimum: 0
maximum: 255
description: Green component
b:
type: integer
minimum: 0
maximum: 255
description: Blue component
responses:
'200':
description: NeoPixel control successful
content:
application/json:
schema:
$ref: '#/components/schemas/NeoPixelControlResponse'
/api/relay/status:
get:
summary: Get relay status
description: |
Returns current relay status and configuration.
tags:
- Hardware Services
responses:
'200':
description: Relay status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/RelayStatusResponse'
/api/relay:
post:
summary: Control relay
description: |
Controls relay state (on/off/toggle).
tags:
- Hardware Services
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- state
properties:
state:
type: string
enum: [on, off, toggle]
description: Desired relay state
example: "on"
responses:
'200':
description: Relay control successful
content:
application/json:
schema:
$ref: '#/components/schemas/RelayControlResponse'
'400':
description: Invalid state parameter
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/neopattern/status:
get:
summary: Get NeoPattern service status
description: |
Returns NeoPattern service status and configuration.
tags:
- Hardware Services
responses:
'200':
description: NeoPattern status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/NeoPatternStatusResponse'
/api/neopattern/patterns:
get:
summary: Get available pattern types
description: |
Returns a list of available pattern types for NeoPattern service.
tags:
- Hardware Services
responses:
'200':
description: Patterns retrieved successfully
content:
application/json:
schema:
type: array
items:
type: string
example: ["off", "rainbow_cycle", "theater_chase", "color_wipe", "scanner", "fade", "fire"]
/api/neopattern:
post:
summary: Control NeoPattern service
description: |
Controls advanced LED patterns with multiple parameters.
tags:
- Hardware Services
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
pattern:
type: string
description: Pattern name
example: "rainbow_cycle"
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
minimum: 0
maximum: 255
description: Brightness level
example: 50
color:
type: string
description: Primary color (hex value)
example: "0xFF0000"
color2:
type: string
description: Secondary color (hex value)
example: "0x0000FF"
r:
type: integer
minimum: 0
maximum: 255
description: Red component
g:
type: integer
minimum: 0
maximum: 255
description: Green component
b:
type: integer
minimum: 0
maximum: 255
description: Blue component
r2:
type: integer
minimum: 0
maximum: 255
description: Secondary red component
g2:
type: integer
minimum: 0
maximum: 255
description: Secondary green component
b2:
type: integer
minimum: 0
maximum: 255
description: Secondary blue component
total_steps:
type: integer
description: Pattern step count
example: 32
direction:
type: string
enum: [forward, reverse]
description: Pattern direction
example: "forward"
responses:
'200':
description: NeoPattern control successful
content:
application/json:
schema:
$ref: '#/components/schemas/NeoPatternControlResponse'
components: components:
schemas: schemas:
TaskStatusResponse: TaskStatusResponse:
@@ -501,11 +958,393 @@ components:
type: integer type: integer
description: Flash chip size in bytes description: Flash chip size in bytes
example: 1048576 example: 1048576
labels:
type: object
description: Node labels and metadata
additionalProperties:
type: string
example:
location: "kitchen"
type: "sensor"
api: api:
type: array type: array
items: items:
$ref: '#/components/schemas/ApiEndpoint' $ref: '#/components/schemas/ApiEndpoint'
CapabilitiesResponse:
type: object
required:
- endpoints
properties:
endpoints:
type: array
items:
$ref: '#/components/schemas/EndpointCapability'
EndpointCapability:
type: object
required:
- uri
- method
properties:
uri:
type: string
description: Endpoint URI path
example: "/api/tasks/control"
method:
type: string
description: HTTP method
example: "POST"
params:
type: array
items:
$ref: '#/components/schemas/ParameterSpec'
ParameterSpec:
type: object
required:
- name
- location
- required
- type
properties:
name:
type: string
description: Parameter name
example: "task"
location:
type: string
description: Parameter location
example: "body"
required:
type: boolean
description: Whether parameter is required
example: true
type:
type: string
description: Parameter data type
example: "string"
values:
type: array
items:
type: string
description: Allowed values for enum types
example: ["enable", "disable", "start", "stop", "status"]
default:
type: string
description: Default value
example: "10000"
NetworkStatusResponse:
type: object
required:
- wifi
properties:
wifi:
type: object
required:
- connected
- mode
properties:
connected:
type: boolean
description: Whether WiFi is connected
example: true
mode:
type: string
enum: [STA, AP]
description: WiFi mode
example: "STA"
ssid:
type: string
description: Connected network SSID
example: "MyNetwork"
ip:
type: string
format: ipv4
description: Local IP address
example: "192.168.1.100"
mac:
type: string
description: MAC address
example: "AA:BB:CC:DD:EE:FF"
hostname:
type: string
description: Device hostname
example: "spore-node-1"
rssi:
type: integer
description: Signal strength in dBm
example: -45
ap_ip:
type: string
format: ipv4
description: Access point IP (if in AP mode)
example: "192.168.4.1"
ap_mac:
type: string
description: Access point MAC (if in AP mode)
example: "AA:BB:CC:DD:EE:FF"
stations_connected:
type: integer
description: Number of connected stations (if in AP mode)
example: 2
WifiScanResponse:
type: object
required:
- access_points
properties:
access_points:
type: array
items:
$ref: '#/components/schemas/AccessPoint'
AccessPoint:
type: object
required:
- ssid
- rssi
- channel
- encryption_type
- hidden
- bssid
properties:
ssid:
type: string
description: Network name
example: "MyNetwork"
rssi:
type: integer
description: Signal strength in dBm
example: -45
channel:
type: integer
description: WiFi channel
example: 6
encryption_type:
type: integer
description: Security type
example: 4
hidden:
type: boolean
description: Whether network is hidden
example: false
bssid:
type: string
description: Network MAC address
example: "AA:BB:CC:DD:EE:FF"
WifiScanInitResponse:
type: object
required:
- status
- message
properties:
status:
type: string
description: Scan status
example: "scanning"
message:
type: string
description: Status message
example: "WiFi scan started"
WifiConfigResponse:
type: object
required:
- status
- message
- connected
properties:
status:
type: string
description: Configuration status
example: "success"
message:
type: string
description: Status message
example: "WiFi configuration updated"
connected:
type: boolean
description: Whether connection was successful
example: true
ip:
type: string
format: ipv4
description: Assigned IP address (if connected)
example: "192.168.1.100"
NeoPixelStatusResponse:
type: object
required:
- pin
- count
- interval_ms
- brightness
- pattern
properties:
pin:
type: integer
description: GPIO pin number
example: 2
count:
type: integer
description: Number of LEDs in strip
example: 16
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
minimum: 0
maximum: 255
description: Current brightness level
example: 50
pattern:
type: string
description: Current pattern name
example: "rainbow"
NeoPixelControlResponse:
type: object
required:
- ok
- pattern
- interval_ms
- brightness
properties:
ok:
type: boolean
description: Whether control was successful
example: true
pattern:
type: string
description: Current pattern name
example: "rainbow"
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
description: Current brightness level
example: 50
RelayStatusResponse:
type: object
required:
- pin
- state
- uptime
properties:
pin:
type: integer
description: GPIO pin number
example: 5
state:
type: string
enum: [on, off]
description: Current relay state
example: "off"
uptime:
type: integer
description: System uptime in milliseconds
example: 12345
RelayControlResponse:
type: object
required:
- success
- state
properties:
success:
type: boolean
description: Whether control was successful
example: true
state:
type: string
enum: [on, off]
description: Current relay state
example: "on"
message:
type: string
description: Error message (if unsuccessful)
example: "Invalid state. Use: on, off, or toggle"
NeoPatternStatusResponse:
type: object
required:
- pin
- count
- interval_ms
- brightness
- pattern
- total_steps
- color1
- color2
properties:
pin:
type: integer
description: GPIO pin number
example: 2
count:
type: integer
description: Number of LEDs
example: 16
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
minimum: 0
maximum: 255
description: Current brightness level
example: 50
pattern:
type: string
description: Current pattern name
example: "rainbow_cycle"
total_steps:
type: integer
description: Pattern step count
example: 32
color1:
type: integer
description: Primary color value
example: 16711680
color2:
type: integer
description: Secondary color value
example: 255
NeoPatternControlResponse:
type: object
required:
- ok
- pattern
- interval_ms
- brightness
properties:
ok:
type: boolean
description: Whether control was successful
example: true
pattern:
type: string
description: Current pattern name
example: "rainbow_cycle"
interval_ms:
type: integer
description: Update interval in milliseconds
example: 100
brightness:
type: integer
description: Current brightness level
example: 50
ApiEndpoint: ApiEndpoint:
type: object type: object
required: required:
@@ -663,6 +1502,10 @@ tags:
description: Multi-node cluster coordination and health monitoring description: Multi-node cluster coordination and health monitoring
- name: System Management - name: System Management
description: System-level operations like updates and restarts description: System-level operations like updates and restarts
- name: Network Management
description: WiFi and network configuration and monitoring
- name: Hardware Services
description: Hardware control services for LEDs, relays, and other peripherals
externalDocs: externalDocs:
description: SPORE Project Documentation description: SPORE Project Documentation

7
ctl.sh
View File

@@ -14,8 +14,7 @@ function build {
pio run -e $1 pio run -e $1
} }
function all { function all {
target esp01_1m pio run
target d1_mini
} }
${@:-all} ${@:-all}
} }
@@ -51,4 +50,8 @@ function cluster {
${@:-info} ${@:-info}
} }
function monitor {
pio run --target monitor
}
${@:-info} ${@:-info}

View File

@@ -16,10 +16,33 @@ The SPORE system provides a comprehensive RESTful API for monitoring and control
| Endpoint | Method | Description | Response | | Endpoint | Method | Description | Response |
|----------|--------|-------------|----------| |----------|--------|-------------|----------|
| `/api/node/status` | GET | System resource information and API endpoint registry | System metrics and API catalog | | `/api/node/status` | GET | System resource information and API endpoint registry | System metrics and API catalog |
| `/api/node/capabilities` | GET | API endpoint capabilities and parameters | Detailed endpoint specifications |
| `/api/cluster/members` | GET | Cluster membership and node health information | Cluster topology and health status | | `/api/cluster/members` | GET | Cluster membership and node health information | Cluster topology and health status |
| `/api/node/update` | POST | Handle firmware updates via OTA | Update progress and status | | `/api/node/update` | POST | Handle firmware updates via OTA | Update progress and status |
| `/api/node/restart` | POST | Trigger system restart | Restart confirmation | | `/api/node/restart` | POST | Trigger system restart | Restart confirmation |
### Network Management API
| Endpoint | Method | Description | Response |
|----------|--------|-------------|----------|
| `/api/network/status` | GET | WiFi and network status information | Network configuration and status |
| `/api/network/wifi/scan` | GET | Get available WiFi networks | List of discovered networks |
| `/api/network/wifi/scan` | POST | Trigger WiFi network scan | Scan initiation confirmation |
| `/api/network/wifi/config` | POST | Configure WiFi connection | Connection status and result |
### Hardware Services API
| Endpoint | Method | Description | Response |
|----------|--------|-------------|----------|
| `/api/neopixel/status` | GET | NeoPixel LED strip status | LED configuration and state |
| `/api/neopixel` | POST | NeoPixel pattern and color control | Control confirmation |
| `/api/neopixel/patterns` | GET | Available LED patterns | List of supported patterns |
| `/api/relay/status` | GET | Relay state and configuration | Relay pin and state |
| `/api/relay` | POST | Relay control (on/off/toggle) | Control result |
| `/api/neopattern/status` | GET | NeoPattern service status | Pattern configuration |
| `/api/neopattern` | POST | Advanced LED pattern control | Control confirmation |
| `/api/neopattern/patterns` | GET | Available pattern types | List of supported patterns |
## Detailed API Reference ## Detailed API Reference
### Task Management ### Task Management
@@ -125,8 +148,67 @@ Returns comprehensive system resource information including memory usage, chip d
- `sdkVersion`: ESP8266 SDK version - `sdkVersion`: ESP8266 SDK version
- `cpuFreqMHz`: CPU frequency in MHz - `cpuFreqMHz`: CPU frequency in MHz
- `flashChipSize`: Flash chip size in bytes - `flashChipSize`: Flash chip size in bytes
- `labels`: Node labels and metadata (if available)
- `api`: Array of registered API endpoints - `api`: Array of registered API endpoints
**Example Response:**
```json
{
"freeHeap": 48748,
"chipId": 12345678,
"sdkVersion": "3.1.2",
"cpuFreqMHz": 80,
"flashChipSize": 1048576,
"labels": {
"location": "kitchen",
"type": "sensor"
}
}
```
#### GET /api/node/capabilities
Returns detailed information about all available API endpoints, including their parameters, types, and validation rules.
**Response Fields:**
- `endpoints[]`: Array of endpoint capability objects
- `uri`: Endpoint URI path
- `method`: HTTP method (GET, POST, etc.)
- `params[]`: Parameter specifications (if applicable)
- `name`: Parameter name
- `location`: Parameter location (body, query, etc.)
- `required`: Whether parameter is required
- `type`: Parameter data type
- `values[]`: Allowed values (for enums)
- `default`: Default value
**Example Response:**
```json
{
"endpoints": [
{
"uri": "/api/tasks/control",
"method": "POST",
"params": [
{
"name": "task",
"location": "body",
"required": true,
"type": "string"
},
{
"name": "action",
"location": "body",
"required": true,
"type": "string",
"values": ["enable", "disable", "start", "stop", "status"]
}
]
}
]
}
```
#### GET /api/cluster/members #### GET /api/cluster/members
Returns information about all nodes in the cluster, including their health status, resources, and API endpoints. Returns information about all nodes in the cluster, including their health status, resources, and API endpoints.
@@ -154,6 +236,225 @@ Initiates an over-the-air firmware update. The firmware file should be uploaded
Triggers a system restart. The response will be sent before the restart occurs. Triggers a system restart. The response will be sent before the restart occurs.
### Network Management
#### GET /api/network/status
Returns comprehensive WiFi and network status information.
**Response Fields:**
- `wifi.connected`: Whether WiFi is connected
- `wifi.mode`: WiFi mode (STA or AP)
- `wifi.ssid`: Connected network SSID
- `wifi.ip`: Local IP address
- `wifi.mac`: MAC address
- `wifi.hostname`: Device hostname
- `wifi.rssi`: Signal strength
- `wifi.ap_ip`: Access point IP (if in AP mode)
- `wifi.ap_mac`: Access point MAC (if in AP mode)
- `wifi.stations_connected`: Number of connected stations (if in AP mode)
**Example Response:**
```json
{
"wifi": {
"connected": true,
"mode": "STA",
"ssid": "MyNetwork",
"ip": "192.168.1.100",
"mac": "AA:BB:CC:DD:EE:FF",
"hostname": "spore-node-1",
"rssi": -45
}
}
```
#### GET /api/network/wifi/scan
Returns a list of available WiFi networks discovered during the last scan.
**Response Fields:**
- `access_points[]`: Array of discovered networks
- `ssid`: Network name
- `rssi`: Signal strength
- `channel`: WiFi channel
- `encryption_type`: Security type
- `hidden`: Whether network is hidden
- `bssid`: Network MAC address
**Example Response:**
```json
{
"access_points": [
{
"ssid": "MyNetwork",
"rssi": -45,
"channel": 6,
"encryption_type": 4,
"hidden": false,
"bssid": "AA:BB:CC:DD:EE:FF"
}
]
}
```
#### POST /api/network/wifi/scan
Initiates a new WiFi network scan.
**Response:**
```json
{
"status": "scanning",
"message": "WiFi scan started"
}
```
#### POST /api/network/wifi/config
Configures WiFi connection with new credentials.
**Parameters:**
- `ssid` (required): Network SSID
- `password` (required): Network password
- `connect_timeout_ms` (optional): Connection timeout in milliseconds (default: 10000)
- `retry_delay_ms` (optional): Retry delay in milliseconds (default: 500)
**Response:**
```json
{
"status": "success",
"message": "WiFi configuration updated",
"connected": true,
"ip": "192.168.1.100"
}
```
### Hardware Services
#### NeoPixel LED Control
##### GET /api/neopixel/status
Returns current NeoPixel LED strip status and configuration.
**Response Fields:**
- `pin`: GPIO pin number
- `count`: Number of LEDs in strip
- `interval_ms`: Update interval in milliseconds
- `brightness`: Current brightness (0-255)
- `pattern`: Current pattern name
**Example Response:**
```json
{
"pin": 2,
"count": 16,
"interval_ms": 100,
"brightness": 50,
"pattern": "rainbow"
}
```
##### GET /api/neopixel/patterns
Returns list of available LED patterns.
**Response:**
```json
["off", "color_wipe", "rainbow", "rainbow_cycle", "theater_chase", "theater_chase_rainbow"]
```
##### POST /api/neopixel
Controls NeoPixel LED strip patterns and colors.
**Parameters:**
- `pattern` (optional): Pattern name
- `interval_ms` (optional): Update interval in milliseconds
- `brightness` (optional): Brightness level (0-255)
- `color` (optional): Primary color (hex value)
- `r`, `g`, `b` (optional): RGB color values
- `color2` (optional): Secondary color (hex value)
- `r2`, `g2`, `b2` (optional): Secondary RGB values
**Example Request:**
```bash
curl -X POST http://192.168.1.100/api/neopixel \
-d "pattern=rainbow&brightness=100&interval_ms=50"
```
#### Relay Control
##### GET /api/relay/status
Returns current relay status and configuration.
**Response Fields:**
- `pin`: GPIO pin number
- `state`: Current state (on/off)
- `uptime`: System uptime in milliseconds
**Example Response:**
```json
{
"pin": 5,
"state": "off",
"uptime": 12345
}
```
##### POST /api/relay
Controls relay state.
**Parameters:**
- `state` (required): Desired state (on, off, toggle)
**Example Request:**
```bash
curl -X POST http://192.168.1.100/api/relay \
-d "state=on"
```
#### NeoPattern Service
##### GET /api/neopattern/status
Returns NeoPattern service status and configuration.
**Response Fields:**
- `pin`: GPIO pin number
- `count`: Number of LEDs
- `interval_ms`: Update interval
- `brightness`: Current brightness
- `pattern`: Current pattern name
- `total_steps`: Pattern step count
- `color1`: Primary color
- `color2`: Secondary color
##### GET /api/neopattern/patterns
Returns available pattern types.
**Response:**
```json
["off", "rainbow_cycle", "theater_chase", "color_wipe", "scanner", "fade", "fire"]
```
##### POST /api/neopattern
Controls advanced LED patterns.
**Parameters:**
- `pattern` (optional): Pattern name
- `interval_ms` (optional): Update interval
- `brightness` (optional): Brightness level
- `color`, `color2` (optional): Color values
- `r`, `g`, `b`, `r2`, `g2`, `b2` (optional): RGB values
- `total_steps` (optional): Pattern step count
- `direction` (optional): Pattern direction (forward/reverse)
## HTTP Status Codes ## HTTP Status Codes
| Code | Description | Use Case | | Code | Description | Use Case |

View File

@@ -7,6 +7,12 @@
#include "ApiServer.h" #include "ApiServer.h"
#include "TaskManager.h" #include "TaskManager.h"
// Services
#include "services/NodeService.h"
#include "services/NetworkService.h"
#include "services/ClusterService.h"
#include "services/TaskService.h"
using namespace std; using namespace std;
NodeContext ctx({ NodeContext ctx({
@@ -18,6 +24,12 @@ TaskManager taskManager(ctx);
ClusterManager cluster(ctx, taskManager); ClusterManager cluster(ctx, taskManager);
ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port);
// Create services
NodeService nodeService(ctx);
NetworkService networkService(network);
ClusterService clusterService(ctx);
TaskService taskService(taskManager);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
@@ -27,7 +39,11 @@ void setup() {
// Initialize and start all tasks // Initialize and start all tasks
taskManager.initialize(); taskManager.initialize();
// Start the API server // Register services and start API server
apiServer.addService(nodeService);
apiServer.addService(networkService);
apiServer.addService(clusterService);
apiServer.addService(taskService);
apiServer.begin(); apiServer.begin();
// Print initial task status // Print initial task status

View File

@@ -0,0 +1,470 @@
/**
* Original NeoPattern code by Bill Earl
* https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview
*
* TODO
* - cleanup the mess
* - fnc table for patterns to replace switch case
*
* Custom modifications by 0x1d:
* - default OnComplete callback that sets pattern to reverse
* - separate animation update from timer; Update now updates directly, UpdateScheduled uses timer
*/
#ifndef __NeoPattern_INCLUDED__
#define __NeoPattern_INCLUDED__
#include <Adafruit_NeoPixel.h>
using namespace std;
// Pattern types supported:
enum pattern
{
NONE = 0,
RAINBOW_CYCLE = 1,
THEATER_CHASE = 2,
COLOR_WIPE = 3,
SCANNER = 4,
FADE = 5,
FIRE = 6
};
// Patern directions supported:
enum direction
{
FORWARD,
REVERSE
};
// NeoPattern Class - derived from the Adafruit_NeoPixel class
class NeoPattern : public Adafruit_NeoPixel
{
public:
// Member Variables:
pattern ActivePattern = RAINBOW_CYCLE; // which pattern is running
direction Direction = FORWARD; // direction to run the pattern
unsigned long Interval = 150; // milliseconds between updates
unsigned long lastUpdate = 0; // last update of position
uint32_t Color1 = 0;
uint32_t Color2 = 0; // What colors are in use
uint16_t TotalSteps = 32; // total number of steps in the pattern
uint16_t Index; // current step within the pattern
uint16_t completed = 0;
// FIXME return current NeoPatternState
void (*OnComplete)(int); // Callback on completion of pattern
uint8_t *frameBuffer;
int bufferSize = 0;
// Constructor - calls base-class constructor to initialize strip
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int))
: Adafruit_NeoPixel(pixels, pin, type)
{
frameBuffer = (uint8_t *)malloc(768);
OnComplete = callback;
TotalSteps = numPixels();
begin();
}
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
: Adafruit_NeoPixel(pixels, pin, type)
{
frameBuffer = (uint8_t *)malloc(768);
TotalSteps = numPixels();
begin();
}
void handleStream(uint8_t *data, size_t len)
{
//const uint16_t *data16 = (uint16_t *)data;
bufferSize = len;
memcpy(frameBuffer, data, len);
}
void drawFrameBuffer(int w, uint8_t *frame, int length)
{
for (int i = 0; i < length; i++)
{
uint8_t r = frame[i];
uint8_t g = frame[i + 1];
uint8_t b = frame[i + 2];
setPixelColor(i, r, g, b);
}
}
void onCompleteDefault(int pixels)
{
//Serial.println("onCompleteDefault");
// FIXME no specific code
if (ActivePattern == THEATER_CHASE)
{
return;
}
Reverse();
//Serial.println("pattern completed");
}
// Update the pattern
void Update()
{
switch (ActivePattern)
{
case RAINBOW_CYCLE:
RainbowCycleUpdate();
break;
case THEATER_CHASE:
TheaterChaseUpdate();
break;
case COLOR_WIPE:
ColorWipeUpdate();
break;
case SCANNER:
ScannerUpdate();
break;
case FADE:
FadeUpdate();
break;
case FIRE:
Fire(50, 120);
break;
default:
if (bufferSize > 0)
{
drawFrameBuffer(TotalSteps, frameBuffer, bufferSize);
}
break;
}
}
void UpdateScheduled()
{
if ((millis() - lastUpdate) > Interval) // time to update
{
lastUpdate = millis();
Update();
}
}
// Increment the Index and reset at the end
void Increment()
{
completed = 0;
if (Direction == FORWARD)
{
Index++;
if (Index >= TotalSteps)
{
Index = 0;
completed = 1;
if (OnComplete != NULL)
{
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
onCompleteDefault(numPixels());
}
}
}
else // Direction == REVERSE
{
--Index;
if (Index <= 0)
{
Index = TotalSteps - 1;
completed = 1;
if (OnComplete != NULL)
{
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
onCompleteDefault(numPixels());
}
}
}
}
// Reverse pattern direction
void Reverse()
{
if (Direction == FORWARD)
{
Direction = REVERSE;
Index = TotalSteps - 1;
}
else
{
Direction = FORWARD;
Index = 0;
}
}
// Initialize for a RainbowCycle
void RainbowCycle(uint8_t interval, direction dir = FORWARD)
{
ActivePattern = RAINBOW_CYCLE;
Interval = interval;
TotalSteps = 255;
Index = 0;
Direction = dir;
}
// Update the Rainbow Cycle Pattern
void RainbowCycleUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
}
show();
Increment();
}
// Initialize for a Theater Chase
void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD)
{
ActivePattern = THEATER_CHASE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Theater Chase Pattern
void TheaterChaseUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
if ((i + Index) % 3 == 0)
{
setPixelColor(i, Color1);
}
else
{
setPixelColor(i, Color2);
}
}
show();
Increment();
}
// Initialize for a ColorWipe
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD)
{
ActivePattern = COLOR_WIPE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color;
Index = 0;
Direction = dir;
}
// Update the Color Wipe Pattern
void ColorWipeUpdate()
{
setPixelColor(Index, Color1);
show();
Increment();
}
// Initialize for a SCANNNER
void Scanner(uint32_t color1, uint8_t interval)
{
ActivePattern = SCANNER;
Interval = interval;
TotalSteps = (numPixels() - 1) * 2;
Color1 = color1;
Index = 0;
}
// Update the Scanner Pattern
void ScannerUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
if (i == Index) // Scan Pixel to the right
{
setPixelColor(i, Color1);
}
else if (i == TotalSteps - Index) // Scan Pixel to the left
{
setPixelColor(i, Color1);
}
else // Fading tail
{
setPixelColor(i, DimColor(getPixelColor(i)));
}
}
show();
Increment();
}
// Initialize for a Fade
void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD)
{
ActivePattern = FADE;
Interval = interval;
TotalSteps = steps;
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Fade Pattern
void FadeUpdate()
{
// Calculate linear interpolation between Color1 and Color2
// Optimise order of operations to minimize truncation error
uint8_t red = ((Red(Color1) * (TotalSteps - Index)) + (Red(Color2) * Index)) / TotalSteps;
uint8_t green = ((Green(Color1) * (TotalSteps - Index)) + (Green(Color2) * Index)) / TotalSteps;
uint8_t blue = ((Blue(Color1) * (TotalSteps - Index)) + (Blue(Color2) * Index)) / TotalSteps;
ColorSet(Color(red, green, blue));
show();
Increment();
}
// Calculate 50% dimmed version of a color (used by ScannerUpdate)
uint32_t DimColor(uint32_t color)
{
// Shift R, G and B components one bit to the right
uint32_t dimColor = Color(Red(color) >> 1, Green(color) >> 1, Blue(color) >> 1);
return dimColor;
}
// Set all pixels to a color (synchronously)
void ColorSet(uint32_t color)
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, color);
}
show();
}
// Returns the Red component of a 32-bit color
uint8_t Red(uint32_t color)
{
return (color >> 16) & 0xFF;
}
// Returns the Green component of a 32-bit color
uint8_t Green(uint32_t color)
{
return (color >> 8) & 0xFF;
}
// Returns the Blue component of a 32-bit color
uint8_t Blue(uint32_t color)
{
return color & 0xFF;
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(uint8_t WheelPos)
{
//if(WheelPos == 0) return Color(0,0,0);
WheelPos = 255 - WheelPos;
if (WheelPos < 85)
{
return Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
else if (WheelPos < 170)
{
WheelPos -= 85;
return Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
else
{
WheelPos -= 170;
return Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
}
/**
* Effects from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
*/
void Fire(int Cooling, int Sparking)
{
uint8_t heat[numPixels()];
int cooldown;
// Step 1. Cool down every cell a little
for (int i = 0; i < numPixels(); i++)
{
cooldown = random(0, ((Cooling * 10) / numPixels()) + 2);
if (cooldown > heat[i])
{
heat[i] = 0;
}
else
{
heat[i] = heat[i] - cooldown;
}
}
// Step 2. Heat from each cell drifts 'up' and diffuses a little
for (int k = numPixels() - 1; k >= 2; k--)
{
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
}
// Step 3. Randomly ignite new 'sparks' near the bottom
if (random(255) < Sparking)
{
int y = random(7);
heat[y] = heat[y] + random(160, 255);
//heat[y] = random(160,255);
}
// Step 4. Convert heat to LED colors
for (int j = 0; j < numPixels(); j++)
{
setPixelHeatColor(j, heat[j]);
}
showStrip();
}
void setPixelHeatColor(int Pixel, uint8_t temperature)
{
// Scale 'heat' down from 0-255 to 0-191
uint8_t t192 = round((temperature / 255.0) * 191);
// calculate ramp up from
uint8_t heatramp = t192 & 0x3F; // 0..63
heatramp <<= 2; // scale up to 0..252
// figure out which third of the spectrum we're in:
if (t192 > 0x80)
{ // hottest
setPixel(Pixel, 255, 255, heatramp);
}
else if (t192 > 0x40)
{ // middle
setPixel(Pixel, 255, heatramp, 0);
}
else
{ // coolest
setPixel(Pixel, heatramp, 0, 0);
}
}
void setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue)
{
setPixelColor(Pixel, Color(red, green, blue));
}
void showStrip()
{
show();
}
};
#endif

View File

@@ -0,0 +1,187 @@
#include "NeoPatternService.h"
#include "ApiServer.h"
NeoPatternService::NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type)
: taskManager(taskMgr),
pixels(numPixels, pin, type),
updateIntervalMs(100),
brightness(48) {
pixels.setBrightness(brightness);
pixels.show();
registerTasks();
setPatternByName("rainbow_cycle");
}
void NeoPatternService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/neopattern/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopattern", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
ParamSpec{String("interval_ms"), false, String("body"), String("number"), {}, String("100")},
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("50")},
ParamSpec{String("color"), false, String("body"), String("color"), {}},
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
ParamSpec{String("r"), false, String("body"), String("number"), {}},
ParamSpec{String("g"), false, String("body"), String("number"), {}},
ParamSpec{String("b"), false, String("body"), String("number"), {}},
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
ParamSpec{String("g2"), false, String("body"), String("number"), {}},
ParamSpec{String("b2"), false, String("body"), String("number"), {}},
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}},
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}
});
}
void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = pixels.getPin();
doc["count"] = pixels.numPixels();
doc["interval_ms"] = updateIntervalMs;
doc["brightness"] = brightness;
doc["pattern"] = currentPatternName();
doc["total_steps"] = pixels.TotalSteps;
doc["color1"] = pixels.Color1;
doc["color2"] = pixels.Color2;
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto& kv : patternSetters) arr.add(kv.first);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("pattern", true)) {
String name = request->getParam("pattern", true)->value();
setPatternByName(name);
}
if (request->hasParam("interval_ms", true)) {
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
if (v < 1) v = 1;
updateIntervalMs = v;
taskManager.setTaskInterval("neopattern_update", updateIntervalMs);
}
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0; if (b > 255) b = 255;
setBrightness((uint8_t)b);
}
// Accept packed color ints or r,g,b triplets
if (request->hasParam("color", true)) {
pixels.Color1 = (uint32_t)strtoul(request->getParam("color", true)->value().c_str(), nullptr, 0);
}
if (request->hasParam("color2", true)) {
pixels.Color2 = (uint32_t)strtoul(request->getParam("color2", true)->value().c_str(), nullptr, 0);
}
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
pixels.Color1 = pixels.Color(r, g, b);
}
if (request->hasParam("r2", true) || request->hasParam("g2", true) || request->hasParam("b2", true)) {
int r = request->hasParam("r2", true) ? request->getParam("r2", true)->value().toInt() : 0;
int g = request->hasParam("g2", true) ? request->getParam("g2", true)->value().toInt() : 0;
int b = request->hasParam("b2", true) ? request->getParam("b2", true)->value().toInt() : 0;
pixels.Color2 = pixels.Color(r, g, b);
}
if (request->hasParam("total_steps", true)) {
pixels.TotalSteps = request->getParam("total_steps", true)->value().toInt();
}
if (request->hasParam("direction", true)) {
String dir = request->getParam("direction", true)->value();
if (dir.equalsIgnoreCase("forward")) pixels.Direction = FORWARD;
else if (dir.equalsIgnoreCase("reverse")) pixels.Direction = REVERSE;
}
JsonDocument resp;
resp["ok"] = true;
resp["pattern"] = currentPatternName();
resp["interval_ms"] = updateIntervalMs;
resp["brightness"] = brightness;
String json;
serializeJson(resp, json);
request->send(200, "application/json", json);
}
void NeoPatternService::setBrightness(uint8_t b) {
brightness = b;
pixels.setBrightness(brightness);
pixels.show();
}
void NeoPatternService::registerTasks() {
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
Serial.printf("[NeoPattern] pattern=%s interval=%lu ms brightness=%u\n",
currentPatternName().c_str(), updateIntervalMs, brightness);
});
}
void NeoPatternService::registerPatterns() {
patternSetters["off"] = [this]() { pixels.ActivePattern = NONE; };
patternSetters["rainbow_cycle"] = [this]() { pixels.RainbowCycle(updateIntervalMs); };
patternSetters["theater_chase"] = [this]() { pixels.TheaterChase(pixels.Color1 ? pixels.Color1 : pixels.Color(127,127,127), pixels.Color2, updateIntervalMs); };
patternSetters["color_wipe"] = [this]() { pixels.ColorWipe(pixels.Color1 ? pixels.Color1 : pixels.Color(255,0,0), updateIntervalMs); };
patternSetters["scanner"] = [this]() { pixels.Scanner(pixels.Color1 ? pixels.Color1 : pixels.Color(0,0,255), updateIntervalMs); };
patternSetters["fade"] = [this]() { pixels.Fade(pixels.Color1, pixels.Color2, pixels.TotalSteps ? pixels.TotalSteps : 32, updateIntervalMs); };
patternSetters["fire"] = [this]() { pixels.ActivePattern = FIRE; pixels.Interval = updateIntervalMs; };
}
std::vector<String> NeoPatternService::patternNamesVector() {
if (patternSetters.empty()) registerPatterns();
std::vector<String> v;
v.reserve(patternSetters.size());
for (const auto& kv : patternSetters) v.push_back(kv.first);
return v;
}
String NeoPatternService::currentPatternName() {
switch (pixels.ActivePattern) {
case NONE: return String("off");
case RAINBOW_CYCLE: return String("rainbow_cycle");
case THEATER_CHASE: return String("theater_chase");
case COLOR_WIPE: return String("color_wipe");
case SCANNER: return String("scanner");
case FADE: return String("fade");
case FIRE: return String("fire");
}
return String("off");
}
void NeoPatternService::setPatternByName(const String& name) {
if (patternSetters.empty()) registerPatterns();
auto it = patternSetters.find(name);
if (it != patternSetters.end()) {
pixels.Index = 0;
pixels.Direction = FORWARD;
it->second();
}
}
void NeoPatternService::update() {
pixels.Update();
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "services/Service.h"
#include "TaskManager.h"
#include "NeoPattern.cpp"
#include <map>
#include <vector>
class NeoPatternService : public Service {
public:
NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "NeoPattern"; }
void setBrightness(uint8_t b);
void setPatternByName(const String& name);
private:
void registerTasks();
void registerPatterns();
std::vector<String> patternNamesVector();
String currentPatternName();
void update();
// Handlers
void handleStatusRequest(AsyncWebServerRequest* request);
void handlePatternsRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
TaskManager& taskManager;
NeoPattern pixels;
unsigned long updateIntervalMs;
uint8_t brightness;
std::map<String, std::function<void()>> patternSetters;
};

View File

@@ -0,0 +1,31 @@
#ifndef __DEVICE_CONFIG__
#define __DEVICE_CONFIG__
// Scheduler config
#define _TASK_SLEEP_ON_IDLE_RUN
#define _TASK_STD_FUNCTION
#define _TASK_PRIORITY
// Chip config
#define SPROCKET_TYPE "SPROCKET"
#define SERIAL_BAUD_RATE 115200
#define STARTUP_DELAY 1000
// network config
#define SPROCKET_MODE 1
#define WIFI_CHANNEL 11
#define AP_SSID "sprocket"
#define AP_PASSWORD "th3r31sn0sp00n"
#define STATION_SSID "MyAP"
#define STATION_PASSWORD "th3r31sn0sp00n"
#define HOSTNAME "sprocket"
#define CONNECT_TIMEOUT 10000
// NeoPixel conig
#define LED_STRIP_PIN D2
#define LED_STRIP_LENGTH 8
#define LED_STRIP_BRIGHTNESS 48
#define LED_STRIP_UPDATE_INTERVAL 200
#define LED_STRIP_DEFAULT_COLOR 100
#endif

View File

@@ -0,0 +1,71 @@
#include <Arduino.h>
#include <functional>
#include "Globals.h"
#include "NodeContext.h"
#include "NetworkManager.h"
#include "ClusterManager.h"
#include "ApiServer.h"
#include "TaskManager.h"
// Services
#include "services/NodeService.h"
#include "services/NetworkService.h"
#include "services/ClusterService.h"
#include "services/TaskService.h"
#include "NeoPatternService.h"
#ifndef LED_STRIP_PIN
#define LED_STRIP_PIN 2
#endif
#ifndef LED_STRIP_LENGTH
#define LED_STRIP_LENGTH 8
#endif
#ifndef LED_STRIP_TYPE
#define LED_STRIP_TYPE (NEO_GRB + NEO_KHZ800)
#endif
NodeContext ctx({
{"app", "neopattern"},
{"device", "light"},
{"pixels", String(LED_STRIP_LENGTH)},
{"pin", String(LED_STRIP_PIN)}
});
NetworkManager network(ctx);
TaskManager taskManager(ctx);
ClusterManager cluster(ctx, taskManager);
ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port);
// Create services
NodeService nodeService(ctx);
NetworkService networkService(network);
ClusterService clusterService(ctx);
TaskService taskService(taskManager);
NeoPatternService neoPatternService(taskManager, LED_STRIP_LENGTH, LED_STRIP_PIN, LED_STRIP_TYPE);
void setup() {
Serial.begin(115200);
// Setup WiFi first
network.setupWiFi();
// Initialize and start all tasks
taskManager.initialize();
// Register services and start API server
apiServer.addService(nodeService);
apiServer.addService(networkService);
apiServer.addService(clusterService);
apiServer.addService(taskService);
apiServer.addService(neoPatternService);
apiServer.begin();
// Print initial task status
taskManager.printTaskStatus();
}
void loop() {
taskManager.execute();
yield();
}

View File

@@ -0,0 +1,291 @@
#include "NeoPixelService.h"
#include "ApiServer.h"
// Wheel helper: map 0-255 to RGB rainbow
static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) {
pos = 255 - pos;
if (pos < 85) {
return strip.Color(255 - pos * 3, 0, pos * 3);
}
if (pos < 170) {
pos -= 85;
return strip.Color(0, pos * 3, 255 - pos * 3);
}
pos -= 170;
return strip.Color(pos * 3, 255 - pos * 3, 0);
}
NeoPixelService::NeoPixelService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, neoPixelType type)
: taskManager(taskMgr),
strip(numPixels, pin, type),
currentPattern(Pattern::Off),
updateIntervalMs(20),
lastUpdateMs(0),
wipeIndex(0),
wipeColor(strip.Color(255, 0, 0)),
rainbowJ(0),
cycleJ(0),
chaseJ(0),
chaseQ(0),
chasePhaseOn(true),
chaseColor(strip.Color(127, 127, 127)),
brightness(50) {
strip.begin();
strip.setBrightness(brightness);
strip.show();
registerPatterns();
registerTasks();
}
void NeoPixelService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/neopixel/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopixel/patterns", HTTP_GET,
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopixel", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
ParamSpec{String("interval_ms"), false, String("body"), String("number"), {}, String("100")},
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("50")},
ParamSpec{String("color"), false, String("body"), String("color"), {}},
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
ParamSpec{String("r"), false, String("body"), String("number"), {}},
ParamSpec{String("g"), false, String("body"), String("number"), {}},
ParamSpec{String("b"), false, String("body"), String("number"), {}},
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
ParamSpec{String("g2"), false, String("body"), String("number"), {}},
ParamSpec{String("b2"), false, String("body"), String("number"), {}},
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}},
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}
});
}
void NeoPixelService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = strip.getPin();
doc["count"] = strip.numPixels();
doc["interval_ms"] = updateIntervalMs;
doc["brightness"] = brightness;
doc["pattern"] = currentPatternName();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPixelService::handlePatternsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto& kv : patternUpdaters) arr.add(kv.first);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPixelService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("pattern", true)) {
String name = request->getParam("pattern", true)->value();
setPatternByName(name);
}
if (request->hasParam("interval_ms", true)) {
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
if (v < 1) v = 1;
updateIntervalMs = v;
taskManager.setTaskInterval("neopixel_update", updateIntervalMs);
}
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0; if (b > 255) b = 255;
setBrightness((uint8_t)b);
}
// Accept packed color ints or r,g,b triplets
if (request->hasParam("color", true)) {
wipeColor = (uint32_t)strtoul(request->getParam("color", true)->value().c_str(), nullptr, 0);
chaseColor = wipeColor;
}
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
wipeColor = strip.Color(r, g, b);
chaseColor = strip.Color(r / 2, g / 2, b / 2); // dimmer for chase
}
JsonDocument resp;
resp["ok"] = true;
resp["pattern"] = currentPatternName();
resp["interval_ms"] = updateIntervalMs;
resp["brightness"] = brightness;
String json;
serializeJson(resp, json);
request->send(200, "application/json", json);
}
void NeoPixelService::setBrightness(uint8_t b) {
brightness = b;
strip.setBrightness(brightness);
strip.show();
}
void NeoPixelService::registerTasks() {
taskManager.registerTask("neopixel_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopixel_status_print", 10000, [this]() {
Serial.printf("[NeoPixel] pattern=%s interval=%lu ms brightness=%u\n",
currentPatternName().c_str(), updateIntervalMs, brightness);
});
}
void NeoPixelService::registerPatterns() {
patternUpdaters["off"] = [this]() { updateOff(); };
patternUpdaters["color_wipe"] = [this]() { updateColorWipe(); };
patternUpdaters["rainbow"] = [this]() { updateRainbow(); };
patternUpdaters["rainbow_cycle"] = [this]() { updateRainbowCycle(); };
patternUpdaters["theater_chase"] = [this]() { updateTheaterChase(); };
patternUpdaters["theater_chase_rainbow"] = [this]() { updateTheaterChaseRainbow(); };
}
std::vector<String> NeoPixelService::patternNamesVector() const {
std::vector<String> v;
v.reserve(patternUpdaters.size());
for (const auto& kv : patternUpdaters) v.push_back(kv.first);
return v;
}
String NeoPixelService::currentPatternName() const {
switch (currentPattern) {
case Pattern::Off: return String("off");
case Pattern::ColorWipe: return String("color_wipe");
case Pattern::Rainbow: return String("rainbow");
case Pattern::RainbowCycle: return String("rainbow_cycle");
case Pattern::TheaterChase: return String("theater_chase");
case Pattern::TheaterChaseRainbow: return String("theater_chase_rainbow");
}
return String("off");
}
NeoPixelService::Pattern NeoPixelService::nameToPattern(const String& name) const {
if (name.equalsIgnoreCase("color_wipe")) return Pattern::ColorWipe;
if (name.equalsIgnoreCase("rainbow")) return Pattern::Rainbow;
if (name.equalsIgnoreCase("rainbow_cycle")) return Pattern::RainbowCycle;
if (name.equalsIgnoreCase("theater_chase")) return Pattern::TheaterChase;
if (name.equalsIgnoreCase("theater_chase_rainbow")) return Pattern::TheaterChaseRainbow;
return Pattern::Off;
}
void NeoPixelService::setPatternByName(const String& name) {
Pattern p = nameToPattern(name);
resetStateForPattern(p);
}
void NeoPixelService::resetStateForPattern(Pattern p) {
strip.clear();
strip.show();
wipeIndex = 0;
rainbowJ = 0;
cycleJ = 0;
chaseJ = 0;
chaseQ = 0;
chasePhaseOn = true;
lastUpdateMs = 0;
currentPattern = p;
}
void NeoPixelService::update() {
unsigned long now = millis();
if (now - lastUpdateMs < updateIntervalMs) return;
lastUpdateMs = now;
const String name = currentPatternName();
auto it = patternUpdaters.find(name);
if (it != patternUpdaters.end()) {
it->second();
} else {
updateOff();
}
}
void NeoPixelService::updateOff() {
strip.clear();
strip.show();
}
void NeoPixelService::updateColorWipe() {
if (wipeIndex < strip.numPixels()) {
strip.setPixelColor(wipeIndex, wipeColor);
++wipeIndex;
strip.show();
} else {
strip.clear();
wipeIndex = 0;
}
}
void NeoPixelService::updateRainbow() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, colorWheel(strip, (i + rainbowJ) & 255));
}
strip.show();
rainbowJ = (rainbowJ + 1) & 0xFF;
}
void NeoPixelService::updateRainbowCycle() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
uint8_t pos = ((i * 256 / strip.numPixels()) + cycleJ) & 0xFF;
strip.setPixelColor(i, colorWheel(strip, pos));
}
strip.show();
cycleJ = (cycleJ + 1) & 0xFF;
}
void NeoPixelService::updateTheaterChase() {
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, chaseColor);
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3;
chaseJ = (chaseJ + 1) % 10;
}
}
void NeoPixelService::updateTheaterChaseRainbow() {
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, colorWheel(strip, (idx + chaseJ) % 255));
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3;
chaseJ = (chaseJ + 1) & 0xFF;
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "services/Service.h"
#include "TaskManager.h"
#include <Adafruit_NeoPixel.h>
#include <map>
#include <vector>
class NeoPixelService : public Service {
public:
enum class Pattern {
Off,
ColorWipe,
Rainbow,
RainbowCycle,
TheaterChase,
TheaterChaseRainbow
};
NeoPixelService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, neoPixelType type);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "NeoPixel"; }
void setPatternByName(const String& name);
void setBrightness(uint8_t b);
private:
void registerTasks();
void registerPatterns();
std::vector<String> patternNamesVector() const;
String currentPatternName() const;
Pattern nameToPattern(const String& name) const;
void resetStateForPattern(Pattern p);
void update();
// Pattern updaters
void updateOff();
void updateColorWipe();
void updateRainbow();
void updateRainbowCycle();
void updateTheaterChase();
void updateTheaterChaseRainbow();
// Handlers
void handleStatusRequest(AsyncWebServerRequest* request);
void handlePatternsRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
TaskManager& taskManager;
Adafruit_NeoPixel strip;
std::map<String, std::function<void()>> patternUpdaters;
Pattern currentPattern;
unsigned long updateIntervalMs;
unsigned long lastUpdateMs;
// State for patterns
uint16_t wipeIndex;
uint32_t wipeColor;
uint8_t rainbowJ;
uint8_t cycleJ;
int chaseJ;
int chaseQ;
bool chasePhaseOn;
uint32_t chaseColor;
uint8_t brightness;
};

View File

@@ -1,9 +1,5 @@
#include <Arduino.h> #include <Arduino.h>
#include <functional> #include <functional>
#include <map>
#include <vector>
#include <Adafruit_NeoPixel.h>
#include "Globals.h" #include "Globals.h"
#include "NodeContext.h" #include "NodeContext.h"
#include "NetworkManager.h" #include "NetworkManager.h"
@@ -11,6 +7,13 @@
#include "ApiServer.h" #include "ApiServer.h"
#include "TaskManager.h" #include "TaskManager.h"
// Services
#include "services/NodeService.h"
#include "services/NetworkService.h"
#include "services/ClusterService.h"
#include "services/TaskService.h"
#include "NeoPixelService.h"
#ifndef NEOPIXEL_PIN #ifndef NEOPIXEL_PIN
#define NEOPIXEL_PIN 2 #define NEOPIXEL_PIN 2
#endif #endif
@@ -23,348 +26,46 @@
#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800)
#endif #endif
// Wheel helper: map 0-255 to RGB rainbow
static uint32_t colorWheel(Adafruit_NeoPixel &strip, uint8_t pos) {
pos = 255 - pos;
if (pos < 85) {
return strip.Color(255 - pos * 3, 0, pos * 3);
}
if (pos < 170) {
pos -= 85;
return strip.Color(0, pos * 3, 255 - pos * 3);
}
pos -= 170;
return strip.Color(pos * 3, 255 - pos * 3, 0);
}
class NeoPixelService {
public:
enum class Pattern {
Off,
ColorWipe,
Rainbow,
RainbowCycle,
TheaterChase,
TheaterChaseRainbow
};
NeoPixelService(NodeContext &ctx, TaskManager &taskMgr,
uint16_t numPixels,
uint8_t pin,
neoPixelType type)
: ctx(ctx), taskManager(taskMgr),
strip(numPixels, pin, type),
currentPattern(Pattern::Off),
updateIntervalMs(20),
lastUpdateMs(0),
wipeIndex(0),
wipeColor(strip.Color(255, 0, 0)),
rainbowJ(0),
cycleJ(0),
chaseJ(0),
chaseQ(0),
chasePhaseOn(true),
chaseColor(strip.Color(127, 127, 127)),
brightness(50) {
strip.begin();
strip.setBrightness(brightness);
strip.show();
registerPatterns();
registerTasks();
}
void registerApi(ApiServer &api) {
api.addEndpoint("/api/neopixel/status", HTTP_GET, [this](AsyncWebServerRequest *request) {
JsonDocument doc;
doc["pin"] = NEOPIXEL_PIN;
doc["count"] = strip.numPixels();
doc["interval_ms"] = updateIntervalMs;
doc["brightness"] = brightness;
doc["pattern"] = currentPatternName();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
});
api.addEndpoint("/api/neopixel/patterns", HTTP_GET, [this](AsyncWebServerRequest *request) {
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto &kv : patternUpdaters) arr.add(kv.first);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
});
api.addEndpoint("/api/neopixel", HTTP_POST,
[this](AsyncWebServerRequest *request) {
String pattern = request->hasParam("pattern", true) ? request->getParam("pattern", true)->value() : "";
if (pattern.length()) {
setPatternByName(pattern);
}
if (request->hasParam("interval_ms", true)) {
updateIntervalMs = request->getParam("interval_ms", true)->value().toInt();
if (updateIntervalMs < 1) updateIntervalMs = 1;
taskManager.setTaskInterval("neopixel_update", updateIntervalMs);
}
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0; if (b > 255) b = 255;
setBrightness((uint8_t)b);
}
// Optional RGB for color_wipe and theater_chase
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
wipeColor = strip.Color(r, g, b);
chaseColor = strip.Color(r / 2, g / 2, b / 2); // dimmer for chase
}
JsonDocument resp;
resp["ok"] = true;
resp["pattern"] = currentPatternName();
resp["interval_ms"] = updateIntervalMs;
resp["brightness"] = brightness;
String json;
serializeJson(resp, json);
request->send(200, "application/json", json);
},
std::vector<ApiServer::ParamSpec>{
ApiServer::ParamSpec{ String("pattern"), false, String("body"), String("string"), patternNamesVector() },
ApiServer::ParamSpec{ String("interval_ms"), false, String("body"), String("number"), {} },
ApiServer::ParamSpec{ String("brightness"), false, String("body"), String("number"), {} },
ApiServer::ParamSpec{ String("r"), false, String("body"), String("number"), {} },
ApiServer::ParamSpec{ String("g"), false, String("body"), String("number"), {} },
ApiServer::ParamSpec{ String("b"), false, String("body"), String("number"), {} },
}
);
}
void setPatternByName(const String &name) {
auto it = patternUpdaters.find(name);
if (it != patternUpdaters.end()) {
// Map name to enum for status and reset state
currentPattern = nameToPattern(name);
resetStateForPattern(currentPattern);
}
}
void setBrightness(uint8_t b) {
brightness = b;
strip.setBrightness(brightness);
strip.show();
}
private:
void registerTasks() {
taskManager.registerTask("neopixel_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopixel_status_print", 10000, [this]() {
Serial.printf("[NeoPixel] pattern=%s interval=%lu ms brightness=%u\n",
currentPatternName().c_str(), updateIntervalMs, brightness);
});
}
void registerPatterns() {
patternUpdaters["off"] = [this]() { updateOff(); };
patternUpdaters["color_wipe"] = [this]() { updateColorWipe(); };
patternUpdaters["rainbow"] = [this]() { updateRainbow(); };
patternUpdaters["rainbow_cycle"] = [this]() { updateRainbowCycle(); };
patternUpdaters["theater_chase"] = [this]() { updateTheaterChase(); };
patternUpdaters["theater_chase_rainbow"] = [this]() { updateTheaterChaseRainbow(); };
}
std::vector<String> patternNamesVector() const {
std::vector<String> v;
v.reserve(patternUpdaters.size());
for (const auto &kv : patternUpdaters) v.push_back(kv.first);
return v;
}
String currentPatternName() const {
switch (currentPattern) {
case Pattern::Off: return String("off");
case Pattern::ColorWipe: return String("color_wipe");
case Pattern::Rainbow: return String("rainbow");
case Pattern::RainbowCycle: return String("rainbow_cycle");
case Pattern::TheaterChase: return String("theater_chase");
case Pattern::TheaterChaseRainbow: return String("theater_chase_rainbow");
}
return String("off");
}
Pattern nameToPattern(const String &name) const {
if (name.equalsIgnoreCase("color_wipe")) return Pattern::ColorWipe;
if (name.equalsIgnoreCase("rainbow")) return Pattern::Rainbow;
if (name.equalsIgnoreCase("rainbow_cycle")) return Pattern::RainbowCycle;
if (name.equalsIgnoreCase("theater_chase")) return Pattern::TheaterChase;
if (name.equalsIgnoreCase("theater_chase_rainbow")) return Pattern::TheaterChaseRainbow;
return Pattern::Off;
}
void resetStateForPattern(Pattern p) {
// Clear strip by default when changing pattern
strip.clear();
strip.show();
// Reset indexes/state variables
wipeIndex = 0;
rainbowJ = 0;
cycleJ = 0;
chaseJ = 0;
chaseQ = 0;
chasePhaseOn = true;
lastUpdateMs = 0; // force immediate update on next tick
currentPattern = p;
}
void update() {
unsigned long now = millis();
if (now - lastUpdateMs < updateIntervalMs) return;
lastUpdateMs = now;
const String name = currentPatternName();
auto it = patternUpdaters.find(name);
if (it != patternUpdaters.end()) {
it->second();
} else {
updateOff();
}
}
// Pattern updaters (non-blocking; advance a small step each call)
void updateOff() {
// Ensure off state
strip.clear();
strip.show();
}
void updateColorWipe() {
if (wipeIndex < strip.numPixels()) {
strip.setPixelColor(wipeIndex, wipeColor);
++wipeIndex;
strip.show();
} else {
// Restart
strip.clear();
wipeIndex = 0;
}
}
void updateRainbow() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, colorWheel(strip, (i + rainbowJ) & 255));
}
strip.show();
rainbowJ = (rainbowJ + 1) & 0xFF; // 0..255
}
void updateRainbowCycle() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
uint8_t pos = ((i * 256 / strip.numPixels()) + cycleJ) & 0xFF;
strip.setPixelColor(i, colorWheel(strip, pos));
}
strip.show();
cycleJ = (cycleJ + 1) & 0xFF;
}
void updateTheaterChase() {
// Phase toggles on/off for the current q offset
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, chaseColor);
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3; // move the crawl
chaseJ = (chaseJ + 1) % 10; // cycle count kept for status if needed
}
}
void updateTheaterChaseRainbow() {
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, colorWheel(strip, (idx + chaseJ) % 255));
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3;
chaseJ = (chaseJ + 1) & 0xFF;
}
}
private:
NodeContext &ctx;
TaskManager &taskManager;
Adafruit_NeoPixel strip;
std::map<String, std::function<void()>> patternUpdaters;
Pattern currentPattern;
unsigned long updateIntervalMs;
unsigned long lastUpdateMs;
// State for patterns
uint16_t wipeIndex;
uint32_t wipeColor;
uint8_t rainbowJ;
uint8_t cycleJ;
int chaseJ;
int chaseQ;
bool chasePhaseOn;
uint32_t chaseColor;
uint8_t brightness;
};
NodeContext ctx({ NodeContext ctx({
{"app", "neopixel"}, {"app", "neopixel"},
{"device", "light"}, {"device", "light"},
{"pixels", String(NEOPIXEL_COUNT)}, {"pixels", String(NEOPIXEL_COUNT)},
{"pin", String(NEOPIXEL_PIN)} {"pin", String(NEOPIXEL_PIN)}
}); });
NetworkManager network(ctx); NetworkManager network(ctx);
TaskManager taskManager(ctx); TaskManager taskManager(ctx);
ClusterManager cluster(ctx, taskManager); ClusterManager cluster(ctx, taskManager);
ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port);
NeoPixelService neoService(ctx, taskManager, NEOPIXEL_COUNT, NEOPIXEL_PIN, NEOPIXEL_TYPE);
// Create services
NodeService nodeService(ctx);
NetworkService networkService(network);
ClusterService clusterService(ctx);
TaskService taskService(taskManager);
NeoPixelService neoPixelService(taskManager, NEOPIXEL_COUNT, NEOPIXEL_PIN, NEOPIXEL_TYPE);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
network.setupWiFi(); // Setup WiFi first
network.setupWiFi();
taskManager.initialize(); // Initialize and start all tasks
taskManager.initialize();
apiServer.begin(); // Register services and start API server
neoService.registerApi(apiServer); apiServer.addService(nodeService);
apiServer.addService(networkService);
apiServer.addService(clusterService);
apiServer.addService(taskService);
apiServer.addService(neoPixelService);
apiServer.begin();
taskManager.printTaskStatus(); // Print initial task status
taskManager.printTaskStatus();
} }
void loop() { void loop() {
taskManager.execute(); taskManager.execute();
yield(); yield();
} }

View File

@@ -0,0 +1,89 @@
#include "RelayService.h"
#include "ApiServer.h"
RelayService::RelayService(TaskManager& taskMgr, int pin)
: taskManager(taskMgr), relayPin(pin), relayOn(false) {
pinMode(relayPin, OUTPUT);
// Many relay modules are active LOW. Start in OFF state (relay de-energized).
digitalWrite(relayPin, HIGH);
registerTasks();
}
void RelayService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/relay/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/relay", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("state"), true, String("body"), String("string"),
{String("on"), String("off"), String("toggle")}}
});
}
void RelayService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = relayPin;
doc["state"] = relayOn ? "on" : "off";
doc["uptime"] = millis();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void RelayService::handleControlRequest(AsyncWebServerRequest* request) {
String state = request->hasParam("state", true) ? request->getParam("state", true)->value() : "";
bool ok = false;
if (state.equalsIgnoreCase("on")) {
turnOn();
ok = true;
} else if (state.equalsIgnoreCase("off")) {
turnOff();
ok = true;
} else if (state.equalsIgnoreCase("toggle")) {
toggle();
ok = true;
}
JsonDocument resp;
resp["success"] = ok;
resp["state"] = relayOn ? "on" : "off";
if (!ok) {
resp["message"] = "Invalid state. Use: on, off, or toggle";
}
String json;
serializeJson(resp, json);
request->send(ok ? 200 : 400, "application/json", json);
}
void RelayService::turnOn() {
relayOn = true;
// Active LOW relay
digitalWrite(relayPin, LOW);
Serial.println("[RelayService] Relay ON");
}
void RelayService::turnOff() {
relayOn = false;
digitalWrite(relayPin, HIGH);
Serial.println("[RelayService] Relay OFF");
}
void RelayService::toggle() {
if (relayOn) {
turnOff();
} else {
turnOn();
}
}
void RelayService::registerTasks() {
taskManager.registerTask("relay_status_print", 5000, [this]() {
Serial.printf("[RelayService] Status - pin: %d, state: %s\n",
relayPin, relayOn ? "ON" : "OFF");
});
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "services/Service.h"
#include "TaskManager.h"
#include <ArduinoJson.h>
class RelayService : public Service {
public:
RelayService(TaskManager& taskMgr, int pin);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Relay"; }
void turnOn();
void turnOff();
void toggle();
private:
void registerTasks();
TaskManager& taskManager;
int relayPin;
bool relayOn;
void handleStatusRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
};

View File

@@ -7,6 +7,13 @@
#include "ApiServer.h" #include "ApiServer.h"
#include "TaskManager.h" #include "TaskManager.h"
// Services
#include "services/NodeService.h"
#include "services/NetworkService.h"
#include "services/ClusterService.h"
#include "services/TaskService.h"
#include "RelayService.h"
using namespace std; using namespace std;
// Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board. // Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board.
@@ -14,88 +21,6 @@ using namespace std;
#define RELAY_PIN 0 #define RELAY_PIN 0
#endif #endif
class RelayService {
public:
RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
: ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) {
pinMode(relayPin, OUTPUT);
// Many relay modules are active LOW. Start in OFF state (relay de-energized).
digitalWrite(relayPin, HIGH);
registerTasks();
}
void registerApi(ApiServer& api) {
api.addEndpoint("/api/relay/status", HTTP_GET, [this](AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = relayPin;
doc["state"] = relayOn ? "on" : "off";
doc["uptime"] = millis();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
});
api.addEndpoint("/api/relay", HTTP_POST, [this](AsyncWebServerRequest* request) {
String state = request->hasParam("state", true) ? request->getParam("state", true)->value() : "";
bool ok = false;
if (state.equalsIgnoreCase("on")) {
turnOn();
ok = true;
} else if (state.equalsIgnoreCase("off")) {
turnOff();
ok = true;
} else if (state.equalsIgnoreCase("toggle")) {
toggle();
ok = true;
}
JsonDocument resp;
resp["success"] = ok;
resp["state"] = relayOn ? "on" : "off";
if (!ok) {
resp["message"] = "Invalid state. Use: on, off, or toggle";
}
String json;
serializeJson(resp, json);
request->send(ok ? 200 : 400, "application/json", json);
}, std::vector<ApiServer::ParamSpec>{ ApiServer::ParamSpec{ String("state"), true, String("body"), String("string"), { String("on"), String("off"), String("toggle") } } });
}
void turnOn() {
relayOn = true;
// Active LOW relay
digitalWrite(relayPin, LOW);
Serial.println("[RelayService] Relay ON");
}
void turnOff() {
relayOn = false;
digitalWrite(relayPin, HIGH);
Serial.println("[RelayService] Relay OFF");
}
void toggle() {
if (relayOn) {
turnOff();
} else {
turnOn();
}
}
private:
void registerTasks() {
taskManager.registerTask("relay_status_print", 5000, [this]() {
Serial.printf("[RelayService] Status - pin: %d, state: %s\n", relayPin, relayOn ? "ON" : "OFF");
});
}
NodeContext& ctx;
TaskManager& taskManager;
int relayPin;
bool relayOn;
};
NodeContext ctx({ NodeContext ctx({
{"app", "relay"}, {"app", "relay"},
{"device", "actuator"}, {"device", "actuator"},
@@ -105,7 +30,13 @@ NetworkManager network(ctx);
TaskManager taskManager(ctx); TaskManager taskManager(ctx);
ClusterManager cluster(ctx, taskManager); ClusterManager cluster(ctx, taskManager);
ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port);
RelayService relayService(ctx, taskManager, RELAY_PIN);
// Create services
NodeService nodeService(ctx);
NetworkService networkService(network);
ClusterService clusterService(ctx);
TaskService taskService(taskManager);
RelayService relayService(taskManager, RELAY_PIN);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
@@ -116,9 +47,13 @@ void setup() {
// Initialize and start all tasks // Initialize and start all tasks
taskManager.initialize(); taskManager.initialize();
// Start the API server and expose relay endpoints // Register services and start API server
apiServer.addService(nodeService);
apiServer.addService(networkService);
apiServer.addService(clusterService);
apiServer.addService(taskService);
apiServer.addService(relayService);
apiServer.begin(); apiServer.begin();
relayService.registerApi(apiServer);
// Print initial task status // Print initial task status
taskManager.printTaskStatus(); taskManager.printTaskStatus();

View File

@@ -10,52 +10,33 @@
#include "NodeContext.h" #include "NodeContext.h"
#include "NodeInfo.h" #include "NodeInfo.h"
#include "TaskManager.h" #include "TaskManager.h"
#include "ApiTypes.h"
class Service; // Forward declaration
class ApiServer { class ApiServer {
public: public:
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80); ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
void begin(); void begin();
void addService(Service& service);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler); void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler, void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler); std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
// Minimal capability spec types and registration overloads
struct ParamSpec {
String name;
bool required;
String location; // "query" | "body" | "path" | "header"
String type; // e.g. "string", "number", "boolean"
std::vector<String> values; // optional allowed values
};
struct EndpointCapability {
String uri;
int method;
std::vector<ParamSpec> params;
};
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler, void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params); const std::vector<ParamSpec>& params);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler, void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler, std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params); const std::vector<ParamSpec>& params);
static const char* methodToStr(int method);
private: private:
AsyncWebServer server; AsyncWebServer server;
NodeContext& ctx; NodeContext& ctx;
TaskManager& taskManager; TaskManager& taskManager;
std::vector<std::reference_wrapper<Service>> services;
std::vector<std::tuple<String, int>> serviceRegistry; std::vector<std::tuple<String, int>> serviceRegistry;
std::vector<EndpointCapability> capabilityRegistry;
void onClusterMembersRequest(AsyncWebServerRequest *request);
void methodToStr(const std::tuple<String, int> &endpoint, JsonObject &apiObj);
void onSystemStatusRequest(AsyncWebServerRequest *request);
void onFirmwareUpdateRequest(AsyncWebServerRequest *request);
void onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final);
void onRestartRequest(AsyncWebServerRequest *request);
// Task management endpoints
void onTaskStatusRequest(AsyncWebServerRequest *request);
void onTaskControlRequest(AsyncWebServerRequest *request);
// Capabilities endpoint
void onCapabilitiesRequest(AsyncWebServerRequest *request);
// Internal helpers // Internal helpers
void registerServiceForLocalNode(const String& uri, int method); void registerServiceForLocalNode(const String& uri, int method);

18
include/ApiTypes.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <Arduino.h>
#include <vector>
struct ParamSpec {
String name;
bool required;
String location; // "query" | "body" | "path" | "header"
String type; // e.g. "string", "number", "boolean"
std::vector<String> values; // optional allowed values
String defaultValue; // optional default value (stringified)
};
struct EndpointCapability {
String uri;
int method;
std::vector<ParamSpec> params;
};

View File

@@ -1,12 +1,49 @@
#pragma once #pragma once
#include "NodeContext.h" #include "NodeContext.h"
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <vector>
struct AccessPoint {
String ssid;
int32_t rssi;
uint8_t encryptionType;
uint8_t* bssid;
int32_t channel;
bool isHidden;
};
class NetworkManager { class NetworkManager {
public: public:
NetworkManager(NodeContext& ctx); NetworkManager(NodeContext& ctx);
void setupWiFi(); void setupWiFi();
void setHostnameFromMac(); void setHostnameFromMac();
// WiFi scanning methods
void scanWifi();
void processAccessPoints();
std::vector<AccessPoint> getAccessPoints() const;
// WiFi configuration methods
void setWiFiConfig(const String& ssid, const String& password,
uint32_t connect_timeout_ms = 10000,
uint32_t retry_delay_ms = 500);
// Network status methods
bool isConnected() const { return WiFi.isConnected(); }
String getSSID() const { return WiFi.SSID(); }
IPAddress getLocalIP() const { return WiFi.localIP(); }
String getMacAddress() const { return WiFi.macAddress(); }
String getHostname() const { return WiFi.hostname(); }
int32_t getRSSI() const { return WiFi.RSSI(); }
WiFiMode_t getMode() const { return WiFi.getMode(); }
// AP mode specific methods
IPAddress getAPIP() const { return WiFi.softAPIP(); }
String getAPMacAddress() const { return WiFi.softAPmacAddress(); }
uint8_t getConnectedStations() const { return WiFi.softAPgetStationNum(); }
private: private:
NodeContext& ctx; NodeContext& ctx;
std::vector<AccessPoint> accessPoints;
bool isScanning = false;
}; };

View File

@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <initializer_list> #include <initializer_list>
#include "Config.h" #include "Config.h"
#include "ApiTypes.h"
class NodeContext { class NodeContext {
public: public:
@@ -19,6 +20,7 @@ public:
NodeInfo self; NodeInfo self;
std::map<String, NodeInfo>* memberList; std::map<String, NodeInfo>* memberList;
Config config; Config config;
std::vector<EndpointCapability> capabilities;
using EventCallback = std::function<void(void*)>; using EventCallback = std::function<void(void*)>;
std::map<std::string, std::vector<EventCallback>> eventRegistry; std::map<std::string, std::vector<EventCallback>> eventRegistry;

View File

@@ -0,0 +1,16 @@
#pragma once
#include "services/Service.h"
#include "NodeContext.h"
#include <ArduinoJson.h>
class ClusterService : public Service {
public:
ClusterService(NodeContext& ctx);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Cluster"; }
private:
NodeContext& ctx;
void handleMembersRequest(AsyncWebServerRequest* request);
};

View File

@@ -0,0 +1,22 @@
#pragma once
#include "services/Service.h"
#include "NetworkManager.h"
#include "NodeContext.h"
class NetworkService : public Service {
public:
NetworkService(NetworkManager& networkManager);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Network"; }
private:
NetworkManager& networkManager;
// WiFi scanning endpoints
void handleWifiScanRequest(AsyncWebServerRequest* request);
void handleGetWifiNetworks(AsyncWebServerRequest* request);
// Network status endpoints
void handleNetworkStatus(AsyncWebServerRequest* request);
void handleSetWifiConfig(AsyncWebServerRequest* request);
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "services/Service.h"
#include "NodeContext.h"
#include <ArduinoJson.h>
#include <Updater.h>
class NodeService : public Service {
public:
NodeService(NodeContext& ctx);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Node"; }
private:
NodeContext& ctx;
void handleStatusRequest(AsyncWebServerRequest* request);
void handleUpdateRequest(AsyncWebServerRequest* request);
void handleUpdateUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final);
void handleRestartRequest(AsyncWebServerRequest* request);
void handleCapabilitiesRequest(AsyncWebServerRequest* request);
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include "ApiServer.h"
class Service {
public:
virtual ~Service() = default;
virtual void registerEndpoints(ApiServer& api) = 0;
virtual const char* getName() const = 0;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "services/Service.h"
#include "TaskManager.h"
#include <ArduinoJson.h>
class TaskService : public Service {
public:
TaskService(TaskManager& taskManager);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Task"; }
private:
TaskManager& taskManager;
void handleStatusRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
};

View File

@@ -32,6 +32,7 @@ build_src_filter =
+<examples/base/*.cpp> +<examples/base/*.cpp>
+<src/*.c> +<src/*.c>
+<src/*.cpp> +<src/*.cpp>
+<src/services/*.cpp>
[env:d1_mini] [env:d1_mini]
platform = platformio/espressif8266@^4.2.1 platform = platformio/espressif8266@^4.2.1
@@ -46,6 +47,7 @@ build_src_filter =
+<examples/base/*.cpp> +<examples/base/*.cpp>
+<src/*.c> +<src/*.c>
+<src/*.cpp> +<src/*.cpp>
+<src/services/*.cpp>
[env:esp01_1m_relay] [env:esp01_1m_relay]
platform = platformio/espressif8266@^4.2.1 platform = platformio/espressif8266@^4.2.1
@@ -61,6 +63,7 @@ build_src_filter =
+<examples/relay/*.cpp> +<examples/relay/*.cpp>
+<src/*.c> +<src/*.c>
+<src/*.cpp> +<src/*.cpp>
+<src/services/*.cpp>
[env:esp01_1m_neopixel] [env:esp01_1m_neopixel]
platform = platformio/espressif8266@^4.2.1 platform = platformio/espressif8266@^4.2.1
@@ -77,6 +80,7 @@ build_src_filter =
+<examples/neopixel/*.cpp> +<examples/neopixel/*.cpp>
+<src/*.c> +<src/*.c>
+<src/*.cpp> +<src/*.cpp>
+<src/services/*.cpp>
[env:d1_mini_neopixel] [env:d1_mini_neopixel]
platform = platformio/espressif8266@^4.2.1 platform = platformio/espressif8266@^4.2.1
@@ -92,3 +96,38 @@ build_src_filter =
+<examples/neopixel/*.cpp> +<examples/neopixel/*.cpp>
+<src/*.c> +<src/*.c>
+<src/*.cpp> +<src/*.cpp>
+<src/services/*.cpp>
[env:esp01_1m_neopattern]
platform = platformio/espressif8266@^4.2.1
board = esp01_1m
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.partitions = partitions_ota_1M.csv
board_build.flash_mode = dout
board_build.flash_size = 1M
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_flags = -DLED_STRIP_PIN=2
build_src_filter =
+<examples/neopattern/*.cpp>
+<src/*.c>
+<src/*.cpp>
+<src/services/*.cpp>
[env:d1_mini_neopattern]
platform = platformio/espressif8266@^4.2.1
board = d1_mini
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.flash_mode = dio
board_build.flash_size = 4M
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_src_filter =
+<examples/neopattern/*.cpp>
+<src/*.c>
+<src/*.cpp>
+<src/services/*.cpp>

View File

@@ -1,8 +1,8 @@
#include "ApiServer.h" #include "ApiServer.h"
#include "services/Service.h"
#include <algorithm> #include <algorithm>
// Shared helper for HTTP method to string const char* ApiServer::methodToStr(int method) {
static const char* methodStrFromInt(int method) {
switch (method) { switch (method) {
case HTTP_GET: return "GET"; case HTTP_GET: return "GET";
case HTTP_POST: return "POST"; case HTTP_POST: return "POST";
@@ -39,7 +39,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
// Overloads that also record minimal capability specs // Overloads that also record minimal capability specs
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler, void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params) { const std::vector<ParamSpec>& params) {
capabilityRegistry.push_back(EndpointCapability{uri, method, params}); ctx.capabilities.push_back(EndpointCapability{uri, method, params});
registerServiceForLocalNode(uri, method); registerServiceForLocalNode(uri, method);
server.on(uri.c_str(), method, requestHandler); server.on(uri.c_str(), method, requestHandler);
} }
@@ -47,343 +47,22 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler, void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler, std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params) { const std::vector<ParamSpec>& params) {
capabilityRegistry.push_back(EndpointCapability{uri, method, params}); ctx.capabilities.push_back(EndpointCapability{uri, method, params});
registerServiceForLocalNode(uri, method); registerServiceForLocalNode(uri, method);
server.on(uri.c_str(), method, requestHandler, uploadHandler); server.on(uri.c_str(), method, requestHandler, uploadHandler);
} }
void ApiServer::addService(Service& service) {
services.push_back(service);
Serial.printf("[API] Added service: %s\n", service.getName());
}
void ApiServer::begin() { void ApiServer::begin() {
addEndpoint("/api/node/status", HTTP_GET, // Register all service endpoints
std::bind(&ApiServer::onSystemStatusRequest, this, std::placeholders::_1)); for (auto& service : services) {
addEndpoint("/api/cluster/members", HTTP_GET, service.get().registerEndpoints(*this);
std::bind(&ApiServer::onClusterMembersRequest, this, std::placeholders::_1)); Serial.printf("[API] Registered endpoints for service: %s\n", service.get().getName());
addEndpoint("/api/node/update", HTTP_POST, }
std::bind(&ApiServer::onFirmwareUpdateRequest, this, std::placeholders::_1),
std::bind(&ApiServer::onFirmwareUpload, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)
);
addEndpoint("/api/node/restart", HTTP_POST,
std::bind(&ApiServer::onRestartRequest, this, std::placeholders::_1));
// New: Capabilities endpoint
addEndpoint("/api/capabilities", HTTP_GET,
std::bind(&ApiServer::onCapabilitiesRequest, this, std::placeholders::_1));
// Task management endpoints
addEndpoint("/api/tasks/status", HTTP_GET,
std::bind(&ApiServer::onTaskStatusRequest, this, std::placeholders::_1));
addEndpoint("/api/tasks/control", HTTP_POST,
std::bind(&ApiServer::onTaskControlRequest, this, std::placeholders::_1),
std::vector<ParamSpec>{
ParamSpec{String("task"), true, String("body"), String("string"), {}},
ParamSpec{String("action"), true, String("body"), String("string"), {String("enable"), String("disable"), String("start"), String("stop"), String("status")}}
}
);
server.begin(); server.begin();
} }
void ApiServer::onSystemStatusRequest(AsyncWebServerRequest *request) {
JsonDocument doc;
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
doc["sdkVersion"] = ESP.getSdkVersion();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["flashChipSize"] = ESP.getFlashChipSize();
JsonArray apiArr = doc["api"].to<JsonArray>();
for (const auto& entry : serviceRegistry) {
JsonObject apiObj = apiArr.add<JsonObject>();
apiObj["uri"] = std::get<0>(entry);
apiObj["method"] = std::get<1>(entry);
}
// Include local node labels if present
if (ctx.memberList) {
auto it = ctx.memberList->find(ctx.hostname);
if (it != ctx.memberList->end()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : it->second.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
} else if (!ctx.self.labels.empty()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : ctx.self.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void ApiServer::onClusterMembersRequest(AsyncWebServerRequest *request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();
for (const auto& pair : *ctx.memberList) {
const NodeInfo& node = pair.second;
JsonObject obj = arr.add<JsonObject>();
obj["hostname"] = node.hostname;
obj["ip"] = node.ip.toString();
obj["lastSeen"] = node.lastSeen;
obj["latency"] = node.latency;
obj["status"] = statusToStr(node.status);
obj["resources"]["freeHeap"] = node.resources.freeHeap;
obj["resources"]["chipId"] = node.resources.chipId;
obj["resources"]["sdkVersion"] = node.resources.sdkVersion;
obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz;
obj["resources"]["flashChipSize"] = node.resources.flashChipSize;
JsonArray apiArr = obj["api"].to<JsonArray>();
for (const auto& endpoint : node.apiEndpoints) {
JsonObject apiObj = apiArr.add<JsonObject>();
apiObj["uri"] = std::get<0>(endpoint);
methodToStr(endpoint, apiObj);
}
// Add labels if present
if (!node.labels.empty()) {
JsonObject labelsObj = obj["labels"].to<JsonObject>();
for (const auto& kv : node.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void ApiServer::methodToStr(const std::tuple<String, int> &endpoint, JsonObject &apiObj)
{
int method = std::get<1>(endpoint);
apiObj["method"] = methodStrFromInt(method);
}
void ApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) {
bool success = !Update.hasError();
AsyncWebServerResponse *response = request->beginResponse(200, "application/json", success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
delay(10);
ESP.restart();
});
}
void ApiServer::onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
Serial.print("[OTA] Update Start ");
Serial.println(filename);
Update.runAsync(true);
if(!Update.begin(request->contentLength(), U_FLASH)) {
Serial.println("[OTA] Update failed: not enough space");
Update.printError(Serial);
AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
return;
}
}
if (!Update.hasError()){
if (Update.write(data, len) != len) {
Update.printError(Serial);
}
}
if (final) {
if (Update.end(true)) {
if(Update.isFinished()) {
Serial.print("[OTA] Update Success with ");
Serial.print(index + len);
Serial.println("B");
} else {
Serial.println("[OTA] Update not finished");
}
} else {
Serial.print("[OTA] Update failed: ");
Update.printError(Serial);
}
}
return;
}
void ApiServer::onRestartRequest(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
delay(10);
ESP.restart();
});
}
void ApiServer::onTaskStatusRequest(AsyncWebServerRequest *request) {
// Use a separate document as scratch space for task statuses to avoid interfering with the response root
JsonDocument scratch;
// Get comprehensive task status from TaskManager
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
// Build response document
JsonDocument doc;
// Add summary information
JsonObject summaryObj = doc["summary"].to<JsonObject>();
summaryObj["totalTasks"] = taskStatuses.size();
summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(),
[](const auto& pair) { return pair.second["enabled"]; });
// Add detailed task information
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
for (const auto& taskPair : taskStatuses) {
JsonObject taskObj = tasksArr.add<JsonObject>();
taskObj["name"] = taskPair.first;
taskObj["interval"] = taskPair.second["interval"];
taskObj["enabled"] = taskPair.second["enabled"];
taskObj["running"] = taskPair.second["running"];
taskObj["autoStart"] = taskPair.second["autoStart"];
}
// Add system information
JsonObject systemObj = doc["system"].to<JsonObject>();
systemObj["freeHeap"] = ESP.getFreeHeap();
systemObj["uptime"] = millis();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void ApiServer::onTaskControlRequest(AsyncWebServerRequest *request) {
// Parse the request body for task control commands
if (request->hasParam("task", true) && request->hasParam("action", true)) {
String taskName = request->getParam("task", true)->value();
String action = request->getParam("action", true)->value();
bool success = false;
String message = "";
if (action == "enable") {
taskManager.enableTask(taskName.c_str());
success = true;
message = "Task enabled";
} else if (action == "disable") {
taskManager.disableTask(taskName.c_str());
success = true;
message = "Task disabled";
} else if (action == "start") {
taskManager.startTask(taskName.c_str());
success = true;
message = "Task started";
} else if (action == "stop") {
taskManager.stopTask(taskName.c_str());
success = true;
message = "Task stopped";
} else if (action == "status") {
// Get detailed status for a specific task
success = true;
message = "Task status retrieved";
// Create JsonDocument for status response
JsonDocument statusDoc;
statusDoc["success"] = success;
statusDoc["message"] = message;
statusDoc["task"] = taskName;
statusDoc["action"] = action;
// Add task details to response
statusDoc["taskDetails"] = JsonObject();
JsonObject taskDetails = statusDoc["taskDetails"];
taskDetails["name"] = taskName;
taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str());
taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str());
taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str());
// Add system context
taskDetails["system"] = JsonObject();
JsonObject systemInfo = taskDetails["system"];
systemInfo["freeHeap"] = ESP.getFreeHeap();
systemInfo["uptime"] = millis();
String statusJson;
serializeJson(statusDoc, statusJson);
request->send(200, "application/json", statusJson);
return; // Early return since we've already sent the response
} else {
success = false;
message = "Invalid action. Use: enable, disable, start, stop, or status";
}
JsonDocument doc;
doc["success"] = success;
doc["message"] = message;
doc["task"] = taskName;
doc["action"] = action;
String json;
serializeJson(doc, json);
request->send(success ? 200 : 400, "application/json", json);
} else {
// Missing parameters
JsonDocument doc;
doc["success"] = false;
doc["message"] = "Missing parameters. Required: task, action";
doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}";
String json;
serializeJson(doc, json);
request->send(400, "application/json", json);
}
}
void ApiServer::onCapabilitiesRequest(AsyncWebServerRequest *request) {
JsonDocument doc;
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
// Track seen (uri|method) to avoid duplicates
std::vector<String> seen;
auto makeKey = [](const String& uri, int method) {
String k = uri; k += "|"; k += method; return k;
};
// Rich entries first
for (const auto& cap : capabilityRegistry) {
String key = makeKey(cap.uri, cap.method);
seen.push_back(key);
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = cap.uri;
obj["method"] = methodStrFromInt(cap.method);
if (!cap.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : cap.params) {
JsonObject p = paramsArr.add<JsonObject>();
p["name"] = ps.name;
p["location"] = ps.location;
p["required"] = ps.required;
p["type"] = ps.type;
if (!ps.values.empty()) {
JsonArray allowed = p["values"].to<JsonArray>();
for (const auto& v : ps.values) {
allowed.add(v);
}
}
}
}
}
// Then any endpoints without explicit param specs
for (const auto& entry : serviceRegistry) {
const String& uri = std::get<0>(entry);
int method = std::get<1>(entry);
String key = makeKey(uri, method);
bool exists = false;
for (const auto& s : seen) { if (s == key) { exists = true; break; } }
if (!exists) {
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = uri;
obj["method"] = methodStrFromInt(method);
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -2,8 +2,69 @@
// SSID and password are now configured via Config class // SSID and password are now configured via Config class
void NetworkManager::scanWifi() {
if (!isScanning) {
isScanning = true;
Serial.println("[WiFi] Starting WiFi scan...");
// Start async WiFi scan
WiFi.scanNetworksAsync([this](int networksFound) {
Serial.printf("[WiFi] Scan completed, found %d networks\n", networksFound);
this->processAccessPoints();
this->isScanning = false;
}, true);
} else {
Serial.println("[WiFi] Scan already in progress...");
}
}
void NetworkManager::processAccessPoints() {
int numNetworks = WiFi.scanComplete();
if (numNetworks <= 0) {
Serial.println("[WiFi] No networks found or scan not complete");
return;
}
// Clear existing access points
accessPoints.clear();
// Process each network found
for (int i = 0; i < numNetworks; i++) {
AccessPoint ap;
ap.ssid = WiFi.SSID(i);
ap.rssi = WiFi.RSSI(i);
ap.encryptionType = WiFi.encryptionType(i);
ap.channel = WiFi.channel(i);
ap.isHidden = ap.ssid.length() == 0;
// Copy BSSID
uint8_t* newBssid = new uint8_t[6];
memcpy(newBssid, WiFi.BSSID(i), 6);
ap.bssid = newBssid;
accessPoints.push_back(ap);
Serial.printf("[WiFi] Found network %d: %s, Ch: %d, RSSI: %d\n",
i + 1, ap.ssid.c_str(), ap.channel, ap.rssi);
}
// Free the memory used by the scan
WiFi.scanDelete();
}
std::vector<AccessPoint> NetworkManager::getAccessPoints() const {
return accessPoints;
}
NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {}
void NetworkManager::setWiFiConfig(const String& ssid, const String& password,
uint32_t connect_timeout_ms, uint32_t retry_delay_ms) {
ctx.config.wifi_ssid = ssid;
ctx.config.wifi_password = password;
ctx.config.wifi_connect_timeout_ms = connect_timeout_ms;
ctx.config.wifi_retry_delay_ms = retry_delay_ms;
}
void NetworkManager::setHostnameFromMac() { void NetworkManager::setHostnameFromMac() {
uint8_t mac[6]; uint8_t mac[6];
WiFi.macAddress(mac); WiFi.macAddress(mac);

View File

@@ -0,0 +1,42 @@
#include "services/ClusterService.h"
#include "ApiServer.h"
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
void ClusterService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/cluster/members", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
std::vector<ParamSpec>{});
}
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();
for (const auto& pair : *ctx.memberList) {
const NodeInfo& node = pair.second;
JsonObject obj = arr.add<JsonObject>();
obj["hostname"] = node.hostname;
obj["ip"] = node.ip.toString();
obj["lastSeen"] = node.lastSeen;
obj["latency"] = node.latency;
obj["status"] = statusToStr(node.status);
obj["resources"]["freeHeap"] = node.resources.freeHeap;
obj["resources"]["chipId"] = node.resources.chipId;
obj["resources"]["sdkVersion"] = node.resources.sdkVersion;
obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz;
obj["resources"]["flashChipSize"] = node.resources.flashChipSize;
// Add labels if present
if (!node.labels.empty()) {
JsonObject labelsObj = obj["labels"].to<JsonObject>();
for (const auto& kv : node.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,132 @@
#include "services/NetworkService.h"
#include <ArduinoJson.h>
NetworkService::NetworkService(NetworkManager& networkManager)
: networkManager(networkManager) {}
void NetworkService::registerEndpoints(ApiServer& api) {
// WiFi scanning endpoints
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
std::vector<ParamSpec>{});
// Network status and configuration endpoints
api.addEndpoint("/api/network/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
std::vector<ParamSpec>{
ParamSpec{String("ssid"), true, String("body"), String("string"), {}, String("")},
ParamSpec{String("password"), true, String("body"), String("string"), {}, String("")},
ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")},
ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")}
});
}
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {
networkManager.scanWifi();
JsonDocument doc;
doc["status"] = "scanning";
doc["message"] = "WiFi scan started";
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NetworkService::handleGetWifiNetworks(AsyncWebServerRequest* request) {
auto accessPoints = networkManager.getAccessPoints();
JsonDocument doc;
doc["access_points"].to<JsonArray>();
for (const auto& ap : accessPoints) {
JsonObject apObj = doc["access_points"].add<JsonObject>();
apObj["ssid"] = ap.ssid;
apObj["rssi"] = ap.rssi;
apObj["channel"] = ap.channel;
apObj["encryption_type"] = ap.encryptionType;
apObj["hidden"] = ap.isHidden;
// Convert BSSID to string
char bssid[18];
sprintf(bssid, "%02X:%02X:%02X:%02X:%02X:%02X",
ap.bssid[0], ap.bssid[1], ap.bssid[2],
ap.bssid[3], ap.bssid[4], ap.bssid[5]);
apObj["bssid"] = bssid;
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NetworkService::handleNetworkStatus(AsyncWebServerRequest* request) {
JsonDocument doc;
// WiFi status
doc["wifi"]["connected"] = networkManager.isConnected();
doc["wifi"]["mode"] = networkManager.getMode() == WIFI_AP ? "AP" : "STA";
doc["wifi"]["ssid"] = networkManager.getSSID();
doc["wifi"]["ip"] = networkManager.getLocalIP().toString();
doc["wifi"]["mac"] = networkManager.getMacAddress();
doc["wifi"]["hostname"] = networkManager.getHostname();
doc["wifi"]["rssi"] = networkManager.getRSSI();
// If in AP mode, add AP specific info
if (networkManager.getMode() == WIFI_AP) {
doc["wifi"]["ap_ip"] = networkManager.getAPIP().toString();
doc["wifi"]["ap_mac"] = networkManager.getAPMacAddress();
doc["wifi"]["stations_connected"] = networkManager.getConnectedStations();
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NetworkService::handleSetWifiConfig(AsyncWebServerRequest* request) {
if (!request->hasParam("ssid", true) || !request->hasParam("password", true)) {
request->send(400, "application/json", "{\"error\": \"Missing required parameters\"}");
return;
}
String ssid = request->getParam("ssid", true)->value();
String password = request->getParam("password", true)->value();
// Optional parameters with defaults
uint32_t connect_timeout_ms = 10000; // Default 10 seconds
uint32_t retry_delay_ms = 500; // Default 500ms
if (request->hasParam("connect_timeout_ms", true)) {
connect_timeout_ms = request->getParam("connect_timeout_ms", true)->value().toInt();
}
if (request->hasParam("retry_delay_ms", true)) {
retry_delay_ms = request->getParam("retry_delay_ms", true)->value().toInt();
}
// Update configuration
networkManager.setWiFiConfig(ssid, password, connect_timeout_ms, retry_delay_ms);
// Attempt to connect with new settings
networkManager.setupWiFi();
JsonDocument doc;
doc["status"] = "success";
doc["message"] = "WiFi configuration updated";
doc["connected"] = WiFi.isConnected();
if (WiFi.isConnected()) {
doc["ip"] = WiFi.localIP().toString();
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,157 @@
#include "services/NodeService.h"
#include "ApiServer.h"
NodeService::NodeService(NodeContext& ctx) : ctx(ctx) {}
void NodeService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/node/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
// Update endpoint with file upload
api.addEndpoint("/api/node/update", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleUpdateRequest(request); },
[this](AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) {
handleUpdateUpload(request, filename, index, data, len, final);
},
std::vector<ParamSpec>{
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
});
// Restart endpoint
api.addEndpoint("/api/node/restart", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
std::vector<ParamSpec>{});
// Capabilities endpoint
api.addEndpoint("/api/node/capabilities", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleCapabilitiesRequest(request); },
std::vector<ParamSpec>{});
}
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
doc["sdkVersion"] = ESP.getSdkVersion();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["flashChipSize"] = ESP.getFlashChipSize();
// Include local node labels if present
if (ctx.memberList) {
auto it = ctx.memberList->find(ctx.hostname);
if (it != ctx.memberList->end()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : it->second.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
} else if (!ctx.self.labels.empty()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : ctx.self.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
bool success = !Update.hasError();
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
delay(10);
ESP.restart();
});
}
void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename,
size_t index, uint8_t* data, size_t len, bool final) {
if (!index) {
Serial.print("[OTA] Update Start ");
Serial.println(filename);
Update.runAsync(true);
if(!Update.begin(request->contentLength(), U_FLASH)) {
Serial.println("[OTA] Update failed: not enough space");
Update.printError(Serial);
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
"{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
return;
}
}
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
Update.printError(Serial);
}
}
if (final) {
if (Update.end(true)) {
if(Update.isFinished()) {
Serial.print("[OTA] Update Success with ");
Serial.print(index + len);
Serial.println("B");
} else {
Serial.println("[OTA] Update not finished");
}
} else {
Serial.print("[OTA] Update failed: ");
Update.printError(Serial);
}
}
}
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
"{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
delay(10);
ESP.restart();
});
}
void NodeService::handleCapabilitiesRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
// Add all registered capabilities
for (const auto& cap : ctx.capabilities) {
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = cap.uri;
obj["method"] = ApiServer::methodToStr(cap.method);
if (!cap.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : cap.params) {
JsonObject p = paramsArr.add<JsonObject>();
p["name"] = ps.name;
p["location"] = ps.location;
p["required"] = ps.required;
p["type"] = ps.type;
if (!ps.values.empty()) {
JsonArray allowed = p["values"].to<JsonArray>();
for (const auto& v : ps.values) {
allowed.add(v);
}
}
if (ps.defaultValue.length() > 0) {
p["default"] = ps.defaultValue;
}
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,137 @@
#include "services/TaskService.h"
#include "ApiServer.h"
#include <algorithm>
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
void TaskService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/tasks/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/tasks/control", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{
String("task"),
true,
String("body"),
String("string"),
{},
String("")
},
ParamSpec{
String("action"),
true,
String("body"),
String("string"),
{String("enable"), String("disable"), String("start"), String("stop"), String("status")},
String("")
}
});
}
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument scratch;
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
JsonDocument doc;
JsonObject summaryObj = doc["summary"].to<JsonObject>();
summaryObj["totalTasks"] = taskStatuses.size();
summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(),
[](const auto& pair) { return pair.second["enabled"]; });
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
for (const auto& taskPair : taskStatuses) {
JsonObject taskObj = tasksArr.add<JsonObject>();
taskObj["name"] = taskPair.first;
taskObj["interval"] = taskPair.second["interval"];
taskObj["enabled"] = taskPair.second["enabled"];
taskObj["running"] = taskPair.second["running"];
taskObj["autoStart"] = taskPair.second["autoStart"];
}
JsonObject systemObj = doc["system"].to<JsonObject>();
systemObj["freeHeap"] = ESP.getFreeHeap();
systemObj["uptime"] = millis();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("task", true) && request->hasParam("action", true)) {
String taskName = request->getParam("task", true)->value();
String action = request->getParam("action", true)->value();
bool success = false;
String message = "";
if (action == "enable") {
taskManager.enableTask(taskName.c_str());
success = true;
message = "Task enabled";
} else if (action == "disable") {
taskManager.disableTask(taskName.c_str());
success = true;
message = "Task disabled";
} else if (action == "start") {
taskManager.startTask(taskName.c_str());
success = true;
message = "Task started";
} else if (action == "stop") {
taskManager.stopTask(taskName.c_str());
success = true;
message = "Task stopped";
} else if (action == "status") {
success = true;
message = "Task status retrieved";
JsonDocument statusDoc;
statusDoc["success"] = success;
statusDoc["message"] = message;
statusDoc["task"] = taskName;
statusDoc["action"] = action;
statusDoc["taskDetails"] = JsonObject();
JsonObject taskDetails = statusDoc["taskDetails"];
taskDetails["name"] = taskName;
taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str());
taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str());
taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str());
taskDetails["system"] = JsonObject();
JsonObject systemInfo = taskDetails["system"];
systemInfo["freeHeap"] = ESP.getFreeHeap();
systemInfo["uptime"] = millis();
String statusJson;
serializeJson(statusDoc, statusJson);
request->send(200, "application/json", statusJson);
return;
} else {
success = false;
message = "Invalid action. Use: enable, disable, start, stop, or status";
}
JsonDocument doc;
doc["success"] = success;
doc["message"] = message;
doc["task"] = taskName;
doc["action"] = action;
String json;
serializeJson(doc, json);
request->send(success ? 200 : 400, "application/json", json);
} else {
JsonDocument doc;
doc["success"] = false;
doc["message"] = "Missing parameters. Required: task, action";
doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}";
String json;
serializeJson(doc, json);
request->send(400, "application/json", json);
}
}