Initial sanitized SmartSpeaker snapshot
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# Project Rules for ESP32-S3 Audio Board Development
|
||||
|
||||
This workspace is for the **ESP32-S3 AI Smart Speaker Development Board**. All agents interacting with this project must follow these guidelines:
|
||||
|
||||
- **Target Hardware:** UeeKKoo (Waveshare) ESP32-S3-AUDIO-Board-EN (No Battery version).
|
||||
- **CPU:** Dual-core Xtensa 32-bit LX7 CPU, running at 240 MHz (ESP32-S3R8).
|
||||
- **Memory:** 16 MB Flash, 8 MB Octal PSRAM (0.01 GB RAM total).
|
||||
- **OS:** FreeRTOS.
|
||||
- **Connectivity:** Wi-Fi & Bluetooth.
|
||||
- **USB:** 1x USB port.
|
||||
- **Audio Codec Hardware:**
|
||||
- Playback (DAC): ES8311 low-power mono audio codec chip.
|
||||
- Recording (ADC): ES7210 audio encoder chip.
|
||||
- **I2S / I2C Pin Mapping:**
|
||||
- **I2S MCLK:** GPIO 12
|
||||
- **I2S BCLK:** GPIO 13
|
||||
- **I2S LRCK:** GPIO 14
|
||||
- **I2S DIN (Microphone):** GPIO 15
|
||||
- **I2S DOUT (Speaker):** GPIO 16
|
||||
- **I2C SDA:** GPIO 11
|
||||
- **I2C SCL:** GPIO 10
|
||||
- **WS2812 RGB LEDs:** GPIO 38
|
||||
- **Key Features & Peripherals:**
|
||||
- Support for AI Speech Interaction with Dual Digital Microphone Array.
|
||||
- Hardware/Software Noise Reduction and Echo Cancellation.
|
||||
- Support for External Displays (1.47", 2", 2.8", 3.5" LCDs) and Cameras (OV2640, OV5640).
|
||||
- 7-ring programmable RGB LEDs (WS2812B, GRB/RGB order, connected to GPIO 38).
|
||||
- PCF85063 RTC chip.
|
||||
|
||||
- **Development Environment & Toolchain Details:**
|
||||
- **ESP-IDF Version:** v6.0.1 (Target: `esp32s3`)
|
||||
- **ESP-IDF Directory:** `C:\esp\v6.0.1\esp-idf`
|
||||
- **ESP-IDF Tools Path:** `C:\Espressif\tools`
|
||||
- **Build Command:** `eim run "idf.py build"` or execute `build.bat`
|
||||
- **Flash Command:** `eim run "idf.py flash"` or execute `flash.bat`
|
||||
- **Serial Monitor Command:** `eim run "idf.py monitor"` or execute `monitor.bat`
|
||||
|
||||
- **Helper Scripts Folder (`/scripts`):**
|
||||
- **`build.bat` / `build.ps1`:** Triggers standard incremental project build.
|
||||
- **`flash.bat` / `flash.ps1`:** Automatically rebuilds and flashes project to `COM5` at 460800 baud.
|
||||
- **`monitor.bat` / `monitor.ps1`:** Launches target serial logging monitor on `COM5` (press `Ctrl + ]` to exit).
|
||||
- **`clean.bat` / `clean.ps1`:** Deletes the `/build` folder to enforce clean target configs.
|
||||
|
||||
- **Implemented Wi-Fi Functionality:**
|
||||
- **Credentials Storage:** Wi-Fi credentials are read from the `wifi-credentials` file in the project root. This file is parsed by CMake during the configure step, and dynamically generates `wifi_credentials.h` inside the build directory so credentials are never checked into Git.
|
||||
- **Station Connection:** Initializes Wi-Fi in Station mode and handles auto-reconnect on disconnect (up to 5 retries). Implemented in `wifi_connect.c` and started in `main.c`.
|
||||
|
||||
- **Implemented Audio Functionality:**
|
||||
- **Startup Sound:** Plays `startup.mp3` immediately on boot.
|
||||
- **Chime Assets:** The audio files in SPIFFS were generated using `agentctl tts`:
|
||||
- `startup.mp3`: *"Ding Dong, I'm turned on!"*
|
||||
- `connecting_wifi.mp3`: *"Connecting to the wifi"*
|
||||
- `connected_wifi.mp3`: *"I'm now connected to the wifi"*
|
||||
- `connected.mp3`: *"I'm connected!"*
|
||||
- `connect_fail.mp3`: *"I could not connect"*
|
||||
- Packaged into internal SPIFFS partition `model` on the device's flash.
|
||||
- **Playback Management:** Implemented `Audio_Play_Music_To_End` in `audio_driver.c` to perform blocking playback, preventing overlapping audio playback during initialization.
|
||||
- **Codec Init:** Initializes ES8311 DAC via I2C at sample rate 16000Hz, stereo (2 channels), 16-bit depth.
|
||||
- **PA Enable:** Drivers enable the speaker power amplifier (`PA_CTRL`) on pin 8 of the TCA9555 IO expander.
|
||||
- **Volume Adjust:** Set to `85` (0 to 100 range scale) via `Volume_Adjustment(85)` inside `main/main.c`.
|
||||
|
||||
- **Implemented LED Ring Functionality:**
|
||||
- **State-driven Display:** Background FreeRTOS task `led_rainbow_task` drives the 7 WS2812B LEDs connected to GPIO 38 based on the global connection state:
|
||||
- `LED_STATE_IDLE`: LEDs remain **off** during initial startup sounds.
|
||||
- `LED_STATE_CONNECTING`: LEDs **flash orange** (approx. 500ms intervals) during connection.
|
||||
- `LED_STATE_FAILED`: LEDs glow **solid red** at 20% brightness on failure.
|
||||
- `LED_STATE_CONNECTED`: LEDs run a **vibrant, moving color fade** using HSV-to-RGB translation rotating around the ring (speed set to approx. 2.25 seconds per cycle at 20% brightness).
|
||||
- **Dependency:** Linked and integrated using the `espressif/led_strip` (^3.0.1) component.
|
||||
|
||||
- **Build & Partition Configurations:**
|
||||
- **Partition Table:** Defined in `partitions.csv` with custom `model` (SPIFFS, 5900K size) and `flash_test` (FATFS, 528K size) partitions.
|
||||
- **SPIFFS Generation:** The `model` partition image is automatically built from folder `main/spiffs` and flashed using CMake macro `spiffs_create_partition_image(model spiffs FLASH_IN_PROJECT)` declared inside `main/CMakeLists.txt`.
|
||||
- **Flash/PSRAM config:** Configured for 16MB flash (`CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y`) and 8MB Octal SPIRAM (`CONFIG_SPIRAM=y` & `CONFIG_SPIRAM_MODE_OCT=y`) via pre-configured `sdkconfig` imported from Waveshare's demo.
|
||||
|
||||
- **Troubleshooting & Porting to ESP-IDF v6.x:**
|
||||
- **I2S Types:** ESP-IDF v6 removes the legacy I2S driver definitions. Any parameter defined as legacy `i2s_port_t` in BSP libraries (e.g., `bsp_board.c`) must be changed to `int`.
|
||||
- **Serial Monitoring:** The standard serial monitor `idf.py monitor` requires an attached interactive TTY. In sandboxed running environments, raw serial read scripts should be used instead.
|
||||
|
||||
- **Development Socket Server & Cloudflare Tunneling:**
|
||||
- **Dev Server Folder (`/dev-server`):** Node.js/TypeScript Express & WebSockets server listening on port `8901`.
|
||||
- **Auto-Reload:** Configured via `nodemon` and `ts-node` using `npm run dev` in `/dev-server`.
|
||||
- **Logging:** Console logs when clients connect, send messages, disconnect, or trigger errors.
|
||||
- **Cloudflare Tunnel Routing:** Local configuration `config.yml` is mapped to forward public traffic for `example.com` and `dashboard.example.com` directly to loopback `http://127.0.0.1:8901`.
|
||||
- **DNS Mappings:** Configure your public hostnames to point to your deployment tunnel or reverse proxy.
|
||||
- **Command Routing:** Monitor clients can broadcast `control_device` command payloads (volume level adjustments, voice text-to-speech output, LED strip animations, or chime prompts) targeting specific client IDs, which the dev-server routes to standard client device sockets.
|
||||
|
||||
- **Real-Time Monitor Dashboard Server (`/monitor-server`):**
|
||||
- **Service Port:** Port `8902` (running via Express). Starts with `npm start`.
|
||||
- **Layout & Structure:** Built with an enterprise-grade dark theme layout featuring a collapsible left sidebar, header operations cards, and responsive page views (`Overview`, `Device Fleet Manager`, and `Telemetry Terminal Console`).
|
||||
- **Dynamic Elements:** Includes dynamic search boxes, severity filter button groups, CSV/JSON log exporters, device diagnostic modals, and volume/LED controllers that dispatch control instructions directly to connected ESP32-S3 boards.
|
||||
|
||||
- **Hardware Model Variants (DEV-1 and PROTO-1):**
|
||||
- **Model Configuration:** Defined in the `device-config` file in the project root. Edit this file to switch the active hardware profile:
|
||||
```
|
||||
MODEL=DEV-1 # Standard Waveshare development board (default)
|
||||
MODEL=PROTO-1 # Prototype board with additional peripherals
|
||||
```
|
||||
- **How it works:** CMake reads `device-config` at configure-time, generates `device_config.h` in the build directory, and defines either `MODEL_DEV_1=1` or `MODEL_PROTO_1=1` as a compiler macro on the `${COMPONENT_LIB}` target.
|
||||
- **⚠️ After changing `device-config`:** Always run `scripts/clean.bat` followed by `scripts/build.bat` to force CMake to regenerate `device_config.h` with the new model value. An incremental build alone will NOT pick up the change.
|
||||
- **DEV-1 Variant:** Includes all core firmware functionalities (Wi-Fi, audio playback, WS2812B LEDs on GPIO 38, WebSocket telemetry).
|
||||
- **PROTO-1 Variant:** Inherits DEV-1 baseline and introduces:
|
||||
- **Audio I2C/I2S Interface:** Same as DEV-1 (GPIO 10–16, EXIO 8 PA_EN), but DIN/DOUT are physically swapped on the PROTO board.
|
||||
- **GPIO 9:** WS2812 RGB LED strip signal *(replaces GPIO 38 used on DEV-1)*
|
||||
- **GPIO 5:** Haptics motor — PWM output via LEDC driver (1kHz, 10-bit duty cycle)
|
||||
- **GPIO 4:** Ambient light sensor (LDR) — ADC1 Channel 3, analog oneshot read, scaled 0–4095 → 0–100%
|
||||
- **GPIO 7:** Capacitive touch sensor — digital input, active HIGH when touched
|
||||
- Background FreeRTOS task `proto1_sensor_task` polls GPIO 4 and GPIO 7 every 100ms and updates `device_state_t` fields: `ambient_light` (int) and `touch_active` (bool).
|
||||
- Haptic intensity is downlink-controllable via `set_haptics` WebSocket command (value 0–100).
|
||||
|
||||
- **Protocol-First Development Rules:**
|
||||
- **API Specification Priority:** When implementing any new client/device functionality or modifying messaging structures, you MUST always update `API.md` **FIRST** as the absolute source of truth before editing any application or driver code.
|
||||
- **Implementation Sequence:**
|
||||
1. Update/define the JSON schema, commands, and fields in `API.md`.
|
||||
2. Write the corresponding host (gateway/server) and client (firmware) code changes matching the specification.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# ESP-IDF Build & Config
|
||||
build/
|
||||
sdkconfig.old
|
||||
dependencies.lock
|
||||
managed_components/
|
||||
releases/*.bin
|
||||
|
||||
# Secrets and Generated Configuration Headers
|
||||
wifi-credentials
|
||||
main/wifi_credentials.h
|
||||
|
||||
# Node.js / dev-server
|
||||
dev-server/node_modules/
|
||||
monitor-server/node_modules/
|
||||
dev-server/dist/
|
||||
dev-server/package-lock.json
|
||||
|
||||
# Local IDE and System Files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
@@ -0,0 +1,318 @@
|
||||
# ESP32-S3 Audio Board - WebSocket API Specification
|
||||
|
||||
This document serves as the official protocol source of truth for developing the host server (Dev Gateway / Monitor Dashboard) and client (ESP32-S3 device) software.
|
||||
|
||||
All communications over the WebSocket connection use JSON text frames.
|
||||
|
||||
---
|
||||
|
||||
## 1. Downlink Control Commands (Server ➔ Device)
|
||||
|
||||
Commands are sent from the server to control the device's state. The message structure is:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "<command_name>",
|
||||
"value": <command_value>
|
||||
}
|
||||
```
|
||||
|
||||
### 1.1. Set Volume (`set_volume`)
|
||||
Sets the output gain level on the ES8311 mono DAC audio codec.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "set_volume",
|
||||
"value": 85
|
||||
}
|
||||
```
|
||||
* **Parameters:** `value` must be an integer between `0` (mute) and `100` (max amplification).
|
||||
|
||||
### 1.2. Query State (`query_state`)
|
||||
Requests the device to compile and send its complete state telemetry payload immediately.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "query_state"
|
||||
}
|
||||
```
|
||||
* **Parameters:** None. The device will reply immediately with a `state_report` payload (see Section 2).
|
||||
|
||||
### 1.3. Set LED Mode (`set_led`)
|
||||
Sets the active visual feedback preset mode on the WS2812B LED ring.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "set_led",
|
||||
"value": "rainbow"
|
||||
}
|
||||
```
|
||||
* **Parameters:** `value` must be a string: `"rainbow"` (moving rainbow), `"connecting"` (flashing orange), `"failed"` (solid red), `"idle"` (off), or `"updating"` (breathing blue - OTA flash).
|
||||
|
||||
### 1.4. Play Audio Track (`play_audio`)
|
||||
Triggers the ES8311 DAC codec to play an internal chime asset file from flash SPIFFS.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "play_audio",
|
||||
"value": "startup"
|
||||
}
|
||||
```
|
||||
* **Parameters:** `value` must be a string: `"startup"`, `"wifi_connecting"`, or `"wifi_connected"`.
|
||||
|
||||
### 1.5. Set LED Brightness (`set_brightness`)
|
||||
Sets the overall brightness coefficient for the WS2812B LED ring.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "set_brightness",
|
||||
"value": 0.25
|
||||
}
|
||||
```
|
||||
* **Parameters:** `value` must be a float between `0.0` (completely dark) and `1.0` (maximum intensity).
|
||||
|
||||
### 1.6. Set Haptic Vibration (`set_haptics`) [PROTO-1 Only]
|
||||
Sets the PWM vibration intensity on the GPIO 5 haptic motor.
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "set_haptics",
|
||||
"value": 50
|
||||
}
|
||||
```
|
||||
* **Parameters:** `value` must be an integer between `0` (off) and `100` (max vibration).
|
||||
|
||||
---
|
||||
|
||||
## 2. Uplink State Reporting (Device ➔ Server)
|
||||
|
||||
The device reports its complete operational state to the server.
|
||||
* **Trigger Conditions:**
|
||||
1. **Periodic Telemetry:** Broadcasted automatically every `5` seconds.
|
||||
2. **Event-Driven:** Broadcasted immediately when a state changes (e.g. volume adjustment, LED mode transition, or a hardware button press).
|
||||
3. **On-Demand:** Sent in direct response to a `query_state` command.
|
||||
|
||||
* **State Report Payload:**
|
||||
```json
|
||||
{
|
||||
"type": "state_report",
|
||||
"deviceId": "ESP32-S3-A2D4",
|
||||
"timestamp": "2026-06-28T05:12:00Z",
|
||||
"system": {
|
||||
"mac_address": "A0:F2:62:E3:5C:F0",
|
||||
"model": "DEV-1",
|
||||
"firmware_version": "v1.0.0",
|
||||
"idf_version": "v6.0.1",
|
||||
"cpu_freq_mhz": 240,
|
||||
"flash_size_mb": 16,
|
||||
"psram_size_mb": 8
|
||||
},
|
||||
"health": {
|
||||
"uptime_seconds": 1205,
|
||||
"free_heap_bytes": 3842420,
|
||||
"min_free_heap_bytes": 3720100,
|
||||
"reset_reason": "POWERON_RESET"
|
||||
},
|
||||
"network": {
|
||||
"wifi_connected": true,
|
||||
"wifi_ssid": "ExampleWiFi",
|
||||
"wifi_rssi": -47,
|
||||
"ip_address": "192.0.2.10",
|
||||
"websocket_connected": true
|
||||
},
|
||||
"peripherals": {
|
||||
"audio": {
|
||||
"volume": 85,
|
||||
"player_state": "idle",
|
||||
"current_track": "none"
|
||||
},
|
||||
"led_ring": {
|
||||
"state_id": 5,
|
||||
"state_name": "rainbow",
|
||||
"brightness_coef": 0.2
|
||||
},
|
||||
"buttons": {
|
||||
"btn_play": false,
|
||||
"btn_mute": false,
|
||||
"btn_vol_up": false,
|
||||
"btn_vol_down": false
|
||||
},
|
||||
"haptic_intensity": 50,
|
||||
"ambient_light": 2048,
|
||||
"touch_active": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1. Payload Field Definitions
|
||||
|
||||
#### System Info (`system`)
|
||||
* `mac_address` (string): Globally unique MAC address of the device network interface.
|
||||
* `model` (string): Physical hardware configuration model identifier ("DEV-1" or "PROTO-1").
|
||||
* `firmware_version` (string): Compiled application version.
|
||||
* `idf_version` (string): ESP-IDF framework version.
|
||||
* `cpu_freq_mhz` (integer): Dual-core clock speed in MHz (typically `240`).
|
||||
* `flash_size_mb` / `psram_size_mb` (integer): Total sizes of physical flash and PSRAM chips.
|
||||
|
||||
#### Health (`health`)
|
||||
* `uptime_seconds` (integer): Elapsed seconds since device boot.
|
||||
* `free_heap_bytes` (integer): Available dynamic DRAM size.
|
||||
* `min_free_heap_bytes` (integer): Lowest free memory watermark (critical for memory leak analysis).
|
||||
* `reset_reason` (string): Hardware restart source code (e.g. `POWERON_RESET`, `SW_RESET`).
|
||||
|
||||
#### Network (`network`)
|
||||
* `wifi_connected` (boolean): `true` if connected to Wi-Fi.
|
||||
* `wifi_ssid` (string): Connected Wi-Fi AP name.
|
||||
* `wifi_rssi` (integer): Wi-Fi Signal Strength in dBm (e.g., `-50` is strong, `-85` is weak).
|
||||
* `ip_address` (string): Device local IP.
|
||||
* `websocket_connected` (boolean): `true` if active control socket link is connected.
|
||||
|
||||
#### Peripherals (`peripherals`)
|
||||
* `audio` (object): DAC output status:
|
||||
* `volume` (integer): Gain scaling (`0` to `100`).
|
||||
* `player_state` (string): Current player thread status (`"idle"`, `"playing"`, `"paused"`, `"stopped"`).
|
||||
* `current_track` (string): Active file URI (e.g. `file://spiffs/startup.mp3`) or `"none"`.
|
||||
* `led_ring` (object): WS2812B visual task status:
|
||||
* `state_id` (integer): Numeric preset state enum value:
|
||||
* `0`: `LED_STATE_OFF` (Off)
|
||||
* `1`: `LED_STATE_WIFI_CONNECTING` (Flashing Red)
|
||||
* `2`: `LED_STATE_WIFI_FAILED` (Solid Red)
|
||||
* `3`: `LED_STATE_SERVER_CONNECTING` (Flashing Orange)
|
||||
* `4`: `LED_STATE_SERVER_FAILED` (Solid Orange)
|
||||
* `5`: `LED_STATE_CONNECTED` (Rainbow Rotate)
|
||||
* `6`: `LED_STATE_UPDATING` (Pulse Blue slowly - OTA flashing)
|
||||
* `state_name` (string): Visual pattern tag name (`"rainbow"`, `"connecting"`, `"failed"`, `"idle"`, `"updating"`).
|
||||
* `brightness_coef` (float): Active brightness coefficient between `0.0` and `1.0`.
|
||||
* `buttons` (object): Realtime physical state of the TCA9555 IO expander button pins (`true` if key is pressed down).
|
||||
* `haptic_intensity` (integer) [PROTO-1 Only]: Active haptic vibration level (`0` to `100`).
|
||||
* `ambient_light` (integer) [PROTO-1 Only]: Measured ambient light intensity (`0` to `4095`).
|
||||
* `touch_active` (boolean) [PROTO-1 Only]: `true` if capacitive/digital touch sensor is active.
|
||||
|
||||
---
|
||||
|
||||
## 3. Monitor Dashboard Commands (Dashboard ➔ Server)
|
||||
|
||||
Commands sent by the Monitor Dashboard client to the Dev Gateway server to manage connected device sessions. These differ from Section 1 commands in that they are **server-side operations** — the server acts on them directly rather than forwarding them to a device.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "<command_type>",
|
||||
"targetId": "<client_id>"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.1. Boot Device (`boot_device`)
|
||||
Forces a connected device to disconnect by closing its WebSocket connection server-side. Used to simulate a disconnect event, trigger device reconnection logic, or evict a misbehaving client.
|
||||
|
||||
* **Sender:** Monitor Dashboard
|
||||
* **Receiver:** Dev Gateway Server (not forwarded to device)
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"type": "boot_device",
|
||||
"targetId": "esp32-a2d4f1"
|
||||
}
|
||||
```
|
||||
* **Parameters:** `targetId` (string) — the unique client session ID of the device to disconnect.
|
||||
* **Server Behaviour:** The server locates the WebSocket for `targetId`, sends a close frame (`1000 Normal Closure`), logs a `boot` event, and broadcasts an updated `status_update` to all monitors.
|
||||
* **Device Behaviour:** The device's WebSocket client receives the close event and triggers its reconnect logic (up to 5 retries with backoff).
|
||||
|
||||
### 3.2. Control Device (`control_device`)
|
||||
Routes a downlink command (from Section 1) from the Monitor Dashboard through the server to a specific target device.
|
||||
|
||||
* **Sender:** Monitor Dashboard
|
||||
* **Receiver:** Dev Gateway Server → forwarded to target device
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"type": "control_device",
|
||||
"targetId": "esp32-a2d4f1",
|
||||
"payload": {
|
||||
"command": "set_volume",
|
||||
"value": 70
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Parameters:** `targetId` (string), `payload` (object) — any valid Section 1 command object.
|
||||
|
||||
### 3.3. Trigger OTA Update (`trigger_ota`)
|
||||
Instructs the server to push a firmware binary (previously uploaded via HTTP POST `/ota/upload`) to a target device and begin the OTA flash process.
|
||||
|
||||
* **Sender:** Monitor Dashboard
|
||||
* **Receiver:** Dev Gateway Server → forwards `start_ota` downlink to target device
|
||||
* **Payload:**
|
||||
```json
|
||||
{
|
||||
"type": "trigger_ota",
|
||||
"targetId": "esp32-a2d4f1",
|
||||
"version": "latest"
|
||||
}
|
||||
```
|
||||
* **Parameters:**
|
||||
* `targetId` (string) — the ID of the target ESP32 device.
|
||||
* `version` (string, optional) — select which firmware version to deploy:
|
||||
* `"latest"` (default): Deploys the latest catalog release built by `release.ps1`.
|
||||
* `"<semver>"` (e.g. `"v0.0.32-4471303"`): Deploys a specific catalog release version.
|
||||
* `"custom"` (or omitted): Deploys the manually uploaded binary from `/ota/firmware.bin`.
|
||||
* **Server Behaviour:** Resolves target URL (either `/releases/firmware-vX.Y.Z.bin` or `/ota/firmware.bin`), sends a `start_ota` command (Section 4.1) to target device. Updates `metadata.otaStatus` to `"pending"` and broadcasts to monitors.
|
||||
|
||||
---
|
||||
|
||||
## 4. OTA Firmware Update Protocol
|
||||
|
||||
### 4.1. Downlink: Start OTA (`start_ota`)
|
||||
Server instructs the device to begin an OTA firmware download and flash.
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "start_ota",
|
||||
"url": "https://example.com/releases/firmware-v0.0.37-proto1.bin",
|
||||
"cert_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
}
|
||||
```
|
||||
|
||||
* **Parameters:**
|
||||
* `url` (string) — HTTP/HTTPS URL from which the device should fetch the firmware binary.
|
||||
* `cert_pem` (string, optional) — PEM-formatted CA certificate used to verify the HTTPS server. If omitted, the device falls back to validating the HTTPS server certificate using the built-in ESP x509 Certificate Bundle.
|
||||
* **Device Behaviour:** Spawns an OTA task. Downloads the binary in chunks. Validates the image. Writes to the inactive OTA partition. On success, commits the new partition and reboots. On failure, rolls back and reports an error.
|
||||
|
||||
### 4.2. Uplink: OTA Progress (`ota_progress`)
|
||||
Device reports download/flash progress.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ota_progress",
|
||||
"percent": 45,
|
||||
"bytes_written": 204800,
|
||||
"bytes_total": 450000
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3. Uplink: OTA Complete (`ota_complete`)
|
||||
Device reports successful flash before rebooting.
|
||||
|
||||
```json
|
||||
{ "type": "ota_complete" }
|
||||
```
|
||||
|
||||
* Device reboots ~500ms after sending this message to apply the new firmware.
|
||||
|
||||
### 4.4. Uplink: OTA Error (`ota_error`)
|
||||
Device reports a failure during download or flash.
|
||||
|
||||
```json
|
||||
{ "type": "ota_error", "reason": "HTTP connection failed: -1" }
|
||||
```
|
||||
|
||||
* Device continues running the existing firmware after an OTA error. No rollback is needed.
|
||||
|
||||
### 4.5. Server HTTP Endpoints (OTA)
|
||||
|
||||
| Method | Path | Description |
|
||||
|:---|:---|:---|
|
||||
| `POST` | `/ota/upload` | Upload a `.bin` firmware file (multipart/form-data, field: `firmware`) |
|
||||
| `GET` | `/ota/firmware.bin` | Serve the most recently uploaded firmware binary |
|
||||
| `GET` | `/ota/status` | Return current OTA metadata: filename, size, upload time |
|
||||
| `GET` | `/ota/catalog` | Return the full catalog JSON (`firmware-catalog.json`) generated by `release.ps1` |
|
||||
| `GET` | `/releases/*` | Serve versioned release binaries (e.g. `/releases/firmware-vX.Y.Z.bin`) |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(esp32s3_audio)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Hardware Reference: ESP32-S3-AUDIO-Board-EN
|
||||
|
||||
This document contains the hardware specifications, I2C device addresses, pinout mappings, and auxiliary information for the **UeeKKoo (Waveshare) ESP32-S3-AUDIO-Board-EN** (No Battery version).
|
||||
|
||||
---
|
||||
|
||||
## 1. System Specifications
|
||||
|
||||
* **Microcontroller:** Espressif ESP32-S3R8
|
||||
* **CPU:** Dual-core Xtensa 32-bit LX7 CPU, running at 240 MHz.
|
||||
* **ROM:** 384 KB
|
||||
* **SRAM:** 512 KB
|
||||
* **External Storage & Memory:**
|
||||
* **Flash:** 16 MB external NOR Flash.
|
||||
* **PSRAM:** 8 MB stacked PSRAM (connected via Octal SPI).
|
||||
* **Connectivity:**
|
||||
* **Wi-Fi:** 2.4 GHz (802.11 b/g/n).
|
||||
* **Bluetooth:** Bluetooth 5.0 (LE).
|
||||
* **Operating System:** FreeRTOS (standard in ESP-IDF).
|
||||
* **Power Input:** USB Type-C (5V) & onboard lithium battery charging/discharging management.
|
||||
* **USB Ports:** 1x Type-C port (USB-to-UART / Native USB).
|
||||
|
||||
---
|
||||
|
||||
## 2. Onboard Chips & I2C Addresses
|
||||
|
||||
The board utilizes a shared I2C bus on **SDA: GPIO 11** and **SCL: GPIO 10**.
|
||||
|
||||
| Peripheral / Chip | Model | I2C Address (7-bit) | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **GPIO Expander** | TCA9555 | `0x20` | Extends GPIO pins for keys, controls, and status lines. |
|
||||
| **Audio DAC (Playback)** | ES8311 | `0x18` | Low-power mono audio codec chip driving the speaker. |
|
||||
| **Audio ADC (Recording)** | ES7210 | `0x40` | High-performance 4-channel audio ADC for the digital mic array. |
|
||||
| **Real-Time Clock (RTC)** | PCF85063 | `0x51` | Clock and timekeeper. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Direct GPIO Pin Mappings
|
||||
|
||||
### I2S Audio Bus (Shared)
|
||||
| Signal | GPIO Pin | Connected To |
|
||||
| :--- | :--- | :--- |
|
||||
| **I2S MCLK** | GPIO 12 | System Clock for ES8311 & ES7210 |
|
||||
| **I2S BCLK** | GPIO 13 | Bit Clock for I2S |
|
||||
| **I2S LRCK** | GPIO 14 | Left-Right/Word Select Clock |
|
||||
| **I2S DIN** | GPIO 15 | Microphone input (data from ES7210 ADC) |
|
||||
| **I2S DOUT** | GPIO 16 | Speaker output (data to ES8311 DAC) |
|
||||
|
||||
### Control Bus
|
||||
| Signal | GPIO Pin | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **I2C SDA** | GPIO 11 | Shared I2C Data Line |
|
||||
| **I2C SCL** | GPIO 10 | Shared I2C Clock Line |
|
||||
|
||||
### Onboard LEDs
|
||||
| Signal | GPIO Pin | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **WS2812B DIN** | GPIO 38 | Data Line for 7-ring programmable RGB LEDs (GRB/RGB order) |
|
||||
|
||||
---
|
||||
|
||||
## 4. TCA9555 GPIO Expander Mappings (I2C Address `0x20`)
|
||||
|
||||
Peripherals such as the power amplifier enable and physical buttons are routed through the I2C-controlled **TCA9555 expander** to save ESP32-S3 physical pins.
|
||||
|
||||
| Expander Pin | Signal Name | Type | Description | Active State |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Pin 8** | `PA_CTRL` (PA_EN) | Output | Enables the onboard speaker power amplifier. Must be set high for sound. | High (`1`) |
|
||||
| **Pin 9** | `KEY1` (Volume Down) | Input | Onboard physical push button (Key 1). | Low (`0` when pressed) |
|
||||
| **Pin 10** | `KEY2` (Play/Pause) | Input | Onboard physical push button (Key 2). | Low (`0` when pressed) |
|
||||
| **Pin 11** | `KEY3` (Volume Up) | Input | Onboard physical push button (Key 3). | Low (`0` when pressed) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Expansion & Compatibility Interfaces
|
||||
|
||||
* **Display Interface:** Onboard 18-pin FPC header supporting SPI LCD screens:
|
||||
* Compatible sizes: 1.47", 2", 2.8", 3.5" LCDs.
|
||||
* **Camera Interface:** Onboard DVP camera header:
|
||||
* Compatible camera sensors: OV2640, OV5640.
|
||||
* **TF Card Slot:** Onboard MicroSD card slot for external audio asset storage and data logging.
|
||||
@@ -0,0 +1,183 @@
|
||||
# ESP32-S3 AI Smart Speaker Control & Diagnostics Suite
|
||||
|
||||
This repository contains the complete firmware, development socket server, and real-time operations dashboard for the **Waveshare ESP32-S3-AUDIO-Board-EN** (No Battery version).
|
||||
|
||||
The project implements a state-driven smart speaker capable of auto-connecting to Wi-Fi, linking to a remote WebSocket server via a configurable WebSocket endpoint, playing back high-quality audio chimes via I2S, driving WS2812B LEDs, and receiving real-time control actions (such as volume adjustments, sound trigger cues, and voice synthesis broadcasts) from a dedicated operations dashboard.
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Dashboard[Enterprise Monitor Dashboard <br/> Port 8902] <-->|JSON Control Protocol| DevServer[WebSocket API Gateway <br/> Port 8901]
|
||||
ESP32[ESP32-S3 Audio Board] <-->|Secure WebSocket Link| DevServer
|
||||
|
||||
subgraph ESP32-S3 Core Drivers
|
||||
I2S[I2S Audio Output - ES8311 DAC]
|
||||
LED[WS2812B 7-Ring RGB LEDs]
|
||||
WiFi[WiFi Manager State Machine]
|
||||
SPIFFS[SPIFFS Audio Assets Model]
|
||||
end
|
||||
|
||||
ESP32 --> I2S
|
||||
ESP32 --> LED
|
||||
ESP32 --> WiFi
|
||||
ESP32 --> SPIFFS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Hardware Specifications & Pin Mapping
|
||||
|
||||
* **SoC:** ESP32-S3R8 (Dual-core 32-bit LX7, 240 MHz, 16MB Flash, 8MB Octal PSRAM).
|
||||
* **Audio DAC (Playback):** ES8311 mono codec connected via I2C & I2S.
|
||||
* **Audio ADC (Recording):** ES7210 microphone encoder.
|
||||
* **Audio PA Enable:** Controlled via Pin 8 of the TCA9555 I2C I/O expander.
|
||||
* **LED Ring:** 7 WS2812B RGB LEDs connected to GPIO 38.
|
||||
|
||||
| Signal Name | ESP32-S3 GPIO | Signal Description |
|
||||
|:---|:---|:---|
|
||||
| **I2S MCLK** | GPIO 12 | Audio Master Clock |
|
||||
| **I2S BCLK** | GPIO 13 | Audio Bit Clock |
|
||||
| **I2S LRCK** | GPIO 14 | Audio Left/Right Word Clock |
|
||||
| **I2S DIN** | GPIO 15 | Microphone Data Input (ADC) |
|
||||
| **I2S DOUT** | GPIO 16 | Speaker Data Output (DAC) |
|
||||
| **I2C SDA** | GPIO 11 | Codec & Expander I2C Data |
|
||||
| **I2C SCL** | GPIO 10 | Codec & Expander I2C Clock |
|
||||
| **RGB LED** | GPIO 38 | WS2812B Data Line |
|
||||
|
||||
---
|
||||
|
||||
## 2. Workspace Setup & Prerequisites
|
||||
|
||||
### ESP-IDF SDK Target:
|
||||
* Install **ESP-IDF v6.0.1** (target framework `esp32s3`).
|
||||
* Ensure that the environment paths are configured (or run under your standard ESP-IDF terminal shell).
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration & Deployment
|
||||
|
||||
Before building, you must configure your network credentials and target endpoints:
|
||||
|
||||
### Wi-Fi Configuration
|
||||
Copy the template configuration file `wifi-credentials.example` in the project root to a new file named `wifi-credentials` (which is excluded from Git in `.gitignore`):
|
||||
```bash
|
||||
cp wifi-credentials.example wifi-credentials
|
||||
```
|
||||
Open `wifi-credentials` and write your actual credentials:
|
||||
```text
|
||||
SSID="Your_WiFi_Name"
|
||||
PASSWORD="Your_WiFi_Password"
|
||||
```
|
||||
*Note: CMake automatically reads this file during configuration to compile the private header `wifi_credentials.h` inside your build folder. Never commit your private `wifi-credentials` file.*
|
||||
|
||||
### WebSocket Server URL
|
||||
Open `main/spiffs/server_url.txt` and write the target WebSocket server URL (e.g. your local gateway or public Cloudflare domain):
|
||||
```text
|
||||
wss://example.com
|
||||
```
|
||||
*Note: This text file is packaged into the SPIFFS partition and loaded dynamically on board boot.*
|
||||
|
||||
---
|
||||
|
||||
## 4. Hardware Model Variants
|
||||
|
||||
The firmware supports two hardware model profiles: **DEV-1** (default) and **PROTO-1**. The active model is controlled by the `device-config` file in the project root.
|
||||
|
||||
### Switching Models
|
||||
|
||||
Open `device-config` and set the `MODEL` field to either `DEV-1` or `PROTO-1`:
|
||||
|
||||
```text
|
||||
# For the standard development board (default):
|
||||
MODEL=DEV-1
|
||||
|
||||
# For the prototype board with additional peripherals:
|
||||
MODEL=PROTO-1
|
||||
```
|
||||
|
||||
> **Important:** After changing `device-config`, you **must perform a clean rebuild** to regenerate `device_config.h` and recompile the correct pin mappings. Run `scripts/clean.bat` before `scripts/build.bat`.
|
||||
|
||||
---
|
||||
|
||||
### DEV-1 Profile
|
||||
|
||||
The standard Waveshare ESP32-S3-AUDIO-Board-EN pin configuration:
|
||||
|
||||
| Peripheral | GPIO | Mode |
|
||||
|:---|:---|:---|
|
||||
| I2C SCL | GPIO 10 | I2C Clock |
|
||||
| I2C SDA | GPIO 11 | I2C Data |
|
||||
| I2S MCLK | GPIO 12 | Audio Master Clock |
|
||||
| I2S BCLK | GPIO 13 | Audio Bit Clock |
|
||||
| I2S LRCK | GPIO 14 | Audio Word Select |
|
||||
| I2S DIN (Microphone) | GPIO 15 | I2S Input |
|
||||
| I2S DOUT (Speaker) | GPIO 16 | I2S Output |
|
||||
| RGB LEDs (7x WS2812B) | GPIO 38 | WS2812 Signal |
|
||||
| PA Enable | EXIO 8 | TCA9555 I/O Expander |
|
||||
|
||||
---
|
||||
|
||||
### PROTO-1 Profile
|
||||
|
||||
Inherits all DEV-1 audio functionality but remaps the LED signal pin and adds three new peripheral drivers:
|
||||
|
||||
| Peripheral | GPIO | Mode |
|
||||
|:---|:---|:---|
|
||||
| I2S DIN (Microphone) | GPIO 15 | I2S Input *(remapped: DOUT ↔ DIN swapped vs DEV-1)* |
|
||||
| I2S DOUT (Speaker) | GPIO 16 | I2S Output |
|
||||
| RGB LEDs (WS2812) | **GPIO 9** | WS2812 Signal *(changed from GPIO 38)* |
|
||||
| **Haptics Motor** | **GPIO 5** | PWM Output (LEDC, 1kHz, 10-bit) |
|
||||
| **Ambient Light Sensor** | **GPIO 4** | ADC1 Channel 3, Analog Input |
|
||||
| **Touch Sensor** | **GPIO 7** | Digital Input |
|
||||
|
||||
**Additional PROTO-1 runtime behaviours:**
|
||||
- A background FreeRTOS task (`proto1_sensor_task`) polls the LDR and touch GPIO every 100ms and updates the shared device state.
|
||||
- Ambient light ADC values (0–4095) are scaled to 0–100% in the web dashboard.
|
||||
- Haptic intensity is controllable via the WebSocket `set_haptics` command (0–100%).
|
||||
- All three sensor readings are included in uplink telemetry state reports.
|
||||
- The web dashboard automatically unlocks the **Haptic Vibration** control slider for PROTO-1 devices; it appears greyed-out with a *PROTO-1 Only* indicator for DEV-1 devices.
|
||||
|
||||
---
|
||||
|
||||
## 5. Build, Flash & Logging
|
||||
|
||||
Use the preconfigured batch scripts inside the `/scripts` directory to compile and flash the firmware:
|
||||
|
||||
* **Compile Code:** Run `scripts/build.bat` to run incremental CMake compiles.
|
||||
* **Flash Firmware:** Run `scripts/flash.bat` to build and flash the application to `COM5` (at `460800` baud) alongside generated SPIFFS images.
|
||||
* **Monitor Console Logs:** Run `scripts/monitor.bat` to read live serial logs from `COM5` (press `Ctrl + ]` to exit).
|
||||
* **Clean Build:** Run `scripts/clean.bat` to delete compilation cache.
|
||||
|
||||
---
|
||||
|
||||
## 6. Starting the Servers
|
||||
|
||||
To run the full diagnostics suite, launch both the API Gateway and the Dashboard Operations Console:
|
||||
|
||||
### Start the API Gateway (Port 8901)
|
||||
The gateway handles data routing, client registration, and active device command routing:
|
||||
```bash
|
||||
cd dev-server
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Start the Operations Dashboard (Port 8902)
|
||||
The dashboard displays connection metrics, device health parameters, and hosts the command center:
|
||||
```bash
|
||||
cd monitor-server
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
Open **`http://localhost:8902`** in your browser to access the control panel.
|
||||
|
||||
---
|
||||
|
||||
## 7. Remote Routing & Tunnel Configuration
|
||||
|
||||
If deploying publicly, ensure your local configurations route traffic properly:
|
||||
1. Map Cloudflare tunnel configuration to forward requests for `example.com` to `http://localhost:8901`.
|
||||
2. The Dashboard client code automatically scales and shifts its endpoint from `ws://localhost:8901` (when accessed locally) to `wss://example.com` (when accessed remotely) to match your environment.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "dev-server",
|
||||
"version": "1.0.0",
|
||||
"description": "TypeScript Socket Server for ESP32 Audio Board",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"dev": "nodemon"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/multer": "^2.1.0",
|
||||
"express": "^4.19.2",
|
||||
"multer": "^2.2.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
set /p tunnel_name="Enter Cloudflare tunnel name [default: home dev]: "
|
||||
if "%tunnel_name%"=="" set tunnel_name=home dev
|
||||
|
||||
echo Running Cloudflare tunnel "%tunnel_name%" -> http://localhost:8901...
|
||||
"C:\Program Files (x86)\cloudflared\cloudflared.exe" tunnel run "%tunnel_name%"
|
||||
pause
|
||||
@@ -0,0 +1,5 @@
|
||||
param (
|
||||
[string]$Tunnel = "home dev"
|
||||
)
|
||||
Write-Host "Running Cloudflare tunnel: $Tunnel -> http://localhost:8901..." -ForegroundColor Cyan
|
||||
& "C:\Program Files (x86)\cloudflared\cloudflared.exe" tunnel run $Tunnel
|
||||
@@ -0,0 +1,462 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import url from 'url';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import multer from 'multer';
|
||||
import os from 'os';
|
||||
|
||||
function getLocalIp(): string {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name] || []) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 8901;
|
||||
|
||||
// CORS — allow the monitor-server dashboard (port 8902) to call OTA HTTP endpoints
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
next();
|
||||
});
|
||||
app.options('*', (_req, res) => res.sendStatus(200));
|
||||
|
||||
// OTA firmware storage
|
||||
const OTA_DIR = path.join(__dirname, '..', 'ota_uploads');
|
||||
if (!fs.existsSync(OTA_DIR)) fs.mkdirSync(OTA_DIR, { recursive: true });
|
||||
const OTA_FILE = path.join(OTA_DIR, 'firmware.bin');
|
||||
|
||||
let otaFirmware: { filename: string; size: number; uploadedAt: string } | null = null;
|
||||
|
||||
const otaStorage = multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, OTA_DIR),
|
||||
filename: (_req, _file, cb) => cb(null, 'firmware.bin'),
|
||||
});
|
||||
const upload = multer({
|
||||
storage: otaStorage,
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (file.originalname.endsWith('.bin')) cb(null, true);
|
||||
else cb(new Error('Only .bin firmware files are accepted'));
|
||||
},
|
||||
limits: { fileSize: 8 * 1024 * 1024 }, // 8 MB max
|
||||
});
|
||||
|
||||
// Simple HTTP health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK', time: new Date() });
|
||||
});
|
||||
|
||||
// Debug endpoint: dump connected clients as JSON
|
||||
app.get('/clients', (_req, res) => {
|
||||
res.json(Array.from(clients.values()));
|
||||
});
|
||||
|
||||
// OTA: upload firmware binary
|
||||
app.post('/ota/upload', upload.single('firmware'), (req, res) => {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: 'No firmware file provided' });
|
||||
return;
|
||||
}
|
||||
otaFirmware = {
|
||||
filename: req.file.originalname,
|
||||
size: req.file.size,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
console.log(`[OTA] Firmware uploaded: ${req.file.originalname} (${req.file.size} bytes)`);
|
||||
addLog('message', 'dashboard', `OTA firmware uploaded: ${req.file.originalname} (${(req.file.size / 1024).toFixed(1)} KB)`);
|
||||
broadcastToMonitors();
|
||||
res.json({ ok: true, ...otaFirmware });
|
||||
});
|
||||
|
||||
// OTA: serve firmware binary
|
||||
app.get('/ota/firmware.bin', (_req, res) => {
|
||||
if (!otaFirmware || !fs.existsSync(OTA_FILE)) {
|
||||
res.status(404).json({ error: 'No firmware uploaded yet' });
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="firmware.bin"');
|
||||
res.sendFile(OTA_FILE);
|
||||
});
|
||||
|
||||
// OTA: metadata status (manually uploaded binary)
|
||||
app.get('/ota/status', (_req, res) => {
|
||||
res.json(otaFirmware || { error: 'No firmware uploaded' });
|
||||
});
|
||||
|
||||
// OTA: firmware release catalog (populated by scripts/release.ps1)
|
||||
const RELEASES_DIR = path.join(__dirname, '..', '..', 'releases');
|
||||
const CATALOG_FILE = path.join(RELEASES_DIR, 'firmware-catalog.json');
|
||||
app.get('/ota/catalog', (_req, res) => {
|
||||
if (!fs.existsSync(CATALOG_FILE)) {
|
||||
res.json({ latest: '', releases: [] });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let content = fs.readFileSync(CATALOG_FILE, 'utf8');
|
||||
// Strip UTF-8 BOM if present (\uFEFF)
|
||||
if (content.charCodeAt(0) === 0xFEFF) {
|
||||
content = content.slice(1);
|
||||
}
|
||||
const catalog = JSON.parse(content);
|
||||
res.json(catalog);
|
||||
} catch (e: any) {
|
||||
console.error('[Server] Catalog parse error:', e.message);
|
||||
res.status(500).json({ error: 'Failed to parse firmware-catalog.json', details: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// OTA: serve versioned release binaries from releases/ directory
|
||||
app.use('/releases', express.static(RELEASES_DIR, { dotfiles: 'deny' }));
|
||||
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
interface ClientMetadata {
|
||||
id: string;
|
||||
ip: string;
|
||||
connectedAt: string;
|
||||
messageCount: number;
|
||||
lastActive: string;
|
||||
userAgent?: string;
|
||||
otaStatus?: 'idle' | 'pending' | 'flashing' | 'complete' | 'error';
|
||||
otaProgress?: number;
|
||||
otaError?: string;
|
||||
state?: {
|
||||
model?: string;
|
||||
heap?: number;
|
||||
uptime?: number;
|
||||
rssi?: number;
|
||||
volume?: number;
|
||||
led_state?: number;
|
||||
led_brightness?: number;
|
||||
haptic_intensity?: number;
|
||||
ambient_light?: number;
|
||||
touch_active?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ActivityLog {
|
||||
timestamp: string;
|
||||
type: 'connect' | 'disconnect' | 'message' | 'error';
|
||||
ip: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
// Track clients and monitors
|
||||
const clients = new Map<WebSocket, ClientMetadata>();
|
||||
const monitors = new Set<WebSocket>();
|
||||
const activityLogs: ActivityLog[] = [];
|
||||
|
||||
// Helper to push a log
|
||||
function addLog(type: ActivityLog['type'], ip: string, details: string) {
|
||||
activityLogs.unshift({
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
ip,
|
||||
details
|
||||
});
|
||||
if (activityLogs.length > 50) {
|
||||
activityLogs.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to cleanly remove a client and notify monitors
|
||||
function removeClient(ws: WebSocket, clientId: string, ip: string, reason: string) {
|
||||
if (clients.has(ws)) {
|
||||
clients.delete(ws);
|
||||
addLog('disconnect', ip, `Client [${clientId}] ${reason}`);
|
||||
console.log(`[Server] Client removed [${clientId}] from ${ip} — ${reason}`);
|
||||
broadcastToMonitors();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to serialize and broadcast state to all monitors
|
||||
// Also prunes dead monitor sockets in-place
|
||||
function broadcastToMonitors() {
|
||||
// Prune stale clients (sockets not in OPEN state)
|
||||
for (const [clientWs, meta] of clients.entries()) {
|
||||
if (clientWs.readyState !== WebSocket.OPEN) {
|
||||
console.log(`[Server] Pruning stale client [${meta.id}] (readyState=${clientWs.readyState})`);
|
||||
clients.delete(clientWs);
|
||||
addLog('disconnect', meta.ip, `Client [${meta.id}] pruned (stale socket)`);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedClients = Array.from(clients.values());
|
||||
const payload = JSON.stringify({
|
||||
type: 'status_update',
|
||||
clients: connectedClients,
|
||||
logs: activityLogs
|
||||
});
|
||||
|
||||
for (const monitor of monitors) {
|
||||
if (monitor.readyState === WebSocket.OPEN) {
|
||||
monitor.send(payload);
|
||||
} else {
|
||||
monitors.delete(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wss.on('connection', (ws: WebSocket, req) => {
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
const rawIp = (typeof forwarded === 'string' ? forwarded.split(',')[0].trim() : null)
|
||||
|| req.socket.remoteAddress
|
||||
|| 'unknown';
|
||||
// Normalize IPv4-mapped IPv6 (::ffff:1.2.3.4 → 1.2.3.4)
|
||||
const ip = rawIp.startsWith('::ffff:') ? rawIp.slice(7) : rawIp;
|
||||
const userAgent = req.headers['user-agent'] || 'Unknown Device';
|
||||
|
||||
// Parse query parameters
|
||||
const parsedUrl = url.parse(req.url || '', true);
|
||||
const role = parsedUrl.query.role;
|
||||
|
||||
if (role === 'monitor') {
|
||||
console.log(`[Server] Monitor client connected from ${ip}`);
|
||||
monitors.add(ws);
|
||||
|
||||
// Send initial state immediately
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status_update',
|
||||
clients: Array.from(clients.values()),
|
||||
logs: activityLogs
|
||||
}));
|
||||
|
||||
ws.on('message', (message: string) => {
|
||||
try {
|
||||
const commandData = JSON.parse(message);
|
||||
if (commandData.type === 'control_device') {
|
||||
const { targetId, payload } = commandData;
|
||||
let found = false;
|
||||
for (const [clientWs, meta] of clients.entries()) {
|
||||
if (meta.id === targetId) {
|
||||
if (clientWs.readyState === WebSocket.OPEN) {
|
||||
clientWs.send(JSON.stringify(payload));
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
console.log(`[Server] Routed control command from monitor [${ip}] to client [${targetId}]:`, payload);
|
||||
addLog('message', ip, `Control: Sent [${payload.command}] to [${targetId}]`);
|
||||
} else {
|
||||
console.log(`[Server] Control command target client [${targetId}] not found or offline.`);
|
||||
ws.send(JSON.stringify({ type: 'error', message: `Device ${targetId} not found or offline.` }));
|
||||
}
|
||||
broadcastToMonitors();
|
||||
} else if (commandData.type === 'boot_device') {
|
||||
const { targetId } = commandData;
|
||||
let found = false;
|
||||
for (const [clientWs, meta] of clients.entries()) {
|
||||
if (meta.id === targetId) {
|
||||
found = true;
|
||||
console.log(`[Server] Booting client [${targetId}] by request from monitor [${ip}]`);
|
||||
addLog('disconnect', meta.ip || ip, `Device [${targetId}] forcibly booted by monitor`);
|
||||
clientWs.terminate();
|
||||
clients.delete(clientWs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `Device ${targetId} not found or already offline.` }));
|
||||
}
|
||||
broadcastToMonitors();
|
||||
} else if (commandData.type === 'trigger_ota') {
|
||||
const { targetId, version } = commandData;
|
||||
let serverHost = req.headers.host || `localhost:${port}`;
|
||||
const protocol = (serverHost.includes('localhost') || serverHost.includes('127.0.0.1') || serverHost.includes('[::1]')) ? 'http' : 'https';
|
||||
if (serverHost.includes('localhost') || serverHost.includes('127.0.0.1') || serverHost.includes('[::1]')) {
|
||||
const localIp = getLocalIp();
|
||||
serverHost = serverHost.replace('localhost', localIp).replace('127.0.0.1', localIp).replace('[::1]', localIp);
|
||||
}
|
||||
let firmwareUrl: string | null = null;
|
||||
let firmwareLabel: string = '';
|
||||
|
||||
if (version && version !== 'custom') {
|
||||
// Load catalog to find the versioned binary
|
||||
try {
|
||||
if (fs.existsSync(CATALOG_FILE)) {
|
||||
let content = fs.readFileSync(CATALOG_FILE, 'utf8');
|
||||
if (content.charCodeAt(0) === 0xFEFF) {
|
||||
content = content.slice(1);
|
||||
}
|
||||
const catalog = JSON.parse(content);
|
||||
// Lookup target device model
|
||||
let targetModel = 'DEV-1';
|
||||
for (const [_, clientMeta] of clients.entries()) {
|
||||
if (clientMeta.id === targetId && clientMeta.state) {
|
||||
targetModel = clientMeta.state.model || 'DEV-1';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedVersion = (version === 'latest') ? catalog.latest : version;
|
||||
const entry = (catalog.releases || []).find(
|
||||
(r: { version: string, model: string }) => r.version === resolvedVersion && r.model === targetModel
|
||||
);
|
||||
if (entry) {
|
||||
firmwareUrl = `${protocol}://${serverHost}/releases/${entry.filename}`;
|
||||
firmwareLabel = `${entry.filename} (${entry.version} - ${entry.model})`;
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `Firmware version '${resolvedVersion}' for model '${targetModel}' not found in catalog.` }));
|
||||
}
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'No firmware catalog available. Run scripts/release.ps1 first.' }));
|
||||
}
|
||||
} catch (e) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'Failed to read firmware catalog.' }));
|
||||
}
|
||||
} else {
|
||||
// Manual upload path
|
||||
if (!otaFirmware || !fs.existsSync(OTA_FILE)) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: 'No firmware uploaded. Use POST /ota/upload first.' }));
|
||||
} else {
|
||||
firmwareUrl = `${protocol}://${serverHost}/ota/firmware.bin`;
|
||||
firmwareLabel = otaFirmware.filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (firmwareUrl) {
|
||||
let found = false;
|
||||
for (const [clientWs, meta] of clients.entries()) {
|
||||
if (meta.id === targetId && clientWs.readyState === WebSocket.OPEN) {
|
||||
found = true;
|
||||
meta.otaStatus = 'pending';
|
||||
meta.otaProgress = 0;
|
||||
meta.otaError = undefined;
|
||||
|
||||
const otaPayload: any = { command: 'start_ota', url: firmwareUrl };
|
||||
|
||||
clientWs.send(JSON.stringify(otaPayload));
|
||||
console.log(`[OTA] Triggered OTA on [${targetId}] → ${firmwareUrl}`);
|
||||
addLog('message', meta.ip, `OTA triggered on [${targetId}]: ${firmwareLabel} → ${firmwareUrl}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ws.send(JSON.stringify({ type: 'error', message: `Device ${targetId} not found or offline.` }));
|
||||
}
|
||||
}
|
||||
broadcastToMonitors();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Server] Failed to process monitor message:', e);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log(`[Server] Monitor client disconnected from ${ip}`);
|
||||
monitors.delete(ws);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle standard device connection
|
||||
const clientId = Math.random().toString(36).substring(2, 9).toUpperCase();
|
||||
const metadata: ClientMetadata = {
|
||||
id: clientId,
|
||||
ip,
|
||||
connectedAt: new Date().toISOString(),
|
||||
messageCount: 0,
|
||||
lastActive: new Date().toISOString(),
|
||||
userAgent
|
||||
};
|
||||
|
||||
clients.set(ws, metadata);
|
||||
console.log(`[Server] New client connected [${clientId}] from ${ip} (${userAgent})`);
|
||||
addLog('connect', ip, `Client [${clientId}] connected`);
|
||||
broadcastToMonitors();
|
||||
|
||||
// Send welcome package and request state immediately
|
||||
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected to dev-server!' }));
|
||||
ws.send(JSON.stringify({ command: 'query_state' }));
|
||||
|
||||
ws.on('message', (message: string) => {
|
||||
console.log(`[Server] Received message from [${clientId}]: ${message}`);
|
||||
metadata.messageCount++;
|
||||
metadata.lastActive = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
if (data.type === 'state_report') {
|
||||
metadata.state = {
|
||||
model: data.system?.model,
|
||||
heap: data.health?.free_heap_bytes,
|
||||
uptime: data.health?.uptime_seconds,
|
||||
rssi: data.network?.wifi_rssi,
|
||||
volume: data.peripherals?.audio?.volume,
|
||||
led_state: data.peripherals?.led_ring?.state_id,
|
||||
led_brightness: data.peripherals?.led_ring?.brightness_coef,
|
||||
haptic_intensity: data.peripherals?.haptic_intensity,
|
||||
ambient_light: data.peripherals?.ambient_light,
|
||||
touch_active: data.peripherals?.touch_active
|
||||
};
|
||||
addLog('message', ip, `Telemetry from [${clientId}]: Model=${metadata.state.model}, Vol=${metadata.state.volume}%, Brightness=${metadata.state.led_brightness}, Haptics=${metadata.state.haptic_intensity}%, Light=${metadata.state.ambient_light}, Touch=${metadata.state.touch_active}`);
|
||||
} else if (data.type === 'ota_progress') {
|
||||
metadata.otaStatus = 'flashing';
|
||||
metadata.otaProgress = data.percent ?? 0;
|
||||
console.log(`[OTA] [${clientId}] progress: ${data.percent}% (${data.bytes_written}/${data.bytes_total} bytes)`);
|
||||
} else if (data.type === 'ota_complete') {
|
||||
metadata.otaStatus = 'complete';
|
||||
metadata.otaProgress = 100;
|
||||
addLog('message', ip, `OTA complete on [${clientId}] — device rebooting`);
|
||||
console.log(`[OTA] [${clientId}] OTA complete, rebooting`);
|
||||
} else if (data.type === 'ota_error') {
|
||||
metadata.otaStatus = 'error';
|
||||
metadata.otaError = data.reason ?? 'unknown error';
|
||||
addLog('error', ip, `OTA error on [${clientId}]: ${data.reason}`);
|
||||
console.error(`[OTA] [${clientId}] error: ${data.reason}`);
|
||||
} else {
|
||||
addLog('message', ip, `[${clientId}] sent message`);
|
||||
}
|
||||
ws.send(JSON.stringify({ type: 'echo', data }));
|
||||
} catch (e) {
|
||||
addLog('message', ip, `[${clientId}] sent raw message`);
|
||||
ws.send(JSON.stringify({ type: 'raw', data: message.toString() }));
|
||||
}
|
||||
broadcastToMonitors();
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
removeClient(ws, clientId, ip, 'disconnected');
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error(`[Server] WebSocket error for [${clientId}]:`, error.message);
|
||||
addLog('error', ip, `Client [${clientId}] error: ${error.message}`);
|
||||
removeClient(ws, clientId, ip, `disconnected with error: ${error.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Periodic stale-client sweep every 30 seconds
|
||||
// Catches connections that drop without sending a close frame (e.g. network cuts through Cloudflare tunnel)
|
||||
setInterval(() => {
|
||||
let pruned = 0;
|
||||
for (const [clientWs, meta] of clients.entries()) {
|
||||
if (clientWs.readyState !== WebSocket.OPEN) {
|
||||
clients.delete(clientWs);
|
||||
addLog('disconnect', meta.ip, `Client [${meta.id}] pruned by sweep (readyState=${clientWs.readyState})`);
|
||||
pruned++;
|
||||
}
|
||||
}
|
||||
if (pruned > 0) {
|
||||
console.log(`[Server] Stale sweep pruned ${pruned} client(s)`);
|
||||
broadcastToMonitors();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Socket server running on port ${port}`);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const url = process.env.WS_URL || 'ws://localhost:8901';
|
||||
|
||||
console.log(`[Test Client] Attempting connection to ${url}...`);
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('[Test Client] Connected successfully!');
|
||||
// Send test packet
|
||||
const payload = JSON.stringify({
|
||||
type: 'test_connection',
|
||||
sender: 'Node.js Test Client',
|
||||
timestamp: new Date()
|
||||
});
|
||||
ws.send(payload);
|
||||
console.log(`[Test Client] Sent payload: ${payload}`);
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
console.log(`[Test Client] Received from server: ${data}`);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
console.log(`[Test Client] Connection closed. Code: ${code}, Reason: ${reason || 'None'}`);
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
console.error('[Test Client] WebSocket error:', err.message || err);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MODEL=PROTO-1
|
||||
@@ -0,0 +1,28 @@
|
||||
path = 'C:\esp'
|
||||
idf_path = 'C:\esp\v6.0.1\esp-idf'
|
||||
esp_idf_json_path = 'C:\Espressif\tools'
|
||||
tool_download_folder_name = 'C:\Espressif\dist'
|
||||
tool_install_folder_name = 'C:\Espressif\tools'
|
||||
target = ["esp32s3"]
|
||||
idf_versions = ["v6.0.1"]
|
||||
tools_json_file = "tools/tools.json"
|
||||
config_file_save_path = "eim_config.toml"
|
||||
non_interactive = true
|
||||
wizard_all_questions = false
|
||||
mirror = "https://dl.espressif.com/github_assets"
|
||||
idf_mirror = "https://github.com"
|
||||
pypi_mirror = "https://pypi.org/simple"
|
||||
recurse_submodules = true
|
||||
install_all_prerequisites = true
|
||||
skip_prerequisites_check = false
|
||||
cleanup = false
|
||||
python_env_folder_name = "python"
|
||||
activation_script_path_override = 'C:\Espressif\tools'
|
||||
python_version_override = "python313"
|
||||
create_bat_activation_script = false
|
||||
|
||||
[idf_features_per_version]
|
||||
"v6.0.1" = ["core"]
|
||||
|
||||
[idf_tools_per_version]
|
||||
"v6.0.1" = ["xtensa-esp-elf-gdb", "xtensa-esp-elf", "esp-clang", "esp-clang-libs", "riscv32-esp-elf", "esp32ulp-elf", "cmake", "openocd-esp32", "ninja", "idf-exe", "ccache", "dfu-util", "esp-rom-elfs"]
|
||||
@@ -0,0 +1,73 @@
|
||||
# Parse wifi-credentials from the project root directory at configure-time
|
||||
set(PASSWORD_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../wifi-credentials")
|
||||
if(EXISTS "${PASSWORD_FILE}")
|
||||
file(READ "${PASSWORD_FILE}" PASSWORD_CONTENT)
|
||||
string(REGEX MATCH "SSID=([^\r\n]*)" _ "${PASSWORD_CONTENT}")
|
||||
set(WIFI_SSID "${CMAKE_MATCH_1}")
|
||||
string(REGEX MATCH "PASSWORD=([^\r\n]*)" _ "${PASSWORD_CONTENT}")
|
||||
set(WIFI_PASSWORD "${CMAKE_MATCH_1}")
|
||||
else()
|
||||
set(WIFI_SSID "YOUR_SSID")
|
||||
set(WIFI_PASSWORD "YOUR_PASSWORD")
|
||||
endif()
|
||||
|
||||
# Generate wifi_credentials.h inside the build directory so credentials aren't checked into Git
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/wifi_credentials.h"
|
||||
"// Generated by CMake. Do not edit manually.\n\
|
||||
#define WIFI_SSID \"${WIFI_SSID}\"\n\
|
||||
#define WIFI_PASSWORD \"${WIFI_PASSWORD}\"\n"
|
||||
)
|
||||
|
||||
# Parse device model configuration from the project root directory at configure-time
|
||||
set(CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../device-config")
|
||||
if(EXISTS "${CONFIG_FILE}")
|
||||
file(READ "${CONFIG_FILE}" CONFIG_CONTENT)
|
||||
string(REGEX MATCH "MODEL=([^\r\n]*)" _ "${CONFIG_CONTENT}")
|
||||
set(DEVICE_MODEL "${CMAKE_MATCH_1}")
|
||||
else()
|
||||
set(DEVICE_MODEL "DEV-1")
|
||||
endif()
|
||||
|
||||
# Generate device_config.h inside the build directory
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/device_config.h"
|
||||
"// Generated by CMake. Do not edit manually.\n\
|
||||
#define DEVICE_MODEL \"${DEVICE_MODEL}\"\n"
|
||||
)
|
||||
|
||||
# Register all project source files
|
||||
set(SOURCES
|
||||
"main.c"
|
||||
"wifi_connect.c"
|
||||
"socket_client.c"
|
||||
"state_manager.c"
|
||||
"ota_manager.c"
|
||||
"hardeware_driver/bsp_board.c"
|
||||
"audio_play_driver/audio_driver.c"
|
||||
"tca9555_driver/tca9555_driver.c"
|
||||
)
|
||||
|
||||
# Set include directories, including the generated binary directory
|
||||
set(INCLUDE_DIRS
|
||||
"."
|
||||
"hardeware_driver"
|
||||
"audio_play_driver"
|
||||
"tca9555_driver"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
|
||||
# Register the main component with dependencies
|
||||
idf_component_register(
|
||||
SRCS ${SOURCES}
|
||||
INCLUDE_DIRS ${INCLUDE_DIRS}
|
||||
PRIV_REQUIRES esp_driver_i2s esp_driver_i2c spi_flash esp_audio_simple_player esp_io_expander_tca95xx_16bit fatfs sdmmc spiffs led_strip esp_wifi nvs_flash esp_event esp_websocket_client esp-tls esp_https_ota app_update esp_driver_ledc esp_adc
|
||||
)
|
||||
|
||||
# Add compile definition based on model to the component library target
|
||||
if(DEVICE_MODEL STREQUAL "PROTO-1")
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE MODEL_PROTO_1=1)
|
||||
else()
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE MODEL_DEV_1=1)
|
||||
endif()
|
||||
|
||||
# Package and build the spiffs partition containing startup.mp3
|
||||
spiffs_create_partition_image(model spiffs FLASH_IN_PROJECT)
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "audio_driver.h"
|
||||
|
||||
#include "tca9555_driver.h"
|
||||
#include "bsp_board.h"
|
||||
|
||||
static const char *TAG = "audio play";
|
||||
|
||||
|
||||
static uint8_t Volume = PLAYER_VOLUME;
|
||||
static esp_asp_handle_t handle = NULL;
|
||||
|
||||
static void Audio_PA_EN(void)
|
||||
{
|
||||
Set_EXIO(IO_EXPANDER_PIN_NUM_8,true);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
static void Audio_PA_DIS(void)
|
||||
{
|
||||
Set_EXIO(IO_EXPANDER_PIN_NUM_8,false);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
static int out_data_callback(uint8_t *data, int data_size, void *ctx)
|
||||
{
|
||||
esp_audio_play((int16_t*)data,data_size,500 / portTICK_PERIOD_MS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int in_data_callback(uint8_t *data, int data_size, void *ctx)
|
||||
{
|
||||
int ret = fread(data, 1, data_size, ctx);
|
||||
ESP_LOGD(TAG, "%s-%d,rd size:%d", __func__, __LINE__, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int mock_event_callback(esp_asp_event_pkt_t *event, void *ctx)
|
||||
{
|
||||
if (event->type == ESP_ASP_EVENT_TYPE_MUSIC_INFO)
|
||||
{
|
||||
esp_asp_music_info_t info = {0};
|
||||
memcpy(&info, event->payload, event->payload_size);
|
||||
ESP_LOGI(TAG, "Get info, rate:%d, channels:%d, bits:%d ,bitrate = %d", info.sample_rate, info.channels, info.bits,info.bitrate);
|
||||
}
|
||||
else if (event->type == ESP_ASP_EVENT_TYPE_STATE)
|
||||
{
|
||||
esp_asp_state_t st = 0;
|
||||
memcpy(&st, event->payload, event->payload_size);
|
||||
|
||||
ESP_LOGI(TAG, "Get State, %d,%s", st, esp_audio_simple_player_state_to_str(st));
|
||||
/*if(st == ESP_ASP_STATE_FINISHED)
|
||||
{
|
||||
|
||||
}*/
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void pipeline_init(void)
|
||||
{
|
||||
esp_log_level_set("*", ESP_LOG_INFO);
|
||||
|
||||
esp_asp_cfg_t cfg = {
|
||||
.in.cb = NULL,
|
||||
.in.user_ctx = NULL,
|
||||
.out.cb = out_data_callback,
|
||||
.out.user_ctx = NULL,
|
||||
};
|
||||
|
||||
esp_gmf_err_t err = esp_audio_simple_player_new(&cfg, &handle);
|
||||
err = esp_audio_simple_player_set_event(handle, mock_event_callback, NULL);
|
||||
}
|
||||
|
||||
|
||||
esp_gmf_err_t Audio_Play_Music(const char* url)
|
||||
{
|
||||
esp_audio_simple_player_stop(handle);
|
||||
esp_gmf_err_t err = esp_audio_simple_player_run(handle, url, NULL);
|
||||
Audio_PA_EN();
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_gmf_err_t Audio_Play_Music_To_End(const char* url)
|
||||
{
|
||||
esp_audio_simple_player_stop(handle);
|
||||
Audio_PA_EN();
|
||||
esp_gmf_err_t err = esp_audio_simple_player_run_to_end(handle, url, NULL);
|
||||
Audio_PA_DIS();
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_gmf_err_t Audio_Stop_Play(void)
|
||||
{
|
||||
esp_gmf_err_t err = esp_audio_simple_player_stop(handle);
|
||||
Audio_PA_DIS();
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_gmf_err_t Audio_Resume_Play(void)
|
||||
{
|
||||
esp_gmf_err_t err = esp_audio_simple_player_resume(handle);
|
||||
Audio_PA_EN();
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_gmf_err_t Audio_Pause_Play(void)
|
||||
{
|
||||
esp_gmf_err_t err = esp_audio_simple_player_pause(handle);
|
||||
Audio_PA_DIS();
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_asp_state_t Audio_Get_Current_State(void)
|
||||
{
|
||||
esp_asp_state_t state;
|
||||
esp_gmf_err_t err = esp_audio_simple_player_get_state(handle, &state);
|
||||
if (err != ESP_GMF_ERR_OK) {
|
||||
ESP_LOGE("AUDIO", "Get state failed: %d", err);
|
||||
return ESP_ASP_STATE_ERROR;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
void Audio_Play_Init(void)
|
||||
{
|
||||
pipeline_init();
|
||||
}
|
||||
|
||||
void Volume_Adjustment(uint8_t Vol)
|
||||
{
|
||||
if(Vol > Volume_MAX )
|
||||
{
|
||||
printf("Audio : The volume value is incorrect. Please enter 0 to 21\r\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
esp_audio_set_play_vol(Vol);
|
||||
Volume = Vol;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t get_audio_volume(void)
|
||||
{
|
||||
return Volume;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_gmf_element.h"
|
||||
#include "esp_gmf_pipeline.h"
|
||||
#include "esp_gmf_pool.h"
|
||||
#include "esp_gmf_alc.h"
|
||||
#include "esp_audio_simple_player.h"
|
||||
#include "esp_audio_simple_player_advance.h"
|
||||
#include "esp_codec_dev.h"
|
||||
#include "esp_gmf_io.h"
|
||||
#include "esp_gmf_io_embed_flash.h"
|
||||
#include "esp_gmf_audio_dec.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define Volume_MAX 100
|
||||
|
||||
void Audio_Play_Init(void);
|
||||
void Volume_Adjustment(uint8_t Vol);
|
||||
uint8_t get_audio_volume(void);
|
||||
|
||||
esp_gmf_err_t Audio_Play_Music(const char* url);
|
||||
esp_gmf_err_t Audio_Play_Music_To_End(const char* url);
|
||||
esp_gmf_err_t Audio_Stop_Play(void);
|
||||
esp_gmf_err_t Audio_Resume_Play(void);
|
||||
esp_gmf_err_t Audio_Pause_Play(void);
|
||||
esp_asp_state_t Audio_Get_Current_State(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,684 @@
|
||||
#include "bsp_board.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "string.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
|
||||
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "dirent.h"
|
||||
#include "driver/sdmmc_host.h"
|
||||
#include <errno.h>
|
||||
|
||||
#if ((SOC_SDMMC_HOST_SUPPORTED) && (FUNC_SDMMC_EN))
|
||||
#include "driver/sdmmc_host.h"
|
||||
#endif /* ((SOC_SDMMC_HOST_SUPPORTED) && (FUNC_SDMMC_EN)) */
|
||||
|
||||
|
||||
#define ADC_I2S_CHANNEL 4
|
||||
|
||||
static sdmmc_card_t *card;
|
||||
static const char *TAG = "board";
|
||||
static int s_play_sample_rate = 16000;
|
||||
static int s_play_channel_format = 1;
|
||||
static int s_bits_per_chan = 16;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
static i2s_chan_handle_t tx_handle = NULL; // I2S tx channel handler
|
||||
static i2s_chan_handle_t rx_handle = NULL; // I2S rx channel handler
|
||||
#endif
|
||||
static audio_codec_data_if_t *record_data_if = NULL;
|
||||
static audio_codec_ctrl_if_t *record_ctrl_if = NULL;
|
||||
static audio_codec_if_t *record_codec_if = NULL;
|
||||
static esp_codec_dev_handle_t record_dev = NULL;
|
||||
|
||||
static audio_codec_data_if_t *play_data_if = NULL;
|
||||
static audio_codec_ctrl_if_t *play_ctrl_if = NULL;
|
||||
static audio_codec_gpio_if_t *play_gpio_if = NULL;
|
||||
static audio_codec_if_t *play_codec_if = NULL;
|
||||
static esp_codec_dev_handle_t play_dev = NULL;
|
||||
static i2c_master_bus_handle_t i2c_bus_handle = NULL;
|
||||
static uint32_t SDCard_Size = 0;
|
||||
|
||||
|
||||
esp_codec_dev_handle_t esp_ret_play_dev(void)
|
||||
{
|
||||
return play_dev;
|
||||
}
|
||||
|
||||
i2c_master_bus_handle_t esp_ret_i2c_handle(void)
|
||||
{
|
||||
return i2c_bus_handle;
|
||||
}
|
||||
|
||||
uint32_t Get_SD_Size(void)
|
||||
{
|
||||
return SDCard_Size;
|
||||
}
|
||||
|
||||
static esp_err_t i2c_master_init(void)
|
||||
{
|
||||
const i2c_master_bus_config_t bus_config = {
|
||||
.i2c_port = I2C_NUM,
|
||||
.sda_io_num = GPIO_I2C_SDA,
|
||||
.scl_io_num = GPIO_I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
};
|
||||
|
||||
esp_err_t ret = i2c_new_master_bus(&bus_config, &i2c_bus_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize I2C bus: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "I2C bus initialized successfully");
|
||||
return ESP_OK; // 返回 ESP_OK 表示成功
|
||||
}
|
||||
|
||||
|
||||
esp_err_t bsp_codec_adc_init(int sample_rate)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
// Do initialize of related interface: data_if, ctrl_if and gpio_if
|
||||
audio_codec_i2s_cfg_t i2s_cfg = {
|
||||
.port = I2S_NUM_1,
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
.rx_handle = rx_handle,
|
||||
.tx_handle = NULL,
|
||||
#endif
|
||||
};
|
||||
record_data_if = audio_codec_new_i2s_data(&i2s_cfg);
|
||||
|
||||
audio_codec_i2c_cfg_t i2c_cfg = {.addr = ES7210_CODEC_DEFAULT_ADDR,.bus_handle = i2c_bus_handle};
|
||||
record_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
|
||||
// New input codec interface
|
||||
es7210_codec_cfg_t es7210_cfg = {
|
||||
.ctrl_if = record_ctrl_if,
|
||||
.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4,
|
||||
};
|
||||
record_codec_if = es7210_codec_new(&es7210_cfg);
|
||||
// New input codec device
|
||||
esp_codec_dev_cfg_t dev_cfg = {
|
||||
.codec_if = record_codec_if,
|
||||
.data_if = record_data_if,
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN,
|
||||
};
|
||||
record_dev = esp_codec_dev_new(&dev_cfg);
|
||||
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.sample_rate = 16000,
|
||||
.channel = 2,
|
||||
.bits_per_sample = 32,
|
||||
};
|
||||
esp_codec_dev_open(record_dev, &fs);
|
||||
// esp_codec_dev_set_in_gain(record_dev, RECORD_VOLUME);
|
||||
esp_codec_dev_set_in_channel_gain(record_dev, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), RECORD_VOLUME);
|
||||
esp_codec_dev_set_in_channel_gain(record_dev, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1), RECORD_VOLUME);
|
||||
esp_codec_dev_set_in_channel_gain(record_dev, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(2), RECORD_VOLUME);
|
||||
esp_codec_dev_set_in_channel_gain(record_dev, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(3), RECORD_VOLUME);
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t bsp_codec_dac_init(int sample_rate, int channel_format, int bits_per_chan)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
// Do initialize of related interface: data_if, ctrl_if and gpio_if
|
||||
audio_codec_i2s_cfg_t i2s_cfg = {
|
||||
.port = I2S_NUM_1,
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
.rx_handle = NULL,
|
||||
.tx_handle = tx_handle,
|
||||
#endif
|
||||
};
|
||||
play_data_if = audio_codec_new_i2s_data(&i2s_cfg);
|
||||
|
||||
audio_codec_i2c_cfg_t i2c_cfg = {.addr = ES8311_CODEC_DEFAULT_ADDR,.bus_handle = i2c_bus_handle};
|
||||
play_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
|
||||
play_gpio_if = audio_codec_new_gpio();
|
||||
// New output codec interface
|
||||
es8311_codec_cfg_t es8311_cfg = {
|
||||
.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC,
|
||||
.ctrl_if = play_ctrl_if,
|
||||
.gpio_if = play_gpio_if,
|
||||
.pa_pin = GPIO_PWR_CTRL,
|
||||
.use_mclk = false,
|
||||
};
|
||||
play_codec_if = es8311_codec_new(&es8311_cfg);
|
||||
// New output codec device
|
||||
esp_codec_dev_cfg_t dev_cfg = {
|
||||
.codec_if = play_codec_if,
|
||||
.data_if = play_data_if,
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
|
||||
};
|
||||
play_dev = esp_codec_dev_new(&dev_cfg);
|
||||
|
||||
esp_codec_dev_sample_info_t fs = {
|
||||
.bits_per_sample = bits_per_chan,
|
||||
.sample_rate = sample_rate,
|
||||
.channel = channel_format,
|
||||
};
|
||||
esp_codec_dev_set_out_vol(play_dev, PLAYER_VOLUME);
|
||||
esp_codec_dev_open(play_dev, &fs);
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_codec_adc_deinit()
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
if (record_dev) {
|
||||
esp_codec_dev_close(record_dev);
|
||||
esp_codec_dev_delete(record_dev);
|
||||
record_dev = NULL;
|
||||
}
|
||||
|
||||
// Delete codec interface
|
||||
if (record_codec_if) {
|
||||
audio_codec_delete_codec_if(record_codec_if);
|
||||
record_codec_if = NULL;
|
||||
}
|
||||
|
||||
// Delete codec control interface
|
||||
if (record_ctrl_if) {
|
||||
audio_codec_delete_ctrl_if(record_ctrl_if);
|
||||
record_ctrl_if = NULL;
|
||||
}
|
||||
|
||||
// Delete codec data interface
|
||||
if (record_data_if) {
|
||||
audio_codec_delete_data_if(record_data_if);
|
||||
record_data_if = NULL;
|
||||
}
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_codec_dac_deinit()
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
if (play_dev) {
|
||||
esp_codec_dev_close(play_dev);
|
||||
esp_codec_dev_delete(play_dev);
|
||||
play_dev = NULL;
|
||||
}
|
||||
|
||||
// Delete codec interface
|
||||
if (play_codec_if) {
|
||||
audio_codec_delete_codec_if(play_codec_if);
|
||||
play_codec_if = NULL;
|
||||
}
|
||||
|
||||
// Delete codec control interface
|
||||
if (play_ctrl_if) {
|
||||
audio_codec_delete_ctrl_if(play_ctrl_if);
|
||||
play_ctrl_if = NULL;
|
||||
}
|
||||
|
||||
if (play_gpio_if) {
|
||||
audio_codec_delete_gpio_if(play_gpio_if);
|
||||
play_gpio_if = NULL;
|
||||
}
|
||||
|
||||
// Delete codec data interface
|
||||
if (play_data_if) {
|
||||
audio_codec_delete_data_if(play_data_if);
|
||||
play_data_if = NULL;
|
||||
}
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t esp_audio_set_play_vol(int volume)
|
||||
{
|
||||
if (!play_dev) {
|
||||
ESP_LOGE(TAG, "DAC codec init fail");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
esp_codec_dev_set_out_vol(play_dev, volume);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_audio_get_play_vol(int *volume)
|
||||
{
|
||||
if (!play_dev) {
|
||||
ESP_LOGE(TAG, "DAC codec init fail");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
esp_codec_dev_get_out_vol(play_dev, volume);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// static esp_err_t bsp_i2s_init(i2s_port_t i2s_num, uint32_t sample_rate, i2s_channel_fmt_t channel_format, i2s_bits_per_chan_t bits_per_chan)
|
||||
static esp_err_t bsp_i2s_init(int i2s_num, uint32_t sample_rate, int channel_format, int bits_per_chan)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
i2s_slot_mode_t channel_fmt = I2S_SLOT_MODE_STEREO;
|
||||
if (channel_format == 1) {
|
||||
channel_fmt = I2S_SLOT_MODE_MONO;
|
||||
} else if (channel_format == 2) {
|
||||
channel_fmt = I2S_SLOT_MODE_STEREO;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unable to configure channel_format %d", channel_format);
|
||||
channel_format = 1;
|
||||
channel_fmt = I2S_SLOT_MODE_MONO;
|
||||
}
|
||||
|
||||
if (bits_per_chan != 16 && bits_per_chan != 32) {
|
||||
ESP_LOGE(TAG, "Unable to configure bits_per_chan %d", bits_per_chan);
|
||||
bits_per_chan = 32;
|
||||
}
|
||||
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(i2s_num, I2S_ROLE_MASTER);
|
||||
ret_val |= i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle);
|
||||
i2s_std_config_t std_cfg = I2S_CONFIG_DEFAULT(sample_rate, channel_fmt, bits_per_chan);
|
||||
ret_val |= i2s_channel_init_std_mode(tx_handle, &std_cfg);
|
||||
ret_val |= i2s_channel_init_std_mode(rx_handle, &std_cfg);
|
||||
ret_val |= i2s_channel_enable(tx_handle);
|
||||
ret_val |= i2s_channel_enable(rx_handle);
|
||||
#else
|
||||
i2s_channel_fmt_t channel_fmt = I2S_CHANNEL_FMT_RIGHT_LEFT;
|
||||
if (channel_format == 1) {
|
||||
channel_fmt = I2S_CHANNEL_FMT_ONLY_LEFT;
|
||||
} else if (channel_format == 2) {
|
||||
channel_fmt = I2S_CHANNEL_FMT_RIGHT_LEFT;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unable to configure channel_format %d", channel_format);
|
||||
channel_format = 1;
|
||||
channel_fmt = I2S_CHANNEL_FMT_ONLY_LEFT;
|
||||
}
|
||||
|
||||
if (bits_per_chan != 16 && bits_per_chan != 32) {
|
||||
ESP_LOGE(TAG, "Unable to configure bits_per_chan %d", bits_per_chan);
|
||||
bits_per_chan = 16;
|
||||
}
|
||||
|
||||
i2s_config_t i2s_config = I2S_CONFIG_DEFAULT(sample_rate, channel_fmt, bits_per_chan);
|
||||
|
||||
i2s_pin_config_t pin_config = {
|
||||
.bck_io_num = GPIO_I2S_SCLK,
|
||||
.ws_io_num = GPIO_I2S_LRCK,
|
||||
.data_out_num = GPIO_I2S_DOUT,
|
||||
.data_in_num = GPIO_I2S_SDIN,
|
||||
.mck_io_num = GPIO_I2S_MCLK,
|
||||
};
|
||||
|
||||
ret_val |= i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
|
||||
ret_val |= i2s_set_pin(i2s_num, &pin_config);
|
||||
#endif
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_i2s_deinit(int i2s_num)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
if (i2s_num == I2S_NUM_1 && rx_handle) {
|
||||
ret_val |= i2s_channel_disable(rx_handle);
|
||||
ret_val |= i2s_del_channel(rx_handle);
|
||||
rx_handle = NULL;
|
||||
} else if (i2s_num == I2S_NUM_0 && tx_handle) {
|
||||
ret_val |= i2s_channel_disable(tx_handle);
|
||||
ret_val |= i2s_del_channel(tx_handle);
|
||||
tx_handle = NULL;
|
||||
}
|
||||
#else
|
||||
ret_val |= i2s_stop(i2s_num);
|
||||
ret_val |= i2s_driver_uninstall(i2s_num);
|
||||
#endif
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_codec_init(int adc_sample_rate, int dac_sample_rate, int dac_channel_format, int dac_bits_per_chan)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
ret_val |= bsp_codec_adc_init(adc_sample_rate);
|
||||
ret_val |= bsp_codec_dac_init(dac_sample_rate, dac_channel_format, dac_bits_per_chan);
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_codec_deinit()
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
ret_val |= bsp_codec_adc_deinit();
|
||||
ret_val |= bsp_codec_dac_deinit();
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t esp_audio_play(const int16_t* data, int length, uint32_t ticks_to_wait)
|
||||
{
|
||||
size_t bytes_write = 0;
|
||||
esp_err_t ret = ESP_OK;
|
||||
if (!play_dev) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int out_length= length;
|
||||
int audio_time = 1;
|
||||
audio_time *= (16000 / s_play_sample_rate);
|
||||
audio_time *= (2 / s_play_channel_format);
|
||||
|
||||
int *data_out = NULL;
|
||||
if (s_bits_per_chan != 32) {
|
||||
out_length = length * 2;
|
||||
data_out = malloc(out_length);
|
||||
for (int i = 0; i < length / sizeof(int16_t); i++) {
|
||||
int ret = data[i];
|
||||
data_out[i] = ret << 16;
|
||||
}
|
||||
}
|
||||
|
||||
int *data_out_1 = NULL;
|
||||
if (s_play_channel_format != 2 || s_play_sample_rate != 16000) {
|
||||
out_length *= audio_time;
|
||||
data_out_1 = malloc(out_length);
|
||||
int *tmp_data = NULL;
|
||||
if (data_out != NULL) {
|
||||
tmp_data = data_out;
|
||||
} else {
|
||||
tmp_data = (int *)data;
|
||||
}
|
||||
|
||||
for (int i = 0; i < out_length / (audio_time * sizeof(int)); i++) {
|
||||
for (int j = 0; j < audio_time; j++) {
|
||||
data_out_1[audio_time * i + j] = tmp_data[i];
|
||||
}
|
||||
}
|
||||
if (data_out != NULL) {
|
||||
free(data_out);
|
||||
data_out = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (data_out != NULL) {
|
||||
ret = esp_codec_dev_write(play_dev, (void *)data_out, out_length);
|
||||
free(data_out);
|
||||
} else if (data_out_1 != NULL) {
|
||||
ret = esp_codec_dev_write(play_dev, (void *)data_out_1, out_length);
|
||||
free(data_out_1);
|
||||
} else {
|
||||
ret = esp_codec_dev_write(play_dev, (void *)data, length);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t esp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
size_t bytes_read;
|
||||
int audio_chunksize = buffer_len / (sizeof(int16_t) * ADC_I2S_CHANNEL);
|
||||
|
||||
ret = esp_codec_dev_read(record_dev, (void *)buffer, buffer_len);
|
||||
if (!is_get_raw_channel) {
|
||||
for (int i = 0; i < audio_chunksize; i++) {
|
||||
int16_t ref = buffer[4 * i + 0];
|
||||
buffer[3 * i + 0] = buffer[4 * i + 1];
|
||||
buffer[3 * i + 1] = buffer[4 * i + 3];
|
||||
buffer[3 * i + 2] = ref;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int esp_get_feed_channel(void)
|
||||
{
|
||||
return ADC_I2S_CHANNEL;
|
||||
}
|
||||
|
||||
char* esp_get_input_format(void)
|
||||
{
|
||||
return "RMNM";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
esp_err_t esp_board_init(uint32_t sample_rate, int channel_format, int bits_per_chan)
|
||||
{
|
||||
/*!< Initialize I2C bus, used for audio codec*/
|
||||
|
||||
i2c_master_init();
|
||||
s_play_sample_rate = sample_rate;
|
||||
|
||||
if (channel_format != 2 && channel_format != 1) {
|
||||
ESP_LOGE(TAG, "Unable to configure channel_format");
|
||||
channel_format = 2;
|
||||
}
|
||||
s_play_channel_format = channel_format;
|
||||
|
||||
if (bits_per_chan != 32 && bits_per_chan != 16) {
|
||||
ESP_LOGE(TAG, "Unable to configure bits_per_chan");
|
||||
bits_per_chan = 32;
|
||||
}
|
||||
s_bits_per_chan = bits_per_chan;
|
||||
|
||||
bsp_i2s_init(I2S_NUM_1, 16000, 2, 32);
|
||||
// Because record and play use the same i2s.
|
||||
bsp_codec_init(16000, 16000, 2, 32);
|
||||
|
||||
/* Initialize PA */
|
||||
/*gpio_config_t io_conf;
|
||||
memset(&io_conf, 0, sizeof(io_conf));
|
||||
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pin_bit_mask = ((1ULL << GPIO_PWR_CTRL));
|
||||
io_conf.pull_down_en = 0;
|
||||
io_conf.pull_up_en = 0;
|
||||
gpio_config(&io_conf);
|
||||
gpio_set_level(GPIO_PWR_CTRL, 1);*/
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_sdcard_init(char *mount_point, size_t max_files)
|
||||
{
|
||||
if (NULL != card) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Check if SD crad is supported */
|
||||
if (!FUNC_SDMMC_EN && !FUNC_SDSPI_EN) {
|
||||
ESP_LOGE(TAG, "SDMMC and SDSPI not supported on this board!");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
/**
|
||||
* @brief Options for mounting the filesystem.
|
||||
* If format_if_mount_failed is set to true, SD card will be partitioned and
|
||||
* formatted in case when mounting fails.
|
||||
*
|
||||
*/
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
.format_if_mount_failed = false,
|
||||
.max_files = max_files,
|
||||
.allocation_unit_size = 16 * 1024
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Use settings defined above to initialize SD card and mount FAT filesystem.
|
||||
* Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions.
|
||||
* Please check its source code and implement error recovery when developing
|
||||
* production applications.
|
||||
*
|
||||
*/
|
||||
sdmmc_host_t host =
|
||||
#if FUNC_SDMMC_EN
|
||||
SDMMC_HOST_DEFAULT();
|
||||
#else
|
||||
SDSPI_HOST_DEFAULT();
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = GPIO_SDSPI_MOSI,
|
||||
.miso_io_num = GPIO_SDSPI_MISO,
|
||||
.sclk_io_num = GPIO_SDSPI_SCLK,
|
||||
.quadwp_io_num = GPIO_NUM_NC,
|
||||
.quadhd_io_num = GPIO_NUM_NC,
|
||||
.max_transfer_sz = 4000,
|
||||
};
|
||||
ret_val = spi_bus_initialize(host.slot, &bus_cfg, SPI_DMA_CH_AUTO);
|
||||
if (ret_val != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize bus.");
|
||||
return ret_val;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
* Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
*
|
||||
*/
|
||||
#if FUNC_SDMMC_EN
|
||||
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
|
||||
#else
|
||||
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
|
||||
#endif
|
||||
|
||||
#if FUNC_SDMMC_EN
|
||||
/* Config SD data width. 0, 4 or 8. Currently for SD card, 8 bit is not supported. */
|
||||
slot_config.width = SDMMC_BUS_WIDTH;
|
||||
|
||||
/**
|
||||
* @brief On chips where the GPIOs used for SD card can be configured, set them in
|
||||
* the slot_config structure.
|
||||
*
|
||||
*/
|
||||
#if SOC_SDMMC_USE_GPIO_MATRIX
|
||||
slot_config.clk = GPIO_SDMMC_CLK;
|
||||
slot_config.cmd = GPIO_SDMMC_CMD;
|
||||
slot_config.d0 = GPIO_SDMMC_D0;
|
||||
slot_config.d1 = GPIO_SDMMC_D1;
|
||||
slot_config.d2 = GPIO_SDMMC_D2;
|
||||
slot_config.d3 = GPIO_SDMMC_D3;
|
||||
#endif
|
||||
slot_config.cd = GPIO_SDMMC_DET;
|
||||
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
|
||||
#else
|
||||
slot_config.gpio_cs = GPIO_SDSPI_CS;
|
||||
slot_config.host_id = host.slot;
|
||||
#endif
|
||||
/**
|
||||
* @brief Enable internal pullups on enabled pins. The internal pullups
|
||||
* are insufficient however, please make sure 10k external pullups are
|
||||
* connected on the bus. This is for debug / example purpose only.
|
||||
*/
|
||||
|
||||
/* get FAT filesystem on SD card registered in VFS. */
|
||||
ret_val =
|
||||
#if FUNC_SDMMC_EN
|
||||
esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);
|
||||
#else
|
||||
esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
|
||||
#endif
|
||||
|
||||
/* Check for SDMMC mount result. */
|
||||
if (ret_val != ESP_OK) {
|
||||
if (ret_val == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret_val));
|
||||
}
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
/* Card has been initialized, print its properties. */
|
||||
sdmmc_card_print_info(stdout, card);
|
||||
SDCard_Size = ((uint64_t) card->csd.capacity) * card->csd.sector_size / (1024 * 1024);
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t esp_sdcard_deinit(char *mount_point)
|
||||
{
|
||||
if (NULL == mount_point) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Unmount an SD card from the FAT filesystem and release resources acquired */
|
||||
esp_err_t ret_val = esp_vfs_fat_sdcard_unmount(mount_point, card);
|
||||
|
||||
/* Make SD/MMC card information structure pointer NULL */
|
||||
card = NULL;
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
#define MOUNT_POINT "/sdcard"
|
||||
|
||||
static const char *SD_TAG = "SD";
|
||||
|
||||
|
||||
|
||||
uint16_t Folder_retrieval(const char* directory, const char* fileExtension, char File_Name[][MAX_FILE_NAME_SIZE], uint16_t maxFiles)
|
||||
{
|
||||
DIR *dir = opendir(directory);
|
||||
if (dir == NULL) {
|
||||
ESP_LOGE(SD_TAG, "Path: <%s> does not exist", directory);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t fileCount = 0;
|
||||
struct dirent *entry;
|
||||
|
||||
while ((entry = readdir(dir)) != NULL && fileCount < maxFiles) {
|
||||
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *dot = strrchr(entry->d_name, '.');
|
||||
if (dot != NULL && dot != entry->d_name) {
|
||||
|
||||
if (strcasecmp(dot, fileExtension) == 0) {
|
||||
strncpy(File_Name[fileCount], entry->d_name, MAX_FILE_NAME_SIZE - 1);
|
||||
File_Name[fileCount][MAX_FILE_NAME_SIZE - 1] = '\0';
|
||||
|
||||
char filePath[MAX_PATH_SIZE];
|
||||
snprintf(filePath, MAX_PATH_SIZE, "%s/%s", directory, entry->d_name);
|
||||
|
||||
printf("File found: %s\r\n", filePath);
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
else{
|
||||
|
||||
// printf("No extension found for file: %s\r\n", entry->d_name);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
if (fileCount > 0) {
|
||||
ESP_LOGI(SD_TAG, "Retrieved %d files with extension '%s'", fileCount, fileExtension);
|
||||
} else {
|
||||
ESP_LOGW(SD_TAG, "No files with extension '%s' found in directory: %s", fileExtension, directory);
|
||||
}
|
||||
|
||||
return fileCount;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "driver/i2s_tdm.h"
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
#include "esp_idf_version.h"
|
||||
|
||||
#include "esp_codec_dev.h"
|
||||
#include "esp_codec_dev_defaults.h"
|
||||
#include "esp_codec_dev_os.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "device_config.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAX_FILE_NAME_SIZE 100 // Define maximum file name size
|
||||
#define MAX_PATH_SIZE 512 // Define a larger size for the full path
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-CAM-OVxxxx I2C GPIO defineation
|
||||
*
|
||||
*/
|
||||
#define I2C_NUM (0)
|
||||
#define GPIO_I2C_SCL (GPIO_NUM_10)
|
||||
#define GPIO_I2C_SDA (GPIO_NUM_11)
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-CAM-OVxxxx SDMMC GPIO defination
|
||||
*
|
||||
* @note Only avaliable when PMOD connected
|
||||
*/
|
||||
#define FUNC_SDMMC_EN (1)
|
||||
#define SDMMC_BUS_WIDTH (1)
|
||||
#define GPIO_SDMMC_CLK (GPIO_NUM_40)
|
||||
#define GPIO_SDMMC_CMD (GPIO_NUM_42)
|
||||
#define GPIO_SDMMC_D0 (GPIO_NUM_41)
|
||||
#define GPIO_SDMMC_D1 (GPIO_NUM_NC)
|
||||
#define GPIO_SDMMC_D2 (GPIO_NUM_NC)
|
||||
#define GPIO_SDMMC_D3 (GPIO_NUM_NC)
|
||||
#define GPIO_SDMMC_DET (GPIO_NUM_NC)
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-CAM-OVxxxx SDSPI GPIO definationv
|
||||
*
|
||||
*/
|
||||
#define FUNC_SDSPI_EN (0)
|
||||
#define SDSPI_HOST (SPI2_HOST)
|
||||
#define GPIO_SDSPI_CS (GPIO_NUM_NC)
|
||||
#define GPIO_SDSPI_SCLK (GPIO_NUM_NC)
|
||||
#define GPIO_SDSPI_MISO (GPIO_NUM_NC)
|
||||
#define GPIO_SDSPI_MOSI (GPIO_NUM_NC)
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-CAM-OVxxxx I2S GPIO defination
|
||||
*
|
||||
*/
|
||||
#define FUNC_I2S_EN (1)
|
||||
#define GPIO_I2S_LRCK (GPIO_NUM_14)
|
||||
#define GPIO_I2S_MCLK (GPIO_NUM_12)
|
||||
#define GPIO_I2S_SCLK (GPIO_NUM_13)
|
||||
|
||||
#ifdef MODEL_PROTO_1
|
||||
#define GPIO_I2S_SDIN (GPIO_NUM_16) // Swapped for PROTO-1
|
||||
#define GPIO_I2S_DOUT (GPIO_NUM_15) // Swapped for PROTO-1
|
||||
#else
|
||||
#define GPIO_I2S_SDIN (GPIO_NUM_15)
|
||||
#define GPIO_I2S_DOUT (GPIO_NUM_16)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-CAM-OVxxxx I2S GPIO defination
|
||||
*
|
||||
*/
|
||||
#define FUNC_I2S0_EN (0)
|
||||
#define GPIO_I2S0_LRCK (GPIO_NUM_NC)
|
||||
#define GPIO_I2S0_MCLK (GPIO_NUM_NC)
|
||||
#define GPIO_I2S0_SCLK (GPIO_NUM_NC)
|
||||
#define GPIO_I2S0_SDIN (GPIO_NUM_NC)
|
||||
#define GPIO_I2S0_DOUT (GPIO_NUM_NC)
|
||||
|
||||
/**
|
||||
* @brief record configurations
|
||||
*
|
||||
*/
|
||||
#define RECORD_VOLUME (30.0)
|
||||
|
||||
/**
|
||||
* @brief player configurations
|
||||
*
|
||||
*/
|
||||
#define PLAYER_VOLUME (60)
|
||||
|
||||
/**
|
||||
* @brief ESP32-S3-HMI-DevKit power control IO
|
||||
*
|
||||
* @note Some power control pins might not be listed yet
|
||||
*
|
||||
*/
|
||||
//#define FUNC_PWR_CTRL (1)
|
||||
#define GPIO_PWR_CTRL (-1)
|
||||
#define GPIO_PWR_ON_LEVEL (1)
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
|
||||
#define I2S_CONFIG_DEFAULT(sample_rate, channel_fmt, bits_per_chan) { \
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000), \
|
||||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(32, I2S_SLOT_MODE_STEREO), \
|
||||
.gpio_cfg = { \
|
||||
.mclk = GPIO_I2S_MCLK, \
|
||||
.bclk = GPIO_I2S_SCLK, \
|
||||
.ws = GPIO_I2S_LRCK, \
|
||||
.dout = GPIO_I2S_DOUT, \
|
||||
.din = GPIO_I2S_SDIN, \
|
||||
}, \
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#define I2S_CONFIG_DEFAULT(sample_rate, channel_fmt, bits_per_chan) { \
|
||||
.mode = I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX, \
|
||||
.sample_rate = 16000, \
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, \
|
||||
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, \
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S, \
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, \
|
||||
.dma_buf_count = 6, \
|
||||
.dma_buf_len = 160, \
|
||||
.use_apll = false, \
|
||||
.tx_desc_auto_clear = true, \
|
||||
.fixed_mclk = 0, \
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, \
|
||||
.bits_per_chan = I2S_BITS_PER_CHAN_32BIT, \
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/* LCD settings */
|
||||
#define EXAMPLE_LCD_SPI_NUM (SPI3_HOST)
|
||||
#define EXAMPLE_LCD_PIXEL_CLK_HZ (80 * 1000 * 1000)
|
||||
#define EXAMPLE_LCD_CMD_BITS (8)
|
||||
#define EXAMPLE_LCD_PARAM_BITS (8)
|
||||
#define EXAMPLE_LCD_BITS_PER_PIXEL (16)
|
||||
#define EXAMPLE_LCD_DRAW_BUFF_DOUBLE (1)
|
||||
#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT (50)
|
||||
#define EXAMPLE_LCD_BL_ON_LEVEL (1)
|
||||
#define Backlight_MAX 100
|
||||
#define DEFAULT_BACKLIGHT 80
|
||||
|
||||
/* LCD pins */
|
||||
#define EXAMPLE_LCD_GPIO_SCLK (GPIO_NUM_5)
|
||||
#define EXAMPLE_LCD_GPIO_MOSI (GPIO_NUM_1)
|
||||
#define EXAMPLE_LCD_GPIO_RST (GPIO_NUM_NC)
|
||||
#define EXAMPLE_LCD_GPIO_DC (GPIO_NUM_3)
|
||||
#define EXAMPLE_LCD_GPIO_CS (GPIO_NUM_6)
|
||||
#define EXAMPLE_LCD_GPIO_BL (GPIO_NUM_NC)
|
||||
|
||||
/* LCD touch pins */
|
||||
#define EXAMPLE_TOUCH_GPIO_RST (GPIO_NUM_NC)
|
||||
|
||||
#ifdef CONFIG_WAVESHARE_1_47INCH_TOUCH_LCD
|
||||
#define EXAMPLE_LCD_H_RES (172)
|
||||
#define EXAMPLE_LCD_V_RES (320)
|
||||
#define EXAMPLE_TOUCH_GPIO_INT (GPIO_NUM_9)
|
||||
#elif defined(CONFIG_WAVESHARE_2INCH_TOUCH_LCD)
|
||||
#define EXAMPLE_LCD_H_RES (240)
|
||||
#define EXAMPLE_LCD_V_RES (320)
|
||||
#define EXAMPLE_TOUCH_GPIO_INT (GPIO_NUM_9)
|
||||
#elif defined(CONFIG_WAVESHARE_2_8INCH_TOUCH_LCD)
|
||||
#define EXAMPLE_LCD_H_RES (240)
|
||||
#define EXAMPLE_LCD_V_RES (320)
|
||||
#define EXAMPLE_TOUCH_GPIO_INT (GPIO_NUM_NC)
|
||||
#elif defined(CONFIG_WAVESHARE_3_5INCH_TOUCH_LCD)
|
||||
#define EXAMPLE_LCD_H_RES (320)
|
||||
#define EXAMPLE_LCD_V_RES (480)
|
||||
#define EXAMPLE_TOUCH_GPIO_INT (GPIO_NUM_9)
|
||||
#endif
|
||||
|
||||
// GPIO assignment
|
||||
#ifdef MODEL_PROTO_1
|
||||
#define LED_STRIP_GPIO_PIN 9
|
||||
#else
|
||||
#define LED_STRIP_GPIO_PIN 38
|
||||
#endif
|
||||
#define LED_STRIP_LED_COUNT 7
|
||||
|
||||
|
||||
esp_err_t esp_board_init(uint32_t sample_rate, int channel_format, int bits_per_chan);
|
||||
|
||||
esp_err_t esp_sdcard_init(char *mount_point, size_t max_files);
|
||||
esp_err_t esp_sdcard_deinit(char *mount_point);
|
||||
|
||||
esp_err_t esp_audio_play(const int16_t* data, int length, uint32_t ticks_to_wait);
|
||||
|
||||
esp_err_t esp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len);
|
||||
int esp_get_feed_channel(void);
|
||||
char* esp_get_input_format(void);
|
||||
esp_err_t esp_audio_set_play_vol(int volume);
|
||||
esp_err_t esp_audio_get_play_vol(int *volume);
|
||||
|
||||
i2c_master_bus_handle_t esp_ret_i2c_handle(void);
|
||||
esp_codec_dev_handle_t esp_ret_play_dev();
|
||||
uint32_t Get_SD_Size(void);
|
||||
uint16_t Folder_retrieval(const char* directory, const char* fileExtension, char File_Name[][MAX_FILE_NAME_SIZE], uint16_t maxFiles) ;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
espressif/esp_audio_simple_player: ^0.9.4~1
|
||||
espressif/esp_io_expander_tca95xx_16bit: ^2.0.1
|
||||
espressif/led_strip: ^3.0.1~1
|
||||
espressif/esp_websocket_client: "^1.7.0"
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "led_strip.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "bsp_board.h"
|
||||
#include "tca9555_driver.h"
|
||||
#include "audio_driver.h"
|
||||
#include "wifi_connect.h"
|
||||
#include "socket_client.h"
|
||||
#include "state_manager.h"
|
||||
|
||||
static const char *TAG = "main";
|
||||
|
||||
// Converts HSV color space (Hue 0-359, Saturation 0-100, Value 0-100) to RGB (0-255)
|
||||
static void hsv_to_rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b)
|
||||
{
|
||||
h %= 360;
|
||||
uint32_t rgb_max = (v * 255) / 100;
|
||||
uint32_t rgb_min = (rgb_max * (100 - s)) / 100;
|
||||
uint32_t diff = h % 60;
|
||||
uint32_t rgb_adj = ((rgb_max - rgb_min) * diff) / 60;
|
||||
|
||||
switch (h / 60) {
|
||||
case 0:
|
||||
*r = rgb_max;
|
||||
*g = rgb_min + rgb_adj;
|
||||
*b = rgb_min;
|
||||
break;
|
||||
case 1:
|
||||
*r = rgb_max - rgb_adj;
|
||||
*g = rgb_max;
|
||||
*b = rgb_min;
|
||||
break;
|
||||
case 2:
|
||||
*r = rgb_min;
|
||||
*g = rgb_max;
|
||||
*b = rgb_min + rgb_adj;
|
||||
break;
|
||||
case 3:
|
||||
*r = rgb_min;
|
||||
*g = rgb_max - rgb_adj;
|
||||
*b = rgb_max;
|
||||
break;
|
||||
case 4:
|
||||
*r = rgb_min + rgb_adj;
|
||||
*g = rgb_min;
|
||||
*b = rgb_max;
|
||||
break;
|
||||
default:
|
||||
*r = rgb_max;
|
||||
*g = rgb_min;
|
||||
*b = rgb_max - rgb_adj;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static volatile led_state_t g_led_state = LED_STATE_OFF;
|
||||
|
||||
void set_led_state(led_state_t state)
|
||||
{
|
||||
g_led_state = state;
|
||||
ESP_LOGI(TAG, "LED state changed to: %d", state);
|
||||
}
|
||||
|
||||
led_state_t get_led_state(void)
|
||||
{
|
||||
return g_led_state;
|
||||
}
|
||||
|
||||
|
||||
// Background task to slowly fade the 7-LED ring in a smooth rainbow pattern or show status colors
|
||||
static void led_rainbow_task(void *pvParameters)
|
||||
{
|
||||
// LED strip general initialization
|
||||
led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = LED_STRIP_GPIO_PIN, // Use configured model pin (38 on DEV-1, 9 on PROTO-1)
|
||||
.max_leds = 7, // 7 LEDs in the ring
|
||||
.led_model = LED_MODEL_WS2812, // WS2812 model type
|
||||
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_RGB,
|
||||
.flags = {
|
||||
.invert_out = false,
|
||||
}
|
||||
};
|
||||
|
||||
// LED strip backend configuration (RMT)
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||
.mem_block_symbols = 0,
|
||||
.flags = {
|
||||
.with_dma = 0,
|
||||
}
|
||||
};
|
||||
|
||||
led_strip_handle_t led_strip;
|
||||
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
ESP_LOGI(TAG, "LED Strip initialized successfully.");
|
||||
|
||||
uint32_t hue = 0;
|
||||
bool blink_toggle = false;
|
||||
|
||||
while (1) {
|
||||
device_state_t st;
|
||||
state_get_current(&st);
|
||||
float br = st.led_brightness; // 0.0 to 1.0 coefficient
|
||||
|
||||
if (g_led_state == LED_STATE_OFF || br <= 0.001f) {
|
||||
// Turn off LEDs
|
||||
led_strip_clear(led_strip);
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // check back in 100ms
|
||||
}
|
||||
else if (g_led_state == LED_STATE_WIFI_CONNECTING) {
|
||||
// Flash red
|
||||
uint8_t r = (uint8_t)(255.0f * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (blink_toggle) {
|
||||
led_strip_set_pixel(led_strip, i, r, 0, 0);
|
||||
} else {
|
||||
led_strip_set_pixel(led_strip, i, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
blink_toggle = !blink_toggle;
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // flash every 500ms
|
||||
}
|
||||
else if (g_led_state == LED_STATE_WIFI_FAILED) {
|
||||
// Solid red
|
||||
uint8_t r = (uint8_t)(255.0f * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
led_strip_set_pixel(led_strip, i, r, 0, 0);
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // check back in 100ms
|
||||
}
|
||||
else if (g_led_state == LED_STATE_SERVER_CONNECTING) {
|
||||
// Flash orange: RGB (255, 128, 0)
|
||||
uint8_t r = (uint8_t)(255.0f * br);
|
||||
uint8_t g = (uint8_t)(128.0f * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (blink_toggle) {
|
||||
led_strip_set_pixel(led_strip, i, r, g, 0);
|
||||
} else {
|
||||
led_strip_set_pixel(led_strip, i, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
blink_toggle = !blink_toggle;
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // flash every 500ms
|
||||
}
|
||||
else if (g_led_state == LED_STATE_SERVER_FAILED) {
|
||||
// Solid red
|
||||
uint8_t r = (uint8_t)(255.0f * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
led_strip_set_pixel(led_strip, i, r, 0, 0);
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // check back in 100ms
|
||||
}
|
||||
else if (g_led_state == LED_STATE_UPDATING) {
|
||||
// Pulse blue slowly (triangle wave breathing animation)
|
||||
static int pulse_dir = 1;
|
||||
static int pulse_level = 20;
|
||||
|
||||
pulse_level += (pulse_dir * 8);
|
||||
if (pulse_level >= 255) {
|
||||
pulse_level = 255;
|
||||
pulse_dir = -1;
|
||||
} else if (pulse_level <= 20) {
|
||||
pulse_level = 20;
|
||||
pulse_dir = 1;
|
||||
}
|
||||
|
||||
uint8_t b_val = (uint8_t)(pulse_level * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
led_strip_set_pixel(led_strip, i, 0, 0, b_val);
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
vTaskDelay(pdMS_TO_TICKS(40)); // ~25 steps per second
|
||||
}
|
||||
else {
|
||||
// LED_STATE_CONNECTED: Rainbow rotate
|
||||
uint32_t val = (uint32_t)(100.0f * br);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
uint32_t r = 0, g = 0, b = 0;
|
||||
// Shift the hue slightly for each consecutive LED on the ring to create a rotating color wheel
|
||||
uint32_t shifted_hue = (hue + (i * 360 / 7)) % 360;
|
||||
|
||||
hsv_to_rgb(shifted_hue, 100, val, &r, &g, &b);
|
||||
led_strip_set_pixel(led_strip, i, r, g, b);
|
||||
}
|
||||
led_strip_refresh(led_strip);
|
||||
|
||||
// Advance the base hue color for the fade effect
|
||||
hue = (hue + 4) % 360;
|
||||
|
||||
// Delay of 25ms controls the speed of the rainbow rotation
|
||||
vTaskDelay(pdMS_TO_TICKS(25));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void mount_spiffs(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SPIFFS partition 'model'...");
|
||||
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = "/spiffs",
|
||||
.partition_label = "model",
|
||||
.max_files = 5,
|
||||
.format_if_mount_failed = true // format if it fails to mount
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_spiffs_register(&conf);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount or format filesystem");
|
||||
} else if (ret == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGE(TAG, "Failed to find SPIFFS partition in partition table");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_spiffs_info("model", &total, &used);
|
||||
if (ret == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MODEL_PROTO_1
|
||||
#include "driver/ledc.h"
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
|
||||
#define HAPTIC_PWM_GPIO GPIO_NUM_5
|
||||
#define HAPTIC_LEDC_CHANNEL LEDC_CHANNEL_0
|
||||
#define HAPTIC_LEDC_TIMER LEDC_TIMER_0
|
||||
#define HAPTIC_LEDC_MODE LEDC_LOW_SPEED_MODE
|
||||
|
||||
#define TOUCH_SENSOR_GPIO GPIO_NUM_7
|
||||
|
||||
static adc_oneshot_unit_handle_t s_adc_handle = NULL;
|
||||
|
||||
void proto1_haptics_init(void)
|
||||
{
|
||||
ledc_timer_config_t ledc_timer = {
|
||||
.speed_mode = HAPTIC_LEDC_MODE,
|
||||
.timer_num = HAPTIC_LEDC_TIMER,
|
||||
.duty_resolution = LEDC_TIMER_10_BIT,
|
||||
.freq_hz = 1000,
|
||||
.clk_cfg = LEDC_AUTO_CLK
|
||||
};
|
||||
ledc_timer_config(&ledc_timer);
|
||||
|
||||
ledc_channel_config_t ledc_channel = {
|
||||
.speed_mode = HAPTIC_LEDC_MODE,
|
||||
.channel = HAPTIC_LEDC_CHANNEL,
|
||||
.timer_sel = HAPTIC_LEDC_TIMER,
|
||||
.intr_type = LEDC_INTR_DISABLE,
|
||||
.gpio_num = HAPTIC_PWM_GPIO,
|
||||
.duty = 0,
|
||||
.hpoint = 0
|
||||
};
|
||||
ledc_channel_config(&ledc_channel);
|
||||
ESP_LOGI(TAG, "PROTO-1 haptic driver initialized on GPIO 5.");
|
||||
}
|
||||
|
||||
void proto1_haptics_set_intensity(uint8_t intensity)
|
||||
{
|
||||
if (intensity > 100) intensity = 100;
|
||||
uint32_t duty = (uint32_t)((intensity / 100.0f) * 1023.0f);
|
||||
ledc_set_duty(HAPTIC_LEDC_MODE, HAPTIC_LEDC_CHANNEL, duty);
|
||||
ledc_update_duty(HAPTIC_LEDC_MODE, HAPTIC_LEDC_CHANNEL);
|
||||
}
|
||||
|
||||
void proto1_adc_init(void)
|
||||
{
|
||||
adc_oneshot_unit_init_cfg_t init_config = {
|
||||
.unit_id = ADC_UNIT_1, // GPIO 4 is ADC1 Channel 3
|
||||
.ulp_mode = ADC_ULP_MODE_DISABLE,
|
||||
};
|
||||
if (adc_oneshot_new_unit(&init_config, &s_adc_handle) == ESP_OK) {
|
||||
adc_oneshot_chan_cfg_t config = {
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
};
|
||||
adc_oneshot_config_channel(s_adc_handle, ADC_CHANNEL_3, &config);
|
||||
ESP_LOGI(TAG, "PROTO-1 ADC light sensor driver initialized on GPIO 4 (ADC1_CH3).");
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t proto1_adc_read(void)
|
||||
{
|
||||
int raw_val = 0;
|
||||
if (s_adc_handle != NULL) {
|
||||
adc_oneshot_read(s_adc_handle, ADC_CHANNEL_3, &raw_val);
|
||||
}
|
||||
return (uint16_t)raw_val;
|
||||
}
|
||||
|
||||
void proto1_touch_init(void)
|
||||
{
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << TOUCH_SENSOR_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
ESP_LOGI(TAG, "PROTO-1 digital touch sensor driver initialized on GPIO 7.");
|
||||
}
|
||||
|
||||
static void proto1_sensor_task(void *pvParameters)
|
||||
{
|
||||
proto1_haptics_init();
|
||||
proto1_adc_init();
|
||||
proto1_touch_init();
|
||||
|
||||
while (1) {
|
||||
uint16_t light = proto1_adc_read();
|
||||
uint8_t touch = (uint8_t)(gpio_get_level(TOUCH_SENSOR_GPIO) == 0); // active low capacitive touch
|
||||
|
||||
state_set_sensors(light, touch);
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // poll every 100ms
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Ding Dong Audio Project Starting Up...");
|
||||
|
||||
// Initialize the centralized Unified State Manager (USM)
|
||||
state_manager_init();
|
||||
|
||||
// Initialize NVS storage (required for storing Wi-Fi configurations)
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// 1. Initialize board support package (audio codec, I2C master, and I2S)
|
||||
ESP_ERROR_CHECK(esp_board_init(16000, 2, 16));
|
||||
|
||||
// 2. Initialize TCA9555 IO expander driver (expands GPIO pins for key buttons/amplifiers)
|
||||
tca9555_driver_init();
|
||||
|
||||
// 3. Mount SPIFFS containing startup.mp3
|
||||
mount_spiffs();
|
||||
|
||||
// 4. Initialize simple audio player pipeline
|
||||
Audio_Play_Init();
|
||||
|
||||
// Adjust volume (0 to 100)
|
||||
Volume_Adjustment(85);
|
||||
|
||||
// 5. Start background task for the smooth rainbow LED fade
|
||||
xTaskCreate(led_rainbow_task, "led_rainbow_task", 4096, NULL, 5, NULL);
|
||||
|
||||
#ifdef MODEL_PROTO_1
|
||||
// 5b. Start background task to poll PROTO-1 hardware sensors (light & touch)
|
||||
xTaskCreate(proto1_sensor_task, "proto1_sensor_task", 4096, NULL, 4, NULL);
|
||||
#endif
|
||||
|
||||
// 6. Play startup.mp3 from SPIFFS partition (blocking until it finishes)
|
||||
ESP_LOGI(TAG, "Playing startup audio: 'Ding Dong, I'm turned on!'");
|
||||
esp_gmf_err_t err = Audio_Play_Music_To_End("file://spiffs/startup.mp3");
|
||||
if (err != ESP_GMF_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start audio playback: %d", err);
|
||||
}
|
||||
|
||||
// 7. Play connecting chime (blocking until it finishes)
|
||||
ESP_LOGI(TAG, "Playing connecting audio...");
|
||||
err = Audio_Play_Music_To_End("file://spiffs/connecting_wifi.mp3");
|
||||
if (err != ESP_GMF_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to play connecting audio: %d", err);
|
||||
}
|
||||
|
||||
// 8. Connect to Wi-Fi using credentials in wifi-credentials
|
||||
ESP_LOGI(TAG, "Starting Wi-Fi connection...");
|
||||
if (wifi_init_sta() == ESP_OK) {
|
||||
// 9. Attempt to connect to dev server
|
||||
ESP_LOGI(TAG, "Wi-Fi connected. Connecting to dev server...");
|
||||
socket_client_init();
|
||||
}
|
||||
|
||||
// Keep main task alive and monitor state
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_asp_state_t state = Audio_Get_Current_State();
|
||||
ESP_LOGD(TAG, "Audio player state: %s", esp_audio_simple_player_state_to_str(state));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_https_ota.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_netif_sntp.h"
|
||||
|
||||
#include "ota_manager.h"
|
||||
#include "socket_client.h" // for socket_send_raw()
|
||||
#include "state_manager.h" // for state_set_led()
|
||||
#include "esp_crt_bundle.h" // for esp_crt_bundle_attach()
|
||||
|
||||
static const char *TAG = "ota_manager";
|
||||
|
||||
// Guard against concurrent OTA runs
|
||||
static volatile bool s_ota_running = false;
|
||||
|
||||
// URL is copied here so the task owns its memory
|
||||
#define OTA_URL_MAX_LEN 256
|
||||
static char s_ota_url[OTA_URL_MAX_LEN];
|
||||
|
||||
|
||||
static esp_err_t ota_http_event(esp_http_client_event_t *evt)
|
||||
{
|
||||
if (evt->event_id == HTTP_EVENT_ERROR ||
|
||||
evt->event_id == HTTP_EVENT_DISCONNECTED) {
|
||||
|
||||
int tls_error = 0;
|
||||
int tls_flags = 0;
|
||||
|
||||
esp_err_t tls_result =
|
||||
esp_http_client_get_and_clear_last_tls_error(
|
||||
evt->client,
|
||||
&tls_error,
|
||||
&tls_flags
|
||||
);
|
||||
|
||||
ESP_LOGE(
|
||||
"OTA",
|
||||
"HTTP failure: tls_result=%s (0x%x), "
|
||||
"tls_error=0x%x, tls_flags=0x%x, errno=%d",
|
||||
esp_err_to_name(tls_result),
|
||||
tls_result,
|
||||
tls_error,
|
||||
tls_flags,
|
||||
esp_http_client_get_errno(evt->client)
|
||||
);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* OTA task
|
||||
* ------------------------------------------------------------------ */
|
||||
static void ota_task(void *pvParam)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "OTA task started. Firmware URL: %s", s_ota_url);
|
||||
|
||||
// Pulse blue slowly during update
|
||||
state_set_led(LED_STATE_UPDATING);
|
||||
|
||||
// ---- Configure HTTP client ----
|
||||
esp_http_client_config_t http_cfg = {
|
||||
.url = s_ota_url,
|
||||
.event_handler = ota_http_event,
|
||||
.timeout_ms = 30000,
|
||||
.keep_alive_enable = true,
|
||||
.max_redirection_count = 5,
|
||||
.skip_cert_common_name_check = false, // Keep false to ensure SNI remains enabled for Cloudflare Host mapping
|
||||
.crt_bundle_attach = esp_crt_bundle_attach, // Use system root CAs to validate Cloudflare certificates
|
||||
};
|
||||
|
||||
esp_https_ota_config_t ota_cfg = {
|
||||
.http_config = &http_cfg,
|
||||
};
|
||||
|
||||
// Gate HTTPS OTA until system clock is synchronized (prevents TLS certificate validity window checks from failing)
|
||||
bool is_https = (strncmp(s_ota_url, "https", 5) == 0);
|
||||
time_t now;
|
||||
time(&now);
|
||||
if (is_https) {
|
||||
if (now < 1704067200) {
|
||||
ESP_LOGI(TAG, "Waiting for system clock synchronization (up to 15s)...");
|
||||
esp_err_t sync_err = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(15000));
|
||||
time(&now);
|
||||
if (sync_err != ESP_OK || now < 1704067200) {
|
||||
ESP_LOGE(TAG, "HTTPS OTA blocked: system clock not synchronized");
|
||||
socket_send_ota_error("System clock not synchronized");
|
||||
state_set_led(LED_STATE_CONNECTED); // restore LED state
|
||||
s_ota_running = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log diagnostics: exact URL, current Unix time, total heap, internal heap
|
||||
ESP_LOGI(TAG, "Diagnostics before OTA begin: URL=%s, unix_time=%ld, free_heap_total=%d, free_heap_internal=%d",
|
||||
s_ota_url, (long)now, (int)esp_get_free_heap_size(), (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
||||
|
||||
// Enable verbose TLS logging to pinpoint exactly where the handshake fails
|
||||
esp_log_level_set("esp-tls", ESP_LOG_VERBOSE);
|
||||
esp_log_level_set("mbedtls", ESP_LOG_VERBOSE);
|
||||
|
||||
// ---- Begin OTA handle ----
|
||||
esp_https_ota_handle_t ota_handle = NULL;
|
||||
err = esp_https_ota_begin(&ota_cfg, &ota_handle);
|
||||
if (err != ESP_OK || ota_handle == NULL) {
|
||||
char reason[64];
|
||||
snprintf(reason, sizeof(reason), "esp_https_ota_begin failed: 0x%x", err);
|
||||
ESP_LOGE(TAG, "%s", reason);
|
||||
socket_send_ota_error(reason);
|
||||
state_set_led(LED_STATE_CONNECTED); // restore LED state
|
||||
s_ota_running = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Get image size for progress reporting ----
|
||||
int image_size = esp_https_ota_get_image_size(ota_handle);
|
||||
if (image_size <= 0) {
|
||||
// Some servers don't send Content-Length; treat as unknown
|
||||
image_size = 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "Firmware image size: %d bytes", image_size);
|
||||
|
||||
// ---- Stream and flash ----
|
||||
int bytes_written = 0;
|
||||
int last_reported_pct = -1;
|
||||
|
||||
while (1) {
|
||||
err = esp_https_ota_perform(ota_handle);
|
||||
if (err == ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
|
||||
bytes_written = esp_https_ota_get_image_len_read(ota_handle);
|
||||
|
||||
// Report progress every 5% (or every ~32KB if size unknown)
|
||||
int pct = (image_size > 0)
|
||||
? (int)((bytes_written * 100LL) / image_size)
|
||||
: -1;
|
||||
|
||||
if (pct >= 0 && pct != last_reported_pct && (pct % 5 == 0)) {
|
||||
socket_send_ota_progress(pct, bytes_written, image_size);
|
||||
last_reported_pct = pct;
|
||||
ESP_LOGI(TAG, "OTA progress: %d%% (%d / %d bytes)", pct, bytes_written, image_size);
|
||||
} else if (pct < 0 && (bytes_written - (last_reported_pct * 32768)) >= 32768) {
|
||||
socket_send_ota_progress(0, bytes_written, 0);
|
||||
last_reported_pct = bytes_written / 32768;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err == ESP_OK) {
|
||||
// Download complete
|
||||
break;
|
||||
}
|
||||
|
||||
// Any other error is fatal
|
||||
char reason[64];
|
||||
snprintf(reason, sizeof(reason), "esp_https_ota_perform failed: 0x%x", err);
|
||||
ESP_LOGE(TAG, "%s", reason);
|
||||
socket_send_ota_error(reason);
|
||||
esp_https_ota_abort(ota_handle);
|
||||
state_set_led(LED_STATE_CONNECTED); // restore LED state
|
||||
s_ota_running = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
bytes_written = esp_https_ota_get_image_len_read(ota_handle);
|
||||
|
||||
// ---- Validate and commit ----
|
||||
if (!esp_https_ota_is_complete_data_received(ota_handle)) {
|
||||
const char *reason = "Incomplete image received";
|
||||
ESP_LOGE(TAG, "%s", reason);
|
||||
socket_send_ota_error(reason);
|
||||
esp_https_ota_abort(ota_handle);
|
||||
state_set_led(LED_STATE_CONNECTED); // restore LED state
|
||||
s_ota_running = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_https_ota_finish(ota_handle);
|
||||
if (err != ESP_OK) {
|
||||
char reason[64];
|
||||
snprintf(reason, sizeof(reason), "esp_https_ota_finish failed: 0x%x", err);
|
||||
ESP_LOGE(TAG, "%s", reason);
|
||||
socket_send_ota_error(reason);
|
||||
state_set_led(LED_STATE_CONNECTED); // restore LED state
|
||||
s_ota_running = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Success ----
|
||||
socket_send_ota_progress(100, bytes_written, bytes_written);
|
||||
ESP_LOGI(TAG, "OTA complete! %d bytes written. Rebooting in 500ms...", bytes_written);
|
||||
socket_send_ota_complete();
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Public API
|
||||
* ------------------------------------------------------------------ */
|
||||
void ota_start(const char *url)
|
||||
{
|
||||
if (s_ota_running) {
|
||||
ESP_LOGW(TAG, "OTA already in progress, ignoring request.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (url == NULL || strlen(url) == 0) {
|
||||
ESP_LOGE(TAG, "ota_start called with empty URL");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy URL before spawning task
|
||||
strlcpy(s_ota_url, url, sizeof(s_ota_url));
|
||||
|
||||
s_ota_running = true;
|
||||
|
||||
ESP_LOGI(TAG, "Spawning OTA task for URL: %s", s_ota_url);
|
||||
|
||||
BaseType_t ret = xTaskCreate(
|
||||
ota_task,
|
||||
"ota_task",
|
||||
8192, // 8KB stack — enough for HTTP + flash ops
|
||||
NULL,
|
||||
5, // Priority: slightly above normal tasks
|
||||
NULL
|
||||
);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create OTA task");
|
||||
s_ota_running = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Start an OTA update from the given HTTP/HTTPS URL.
|
||||
*
|
||||
* Spawns a background FreeRTOS task that:
|
||||
* 1. Downloads the firmware binary from `url` using esp_http_client
|
||||
* 2. Writes it to the inactive OTA partition via esp_ota_ops
|
||||
* 3. Reports progress back to the server via WebSocket ota_progress messages
|
||||
* 4. On success: sends ota_complete, waits 500ms, then reboots
|
||||
* 5. On failure: sends ota_error with reason string
|
||||
*
|
||||
* HTTPS connections use the system certificate bundle
|
||||
* (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL) which includes all
|
||||
* standard root CAs and works with Cloudflare, AWS, etc.
|
||||
*
|
||||
* Only one OTA task may run at a time. Subsequent calls while one is
|
||||
* already running are silently ignored.
|
||||
*
|
||||
* @param url Null-terminated HTTP/HTTPS URL string for the firmware binary.
|
||||
* The string is copied internally; the caller may free it.
|
||||
*/
|
||||
void ota_start(const char *url);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,384 @@
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_websocket_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "socket_client.h"
|
||||
#include "audio_driver.h"
|
||||
#include "wifi_connect.h"
|
||||
#include "state_manager.h"
|
||||
#include "device_config.h"
|
||||
#include "ota_manager.h"
|
||||
|
||||
static const char *TAG = "socket_client";
|
||||
|
||||
static EventGroupHandle_t s_ws_event_group;
|
||||
#define WS_CONNECTED_BIT BIT0
|
||||
|
||||
static esp_websocket_client_handle_t s_client = NULL;
|
||||
|
||||
static void send_telemetry(void)
|
||||
{
|
||||
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
device_state_t st;
|
||||
state_get_current(&st);
|
||||
|
||||
int free_heap = esp_get_free_heap_size();
|
||||
int uptime = esp_timer_get_time() / 1000000; // seconds
|
||||
int rssi = 0;
|
||||
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
rssi = ap_info.rssi;
|
||||
}
|
||||
|
||||
char payload[512];
|
||||
int len = snprintf(payload, sizeof(payload),
|
||||
"{\"type\":\"state_report\",\"system\":{\"model\":\"%s\"},\"health\":{\"free_heap_bytes\":%d,\"uptime_seconds\":%d},\"network\":{\"wifi_rssi\":%d},\"peripherals\":{\"audio\":{\"volume\":%d},\"led_ring\":{\"state_id\":%d,\"brightness_coef\":%.2f},\"haptic_intensity\":%d,\"ambient_light\":%d,\"touch_active\":%s}}",
|
||||
DEVICE_MODEL, free_heap, uptime, rssi, st.volume, st.led_state, st.led_brightness,
|
||||
st.haptic_intensity, st.ambient_light, st.touch_active ? "true" : "false");
|
||||
|
||||
if (len > 0 && len < sizeof(payload)) {
|
||||
ESP_LOGI(TAG, "Sending state report: %s", payload);
|
||||
esp_websocket_client_send_text(s_client, payload, len, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
void trigger_telemetry_broadcast(void)
|
||||
{
|
||||
send_telemetry();
|
||||
}
|
||||
|
||||
static char *extract_json_string(const char *json, const char *key)
|
||||
{
|
||||
char *key_pos = strstr(json, key);
|
||||
if (!key_pos) return NULL;
|
||||
|
||||
// Find the colon after key
|
||||
char *colon = strchr(key_pos + strlen(key), ':');
|
||||
if (!colon) return NULL;
|
||||
|
||||
// Find the opening quote of the string value
|
||||
char *start_quote = strchr(colon, '"');
|
||||
if (!start_quote) return NULL;
|
||||
|
||||
// Find the closing quote, making sure we handle escaped quotes \"
|
||||
char *end_ptr = start_quote + 1;
|
||||
while (*end_ptr) {
|
||||
if (*end_ptr == '"' && *(end_ptr - 1) != '\\') {
|
||||
break;
|
||||
}
|
||||
end_ptr++;
|
||||
}
|
||||
if (*end_ptr != '"') return NULL;
|
||||
|
||||
size_t raw_len = end_ptr - start_quote - 1;
|
||||
char *out = malloc(raw_len + 1);
|
||||
if (!out) return NULL;
|
||||
|
||||
// Unescape the string while copying
|
||||
size_t out_idx = 0;
|
||||
for (size_t i = 0; i < raw_len; i++) {
|
||||
if (start_quote[1 + i] == '\\' && i + 1 < raw_len) {
|
||||
char next_char = start_quote[1 + i + 1];
|
||||
if (next_char == 'n') {
|
||||
out[out_idx++] = '\n';
|
||||
i++;
|
||||
} else if (next_char == 'r') {
|
||||
out[out_idx++] = '\r';
|
||||
i++;
|
||||
} else if (next_char == 't') {
|
||||
out[out_idx++] = '\t';
|
||||
i++;
|
||||
} else if (next_char == '\\' || next_char == '"' || next_char == '/') {
|
||||
out[out_idx++] = next_char;
|
||||
i++;
|
||||
} else {
|
||||
out[out_idx++] = '\\';
|
||||
}
|
||||
} else {
|
||||
out[out_idx++] = start_quote[1 + i];
|
||||
}
|
||||
}
|
||||
out[out_idx] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
|
||||
{
|
||||
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
||||
switch (event_id) {
|
||||
case WEBSOCKET_EVENT_CONNECTED:
|
||||
ESP_LOGI(TAG, "Websocket connected to server!");
|
||||
xEventGroupSetBits(s_ws_event_group, WS_CONNECTED_BIT);
|
||||
state_set_websocket_connected(true);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||
ESP_LOGI(TAG, "Websocket disconnected!");
|
||||
xEventGroupClearBits(s_ws_event_group, WS_CONNECTED_BIT);
|
||||
state_set_websocket_connected(false);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DATA:
|
||||
if (data->data_ptr != NULL && data->data_len > 0) {
|
||||
ESP_LOGI(TAG, "Received message: %.*s", data->data_len, (char *)data->data_ptr);
|
||||
|
||||
char *msg_str = malloc(data->data_len + 1);
|
||||
if (msg_str != NULL) {
|
||||
memcpy(msg_str, data->data_ptr, data->data_len);
|
||||
msg_str[data->data_len] = '\0';
|
||||
|
||||
if (strstr(msg_str, "\"set_volume\"") != NULL) {
|
||||
char *val_ptr = strstr(msg_str, "\"value\"");
|
||||
if (val_ptr != NULL) {
|
||||
int level = -1;
|
||||
if (sscanf(val_ptr, "\"value\"%*[^0-9]%d", &level) == 1) {
|
||||
if (level >= 0 && level <= 100) {
|
||||
ESP_LOGI(TAG, "Parsed set_volume command. Mutating state to: %d", level);
|
||||
state_set_volume((uint8_t)level);
|
||||
}
|
||||
}
|
||||
}
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"set_led\"") != NULL) {
|
||||
char *val_ptr = strstr(msg_str, "\"value\"");
|
||||
if (val_ptr != NULL) {
|
||||
if (strstr(val_ptr, "\"rainbow\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed set_led command: rainbow");
|
||||
state_set_led(LED_STATE_CONNECTED);
|
||||
} else if (strstr(val_ptr, "\"connecting\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed set_led command: connecting");
|
||||
state_set_led(LED_STATE_SERVER_CONNECTING);
|
||||
} else if (strstr(val_ptr, "\"failed\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed set_led command: failed");
|
||||
state_set_led(LED_STATE_SERVER_FAILED);
|
||||
} else if (strstr(val_ptr, "\"idle\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed set_led command: idle");
|
||||
state_set_led(LED_STATE_OFF);
|
||||
} else if (strstr(val_ptr, "\"updating\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed set_led command: updating");
|
||||
state_set_led(LED_STATE_UPDATING);
|
||||
}
|
||||
}
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"play_audio\"") != NULL) {
|
||||
char *val_ptr = strstr(msg_str, "\"value\"");
|
||||
if (val_ptr != NULL) {
|
||||
if (strstr(val_ptr, "\"startup\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed play_audio command: startup");
|
||||
Audio_Play_Music_To_End("file://spiffs/startup.mp3");
|
||||
} else if (strstr(val_ptr, "\"wifi_connecting\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed play_audio command: wifi_connecting");
|
||||
Audio_Play_Music_To_End("file://spiffs/connecting_wifi.mp3");
|
||||
} else if (strstr(val_ptr, "\"wifi_connected\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed play_audio command: wifi_connected");
|
||||
Audio_Play_Music_To_End("file://spiffs/connected_wifi.mp3");
|
||||
}
|
||||
}
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"set_brightness\"") != NULL) {
|
||||
char *val_ptr = strstr(msg_str, "\"value\"");
|
||||
if (val_ptr != NULL) {
|
||||
float brightness = -1.0f;
|
||||
if (sscanf(val_ptr, "\"value\"%*[^0-9.-]%f", &brightness) == 1) {
|
||||
if (brightness >= 0.0f && brightness <= 1.0f) {
|
||||
ESP_LOGI(TAG, "Parsed set_brightness command. Mutating state to: %f", brightness);
|
||||
state_set_brightness(brightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"set_haptics\"") != NULL) {
|
||||
char *val_ptr = strstr(msg_str, "\"value\"");
|
||||
if (val_ptr != NULL) {
|
||||
int intensity = -1;
|
||||
if (sscanf(val_ptr, "\"value\"%*[^0-9]%d", &intensity) == 1) {
|
||||
if (intensity >= 0 && intensity <= 100) {
|
||||
ESP_LOGI(TAG, "Parsed set_haptics command. Mutating state to: %d", intensity);
|
||||
state_set_haptic_intensity((uint8_t)intensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"query_state\"") != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed query_state command. Dispatching telemetry update...");
|
||||
send_telemetry();
|
||||
}
|
||||
else if (strstr(msg_str, "\"start_ota\"") != NULL) {
|
||||
char *url = extract_json_string(msg_str, "\"url\"");
|
||||
|
||||
if (url != NULL) {
|
||||
ESP_LOGI(TAG, "Parsed start_ota command. URL: %s", url);
|
||||
ota_start(url);
|
||||
free(url);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "start_ota: missing url field");
|
||||
}
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case WEBSOCKET_EVENT_ERROR:
|
||||
ESP_LOGE(TAG, "Websocket error occurred!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void socket_client_task(void *pvParameters)
|
||||
{
|
||||
bool was_connected = false;
|
||||
|
||||
while (1) {
|
||||
// Wait until Wi-Fi is connected
|
||||
while (!wifi_is_connected()) {
|
||||
state_set_wifi_connected(false);
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
state_set_wifi_connected(true);
|
||||
|
||||
EventBits_t bits = xEventGroupGetBits(s_ws_event_group);
|
||||
if (!(bits & WS_CONNECTED_BIT)) {
|
||||
// We are not connected!
|
||||
if (was_connected) {
|
||||
// We lost connection!
|
||||
ESP_LOGW(TAG, "WebSocket connection lost! Playing alert...");
|
||||
state_set_led(LED_STATE_SERVER_CONNECTING);
|
||||
Audio_Play_Music_To_End("file://spiffs/disconnected_server.mp3");
|
||||
was_connected = false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Attempting WebSocket connection...");
|
||||
state_set_led(LED_STATE_SERVER_CONNECTING);
|
||||
|
||||
// Play connecting audio: "Connecting to the dev server"
|
||||
Audio_Play_Music_To_End("file://spiffs/connecting_server.mp3");
|
||||
|
||||
// Stop (if running) and start connection
|
||||
esp_websocket_client_stop(s_client);
|
||||
esp_websocket_client_start(s_client);
|
||||
|
||||
// Wait up to 10 seconds for connection
|
||||
ESP_LOGI(TAG, "Waiting for websocket connection...");
|
||||
bits = xEventGroupWaitBits(s_ws_event_group, WS_CONNECTED_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(10000));
|
||||
|
||||
if (bits & WS_CONNECTED_BIT) {
|
||||
ESP_LOGI(TAG, "WebSocket connected successfully.");
|
||||
was_connected = true;
|
||||
state_set_led(LED_STATE_CONNECTED);
|
||||
Audio_Play_Music_To_End("file://spiffs/connected_server.mp3");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to connect to dev server within 10 seconds!");
|
||||
state_set_led(LED_STATE_SERVER_FAILED);
|
||||
esp_websocket_client_stop(s_client);
|
||||
Audio_Play_Music_To_End("file://spiffs/connect_server_fail.mp3");
|
||||
|
||||
// Play "Still not working, let me try again in a moment"
|
||||
Audio_Play_Music_To_End("file://spiffs/retry_later.mp3");
|
||||
|
||||
// Wait 10 seconds before next connection attempt
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
} else {
|
||||
// Already connected! Keep monitoring
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void socket_client_init(void)
|
||||
{
|
||||
s_ws_event_group = xEventGroupCreate();
|
||||
|
||||
// Load WebSocket configuration from SPIFFS
|
||||
char url_buf[128] = {0};
|
||||
FILE *f = fopen("/spiffs/server_url.txt", "r");
|
||||
if (f != NULL) {
|
||||
if (fgets(url_buf, sizeof(url_buf), f) != NULL) {
|
||||
// Strip trailing whitespace/newlines
|
||||
size_t len = strlen(url_buf);
|
||||
while (len > 0 && (url_buf[len - 1] == '\n' || url_buf[len - 1] == '\r' || url_buf[len - 1] == ' ')) {
|
||||
url_buf[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
ESP_LOGI(TAG, "Loaded WebSocket URI from SPIFFS: %s", url_buf);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
const char *ws_uri = "wss://example.com"; // Replace with your deployment endpoint
|
||||
if (strlen(url_buf) > 0) {
|
||||
ws_uri = url_buf;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No server_url.txt configuration found or empty, using default: %s", ws_uri);
|
||||
}
|
||||
|
||||
esp_websocket_client_config_t websocket_cfg = {
|
||||
.uri = ws_uri,
|
||||
.buffer_size = 4096,
|
||||
};
|
||||
|
||||
ESP_LOGI(TAG, "Initializing WebSocket client...");
|
||||
s_client = esp_websocket_client_init(&websocket_cfg);
|
||||
|
||||
// Register event handler
|
||||
ESP_ERROR_CHECK(esp_websocket_register_events(s_client, WEBSOCKET_EVENT_ANY, websocket_event_handler, NULL));
|
||||
|
||||
// Create the background connection task
|
||||
ESP_LOGI(TAG, "Starting WebSocket monitor task...");
|
||||
xTaskCreate(socket_client_task, "socket_client_task", 4096, NULL, 5, NULL);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* OTA WebSocket message helpers
|
||||
* Called from ota_manager.c to report OTA state back to the server.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
void socket_send_ota_progress(int percent, int bytes_written, int bytes_total)
|
||||
{
|
||||
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
||||
return;
|
||||
}
|
||||
char buf[128];
|
||||
int len = snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"ota_progress\",\"percent\":%d,\"bytes_written\":%d,\"bytes_total\":%d}",
|
||||
percent, bytes_written, bytes_total);
|
||||
if (len > 0 && len < (int)sizeof(buf)) {
|
||||
esp_websocket_client_send_text(s_client, buf, len, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
void socket_send_ota_complete(void)
|
||||
{
|
||||
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
||||
return;
|
||||
}
|
||||
const char *msg = "{\"type\":\"ota_complete\"}";
|
||||
esp_websocket_client_send_text(s_client, msg, strlen(msg), portMAX_DELAY);
|
||||
}
|
||||
|
||||
void socket_send_ota_error(const char *reason)
|
||||
{
|
||||
if (s_client == NULL || !(xEventGroupGetBits(s_ws_event_group) & WS_CONNECTED_BIT)) {
|
||||
return;
|
||||
}
|
||||
char buf[192];
|
||||
int len = snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"ota_error\",\"reason\":\"%s\"}", reason ? reason : "unknown");
|
||||
if (len > 0 && len < (int)sizeof(buf)) {
|
||||
esp_websocket_client_send_text(s_client, buf, len, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Initializes the websocket client and starts the connection process
|
||||
void socket_client_init(void);
|
||||
|
||||
// Triggers an immediate WebSocket telemetry report push
|
||||
void trigger_telemetry_broadcast(void);
|
||||
|
||||
// OTA progress/result messages (called from ota_manager.c)
|
||||
void socket_send_ota_progress(int percent, int bytes_written, int bytes_total);
|
||||
void socket_send_ota_complete(void);
|
||||
void socket_send_ota_error(const char *reason);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
wss://example.com
|
||||
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "state_manager.h"
|
||||
#include "audio_driver.h"
|
||||
#include "wifi_connect.h"
|
||||
#include "socket_client.h"
|
||||
|
||||
static const char *TAG = "state_manager";
|
||||
|
||||
// Centralized state container protected by a mutex
|
||||
static device_state_t s_device_state;
|
||||
static SemaphoreHandle_t s_state_mutex = NULL;
|
||||
|
||||
void state_manager_init(void)
|
||||
{
|
||||
s_state_mutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Set initial boot default states
|
||||
s_device_state.volume = 85;
|
||||
s_device_state.led_state = LED_STATE_OFF;
|
||||
s_device_state.led_brightness = 0.2f;
|
||||
s_device_state.wifi_connected = false;
|
||||
s_device_state.websocket_connected = false;
|
||||
s_device_state.haptic_intensity = 0;
|
||||
s_device_state.ambient_light = 0;
|
||||
s_device_state.touch_active = 0;
|
||||
|
||||
ESP_LOGI(TAG, "State Manager initialized successfully.");
|
||||
}
|
||||
|
||||
void state_get_current(device_state_t *out_state)
|
||||
{
|
||||
if (s_state_mutex == NULL) {
|
||||
return;
|
||||
}
|
||||
if (xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
memcpy(out_state, &s_device_state, sizeof(device_state_t));
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_volume(uint8_t volume)
|
||||
{
|
||||
if (volume > 100) volume = 100;
|
||||
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.volume != volume) {
|
||||
s_device_state.volume = volume;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "Volume state changed to %d. Updating DAC codec driver...", volume);
|
||||
Volume_Adjustment(volume);
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_led(led_state_t state)
|
||||
{
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.led_state != state) {
|
||||
s_device_state.led_state = state;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "LED state changed to %d. Updating visual animation thread...", state);
|
||||
set_led_state(state);
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_brightness(float brightness)
|
||||
{
|
||||
if (brightness < 0.0f) brightness = 0.0f;
|
||||
if (brightness > 1.0f) brightness = 1.0f;
|
||||
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.led_brightness != brightness) {
|
||||
s_device_state.led_brightness = brightness;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "LED brightness state changed to %f", brightness);
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_wifi_connected(bool connected)
|
||||
{
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.wifi_connected != connected) {
|
||||
s_device_state.wifi_connected = connected;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "Wi-Fi connection state changed to: %s", connected ? "CONNECTED" : "DISCONNECTED");
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_websocket_connected(bool connected)
|
||||
{
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.websocket_connected != connected) {
|
||||
s_device_state.websocket_connected = connected;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "WebSocket server state changed to: %s", connected ? "CONNECTED" : "DISCONNECTED");
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_haptic_intensity(uint8_t intensity)
|
||||
{
|
||||
if (intensity > 100) intensity = 100;
|
||||
|
||||
bool changed = false;
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
if (s_device_state.haptic_intensity != intensity) {
|
||||
s_device_state.haptic_intensity = intensity;
|
||||
changed = true;
|
||||
}
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG, "Haptic intensity changed to %d", intensity);
|
||||
#ifdef MODEL_PROTO_1
|
||||
extern void proto1_haptics_set_intensity(uint8_t intensity);
|
||||
proto1_haptics_set_intensity(intensity);
|
||||
#endif
|
||||
trigger_telemetry_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void state_set_sensors(uint16_t ambient_light, uint8_t touch_active)
|
||||
{
|
||||
if (s_state_mutex != NULL && xSemaphoreTake(s_state_mutex, portMAX_DELAY) == pdTRUE) {
|
||||
s_device_state.ambient_light = ambient_light;
|
||||
s_device_state.touch_active = touch_active;
|
||||
xSemaphoreGive(s_state_mutex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "wifi_connect.h" // for led_state_t definition
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint8_t volume;
|
||||
uint8_t led_state; // Explicit size to avoid enum size mismatch
|
||||
float led_brightness;
|
||||
uint8_t wifi_connected; // Explicit size to avoid bool size mismatch
|
||||
uint8_t websocket_connected; // Explicit size to avoid bool size mismatch
|
||||
uint8_t haptic_intensity; // Haptic PWM intensity (0-100)
|
||||
uint16_t ambient_light; // Analog light value (0-4095)
|
||||
uint8_t touch_active; // Touch sensor input state (0 or 1)
|
||||
} device_state_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize the state manager, locks, and default values.
|
||||
*/
|
||||
void state_manager_init(void);
|
||||
|
||||
/**
|
||||
* @brief Thread-safe query to get a copy of the current system states.
|
||||
*/
|
||||
void state_get_current(device_state_t *out_state);
|
||||
|
||||
/* --- Setter Mutators (Trigger Hooks & Hardware Actions) --- */
|
||||
void state_set_volume(uint8_t volume);
|
||||
void state_set_led(led_state_t state);
|
||||
void state_set_brightness(float brightness);
|
||||
void state_set_wifi_connected(bool connected);
|
||||
void state_set_websocket_connected(bool connected);
|
||||
void state_set_haptic_intensity(uint8_t intensity);
|
||||
void state_set_sensors(uint16_t ambient_light, uint8_t touch_active);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "tca9555_driver.h"
|
||||
#include "esp_log.h"
|
||||
#include "bsp_board.h"
|
||||
|
||||
|
||||
esp_io_expander_handle_t io_expander = NULL;
|
||||
|
||||
|
||||
void tca9555_driver_init(void)
|
||||
{
|
||||
|
||||
i2c_master_bus_handle_t i2c_bus = esp_ret_i2c_handle();
|
||||
esp_err_t ret = esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_expander);
|
||||
|
||||
/* Test output level function */
|
||||
ret = esp_io_expander_set_dir(io_expander, (IO_EXPANDER_PIN_NUM_0 | IO_EXPANDER_PIN_NUM_1 | IO_EXPANDER_PIN_NUM_5 | IO_EXPANDER_PIN_NUM_6 | IO_EXPANDER_PIN_NUM_8), IO_EXPANDER_OUTPUT);
|
||||
if(ret != ESP_OK){
|
||||
printf("EXIO Mode setting failure!!\r\n");
|
||||
}
|
||||
|
||||
ret = esp_io_expander_set_dir(io_expander, (IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_9 | IO_EXPANDER_PIN_NUM_10 | IO_EXPANDER_PIN_NUM_11), IO_EXPANDER_INPUT);
|
||||
if(ret != ESP_OK){
|
||||
printf("EXIO Mode setting failure!!\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************************************************** Set the EXIO output status **********************************************************/
|
||||
void Set_EXIO(uint32_t Pin,uint8_t State) // Sets the level state of the Pin without affecting the other pins(PIN:1~8)
|
||||
{
|
||||
esp_err_t ret;
|
||||
ret = esp_io_expander_set_level(io_expander, Pin, State);
|
||||
if(ret != ESP_OK){
|
||||
printf("EXIO level setting failure!!\r\n");
|
||||
}
|
||||
|
||||
}
|
||||
/********************************************************** Read EXIO status **********************************************************/
|
||||
bool Read_EXIO(uint32_t Pin) // Read the level of the TCA9555PWR Pin
|
||||
{
|
||||
|
||||
esp_err_t ret;
|
||||
uint32_t input_level_mask = 0;
|
||||
ret = esp_io_expander_get_level(io_expander, Pin, &input_level_mask);
|
||||
if(ret != ESP_OK){
|
||||
printf("EXIO level reading failure!!\r\n");
|
||||
}
|
||||
bool bitStatus = input_level_mask & Pin;
|
||||
return bitStatus;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esp_io_expander_tca95xx_16bit.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern esp_io_expander_handle_t io_expander;
|
||||
void tca9555_driver_init(void);
|
||||
void Set_EXIO(uint32_t Pin,uint8_t State);
|
||||
bool Read_EXIO(uint32_t Pin);
|
||||
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,174 @@
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_netif_sntp.h"
|
||||
|
||||
#include "wifi_connect.h"
|
||||
#include "wifi_credentials.h"
|
||||
#include "audio_driver.h"
|
||||
|
||||
#define ESP_MAXIMUM_RETRY 5
|
||||
|
||||
/* FreeRTOS event group to signal when we are connected */
|
||||
static EventGroupHandle_t s_wifi_event_group;
|
||||
|
||||
/* The event group allows multiple bits for each event, but we only care about two events:
|
||||
* - we are connected to the AP with an IP
|
||||
* - we failed to connect after the maximum amount of retries */
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
static const char *TAG = "wifi station";
|
||||
|
||||
static bool s_was_connected = false;
|
||||
static bool s_first_time_connect = true;
|
||||
|
||||
static void event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
ESP_LOGI(TAG, "Disconnected from AP");
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
|
||||
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
xEventGroupClearBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
static void init_sntp(void)
|
||||
{
|
||||
ESP_LOGI("SNTP", "Initializing SNTP...");
|
||||
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
|
||||
ESP_ERROR_CHECK(esp_netif_sntp_init(&config));
|
||||
}
|
||||
|
||||
static void wifi_manager_task(void *pvParameters)
|
||||
{
|
||||
while (1) {
|
||||
EventBits_t bits = xEventGroupGetBits(s_wifi_event_group);
|
||||
if (!(bits & WIFI_CONNECTED_BIT)) {
|
||||
// We are not connected!
|
||||
if (s_was_connected) {
|
||||
// We lost connection!
|
||||
ESP_LOGW(TAG, "Wi-Fi connection lost! Playing alert...");
|
||||
set_led_state(LED_STATE_WIFI_CONNECTING);
|
||||
Audio_Play_Music_To_End("file://spiffs/disconnected_wifi.mp3");
|
||||
s_was_connected = false;
|
||||
}
|
||||
|
||||
int retry_count = 0;
|
||||
bool success = false;
|
||||
while (retry_count < ESP_MAXIMUM_RETRY) {
|
||||
ESP_LOGI(TAG, "Attempting Wi-Fi connection (try %d/%d)...", retry_count + 1, ESP_MAXIMUM_RETRY);
|
||||
set_led_state(LED_STATE_WIFI_CONNECTING);
|
||||
xEventGroupClearBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
esp_wifi_connect();
|
||||
|
||||
// Wait up to 6 seconds for connection bit
|
||||
bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(6000));
|
||||
if (bits & WIFI_CONNECTED_BIT) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
retry_count++;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
ESP_LOGI(TAG, "Wi-Fi Connected successfully.");
|
||||
s_was_connected = true;
|
||||
set_led_state(LED_STATE_SERVER_CONNECTING);
|
||||
|
||||
// Play connected audio prompt
|
||||
Audio_Play_Music_To_End("file://spiffs/connected_wifi.mp3");
|
||||
s_first_time_connect = false;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Wi-Fi connection failed. Retrying in 10 seconds...");
|
||||
set_led_state(LED_STATE_WIFI_FAILED);
|
||||
|
||||
// Play "Still not working, let me try again in a moment"
|
||||
Audio_Play_Music_To_End("file://spiffs/retry_later.mp3");
|
||||
|
||||
// Wait 10 seconds before next connection attempt
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
|
||||
// Play "Connecting to the Wi-Fi" chime before retrying
|
||||
Audio_Play_Music_To_End("file://spiffs/connecting_wifi.mp3");
|
||||
}
|
||||
} else {
|
||||
// Already connected! Keep monitoring
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool wifi_is_connected(void)
|
||||
{
|
||||
if (s_wifi_event_group == NULL) return false;
|
||||
EventBits_t bits = xEventGroupGetBits(s_wifi_event_group);
|
||||
return (bits & WIFI_CONNECTED_BIT) != 0;
|
||||
}
|
||||
|
||||
esp_err_t wifi_init_sta(void)
|
||||
{
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Only create event loop if it hasn't been created yet
|
||||
esp_err_t err = esp_event_loop_create_default();
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
// Initialize SNTP exactly once during startup
|
||||
init_sntp();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&event_handler,
|
||||
NULL,
|
||||
&instance_any_id));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
|
||||
IP_EVENT_STA_GOT_IP,
|
||||
&event_handler,
|
||||
NULL,
|
||||
&instance_got_ip));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.ssid = WIFI_SSID,
|
||||
.password = WIFI_PASSWORD,
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
|
||||
ESP_ERROR_CHECK(esp_wifi_start() );
|
||||
|
||||
ESP_LOGI(TAG, "Starting Wi-Fi manager task...");
|
||||
xTaskCreate(wifi_manager_task, "wifi_manager_task", 4096, NULL, 5, NULL);
|
||||
|
||||
// Wait blockingly on first boot for initial connection
|
||||
ESP_LOGI(TAG, "Waiting for initial Wi-Fi connection...");
|
||||
xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
LED_STATE_OFF = 0,
|
||||
LED_STATE_WIFI_CONNECTING, // Flashing Red
|
||||
LED_STATE_WIFI_FAILED, // Solid Red
|
||||
LED_STATE_SERVER_CONNECTING, // Flashing Orange
|
||||
LED_STATE_SERVER_FAILED, // Solid Orange
|
||||
LED_STATE_CONNECTED, // Rainbow Rotate
|
||||
LED_STATE_UPDATING // Pulse Blue slowly (OTA flashing)
|
||||
} led_state_t;
|
||||
|
||||
// Set the current LED state to update visual indicators
|
||||
void set_led_state(led_state_t state);
|
||||
|
||||
// Get the current LED state
|
||||
led_state_t get_led_state(void);
|
||||
|
||||
|
||||
// Initializes Wi-Fi station mode and starts the connection process
|
||||
esp_err_t wifi_init_sta(void);
|
||||
|
||||
// Returns true if Wi-Fi is currently connected to the AP with an IP
|
||||
bool wifi_is_connected(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Generated
+828
@@ -0,0 +1,828 @@
|
||||
{
|
||||
"name": "monitor-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "monitor-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "monitor-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Dashboard interface to monitor ESP32-S3 IoT connection state and activity stream.",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,250 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Antigravity IoT Enterprise Control Center</title>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<!-- FontAwesome Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="style.css?v=13">
|
||||
</head>
|
||||
<body>
|
||||
<div class="enterprise-layout">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<i class="fa-solid fa-shield-halved brand-icon"></i>
|
||||
<div class="brand-text">
|
||||
<h2>AGY ENTERPRISE</h2>
|
||||
<span>IoT Control Center</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="sidebar-menu">
|
||||
<a href="#" class="menu-item active" data-tab="overview">
|
||||
<i class="fa-solid fa-chart-pie"></i> Overview
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-tab="devices">
|
||||
<i class="fa-solid fa-microchip"></i> Device Fleet
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-tab="terminal">
|
||||
<i class="fa-solid fa-terminal"></i> Live Terminal
|
||||
</a>
|
||||
<a href="#" class="menu-item" data-tab="builds">
|
||||
<i class="fa-solid fa-boxes-packing"></i> Firmware Builds
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="system-status">
|
||||
<div class="status-indicator online"></div>
|
||||
<div class="status-details">
|
||||
<span class="status-title">API Gateway</span>
|
||||
<span class="status-sub" id="sidebar-gateway-url">ws://localhost:8901</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="main-content">
|
||||
<!-- Top Navbar -->
|
||||
<header class="top-nav">
|
||||
<div class="nav-title">
|
||||
<h1>Operations Console</h1>
|
||||
<span id="console-path">Dashboard / Overview</span>
|
||||
</div>
|
||||
<div class="nav-actions">
|
||||
<div class="connection-status-pill" id="dashboard-status">
|
||||
<span class="pulse-dot red"></span>
|
||||
<span class="status-text">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tab 1: Overview -->
|
||||
<div class="tab-page active" id="tab-overview">
|
||||
<!-- Fleet Stats Row -->
|
||||
<section class="metrics-row">
|
||||
<div class="metric-tile">
|
||||
<div class="tile-icon purple"><i class="fa-solid fa-network-wired"></i></div>
|
||||
<div class="tile-body">
|
||||
<span class="tile-label">Active Nodes</span>
|
||||
<span class="tile-val" id="stat-devices-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="tile-icon blue"><i class="fa-solid fa-database"></i></div>
|
||||
<div class="tile-body">
|
||||
<span class="tile-label">Messages Processed</span>
|
||||
<span class="tile-val" id="stat-messages-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="tile-icon green"><i class="fa-solid fa-heartbeat"></i></div>
|
||||
<div class="tile-body">
|
||||
<span class="tile-label">Average Latency</span>
|
||||
<span class="tile-val" id="stat-latency">-- ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-tile">
|
||||
<div class="tile-icon orange"><i class="fa-solid fa-circle-nodes"></i></div>
|
||||
<div class="tile-body">
|
||||
<span class="tile-label">Network Status</span>
|
||||
<span class="tile-val" id="stat-network-state">Stable</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Core Grid -->
|
||||
<div class="workspace-grid">
|
||||
<!-- Devices Fleet Section -->
|
||||
<section class="workspace-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<h3><i class="fa-solid fa-list-check"></i> Connected Fleet Nodes <span class="badge" id="connections-badge">0 Active</span></h3>
|
||||
<span class="header-subtitle">Real-time hardware connections</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="search-box">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input type="text" id="device-search" placeholder="Filter node ID/IP...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="connections-list" id="connections-container">
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<p>Awaiting fleet connection updates...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stream Feed / Audit Log Section -->
|
||||
<section class="workspace-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<h3><i class="fa-solid fa-receipt"></i> System Audit Log</h3>
|
||||
<span class="header-subtitle">Real-time telemetry stream</span>
|
||||
</div>
|
||||
<div class="header-right actions-row">
|
||||
<div class="filter-group">
|
||||
<button class="filter-btn active" data-filter="all">All</button>
|
||||
<button class="filter-btn" data-filter="connect">Connects</button>
|
||||
<button class="filter-btn" data-filter="msg">Messages</button>
|
||||
<button class="filter-btn" data-filter="error">Errors</button>
|
||||
</div>
|
||||
<button class="action-btn" id="export-logs-btn" title="Export logs as JSON">
|
||||
<i class="fa-solid fa-file-arrow-down"></i> Export
|
||||
</button>
|
||||
<button class="action-btn clear" id="clear-console-btn" title="Clear console">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="console-body" id="console-container">
|
||||
<div class="console-line system-line">[SYSTEM] Terminal initialized. Awaiting connection.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Device Fleet Manager -->
|
||||
<div class="tab-page" id="tab-devices">
|
||||
<section class="workspace-card full-view-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<h3><i class="fa-solid fa-microchip"></i> Device Fleet Manager</h3>
|
||||
<span class="header-subtitle">Full diagnostic fleet node view</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="search-box" style="width: 280px;">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input type="text" id="device-search-fleet" placeholder="Search fleet node ID/IP...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="connections-list" id="connections-container-fleet">
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-plug-circle-exclamation"></i>
|
||||
<p>No fleet nodes active.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Telemetry Terminal -->
|
||||
<div class="tab-page" id="tab-terminal">
|
||||
<section class="workspace-card full-view-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<h3><i class="fa-solid fa-terminal"></i> Telemetry Terminal Console</h3>
|
||||
<span class="header-subtitle">Real-time syslog and debug stream</span>
|
||||
</div>
|
||||
<div class="header-right actions-row">
|
||||
<div class="filter-group">
|
||||
<button class="filter-btn active" data-filter="all">All</button>
|
||||
<button class="filter-btn" data-filter="connect">Connects</button>
|
||||
<button class="filter-btn" data-filter="msg">Messages</button>
|
||||
<button class="filter-btn" data-filter="error">Errors</button>
|
||||
</div>
|
||||
<button class="action-btn" id="export-logs-btn-terminal" title="Export logs as JSON">
|
||||
<i class="fa-solid fa-file-arrow-down"></i> Export
|
||||
</button>
|
||||
<button class="action-btn clear" id="clear-console-btn-terminal" title="Clear console">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="console-body" id="console-container-terminal">
|
||||
<div class="console-line system-line">[SYSTEM] Dedicated terminal logger active.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Firmware Builds -->
|
||||
<div class="tab-page" id="tab-builds">
|
||||
<section class="workspace-card full-view-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<h3><i class="fa-solid fa-boxes-packing"></i> Firmware Builds Catalog</h3>
|
||||
<span class="header-subtitle">Inspect version histories, changesets, and download compiled binaries</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="action-btn" id="refresh-builds-btn" title="Refresh builds catalog list">
|
||||
<i class="fa-solid fa-rotate"></i> Refresh List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 1.25rem; overflow-y: auto; flex: 1;">
|
||||
<div class="empty-state" id="builds-empty-state">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<p>Fetching catalog releases...</p>
|
||||
</div>
|
||||
<div id="builds-grid-container" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 1.25rem;">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details/Diagnostics Modal -->
|
||||
<div class="modal" id="details-modal">
|
||||
<div class="modal-content wide">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fa-solid fa-circle-info"></i> Node Diagnostics</h3>
|
||||
<button class="close-modal-btn" id="close-modal-btn">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body-content">
|
||||
<!-- Inserted by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=21"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,951 @@
|
||||
/* Enterprise Layout Core Styling */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0b10;
|
||||
--bg-secondary: #0f111a;
|
||||
--sidebar-bg: #07080c;
|
||||
--card-bg: rgba(22, 26, 44, 0.45);
|
||||
--border-color: rgba(255, 255, 255, 0.06);
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #8d92a3;
|
||||
|
||||
/* Branding Accent Palette */
|
||||
--accent-purple: #a855f7;
|
||||
--accent-blue: #0ea5e9;
|
||||
--accent-green: #10b981;
|
||||
--accent-orange: #f59e0b;
|
||||
--accent-red: #ef4444;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Master Layout */
|
||||
.enterprise-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left Sidebar */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background-color: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 1.8rem;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.8rem;
|
||||
color: var(--accent-purple);
|
||||
filter: drop-shadow(0 0 6px var(--accent-purple));
|
||||
}
|
||||
|
||||
.brand-text h2 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.brand-text span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover, .menu-item.active {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
border-left: 3px solid var(--accent-purple);
|
||||
padding-left: calc(1rem - 3px);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.system-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 8px var(--accent-green);
|
||||
}
|
||||
|
||||
.status-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-sub {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-primary);
|
||||
padding: 1.8rem;
|
||||
gap: 1.5rem;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top Navigation Bar */
|
||||
.top-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-title h1 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
#console-path {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Status Pill */
|
||||
.connection-status-pill {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pulse-dot.red {
|
||||
background-color: var(--accent-red);
|
||||
box-shadow: 0 0 8px var(--accent-red);
|
||||
}
|
||||
|
||||
.pulse-dot.green {
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 8px var(--accent-green);
|
||||
}
|
||||
|
||||
/* Metrics Row */
|
||||
.metrics-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-tile {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1.4rem;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tile-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tile-icon.purple { background: rgba(168, 85, 247, 0.1); color: var(--accent-purple); border: 1px solid rgba(168, 85, 247, 0.2); }
|
||||
.tile-icon.blue { background: rgba(14, 165, 233, 0.1); color: var(--accent-blue); border: 1px solid rgba(14, 165, 233, 0.2); }
|
||||
.tile-icon.green { background: rgba(16, 185, 129, 0.1); color: var(--accent-green); border: 1px solid rgba(16, 185, 129, 0.2); }
|
||||
.tile-icon.orange { background: rgba(245, 158, 11, 0.1); color: var(--accent-orange); border: 1px solid rgba(245, 158, 11, 0.2); }
|
||||
|
||||
.tile-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tile-val {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Tab Pages & Workspace Panels */
|
||||
.tab-page {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
gap: 1.5rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-page.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.workspace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 1.5rem;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tab-page {
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
}
|
||||
.workspace-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.workspace-card {
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.full-view-card {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.2rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Connections Fleet List */
|
||||
.connections-list {
|
||||
padding: 1.2rem 1.5rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: rgba(30, 41, 59, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem 1.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
align-items: stretch;
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
background: rgba(30, 41, 59, 0.55);
|
||||
border-color: rgba(99, 179, 237, 0.35);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.device-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.device-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.device-avatar.esp { background: rgba(16, 185, 129, 0.08); color: var(--accent-green); border: 1px solid rgba(16, 185, 129, 0.2); }
|
||||
.device-avatar.client { background: rgba(14, 165, 233, 0.08); color: var(--accent-blue); border: 1px solid rgba(14, 165, 233, 0.2); }
|
||||
|
||||
.device-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.device-ip {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.device-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.device-msg-badge {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.diagnostics-btn {
|
||||
background: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.diagnostics-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Console Header Actions Row */
|
||||
.actions-row {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-btn.active, .filter-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.action-btn.clear {
|
||||
padding: 0.4rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Audit Log Console */
|
||||
.console-body {
|
||||
background: #050609;
|
||||
padding: 1.2rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
line-height: 1.45;
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.system-line { color: var(--text-secondary); border-color: rgba(255, 255, 255, 0.15); }
|
||||
.connect-line { color: var(--accent-green); border-color: var(--accent-green); }
|
||||
.disconnect-line { color: var(--accent-red); border-color: var(--accent-red); }
|
||||
.msg-line { color: var(--accent-blue); border-color: var(--accent-blue); }
|
||||
.error-line { color: var(--accent-orange); border-color: var(--accent-orange); }
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
color: var(--text-secondary);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 2.2rem;
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
width: 500px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.close-modal-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-modal-btn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.diagnostic-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.diag-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.diag-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.diag-value.high-green {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
/* Node Control Modal Extensions */
|
||||
.diagnostics-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.control-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 1.2rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.slider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.5rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.slider-row i {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.custom-slider {
|
||||
flex-grow: 1;
|
||||
-webkit-appearance: none;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-purple);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 6px var(--accent-purple);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.btn-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border-color: rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.control-btn.warning:hover {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
flex-grow: 1;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid var(--border-color);
|
||||
color: #fff;
|
||||
padding: 0.5rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.custom-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.action-btn.purple {
|
||||
background: var(--accent-purple);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.purple:hover {
|
||||
background: #9333ea;
|
||||
box-shadow: 0 0 10px rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
/* Premium Card Sliders & Layout Overrides */
|
||||
.device-controls-inline {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.inline-volume-slider,
|
||||
.inline-brightness-slider,
|
||||
.inline-haptic-slider {
|
||||
-webkit-appearance: none;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.inline-volume-slider::-webkit-slider-thumb,
|
||||
.inline-brightness-slider::-webkit-slider-thumb,
|
||||
.inline-haptic-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-purple);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 5px var(--accent-purple);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.inline-volume-slider::-webkit-slider-thumb:hover,
|
||||
.inline-brightness-slider::-webkit-slider-thumb:hover,
|
||||
.inline-haptic-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
.inline-volume-slider:disabled::-webkit-slider-thumb,
|
||||
.inline-brightness-slider:disabled::-webkit-slider-thumb,
|
||||
.inline-haptic-slider:disabled::-webkit-slider-thumb {
|
||||
background: #4a5568 !important;
|
||||
box-shadow: none !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inline-volume-val,
|
||||
.inline-brightness-val,
|
||||
.inline-haptic-val {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.device-msg-badge {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.device-msg-badge.touch-badge {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Glassmorphism Details Sync Button */
|
||||
#ctrl-sync-btn:hover {
|
||||
background: rgba(168, 85, 247, 0.2) !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Sleek 2-column modal layout overrides */
|
||||
.modal-content.wide {
|
||||
width: 860px;
|
||||
max-width: 95vw;
|
||||
}
|
||||
|
||||
.modal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-col-left {
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding-right: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.modal-col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.modal-content.wide {
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
.modal-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.modal-col-left {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-right: 0;
|
||||
padding-bottom: 1.2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 8902;
|
||||
|
||||
// Serve static assets from public/ with no caching so edits are always live
|
||||
app.use(express.static(path.join(__dirname, 'public'), {
|
||||
etag: false,
|
||||
lastModified: false,
|
||||
setHeaders: (res) => {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
}
|
||||
}));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`[Monitor Server] Web dashboard running on http://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
# Note: OTA requires otadata + two app partitions (ota_0, ota_1)
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
otadata, data, ota, 0xf000, 0x2000,
|
||||
ota_0, app, ota_0, 0x20000, 3M,
|
||||
ota_1, app, ota_1, , 3M,
|
||||
flash_test, data, fat, , 528K,
|
||||
model, data, spiffs, , 5900K,
|
||||
|
@@ -0,0 +1,573 @@
|
||||
{
|
||||
"latest": "v0.0.50-d38263d",
|
||||
"releases": [
|
||||
{
|
||||
"version": "v0.0.50-d38263d",
|
||||
"filename": "firmware-v0.0.50-d38263d-dev1.bin",
|
||||
"date": "2026-06-29T20:37:05Z",
|
||||
"size": 1992032,
|
||||
"model": "DEV-1",
|
||||
"commitHash": "d38263de0b631da46b424c77964f1fc6482bad28",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.49-5470971",
|
||||
"release: update firmware-catalog.json for v0.0.48-643aa45",
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.50-d38263d",
|
||||
"filename": "firmware-v0.0.50-d38263d-proto1.bin",
|
||||
"date": "2026-06-29T20:37:05Z",
|
||||
"size": 2009952,
|
||||
"model": "PROTO-1",
|
||||
"commitHash": "d38263de0b631da46b424c77964f1fc6482bad28",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.49-5470971",
|
||||
"release: update firmware-catalog.json for v0.0.48-643aa45",
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.49-5470971",
|
||||
"filename": "firmware-v0.0.49-5470971-dev1.bin",
|
||||
"date": "2026-06-29T20:20:10Z",
|
||||
"size": 1992032,
|
||||
"model": "DEV-1",
|
||||
"commitHash": "54709714896f016aa655f8303e070db784af5424",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.48-643aa45",
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.49-5470971",
|
||||
"filename": "firmware-v0.0.49-5470971-proto1.bin",
|
||||
"date": "2026-06-29T20:20:10Z",
|
||||
"size": 2009952,
|
||||
"model": "PROTO-1",
|
||||
"commitHash": "54709714896f016aa655f8303e070db784af5424",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.48-643aa45",
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.48-643aa45",
|
||||
"filename": "firmware-v0.0.48-643aa45-dev1.bin",
|
||||
"date": "2026-06-29T19:44:32Z",
|
||||
"size": 1992032,
|
||||
"model": "DEV-1",
|
||||
"commitHash": "643aa458fc473d97e29b5516b1ebb6f6dc0d8b12",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.48-643aa45",
|
||||
"filename": "firmware-v0.0.48-643aa45-proto1.bin",
|
||||
"date": "2026-06-29T19:44:32Z",
|
||||
"size": 2009952,
|
||||
"model": "PROTO-1",
|
||||
"commitHash": "643aa458fc473d97e29b5516b1ebb6f6dc0d8b12",
|
||||
"changelog": [
|
||||
"release: update firmware-catalog.json for v0.0.47-0665b75",
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.47-0665b75",
|
||||
"filename": "firmware-v0.0.47-0665b75-dev1.bin",
|
||||
"date": "2026-06-29T19:31:01Z",
|
||||
"size": 1991968,
|
||||
"model": "DEV-1",
|
||||
"commitHash": "0665b750bf802398ffeac78cfbace8b60278c8cc",
|
||||
"changelog": [
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.47-0665b75",
|
||||
"filename": "firmware-v0.0.47-0665b75-proto1.bin",
|
||||
"date": "2026-06-29T19:31:01Z",
|
||||
"size": 2009888,
|
||||
"model": "PROTO-1",
|
||||
"commitHash": "0665b750bf802398ffeac78cfbace8b60278c8cc",
|
||||
"changelog": [
|
||||
"style: group node metrics into status, connectivity, and system vertical columns under device identity",
|
||||
"style: restructure node cards with clean grid, text-based visual hierarchy, and inline mid-dot separators",
|
||||
"feat: remove messy inline control sliders from device node cards for corporate overview",
|
||||
"feat: group firmware builds by version and render model-specific download buttons",
|
||||
"feat: implement client-side URL hash routing for operations console dashboard",
|
||||
"clean: remove obsolete binaries and restrict catalog to latest release builds",
|
||||
"release: update firmware-catalog.json for v0.0.40-ea491e9",
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.40-ea491e9",
|
||||
"filename": "firmware-v0.0.40-ea491e9-dev1.bin",
|
||||
"date": "2026-06-29T16:38:39Z",
|
||||
"size": 1991776,
|
||||
"model": "DEV-1",
|
||||
"commitHash": "ea491e9c38d740a8a60fb34b7a0cb5f046c67d61",
|
||||
"changelog": [
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.0.40-ea491e9",
|
||||
"filename": "firmware-v0.0.40-ea491e9-proto1.bin",
|
||||
"date": "2026-06-29T16:38:39Z",
|
||||
"size": 2009744,
|
||||
"model": "PROTO-1",
|
||||
"commitHash": "ea491e9c38d740a8a60fb34b7a0cb5f046c67d61",
|
||||
"changelog": [
|
||||
"fix: resolve public HTTPS OTA connection certificate and SNI validation issue",
|
||||
"feat: compile and catalog both DEV-1 and PROTO-1 variants during releases",
|
||||
"release: update firmware-catalog.json for v0.0.37-2ce051a",
|
||||
"release: update firmware-catalog.json for v0.0.36-e298102",
|
||||
"release: update firmware-catalog.json for v0.0.35-d492173",
|
||||
"feat: implement breathing blue LED visual pulse during OTA updates",
|
||||
"feat: add Firmware Builds tab page in Enterprise Operations Console",
|
||||
"release: update firmware-catalog.json for v0.0.32-4471303",
|
||||
"fix: resolve public dev-server endpoints dynamically using devServerHttpUrl",
|
||||
"feat: firmware release catalog pipeline and versioned OTA selector",
|
||||
"feat: full OTA firmware update system",
|
||||
"fix: normalize ::ffff: IPv4-mapped IPv6 addresses to plain IPv4, add /clients debug endpoint",
|
||||
"fix: ghost clients and IP display bugs in dev-server",
|
||||
"feat: boot_device ΓÇö force-disconnect any device from web dashboard",
|
||||
"fix: read real client IP from X-Forwarded-For header, disable monitor-server static caching",
|
||||
"fix: bump app.js cache buster to v17 to force load diagnostics modal N/A sensor rows",
|
||||
"docs: document DEV-1/PROTO-1 model variant configuration in README and AGENTS",
|
||||
"feat: PROTO-1 peripheral support, web UI overhaul, and diagnostics modal redesign",
|
||||
"Stabilize struct layout with uint8_t types to fix brightness offset bug, fix failed color red mapping, and improve telemetry logging",
|
||||
"Expose inline volume and brightness sliders directly on device cards, implementing in-place DOM diffing to prevent focus loss during updates",
|
||||
"Implement dynamic LED brightness coefficient control across client, dev-server, and admin panel",
|
||||
"Implement set_led and play_audio API parser callbacks and ensure all control commands trigger immediate state report feedback back to gateway",
|
||||
"Update dev-server parser and monitor dashboard diagnostics to dynamically parse and display client state report telemetry",
|
||||
"Refactor firmware to implement the Unified State Manager (USM) pattern, encapsulating volume, LED, and connection states with thread-safe setter APIs",
|
||||
"Update workspace rules in AGENTS.md to enforce protocol-first development flow",
|
||||
"Implement query_state WebSocket command routing flow and client telemetry sender",
|
||||
"Create API.md specifications for WebSocket downlink controls and uplink state reporting",
|
||||
"Create wifi-credentials.example config template and update README.md setup documentation",
|
||||
"Update agent rules and create project README.md with hardware pin mapping, configuration guide, and deployment instructions",
|
||||
"Implement Device Control Center UI in Diagnostics Modal with real-time command routing to client nodes",
|
||||
"Lock Overview grid rows layout with grid-template-rows 1fr and allow viewport scroll overflow in 1200px stacking media queries",
|
||||
"Fix narrow layout squash on Overview tab, add early media stacking at 1200px, and enforce ellipsis on overflow",
|
||||
"Fix tab-page CSS visibility override by removing inline style display none",
|
||||
"Fix connectionsBadge TypeError crash that prevented list rendering on page update",
|
||||
"Fix dashboard layout, resolve double scrollbar, align padding/margins, and sync fleet and terminal tab pages",
|
||||
"Implement dynamic sidebar tab layout switching in the Enterprise Operations Console",
|
||||
"Upgrade monitor-server dashboard to Enterprise Operations Console with diagnostics, logs export, search, and live filters",
|
||||
"Create premium real-time monitor-server dashboard on port 8902 with dynamic client tracking and stream feed",
|
||||
"Implement Wi-Fi and WebSocket background connection monitor tasks with custom retry chimes and 10-second intervals",
|
||||
"Initial commit: ESP32-S3 Audio board dev-server and connection state firmware"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
echo Starting ESP-IDF build...
|
||||
"C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py build"
|
||||
pause
|
||||
@@ -0,0 +1,2 @@
|
||||
Write-Host "Starting ESP-IDF build..." -ForegroundColor Cyan
|
||||
& "C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py build"
|
||||
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
echo Cleaning build files...
|
||||
if exist build (
|
||||
rmdir /s /q build
|
||||
echo Build folder successfully removed.
|
||||
) else (
|
||||
echo Nothing to clean. Build folder does not exist.
|
||||
)
|
||||
pause
|
||||
@@ -0,0 +1,7 @@
|
||||
Write-Host "Cleaning build files..." -ForegroundColor Red
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Recurse -Force
|
||||
Write-Host "Build folder successfully removed." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Nothing to clean. Build folder does not exist." -ForegroundColor Yellow
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
echo Flashing firmware to device on COM5...
|
||||
"C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py -p COM5 flash"
|
||||
pause
|
||||
@@ -0,0 +1,5 @@
|
||||
param (
|
||||
[string]$Port = "COM5"
|
||||
)
|
||||
Write-Host "Flashing firmware to device on $Port..." -ForegroundColor Cyan
|
||||
& "C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py -p $Port flash"
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Opening ESP-IDF monitor on COM5...
|
||||
echo NOTE: To close the monitor interface, press 'Ctrl + ]'.
|
||||
"C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py -p COM5 monitor"
|
||||
pause
|
||||
@@ -0,0 +1,6 @@
|
||||
param (
|
||||
[string]$Port = "COM5"
|
||||
)
|
||||
Write-Host "Opening ESP-IDF monitor on $Port..." -ForegroundColor Cyan
|
||||
Write-Host "NOTE: To close the monitor interface, press 'Ctrl + ]'." -ForegroundColor Yellow
|
||||
& "C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe" run "idf.py -p $Port monitor"
|
||||
@@ -0,0 +1,21 @@
|
||||
$port = New-Object System.IO.Ports.SerialPort COM5, 115200, None, 8, 1
|
||||
$port.ReadTimeout = 1000
|
||||
try {
|
||||
$port.Open()
|
||||
Write-Host "Reading serial port COM5..." -ForegroundColor Cyan
|
||||
$startTime = Get-Date
|
||||
while (((Get-Date) - $startTime).TotalSeconds -lt 15) {
|
||||
try {
|
||||
$line = $port.ReadLine()
|
||||
Write-Output $line
|
||||
} catch [TimeoutException] {
|
||||
# normal timeout
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Error $_
|
||||
} finally {
|
||||
if ($port -ne $null -and $port.IsOpen) {
|
||||
$port.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
echo [Release] Launching ESP32-S3 firmware release pipeline...
|
||||
powershell.exe -ExecutionPolicy Bypass -File "%~dp0release.ps1" %*
|
||||
@@ -0,0 +1,293 @@
|
||||
# ESP32-S3 Firmware Release Script
|
||||
|
||||
param(
|
||||
[string]$Tag = "",
|
||||
[switch]$SkipBuild,
|
||||
[switch]$NoPush
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$EimExe = "C:\Users\micha\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.exe"
|
||||
$BinName = "esp32s3_audio.bin"
|
||||
$BuildBin = Join-Path $ProjectRoot "build\$BinName"
|
||||
$ReleasesDir = Join-Path $ProjectRoot "releases"
|
||||
$CatalogFile = Join-Path $ReleasesDir "firmware-catalog.json"
|
||||
|
||||
# -- Helpers (Using pure ASCII to prevent encoding parser issues) --
|
||||
|
||||
function Write-Step([string]$msg) {
|
||||
Write-Host "`n>> $msg" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Ok([string]$msg) {
|
||||
Write-Host " [OK] $msg" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Err([string]$msg) {
|
||||
Write-Host " [ERROR] $msg" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -- Step 1: Determine version --
|
||||
|
||||
Write-Step "Resolving version..."
|
||||
|
||||
if ($Tag -eq "") {
|
||||
# Try exact tag on current commit first
|
||||
$exactTag = & {
|
||||
$oldEA = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
|
||||
$t = git tag --points-at HEAD 2>$null
|
||||
$ErrorActionPreference = $oldEA
|
||||
$t
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($exactTag) {
|
||||
$Tag = $exactTag.Trim()
|
||||
Write-Ok "Using exact commit tag: $Tag"
|
||||
} else {
|
||||
# Fall back to git describe (nearest tag + distance)
|
||||
$described = & {
|
||||
$oldEA = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
|
||||
$d = git describe --tags --long 2>$null
|
||||
$ErrorActionPreference = $oldEA
|
||||
$d
|
||||
}
|
||||
if ($described) {
|
||||
# If on a tag, described looks like "v1.0.0-0-gabcdef" -> strip -0-g...
|
||||
if ($described -match '^(.+)-0-g[0-9a-f]+$') {
|
||||
$Tag = $Matches[1]
|
||||
Write-Ok "On tagged commit: $Tag"
|
||||
} else {
|
||||
# Not on a clean tag - build a dev version
|
||||
$Tag = $described -replace '-(\d+)-g([0-9a-f]+)$', '+$1.$2'
|
||||
Write-Ok "Dev build version: $Tag"
|
||||
}
|
||||
} else {
|
||||
# No tags at all - generate from commit count
|
||||
$commitCount = & {
|
||||
$oldEA = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
|
||||
$c = git rev-list HEAD --count
|
||||
$ErrorActionPreference = $oldEA
|
||||
$c
|
||||
}
|
||||
if ($commitCount) { $commitCount = $commitCount.Trim() } else { $commitCount = "0" }
|
||||
$shortHash = & {
|
||||
$oldEA = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
|
||||
$h = git rev-parse --short HEAD
|
||||
$ErrorActionPreference = $oldEA
|
||||
$h
|
||||
}
|
||||
if ($shortHash) { $shortHash = $shortHash.Trim() } else { $shortHash = "unknown" }
|
||||
$Tag = "v0.0.$commitCount-$shortHash"
|
||||
Write-Ok "Auto-generated version (no tags): $Tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Ok "Release version: $Tag"
|
||||
|
||||
# -- Step 2: Build and Package DEV-1 and PROTO-1 --
|
||||
|
||||
# Detect original model from device-config file to restore later
|
||||
$deviceConfigFile = Join-Path $ProjectRoot "device-config"
|
||||
$originalModel = "DEV-1"
|
||||
if (Test-Path $deviceConfigFile) {
|
||||
$configLine = Get-Content $deviceConfigFile | Where-Object { $_ -match '^MODEL=' }
|
||||
if ($configLine) { $originalModel = ($configLine -split '=')[1].Trim() }
|
||||
}
|
||||
|
||||
$SafeTag = $Tag -replace '[+/]', '-'
|
||||
|
||||
if (-not (Test-Path $ReleasesDir)) {
|
||||
New-Item -ItemType Directory -Path $ReleasesDir | Out-Null
|
||||
}
|
||||
|
||||
$DevBinSize = 0
|
||||
$ProtoBinSize = 0
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
# 1. Build and copy DEV-1
|
||||
Write-Step "Building firmware for DEV-1 ($Tag)..."
|
||||
"MODEL=DEV-1" | Set-Content $deviceConfigFile
|
||||
Push-Location $ProjectRoot
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
& $EimExe run "idf.py build"
|
||||
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Err "Build failed for DEV-1 (exit code $LASTEXITCODE)" }
|
||||
Pop-Location
|
||||
|
||||
if (-not (Test-Path $BuildBin)) { Write-Err "Binary not found: $BuildBin" }
|
||||
$DevBinSize = (Get-Item $BuildBin).Length
|
||||
$DestDevBin = Join-Path $ReleasesDir "firmware-$SafeTag-dev1.bin"
|
||||
Copy-Item $BuildBin $DestDevBin -Force
|
||||
Write-Ok "DEV-1 release packaged -> releases/firmware-$SafeTag-dev1.bin"
|
||||
|
||||
# 2. Build and copy PROTO-1
|
||||
Write-Step "Building firmware for PROTO-1 ($Tag)..."
|
||||
"MODEL=PROTO-1" | Set-Content $deviceConfigFile
|
||||
Push-Location $ProjectRoot
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
& $EimExe run "idf.py build"
|
||||
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Err "Build failed for PROTO-1 (exit code $LASTEXITCODE)" }
|
||||
Pop-Location
|
||||
|
||||
if (-not (Test-Path $BuildBin)) { Write-Err "Binary not found: $BuildBin" }
|
||||
$ProtoBinSize = (Get-Item $BuildBin).Length
|
||||
$DestProtoBin = Join-Path $ReleasesDir "firmware-$SafeTag-proto1.bin"
|
||||
Copy-Item $BuildBin $DestProtoBin -Force
|
||||
Write-Ok "PROTO-1 release packaged -> releases/firmware-$SafeTag-proto1.bin"
|
||||
|
||||
# Restore original configuration
|
||||
"MODEL=$originalModel" | Set-Content $deviceConfigFile
|
||||
Push-Location $ProjectRoot
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
}
|
||||
Pop-Location
|
||||
} else {
|
||||
Write-Host " (skipping build - using existing output in build/)" -ForegroundColor Yellow
|
||||
if (-not (Test-Path $BuildBin)) {
|
||||
Write-Err "Binary not found in build/: $BuildBin"
|
||||
}
|
||||
$DevBinSize = (Get-Item $BuildBin).Length
|
||||
$ProtoBinSize = $DevBinSize
|
||||
Copy-Item $BuildBin (Join-Path $ReleasesDir "firmware-$SafeTag-dev1.bin") -Force
|
||||
Copy-Item $BuildBin (Join-Path $ReleasesDir "firmware-$SafeTag-proto1.bin") -Force
|
||||
Write-Ok "Copied build/esp32s3_audio.bin to both dev1 and proto1 release outputs."
|
||||
}
|
||||
|
||||
# -- Step 5: Build changelog --
|
||||
|
||||
Write-Step "Generating changelog..."
|
||||
|
||||
$prevTag = & {
|
||||
$oldEA = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
|
||||
$t = git describe --tags --abbrev=0 HEAD~1 2>$null
|
||||
$ErrorActionPreference = $oldEA
|
||||
$t
|
||||
}
|
||||
if ($prevTag) {
|
||||
$logRange = "$($prevTag.Trim())..HEAD"
|
||||
} else {
|
||||
$logRange = "HEAD"
|
||||
}
|
||||
|
||||
$gitLog = git log $logRange --pretty=format:"%s" 2>$null
|
||||
$changelog = @()
|
||||
if ($gitLog) {
|
||||
$changelog = ($gitLog -split "`n") | Where-Object { $_ -match '\S' } | ForEach-Object { $_.Trim() }
|
||||
}
|
||||
if ($changelog.Count -eq 0) {
|
||||
$changelog = @("No changes logged")
|
||||
}
|
||||
Write-Ok "$($changelog.Count) changelog entries"
|
||||
|
||||
# -- Step 6: Read / update firmware-catalog.json --
|
||||
|
||||
Write-Step "Updating firmware-catalog.json..."
|
||||
|
||||
if (Test-Path $CatalogFile) {
|
||||
$catalog = Get-Content $CatalogFile -Raw | ConvertFrom-Json
|
||||
$releases = [System.Collections.Generic.List[object]]@()
|
||||
if ($catalog.releases) {
|
||||
foreach ($item in $catalog.releases) { $releases.Add($item) }
|
||||
}
|
||||
} else {
|
||||
$releases = [System.Collections.Generic.List[object]]@()
|
||||
$catalog = [PSCustomObject]@{ latest = ""; releases = @() }
|
||||
}
|
||||
|
||||
# Remove existing entries for this version if re-releasing
|
||||
$filteredReleases = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($r in $releases) {
|
||||
if ($r.version -ne $Tag) {
|
||||
$filteredReleases.Add($r)
|
||||
}
|
||||
}
|
||||
$releases = $filteredReleases
|
||||
|
||||
# Prepend new entries (DEV-1 first, then PROTO-1)
|
||||
$newDevEntry = [PSCustomObject]@{
|
||||
version = $Tag
|
||||
filename = "firmware-$SafeTag-dev1.bin"
|
||||
date = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
|
||||
size = $DevBinSize
|
||||
model = "DEV-1"
|
||||
commitHash = (git rev-parse HEAD).Trim()
|
||||
changelog = $changelog
|
||||
}
|
||||
|
||||
$newProtoEntry = [PSCustomObject]@{
|
||||
version = $Tag
|
||||
filename = "firmware-$SafeTag-proto1.bin"
|
||||
date = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
|
||||
size = $ProtoBinSize
|
||||
model = "PROTO-1"
|
||||
commitHash = (git rev-parse HEAD).Trim()
|
||||
changelog = $changelog
|
||||
}
|
||||
|
||||
$releases.Insert(0, $newDevEntry)
|
||||
$releases.Insert(1, $newProtoEntry)
|
||||
|
||||
# Update catalog
|
||||
$catalog.latest = $Tag
|
||||
$catalog.releases = $releases.ToArray()
|
||||
|
||||
$catalog | ConvertTo-Json -Depth 10 | Set-Content $CatalogFile -Encoding UTF8
|
||||
Write-Ok "Catalog written: $CatalogFile"
|
||||
Write-Ok "Latest = $Tag | Total releases = $($releases.Count)"
|
||||
|
||||
# -- Step 7: Git tag (only for clean semver tags) --
|
||||
|
||||
Write-Step "Tagging..."
|
||||
$isCleanTag = $Tag -match '^v\d+\.\d+\.\d+$'
|
||||
if ($isCleanTag) {
|
||||
$existingTag = git tag -l $Tag
|
||||
if (-not $existingTag) {
|
||||
git tag -a $Tag -m "Release $Tag"
|
||||
Write-Ok "Created annotated tag: $Tag"
|
||||
} else {
|
||||
Write-Host " Tag $Tag already exists - skipping" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host " Dev version ($Tag) - skipping tag creation" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# -- Step 8: Commit catalog update --
|
||||
|
||||
Write-Step "Committing catalog..."
|
||||
git add (Join-Path $ReleasesDir "firmware-catalog.json")
|
||||
$hasChanges = git diff --quiet HEAD -- (Join-Path $ReleasesDir "firmware-catalog.json")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
git commit -m "release: update firmware-catalog.json for $Tag"
|
||||
Write-Ok "Committed catalog update"
|
||||
} else {
|
||||
Write-Host " No catalog changes to commit" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# -- Step 9: Push --
|
||||
|
||||
if (-not $NoPush) {
|
||||
Write-Step "Pushing to origin..."
|
||||
git push origin master
|
||||
if ($isCleanTag) { git push origin $Tag }
|
||||
Write-Ok "Pushed"
|
||||
} else {
|
||||
Write-Host " (skipping push - -NoPush flag set)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# -- Done --
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=======================================================" -ForegroundColor Magenta
|
||||
Write-Host " Release complete: $Tag" -ForegroundColor Green
|
||||
Write-Host " DEV-1 Bin : releases/firmware-$SafeTag-dev1.bin ($([math]::Round($DevBinSize/1024,1)) KB)" -ForegroundColor White
|
||||
Write-Host " PROTO-1 Bin: releases/firmware-$SafeTag-proto1.bin ($([math]::Round($ProtoBinSize/1024,1)) KB)" -ForegroundColor White
|
||||
Write-Host " Catalog: releases/firmware-catalog.json" -ForegroundColor White
|
||||
Write-Host "=======================================================" -ForegroundColor Magenta
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,8 @@
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
# Allow OTA over plain HTTP (for LAN server)
|
||||
CONFIG_OTA_ALLOW_HTTP=y
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_OFFSET=0x8000
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
@@ -0,0 +1,2 @@
|
||||
SSID="YOUR_WIFI_SSID"
|
||||
PASSWORD="YOUR_WIFI_PASSWORD"
|
||||
Reference in New Issue
Block a user