feat: services (#2)
This commit is contained in:
68
.cursor/rules/cpp.mdc
Normal file
68
.cursor/rules/cpp.mdc
Normal 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 team’s 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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
843
api/openapi.yaml
843
api/openapi.yaml
@@ -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
7
ctl.sh
@@ -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}
|
||||||
|
|||||||
301
docs/API.md
301
docs/API.md
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
470
examples/neopattern/NeoPattern.cpp
Normal file
470
examples/neopattern/NeoPattern.cpp
Normal 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
|
||||||
187
examples/neopattern/NeoPatternService.cpp
Normal file
187
examples/neopattern/NeoPatternService.cpp
Normal 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();
|
||||||
|
}
|
||||||
34
examples/neopattern/NeoPatternService.h
Normal file
34
examples/neopattern/NeoPatternService.h
Normal 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;
|
||||||
|
};
|
||||||
31
examples/neopattern/config.h
Normal file
31
examples/neopattern/config.h
Normal 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
|
||||||
71
examples/neopattern/main.cpp
Normal file
71
examples/neopattern/main.cpp
Normal 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();
|
||||||
|
}
|
||||||
291
examples/neopixel/NeoPixelService.cpp
Normal file
291
examples/neopixel/NeoPixelService.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
examples/neopixel/NeoPixelService.h
Normal file
67
examples/neopixel/NeoPixelService.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -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,322 +26,6 @@
|
|||||||
#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"},
|
||||||
@@ -349,18 +36,32 @@ 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);
|
||||||
|
|
||||||
|
// Setup WiFi first
|
||||||
network.setupWiFi();
|
network.setupWiFi();
|
||||||
|
|
||||||
|
// Initialize and start all tasks
|
||||||
taskManager.initialize();
|
taskManager.initialize();
|
||||||
|
|
||||||
|
// Register services and start API server
|
||||||
|
apiServer.addService(nodeService);
|
||||||
|
apiServer.addService(networkService);
|
||||||
|
apiServer.addService(clusterService);
|
||||||
|
apiServer.addService(taskService);
|
||||||
|
apiServer.addService(neoPixelService);
|
||||||
apiServer.begin();
|
apiServer.begin();
|
||||||
neoService.registerApi(apiServer);
|
|
||||||
|
|
||||||
|
// Print initial task status
|
||||||
taskManager.printTaskStatus();
|
taskManager.printTaskStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
examples/relay/RelayService.cpp
Normal file
89
examples/relay/RelayService.cpp
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
25
examples/relay/RelayService.h
Normal file
25
examples/relay/RelayService.h
Normal 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);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
18
include/ApiTypes.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
16
include/services/ClusterService.h
Normal file
16
include/services/ClusterService.h
Normal 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);
|
||||||
|
};
|
||||||
22
include/services/NetworkService.h
Normal file
22
include/services/NetworkService.h
Normal 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);
|
||||||
|
};
|
||||||
21
include/services/NodeService.h
Normal file
21
include/services/NodeService.h
Normal 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);
|
||||||
|
};
|
||||||
9
include/services/Service.h
Normal file
9
include/services/Service.h
Normal 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;
|
||||||
|
};
|
||||||
17
include/services/TaskService.h
Normal file
17
include/services/TaskService.h
Normal 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);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
42
src/services/ClusterService.cpp
Normal file
42
src/services/ClusterService.cpp
Normal 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);
|
||||||
|
}
|
||||||
132
src/services/NetworkService.cpp
Normal file
132
src/services/NetworkService.cpp
Normal 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);
|
||||||
|
}
|
||||||
157
src/services/NodeService.cpp
Normal file
157
src/services/NodeService.cpp
Normal 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);
|
||||||
|
}
|
||||||
137
src/services/TaskService.cpp
Normal file
137
src/services/TaskService.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user