Initial sanitized SmartSpeaker snapshot

This commit is contained in:
ben
2026-07-13 21:13:19 -07:00
commit ac26dcc238
66 changed files with 11438 additions and 0 deletions
+114
View File
@@ -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 1016, 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 04095 → 0100%
- **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 0100).
- **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
View File
@@ -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
+318
View File
@@ -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`) |
+6
View File
@@ -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)
+81
View File
@@ -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.
+183
View File
@@ -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 (04095) are scaled to 0100% in the web dashboard.
- Haptic intensity is controllable via the WebSocket `set_haptics` command (0100%).
- 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.
+5
View File
@@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "ts",
"exec": "ts-node src/server.ts"
}
+25
View File
@@ -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"
}
}
+7
View File
@@ -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
+5
View File
@@ -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
+462
View File
@@ -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}`);
});
+30
View File
@@ -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);
});
+14
View File
@@ -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/**/*"]
}
+1
View File
@@ -0,0 +1 @@
MODEL=PROTO-1
+28
View File
@@ -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"]
+73
View File
@@ -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)
+146
View File
@@ -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;
}
+36
View File
@@ -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
+684
View File
@@ -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;
}
+213
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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));
}
}
+248
View File
@@ -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;
}
}
+31
View File
@@ -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
+384
View File
@@ -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);
}
}
+22
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
wss://example.com
Binary file not shown.
+167
View File
@@ -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);
}
}
+43
View File
@@ -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
+50
View File
@@ -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(PIN1~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;
}
+19
View File
@@ -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
+174
View File
@@ -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;
}
+34
View File
@@ -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
+828
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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
+250
View File
@@ -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">&times;</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>
+951
View File
@@ -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;
}
}
+22
View File
@@ -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}`);
});
+8
View File
@@ -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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: OTA requires otadata + two app partitions (ota_0, ota_1)
3 nvs, data, nvs, 0x9000, 0x6000,
4 otadata, data, ota, 0xf000, 0x2000,
5 ota_0, app, ota_0, 0x20000, 3M,
6 ota_1, app, ota_1, , 3M,
7 flash_test, data, fat, , 528K,
8 model, data, spiffs, , 5900K,
+573
View File
@@ -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"
]
}
]
}
+4
View File
@@ -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
+2
View File
@@ -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"
+9
View File
@@ -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
+7
View File
@@ -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
}
+4
View File
@@ -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
+5
View File
@@ -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"
+5
View File
@@ -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
+6
View File
@@ -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"
+21
View File
@@ -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()
}
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
echo [Release] Launching ESP32-S3 firmware release pipeline...
powershell.exe -ExecutionPolicy Bypass -File "%~dp0release.ps1" %*
+293
View File
@@ -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 ""
+3153
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
SSID="YOUR_WIFI_SSID"
PASSWORD="YOUR_WIFI_PASSWORD"