From 12caeb0be6a7c728372069212d4dc1ca2bd9f72d Mon Sep 17 00:00:00 2001 From: master Date: Sat, 13 Sep 2025 13:45:24 +0200 Subject: [PATCH] feat: services (#2) --- .cursor/rules/cpp.mdc | 68 ++ README.md | 6 + api/openapi.yaml | 843 ++++++++++++++++++++++ ctl.sh | 7 +- docs/API.md | 301 ++++++++ examples/base/main.cpp | 18 +- examples/neopattern/NeoPattern.cpp | 470 ++++++++++++ examples/neopattern/NeoPatternService.cpp | 187 +++++ examples/neopattern/NeoPatternService.h | 34 + examples/neopattern/config.h | 31 + examples/neopattern/main.cpp | 71 ++ examples/neopixel/NeoPixelService.cpp | 291 ++++++++ examples/neopixel/NeoPixelService.h | 67 ++ examples/neopixel/main.cpp | 369 +--------- examples/relay/RelayService.cpp | 89 +++ examples/relay/RelayService.h | 25 + examples/relay/main.cpp | 107 +-- include/ApiServer.h | 35 +- include/ApiTypes.h | 18 + include/NetworkManager.h | 37 + include/NodeContext.h | 2 + include/services/ClusterService.h | 16 + include/services/NetworkService.h | 22 + include/services/NodeService.h | 21 + include/services/Service.h | 9 + include/services/TaskService.h | 17 + platformio.ini | 39 + src/ApiServer.cpp | 349 +-------- src/NetworkManager.cpp | 61 ++ src/services/ClusterService.cpp | 42 ++ src/services/NetworkService.cpp | 132 ++++ src/services/NodeService.cpp | 157 ++++ src/services/TaskService.cpp | 137 ++++ 33 files changed, 3293 insertions(+), 785 deletions(-) create mode 100644 .cursor/rules/cpp.mdc create mode 100644 examples/neopattern/NeoPattern.cpp create mode 100644 examples/neopattern/NeoPatternService.cpp create mode 100644 examples/neopattern/NeoPatternService.h create mode 100644 examples/neopattern/config.h create mode 100644 examples/neopattern/main.cpp create mode 100644 examples/neopixel/NeoPixelService.cpp create mode 100644 examples/neopixel/NeoPixelService.h create mode 100644 examples/relay/RelayService.cpp create mode 100644 examples/relay/RelayService.h create mode 100644 include/ApiTypes.h create mode 100644 include/services/ClusterService.h create mode 100644 include/services/NetworkService.h create mode 100644 include/services/NodeService.h create mode 100644 include/services/Service.h create mode 100644 include/services/TaskService.h create mode 100644 src/services/ClusterService.cpp create mode 100644 src/services/NetworkService.cpp create mode 100644 src/services/NodeService.cpp create mode 100644 src/services/TaskService.cpp diff --git a/.cursor/rules/cpp.mdc b/.cursor/rules/cpp.mdc new file mode 100644 index 0000000..a480c7e --- /dev/null +++ b/.cursor/rules/cpp.mdc @@ -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 `` (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. diff --git a/README.md b/README.md index 5428fdc..97d3f1c 100644 --- a/README.md +++ b/README.md @@ -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 - **Over-The-Air Updates**: Seamless firmware updates across the cluster - **REST API**: HTTP-based cluster management and monitoring +- **Capability Discovery**: Automatic API endpoint and service capability detection ## Supported Hardware @@ -66,11 +67,15 @@ The system provides a comprehensive RESTful API for monitoring and controlling t | Endpoint | Method | Description | |----------|--------|-------------| | `/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/node/update` | POST | OTA firmware updates | | `/api/node/restart` | POST | System restart | | `/api/tasks/status` | GET | Task management and monitoring | | `/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. @@ -122,6 +127,7 @@ The project uses PlatformIO with Arduino framework and supports multiple ESP8266 - No persistent storage for configuration - Task monitoring and system health metrics - Task execution history and performance analytics not yet implemented +- No authentication or security features implemented ## Troubleshooting diff --git a/api/openapi.yaml b/api/openapi.yaml index ef3e105..72459eb 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -212,6 +212,9 @@ paths: sdkVersion: "3.1.2" cpuFreqMHz: 80 flashChipSize: 1048576 + labels: + location: "kitchen" + type: "sensor" api: - uri: "/api/node/status" method: 1 @@ -220,6 +223,41 @@ paths: - uri: "/api/tasks/control" 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: get: summary: Get cluster membership information @@ -325,6 +363,425 @@ paths: value: 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: schemas: TaskStatusResponse: @@ -501,11 +958,393 @@ components: type: integer description: Flash chip size in bytes example: 1048576 + labels: + type: object + description: Node labels and metadata + additionalProperties: + type: string + example: + location: "kitchen" + type: "sensor" api: type: array items: $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: type: object required: @@ -663,6 +1502,10 @@ tags: description: Multi-node cluster coordination and health monitoring - name: System Management 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: description: SPORE Project Documentation diff --git a/ctl.sh b/ctl.sh index 992ffb9..bf851a2 100755 --- a/ctl.sh +++ b/ctl.sh @@ -14,8 +14,7 @@ function build { pio run -e $1 } function all { - target esp01_1m - target d1_mini + pio run } ${@:-all} } @@ -51,4 +50,8 @@ function cluster { ${@:-info} } +function monitor { + pio run --target monitor +} + ${@:-info} diff --git a/docs/API.md b/docs/API.md index 28fa33c..415c011 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,10 +16,33 @@ The SPORE system provides a comprehensive RESTful API for monitoring and control | Endpoint | Method | Description | Response | |----------|--------|-------------|----------| | `/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/node/update` | POST | Handle firmware updates via OTA | Update progress and status | | `/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 ### Task Management @@ -125,8 +148,67 @@ Returns comprehensive system resource information including memory usage, chip d - `sdkVersion`: ESP8266 SDK version - `cpuFreqMHz`: CPU frequency in MHz - `flashChipSize`: Flash chip size in bytes +- `labels`: Node labels and metadata (if available) - `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 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. +### 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 | Code | Description | Use Case | diff --git a/examples/base/main.cpp b/examples/base/main.cpp index 942c4d3..518e384 100644 --- a/examples/base/main.cpp +++ b/examples/base/main.cpp @@ -7,6 +7,12 @@ #include "ApiServer.h" #include "TaskManager.h" +// Services +#include "services/NodeService.h" +#include "services/NetworkService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" + using namespace std; NodeContext ctx({ @@ -18,6 +24,12 @@ 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); + void setup() { Serial.begin(115200); @@ -27,7 +39,11 @@ void setup() { // Initialize and start all tasks 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(); // Print initial task status diff --git a/examples/neopattern/NeoPattern.cpp b/examples/neopattern/NeoPattern.cpp new file mode 100644 index 0000000..fc090b2 --- /dev/null +++ b/examples/neopattern/NeoPattern.cpp @@ -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 + +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 \ No newline at end of file diff --git a/examples/neopattern/NeoPatternService.cpp b/examples/neopattern/NeoPatternService.cpp new file mode 100644 index 0000000..0862453 --- /dev/null +++ b/examples/neopattern/NeoPatternService.cpp @@ -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{}); + + api.addEndpoint("/api/neopattern/patterns", HTTP_GET, + [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/neopattern", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + 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(); + 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 NeoPatternService::patternNamesVector() { + if (patternSetters.empty()) registerPatterns(); + std::vector 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(); +} diff --git a/examples/neopattern/NeoPatternService.h b/examples/neopattern/NeoPatternService.h new file mode 100644 index 0000000..26d0226 --- /dev/null +++ b/examples/neopattern/NeoPatternService.h @@ -0,0 +1,34 @@ +#pragma once +#include "services/Service.h" +#include "TaskManager.h" +#include "NeoPattern.cpp" +#include +#include + +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 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> patternSetters; +}; diff --git a/examples/neopattern/config.h b/examples/neopattern/config.h new file mode 100644 index 0000000..80ebd5c --- /dev/null +++ b/examples/neopattern/config.h @@ -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 \ No newline at end of file diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp new file mode 100644 index 0000000..3171d83 --- /dev/null +++ b/examples/neopattern/main.cpp @@ -0,0 +1,71 @@ +#include +#include +#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(); +} \ No newline at end of file diff --git a/examples/neopixel/NeoPixelService.cpp b/examples/neopixel/NeoPixelService.cpp new file mode 100644 index 0000000..a66fb1e --- /dev/null +++ b/examples/neopixel/NeoPixelService.cpp @@ -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{}); + + api.addEndpoint("/api/neopixel/patterns", HTTP_GET, + [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/neopixel", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + 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(); + 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 NeoPixelService::patternNamesVector() const { + std::vector 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; + } +} diff --git a/examples/neopixel/NeoPixelService.h b/examples/neopixel/NeoPixelService.h new file mode 100644 index 0000000..b0aee58 --- /dev/null +++ b/examples/neopixel/NeoPixelService.h @@ -0,0 +1,67 @@ +#pragma once +#include "services/Service.h" +#include "TaskManager.h" +#include +#include +#include + +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 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> 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; +}; diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index 064d721..78dc4ec 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -1,9 +1,5 @@ #include #include -#include -#include -#include - #include "Globals.h" #include "NodeContext.h" #include "NetworkManager.h" @@ -11,6 +7,13 @@ #include "ApiServer.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 #define NEOPIXEL_PIN 2 #endif @@ -23,348 +26,46 @@ #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) #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(); - 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{ 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 patternNamesVector() const { - std::vector 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> 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({ - {"app", "neopixel"}, - {"device", "light"}, - {"pixels", String(NEOPIXEL_COUNT)}, - {"pin", String(NEOPIXEL_PIN)} + {"app", "neopixel"}, + {"device", "light"}, + {"pixels", String(NEOPIXEL_COUNT)}, + {"pin", String(NEOPIXEL_PIN)} }); NetworkManager network(ctx); TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); 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() { - Serial.begin(115200); + Serial.begin(115200); - network.setupWiFi(); + // Setup WiFi first + network.setupWiFi(); - taskManager.initialize(); + // Initialize and start all tasks + taskManager.initialize(); - apiServer.begin(); - neoService.registerApi(apiServer); + // Register services and start API server + apiServer.addService(nodeService); + apiServer.addService(networkService); + apiServer.addService(clusterService); + apiServer.addService(taskService); + apiServer.addService(neoPixelService); + apiServer.begin(); - taskManager.printTaskStatus(); + // Print initial task status + taskManager.printTaskStatus(); } void loop() { - taskManager.execute(); - yield(); -} + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/examples/relay/RelayService.cpp b/examples/relay/RelayService.cpp new file mode 100644 index 0000000..9d0200a --- /dev/null +++ b/examples/relay/RelayService.cpp @@ -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{}); + + api.addEndpoint("/api/relay", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + 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"); + }); +} diff --git a/examples/relay/RelayService.h b/examples/relay/RelayService.h new file mode 100644 index 0000000..03913b3 --- /dev/null +++ b/examples/relay/RelayService.h @@ -0,0 +1,25 @@ +#pragma once +#include "services/Service.h" +#include "TaskManager.h" +#include + +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); +}; diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 29e15b3..b342ec1 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -7,6 +7,13 @@ #include "ApiServer.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; // 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 #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{ 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({ {"app", "relay"}, {"device", "actuator"}, @@ -105,7 +30,13 @@ NetworkManager network(ctx); TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); 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() { Serial.begin(115200); @@ -116,9 +47,13 @@ void setup() { // Initialize and start all tasks 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(); - relayService.registerApi(apiServer); // Print initial task status taskManager.printTaskStatus(); @@ -127,4 +62,4 @@ void setup() { void loop() { taskManager.execute(); yield(); -} +} \ No newline at end of file diff --git a/include/ApiServer.h b/include/ApiServer.h index 4856c39..2b8fd59 100644 --- a/include/ApiServer.h +++ b/include/ApiServer.h @@ -10,52 +10,33 @@ #include "NodeContext.h" #include "NodeInfo.h" #include "TaskManager.h" +#include "ApiTypes.h" + +class Service; // Forward declaration class ApiServer { public: ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80); void begin(); + void addService(Service& service); void addEndpoint(const String& uri, int method, std::function requestHandler); void addEndpoint(const String& uri, int method, std::function requestHandler, std::function 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 values; // optional allowed values - }; - struct EndpointCapability { - String uri; - int method; - std::vector params; - }; void addEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params); void addEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params); + + static const char* methodToStr(int method); + private: AsyncWebServer server; NodeContext& ctx; TaskManager& taskManager; + std::vector> services; std::vector> serviceRegistry; - std::vector capabilityRegistry; - void onClusterMembersRequest(AsyncWebServerRequest *request); - void methodToStr(const std::tuple &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 void registerServiceForLocalNode(const String& uri, int method); diff --git a/include/ApiTypes.h b/include/ApiTypes.h new file mode 100644 index 0000000..34a333d --- /dev/null +++ b/include/ApiTypes.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +struct ParamSpec { + String name; + bool required; + String location; // "query" | "body" | "path" | "header" + String type; // e.g. "string", "number", "boolean" + std::vector values; // optional allowed values + String defaultValue; // optional default value (stringified) +}; + +struct EndpointCapability { + String uri; + int method; + std::vector params; +}; diff --git a/include/NetworkManager.h b/include/NetworkManager.h index 9a98672..38a1d4d 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -1,12 +1,49 @@ #pragma once #include "NodeContext.h" #include +#include + +struct AccessPoint { + String ssid; + int32_t rssi; + uint8_t encryptionType; + uint8_t* bssid; + int32_t channel; + bool isHidden; +}; class NetworkManager { public: NetworkManager(NodeContext& ctx); void setupWiFi(); void setHostnameFromMac(); + + // WiFi scanning methods + void scanWifi(); + void processAccessPoints(); + std::vector 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: NodeContext& ctx; + std::vector accessPoints; + bool isScanning = false; }; diff --git a/include/NodeContext.h b/include/NodeContext.h index 76576de..e4c3348 100644 --- a/include/NodeContext.h +++ b/include/NodeContext.h @@ -7,6 +7,7 @@ #include #include #include "Config.h" +#include "ApiTypes.h" class NodeContext { public: @@ -19,6 +20,7 @@ public: NodeInfo self; std::map* memberList; Config config; + std::vector capabilities; using EventCallback = std::function; std::map> eventRegistry; diff --git a/include/services/ClusterService.h b/include/services/ClusterService.h new file mode 100644 index 0000000..41c0145 --- /dev/null +++ b/include/services/ClusterService.h @@ -0,0 +1,16 @@ +#pragma once +#include "services/Service.h" +#include "NodeContext.h" +#include + +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); +}; diff --git a/include/services/NetworkService.h b/include/services/NetworkService.h new file mode 100644 index 0000000..fa4aef8 --- /dev/null +++ b/include/services/NetworkService.h @@ -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); +}; diff --git a/include/services/NodeService.h b/include/services/NodeService.h new file mode 100644 index 0000000..c113fc2 --- /dev/null +++ b/include/services/NodeService.h @@ -0,0 +1,21 @@ +#pragma once +#include "services/Service.h" +#include "NodeContext.h" +#include +#include + +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); +}; diff --git a/include/services/Service.h b/include/services/Service.h new file mode 100644 index 0000000..f78d598 --- /dev/null +++ b/include/services/Service.h @@ -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; +}; diff --git a/include/services/TaskService.h b/include/services/TaskService.h new file mode 100644 index 0000000..9e3791c --- /dev/null +++ b/include/services/TaskService.h @@ -0,0 +1,17 @@ +#pragma once +#include "services/Service.h" +#include "TaskManager.h" +#include + +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); +}; diff --git a/platformio.ini b/platformio.ini index 14449c3..5266d1b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,6 +32,7 @@ build_src_filter = + + + + + [env:d1_mini] platform = platformio/espressif8266@^4.2.1 @@ -46,6 +47,7 @@ build_src_filter = + + + + + [env:esp01_1m_relay] platform = platformio/espressif8266@^4.2.1 @@ -61,6 +63,7 @@ build_src_filter = + + + + + [env:esp01_1m_neopixel] platform = platformio/espressif8266@^4.2.1 @@ -77,6 +80,7 @@ build_src_filter = + + + + + [env:d1_mini_neopixel] platform = platformio/espressif8266@^4.2.1 @@ -92,3 +96,38 @@ build_src_filter = + + + + + + +[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 = + + + + + + + + + +[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 = + + + + + + + + diff --git a/src/ApiServer.cpp b/src/ApiServer.cpp index 60acb94..fb3c0ee 100644 --- a/src/ApiServer.cpp +++ b/src/ApiServer.cpp @@ -1,8 +1,8 @@ #include "ApiServer.h" +#include "services/Service.h" #include -// Shared helper for HTTP method to string -static const char* methodStrFromInt(int method) { +const char* ApiServer::methodToStr(int method) { switch (method) { case HTTP_GET: return "GET"; case HTTP_POST: return "POST"; @@ -39,7 +39,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params) { - capabilityRegistry.push_back(EndpointCapability{uri, method, params}); + ctx.capabilities.push_back(EndpointCapability{uri, method, params}); registerServiceForLocalNode(uri, method); server.on(uri.c_str(), method, requestHandler); } @@ -47,343 +47,22 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params) { - capabilityRegistry.push_back(EndpointCapability{uri, method, params}); + ctx.capabilities.push_back(EndpointCapability{uri, method, params}); registerServiceForLocalNode(uri, method); server.on(uri.c_str(), method, requestHandler, uploadHandler); } -void ApiServer::begin() { - addEndpoint("/api/node/status", HTTP_GET, - std::bind(&ApiServer::onSystemStatusRequest, this, std::placeholders::_1)); - addEndpoint("/api/cluster/members", HTTP_GET, - std::bind(&ApiServer::onClusterMembersRequest, this, std::placeholders::_1)); - 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)); +void ApiServer::addService(Service& service) { + services.push_back(service); + Serial.printf("[API] Added service: %s\n", service.getName()); +} - // 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{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")}} - } - ); +void ApiServer::begin() { + // Register all service endpoints + for (auto& service : services) { + service.get().registerEndpoints(*this); + Serial.printf("[API] Registered endpoints for service: %s\n", service.get().getName()); + } 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(); - for (const auto& entry : serviceRegistry) { - JsonObject apiObj = apiArr.add(); - 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(); - 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(); - 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(); - for (const auto& pair : *ctx.memberList) { - const NodeInfo& node = pair.second; - JsonObject obj = arr.add(); - 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(); - for (const auto& endpoint : node.apiEndpoints) { - JsonObject apiObj = apiArr.add(); - apiObj["uri"] = std::get<0>(endpoint); - methodToStr(endpoint, apiObj); - } - // Add labels if present - if (!node.labels.empty()) { - JsonObject labelsObj = obj["labels"].to(); - 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 &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(); - 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(); - for (const auto& taskPair : taskStatuses) { - JsonObject taskObj = tasksArr.add(); - 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(); - 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(); - - // Track seen (uri|method) to avoid duplicates - std::vector 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(); - obj["uri"] = cap.uri; - obj["method"] = methodStrFromInt(cap.method); - if (!cap.params.empty()) { - JsonArray paramsArr = obj["params"].to(); - for (const auto& ps : cap.params) { - JsonObject p = paramsArr.add(); - 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(); - 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(); - obj["uri"] = uri; - obj["method"] = methodStrFromInt(method); - } - } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); -} \ No newline at end of file diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index cae4ca0..c06cb30 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -2,8 +2,69 @@ // 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 NetworkManager::getAccessPoints() const { + return accessPoints; +} + 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() { uint8_t mac[6]; WiFi.macAddress(mac); diff --git a/src/services/ClusterService.cpp b/src/services/ClusterService.cpp new file mode 100644 index 0000000..7b88d20 --- /dev/null +++ b/src/services/ClusterService.cpp @@ -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{}); +} + +void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + JsonArray arr = doc["members"].to(); + + for (const auto& pair : *ctx.memberList) { + const NodeInfo& node = pair.second; + JsonObject obj = arr.add(); + 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(); + for (const auto& kv : node.labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} diff --git a/src/services/NetworkService.cpp b/src/services/NetworkService.cpp new file mode 100644 index 0000000..237b0a4 --- /dev/null +++ b/src/services/NetworkService.cpp @@ -0,0 +1,132 @@ +#include "services/NetworkService.h" +#include + +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{}); + + api.addEndpoint("/api/network/wifi/scan", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); }, + std::vector{}); + + // Network status and configuration endpoints + api.addEndpoint("/api/network/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleNetworkStatus(request); }, + std::vector{}); + + api.addEndpoint("/api/network/wifi/config", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); }, + std::vector{ + 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(); + + for (const auto& ap : accessPoints) { + JsonObject apObj = doc["access_points"].add(); + 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); +} diff --git a/src/services/NodeService.cpp b/src/services/NodeService.cpp new file mode 100644 index 0000000..ce420ec --- /dev/null +++ b/src/services/NodeService.cpp @@ -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{}); + + // 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{String("firmware"), true, String("body"), String("file"), {}, String("")} + }); + + // Restart endpoint + api.addEndpoint("/api/node/restart", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleRestartRequest(request); }, + std::vector{}); + + // Capabilities endpoint + api.addEndpoint("/api/node/capabilities", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleCapabilitiesRequest(request); }, + std::vector{}); +} + +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(); + 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(); + 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(); + + // Add all registered capabilities + for (const auto& cap : ctx.capabilities) { + JsonObject obj = endpointsArr.add(); + obj["uri"] = cap.uri; + obj["method"] = ApiServer::methodToStr(cap.method); + if (!cap.params.empty()) { + JsonArray paramsArr = obj["params"].to(); + for (const auto& ps : cap.params) { + JsonObject p = paramsArr.add(); + 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(); + 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); +} diff --git a/src/services/TaskService.cpp b/src/services/TaskService.cpp new file mode 100644 index 0000000..52b616f --- /dev/null +++ b/src/services/TaskService.cpp @@ -0,0 +1,137 @@ +#include "services/TaskService.h" +#include "ApiServer.h" +#include + +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{}); + + api.addEndpoint("/api/tasks/control", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + 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(); + 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(); + for (const auto& taskPair : taskStatuses) { + JsonObject taskObj = tasksArr.add(); + 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(); + 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); + } +}