From 0f692012a5796a72891663e41a3112869f473532 Mon Sep 17 00:00:00 2001 From: melancholytron Date: Sat, 24 Jan 2026 12:56:36 -0600 Subject: [PATCH] Initial commit: ESP32 Channel3 RF TV Broadcast ESP32 port of Channel3 - broadcasts analog NTSC/PAL TV signals using I2S DMA at 80MHz. Features include: - RF broadcast on Channel 3 (61.25 MHz) - Web UI for configuration - MQTT integration with Home Assistant - Weather display via Open-Meteo API - Screen rotation with transitions (fade, wipe, dissolve) - 3D graphics engine - Uploaded image display - Settings export/import Hardware: ESP32 with GPIO 22 as RF output Build: ESP-IDF v5.5.2 Co-Authored-By: Claude Opus 4.5 --- .gitignore | 25 + CMakeLists.txt | 12 + README.md | 121 + build.bat | 10 + build.ps1 | 43 + build_helper.ps1 | 11 + build_only.ps1 | 10 + components/tablemaker/CMakeLists.txt | 7 + components/tablemaker/CbTable.c | 55 + components/tablemaker/CbTable.h | 41 + components/tablemaker/broadcast_tables.c | 66 + components/tablemaker/broadcast_tables.h | 32 + flash.ps1 | 26 + flash_only.ps1 | 10 + main/3d.c | 602 +++ main/3d.h | 76 + main/CMakeLists.txt | 21 + main/Kconfig.projbuild | 63 + main/user_main.c | 4272 ++++++++++++++++++++++ main/video_broadcast.c | 497 +++ main/video_broadcast.h | 70 + monitor.ps1 | 10 + partitions.csv | 5 + rebuild.ps1 | 26 + run_build.bat | 19 + run_build.cmd | 5 + sdkconfig | 2231 +++++++++++ sdkconfig.defaults | 38 + tools/README.md | 84 + tools/stream_lcars.bat | 8 + tools/stream_video.py | 295 ++ 31 files changed, 8791 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 build.bat create mode 100644 build.ps1 create mode 100644 build_helper.ps1 create mode 100644 build_only.ps1 create mode 100644 components/tablemaker/CMakeLists.txt create mode 100644 components/tablemaker/CbTable.c create mode 100644 components/tablemaker/CbTable.h create mode 100644 components/tablemaker/broadcast_tables.c create mode 100644 components/tablemaker/broadcast_tables.h create mode 100644 flash.ps1 create mode 100644 flash_only.ps1 create mode 100644 main/3d.c create mode 100644 main/3d.h create mode 100644 main/CMakeLists.txt create mode 100644 main/Kconfig.projbuild create mode 100644 main/user_main.c create mode 100644 main/video_broadcast.c create mode 100644 main/video_broadcast.h create mode 100644 monitor.ps1 create mode 100644 partitions.csv create mode 100644 rebuild.ps1 create mode 100644 run_build.bat create mode 100644 run_build.cmd create mode 100644 sdkconfig create mode 100644 sdkconfig.defaults create mode 100644 tools/README.md create mode 100644 tools/stream_lcars.bat create mode 100644 tools/stream_video.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d5b1b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Build output +build/ +*.bin +*.elf +*.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +build_full.log +build_log*.txt +build_output.txt + +# Backup files +*.old +*.bak +*~ + +# OS files +.DS_Store +Thumbs.db +*.mp4 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..973a5dd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +# Channel3 ESP32 Port - RF Broadcast via I2S DMA +# This is an ESP-IDF project that ports the ESP8266 Channel3 firmware to ESP32 + +cmake_minimum_required(VERSION 3.16) + +# Set default build type to Release if not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(channel3_esp32) diff --git a/README.md b/README.md new file mode 100644 index 0000000..41f7bca --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Channel3 ESP32 Port + +ESP32 port of the Channel3 analog NTSC/PAL television broadcast firmware. + +## Overview + +This project ports the ESP8266 Channel3 firmware to ESP32, maintaining the ability to broadcast RF signals on Channel 3 (61.25 MHz) directly from a GPIO pin. + +**WARNING**: RF broadcast without proper licensing may be illegal in your jurisdiction. This project is for educational purposes only. + +## How It Works + +The ESP32 outputs an 80 MHz bitstream via I2S DMA. Pre-computed waveform patterns create harmonics at the Channel 3 carrier frequency (61.25 MHz for luma). The GPIO pin acts as an antenna, radiating RF directly. + +### Key Differences from ESP8266 Version + +| Component | ESP8266 | ESP32 | +|-----------|---------|-------| +| Clock source | 160 MHz / 2 | 160 MHz / 2 (PLL_D2) | +| DMA | SLC (sdio_queue) | GDMA (lldesc_t) | +| I2S mode | Standard I2S | LCD/parallel mode | +| ISR attach | ets_isr_attach() | esp_intr_alloc() | +| Framework | ESP8266 RTOS SDK | ESP-IDF 5.x | + +## Building + +### Prerequisites + +- ESP-IDF 5.x installed and configured +- ESP32 development board (original ESP32, not ESP32-S2/S3/C3) + +### Build Commands + +```bash +# Set up ESP-IDF environment +. $IDF_PATH/export.sh + +# Configure the project (optional - to change settings) +idf.py menuconfig + +# Build +idf.py build + +# Flash +idf.py -p /dev/ttyUSB0 flash + +# Monitor +idf.py -p /dev/ttyUSB0 monitor +``` + +## Configuration + +Use `idf.py menuconfig` to access settings under "Channel3 Configuration": + +- **Video Standard**: NTSC (default) or PAL +- **I2S Data Output GPIO**: GPIO pin for RF output (default: GPIO22) +- **WiFi SoftAP SSID**: Access point name (default: "Channel3") +- **WiFi SoftAP Password**: Access point password + +## Hardware Setup + +1. Connect a short wire (antenna) to the configured GPIO pin (default GPIO22) +2. Tune an analog TV to Channel 3 +3. The ESP32 will broadcast directly - no external components needed + +**Note**: The RF output is very low power. The antenna must be very close to the TV antenna for reception. + +## Project Structure + +``` +esp32_channel3/ +├── CMakeLists.txt # Main project CMake file +├── sdkconfig.defaults # Default SDK configuration +├── main/ +│ ├── CMakeLists.txt # Main component build file +│ ├── Kconfig.projbuild # Configuration options +│ ├── video_broadcast.c # I2S DMA video generation (ESP32) +│ ├── video_broadcast.h # Video broadcast header +│ ├── 3d.c # Fixed-point 3D graphics engine +│ ├── 3d.h # 3D graphics header +│ └── user_main.c # Application entry & demo screens +└── components/ + └── tablemaker/ + ├── CMakeLists.txt # Component build file + ├── broadcast_tables.c # Premodulated RF waveforms + ├── broadcast_tables.h # Table definitions + ├── CbTable.c # NTSC/PAL line type lookup + └── CbTable.h # Line type definitions +``` + +## Technical Notes + +### I2S LCD Mode + +The ESP32 I2S peripheral is configured in LCD mode for parallel output. This allows continuous DMA output at high bitrates without the overhead of standard I2S framing. + +### Clock Configuration + +The target is 80 MHz output to match the original ESP8266 implementation: +- ESP32 PLL_D2 clock: 160 MHz +- Divider: 2 +- Output: 80 MHz + +### DMA Operation + +DMA descriptors are configured in a circular buffer. The ISR is called on each buffer completion (EOF), filling the buffer with the next line's premodulated data. + +## Known Limitations + +1. **RF Quality**: ESP32 GPIO slew rate and output characteristics differ from ESP8266. RF quality may vary. +2. **Timing Sensitivity**: Video signal generation is timing-critical. Heavy system load may cause visible artifacts. +3. **Legal Restrictions**: Unlicensed RF transmission is illegal in most jurisdictions. + +## Credits + +Original ESP8266 Channel3: Charles Lohr (CNLohr) +ESP32 Port: Based on original architecture + +## License + +See LICENSE file in the parent directory. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..5a42015 --- /dev/null +++ b/build.bat @@ -0,0 +1,10 @@ +@echo off +echo Starting ESP-IDF build... > build_log.txt +call C:\Espressif\idf_cmd_init.bat esp-idf-v5.5.2 >> build_log.txt 2>&1 +echo Changing to project directory... >> build_log.txt +cd /d C:\git\channel3\esp32_channel3 +echo Setting target to esp32... >> build_log.txt +idf.py set-target esp32 >> build_log.txt 2>&1 +echo Building project... >> build_log.txt +idf.py build >> build_log.txt 2>&1 +echo Build complete. Exit code: %ERRORLEVEL% >> build_log.txt diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..c9dd803 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,43 @@ +$ErrorActionPreference = "Continue" + +# Clear MSYS environment variables that confuse ESP-IDF +Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue +Remove-Item Env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue +Remove-Item Env:MSYSTEM_CARCH -ErrorAction SilentlyContinue +Remove-Item Env:MSYSTEM_CHOST -ErrorAction SilentlyContinue +Remove-Item Env:MINGW_CHOST -ErrorAction SilentlyContinue +Remove-Item Env:MINGW_PREFIX -ErrorAction SilentlyContinue +Remove-Item Env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue +$env:MSYSTEM = "" + +# Set ESP-IDF environment variables +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" + +# Add tools to PATH (correct versions) +$toolPaths = @( + "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", + "C:\Espressif\tools\cmake\3.30.2\bin", + "C:\Espressif\tools\ninja\1.12.1", + "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin", + "C:\Espressif\tools\esp32ulp-elf\2.38_20240113\esp32ulp-elf\bin", + "C:\Espressif\tools\idf-git\2.44.0\cmd", + "C:\Espressif\tools\idf-python\3.11.2\python.exe" +) + +$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH + +# Change to project directory +Set-Location "C:\git\channel3\esp32_channel3" + +Write-Host "ESP-IDF Path: $env:IDF_PATH" +Write-Host "Working directory: $(Get-Location)" +Write-Host "Starting build..." + +# Run idf.py +$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" +$idfpy = "$env:IDF_PATH\tools\idf.py" + +Write-Host "Building..." +& $python $idfpy build diff --git a/build_helper.ps1 b/build_helper.ps1 new file mode 100644 index 0000000..01737b3 --- /dev/null +++ b/build_helper.ps1 @@ -0,0 +1,11 @@ +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:MSYSTEM = $null +$env:SHELL = $null +$env:SHLVL = $null +$env:TERM = $null +Set-Location "C:\git\channel3\esp32_channel3" +. "C:\Espressif\Initialize-Idf.ps1" +idf.py build +idf.py -p COM5 flash diff --git a/build_only.ps1 b/build_only.ps1 new file mode 100644 index 0000000..1e1db39 --- /dev/null +++ b/build_only.ps1 @@ -0,0 +1,10 @@ +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:MSYSTEM = $null +$env:SHELL = $null +$env:SHLVL = $null +$env:TERM = $null +Set-Location "C:\git\channel3\esp32_channel3" +. "C:\Espressif\Initialize-Idf.ps1" +idf.py build diff --git a/components/tablemaker/CMakeLists.txt b/components/tablemaker/CMakeLists.txt new file mode 100644 index 0000000..a8415c1 --- /dev/null +++ b/components/tablemaker/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS + "broadcast_tables.c" + "CbTable.c" + INCLUDE_DIRS + "." +) diff --git a/components/tablemaker/CbTable.c b/components/tablemaker/CbTable.c new file mode 100644 index 0000000..e911a49 --- /dev/null +++ b/components/tablemaker/CbTable.c @@ -0,0 +1,55 @@ +/** + * @file CbTable.c + * @brief Line type lookup tables for NTSC/PAL video signal generation + * + * These tables define the signal type for each half-line in a video frame. + * The values are packed as nibbles - even lines use low nibble, odd use high nibble. + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#include "CbTable.h" + +const uint8_t CbLookupPAL[313] = { + 0x11, 0x04, 0x20, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x52, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x13, 0x01, 0x20, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x52, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x66, +}; + +const uint8_t CbLookupNTSC[263] = { + 0x00, 0x10, 0x11, 0x00, 0x20, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x25, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x02, 0x00, 0x13, 0x41, 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x06, +}; diff --git a/components/tablemaker/CbTable.h b/components/tablemaker/CbTable.h new file mode 100644 index 0000000..cfb8aba --- /dev/null +++ b/components/tablemaker/CbTable.h @@ -0,0 +1,41 @@ +/** + * @file CbTable.h + * @brief Line type lookup tables for NTSC/PAL video signal generation + * + * Defines the state machine for each scanline (263 lines NTSC, 313 lines PAL). + * Each entry specifies the line type: sync pulses, blanking, colorburst, active video. + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#ifndef CBTABLE_H +#define CBTABLE_H + +#include +#include "sdkconfig.h" + +// Line type definitions +#define FT_STA_d 0 // Short Sync A +#define FT_STB_d 1 // Long Sync B +#define FT_B_d 2 // Black/Blanking +#define FT_SRA_d 3 // Short to long sync transition +#define FT_SRB_d 4 // Long to short sync transition +#define FT_LIN_d 5 // Active video line +#define FT_CLOSE 6 // End frame +#define FT_MAX_d 7 // Number of line types + +// Line lookup tables (each nibble is a line type) +extern const uint8_t CbLookupPAL[313]; +extern const uint8_t CbLookupNTSC[263]; + +// Select the appropriate table based on video standard +#ifdef CONFIG_VIDEO_PAL + #define VIDEO_LINES 625 + #define CbLookup CbLookupPAL +#else + #define VIDEO_LINES 525 + #define CbLookup CbLookupNTSC +#endif + +#endif // CBTABLE_H diff --git a/components/tablemaker/broadcast_tables.c b/components/tablemaker/broadcast_tables.c new file mode 100644 index 0000000..e5c3b14 --- /dev/null +++ b/components/tablemaker/broadcast_tables.c @@ -0,0 +1,66 @@ +/** + * @file broadcast_tables.c + * @brief Premodulated waveform lookup tables for NTSC/PAL RF broadcast + * + * These tables contain precomputed RF waveform patterns that, when output + * at 80 MHz, create the necessary harmonics for Channel 3 broadcast. + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#include "broadcast_tables.h" + +const uint32_t premodulated_table[918] = { + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbfffddef, 0xffffffff, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xb9dddcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xf7fef7bf, 0x7fef73ff, 0xee77ffff, 0xfff77fff, 0xffffffff, 0xfff7ffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee677333, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfef73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7bff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xeee773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdfffdef, 0x9ffbdeff, 0xffb9cfff, 0xfb9dffdc, 0xbbddffff, 0xfbdfffce, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xb99dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe7fff73b, 0xee77ffff, 0xffff77ff, 0xffffffff, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xe667733b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xbbddffff, 0xfbdffffe, 0xffffffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xe7fef7bf, 0x7fef73ff, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xee77ffff, 0xf7fff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xfef7fffb, 0xef7fff7b, 0xe677733b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xff9deffc, 0xfbdfffde, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffdfff, 0xffffdfff, 0xbb9ddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xee77ffff, 0xefffff3b, 0xffffffff, 0xeffffffb, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xeffffffb, 0xe677773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffb9cfff, 0xfb9dffdc, 0x7bdfffce, 0xbdfffdef, 0x9ffbdeff, 0xbbddffff, 0xfffddfff, 0xffffffff, 0xfffdffff, 0xfffdfffe, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xbb9dccee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xee7fff7b, 0xe7fff73b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xeffff7bf, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xee77773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xdffbdeff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xbbddffff, 0xffffdeff, 0xffffffff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xbb99dcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xee77fff3, 0xef7fff3b, 0xe7fef7bf, 0x7fef73ff, 0xfee73fff, 0xee77ffff, 0xfef7fffb, 0xffffffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xee777733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9dffdcef, 0xfff9ceff, 0xffbdeffc, 0xfbdfffde, 0xb9fffdef, 0xbbddffff, 0xbfffdcff, 0xffffffff, 0xbfffdfff, 0xffffdfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xb9dddcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfff7ffff, 0xfffffffb, 0xeffffffb, 0xfffff7bf, 0xffff7fff, 0xee677733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdfffdef, 0x9ffbdcff, 0xffb9cfff, 0xfb9dfffc, 0x7bdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7fff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xeffff7bf, 0xffff77ff, 0xeee773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdff9cef, 0xdffbdeff, 0xffbdcffd, 0xfb9cffde, 0xbbddffff, 0xbbdfffee, 0xffffffff, 0xfbffffee, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xffdffffe, 0xb99ddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef73ff, 0xfee73fff, 0xfe77fff3, 0xef7fff3b, 0xe7fef7bf, 0xee77ffff, 0xffff7bff, 0xffffffff, 0xffff7fff, 0xfff77fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xeee773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfbdeffde, 0xb9dffdce, 0x9dffdcef, 0xdff9ceff, 0xffbdeffc, 0xbbddffff, 0xfbdfffde, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xbfffdfff, 0xffffdfff, 0xffbdffff, 0xb99dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xf7fe77bf, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xe77ff73b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xfefffffb, 0xefffff7b, 0xe677733b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9dfffc, 0x7bdfffce, 0xb9fffdef, 0x9ffbdcff, 0xffb9cfff, 0xbbddffff, 0xffbdfffe, 0xffffffff, 0xfffdfffe, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffddfff, 0xbb9dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xe77ff73b, 0xf7fe73bf, 0xffe77bff, 0xfef7bff7, 0xee77ff7b, 0xee77ffff, 0xeffff7bf, 0xffffffff, 0xeffff7bf, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xe677773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffbdcffd, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdffbdeff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xfffdffff, 0xffdffffe, 0xfbffffee, 0xbffffdef, 0xffffdfff, 0xbb9dccee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xef7fff7b, 0xe7fff7bf, 0x7fef73ff, 0xfee73fff, 0xfe77fff3, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff77fff, 0xfef7ffff, 0xee77773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xdff9ceff, 0xffbdeffc, 0xfb9effde, 0xb9dffdce, 0x9dffdcef, 0xbbddffff, 0xffffdfff, 0xffffffff, 0xffffdfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffddff, 0xbb99dcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xee77ff73, 0xef7ff73b, 0xf7fef7bf, 0x7fef7bff, 0xfee73ff7, 0xee77ffff, 0xee77fffb, 0xffffffff, 0xfefffffb, 0xefffff7b, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xee777733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9ffbdcff, 0xffb9cfff, 0xff9dfffc, 0x7bdfffce, 0xb9fffdef, 0xbbddffff, 0xbfffdeff, 0xffffffff, 0xffffdeff, 0xfffddfff, 0xffbdfffe, 0xfbdffffe, 0xbffffdef, 0xb9dddcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfef7bff7, 0xee77ff7b, 0xe77ff73b, 0xf7fe73bf, 0xffe77bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfef7ffff, 0xef7ffffb, 0xeffff7bf, 0xfffff7ff, 0xffff7fff, 0xee677733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0x9ffbdeff, 0xffbdcffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xffdffffe, 0xfbdfffee, 0xb9dddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73fff, 0xfe77fff3, 0xef7fff7b, 0xe7fff7bf, 0x7fef73ff, 0xee77ffff, 0xffe77fff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee6773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9dffdce, 0x9dffdcef, 0xdff9deff, 0xffbdeffc, 0xfb9cffde, 0xbbddffff, 0xb9fffdef, 0xffffffff, 0xbffffdef, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfee73ff7, 0xee77ff73, 0xef7ff73b, 0xf7fef7bf, 0xee77ffff, 0xffff7bff, 0xffffffff, 0xffff7fff, 0xfff7ffff, 0xfef7fffb, 0xefffff7b, 0xfffff7bf, 0xeee773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x7bdfffce, 0xb9fffdef, 0x9fffdcff, 0xffb9ceff, 0xff9dfffc, 0xbbddffff, 0xfbdfffde, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdfffe, 0xb99dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xf7fe73bf, 0x7fef7bff, 0xfef7bff7, 0xee77ff7b, 0xe77ff73b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xeffff7bf, 0xe667733b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9dffdc, 0x39dffdce, 0xbdffbcef, 0x9ffbdeff, 0xffbdcffd, 0xbbddffff, 0xff9dfffe, 0xffffffff, 0xffddfffe, 0xfbdfffee, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xe7fff73f, 0x7ffe73bf, 0xffe73bff, 0xfef7fff3, 0xef7fff7b, 0xee77ffff, 0xfffff7bf, 0xffffffff, 0xfffff7bf, 0xffff7fff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xe677773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbdffdcef, 0xdff9deff, 0xbbddffff, 0xffbdffff, 0xffffffff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xbffffdff, 0xffffdfff, 0xbb9dccce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xef7ff73b, 0xf7fef7bf, 0x7fef73ff, 0xfee73ff7, 0xee77ff73, 0xee77ffff, 0xef7fff7b, 0xffffffff, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xfff7ffff, 0xfef7fffb, 0xe677773b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xb9fffdef, 0x9fffdcef, 0xbbddffff, 0xfffddfff, 0xffffffff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xbffffdef, 0xbfffdeff, 0xbb99ccee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xee77ff7b, 0xe77ff73b, 0xf7fe73bf, 0x7fef7bff, 0xfef7bff7, 0xee77ffff, 0xee7fff7b, 0xffffffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xffff7fff, 0xfef7ffff, 0xee777733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x9ffbdeff, 0xffb9cffd, 0xfb9dffdc, 0x39dffdce, 0xbdfffcef, 0xbbddffff, 0xffffdeff, 0xffffffff, 0xffffdeff, 0xfffdffff, 0xffddfffe, 0xfbdffffe, 0xbffffdef, 0xb999dcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfef7fff3, 0xef7fff7b, 0xe7fff73f, 0xfffe73bf, 0xffe77bff, 0xee77ffff, 0xfef7ffff, 0xffffffff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xffff77ff, 0xffff7fff, 0xee777733, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xfb9cffde, 0x39dffdce, 0xbbddffff, 0xbfffddef, 0xffffffff, 0xbffffdff, 0xffffdfff, 0xfffdffff, 0xfbdffffe, 0xbbffffef, 0xb9dddcee, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfee73ff7, 0xee77ff73, 0xef7fff3b, 0xf7fef7bf, 0x7fef73ff, 0xee77ffff, 0xfff77fff, 0xffffffff, 0xfff7ffff, 0xfef7fffb, 0xef7fff7b, 0xfffff7bf, 0xffff7fff, 0xee677333, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xb9fffdef, 0x9fffdcef, 0xfff9ceff, 0xff9deffc, 0xfbdfffce, 0xbbddffff, 0xbdfffdef, 0xffffffff, 0xbffffdef, 0xbfffdeff, 0xfffddfff, 0xffbdffff, 0xfbdffffe, 0xb9dddcce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0x7fef7bff, 0xfef73ff7, 0xee77ff73, 0xe77ff73b, 0xf7fe73bf, 0xee77ffff, 0xffef7bff, 0xffffffff, 0xffff7fff, 0xfef7ffff, 0xef7ffffb, 0xefffffbf, 0xfffff7ff, 0xeee773bb, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0x39dffdce, 0xbdfffdef, 0x9ffbdeff, 0xffb9cfff, 0xfb9dffdc, 0xbbddffff, 0xfbdfffce, 0xffffffff, 0xfbdffffe, 0xbffffdef, 0xffffdeff, 0xfffdffff, 0xfffdfffe, 0xb99dddce, 0x9999cccc, + 0xee77773b, 0xeff777fb, 0xffff773b, 0xfffe73bf, 0xffe77bff, 0xfef7bff3, 0xee7fff7b, 0xe7fff73b, 0xee77ffff, 0xffff77ff, 0xffffffff, 0xffff77ff, 0xffff7fff, 0xfef7ffff, 0xef7fff7b, 0xfffff7bf, 0xe667733b, 0x66663333, + 0xbbddddee, 0xbbfddfee, 0xffffddee, 0xfb9cffde, 0x39dffdce, 0xbdff9cef, 0xdff9deff, 0xffbdeffd, 0xbbddffff, 0xfbdffffe, 0xffffffff, 0xfbdffffe, 0xfbffffef, 0xbffffdef, 0xffffdfff, 0xfffdffff, 0xbb9dddce, 0x9999cccc, +}; diff --git a/components/tablemaker/broadcast_tables.h b/components/tablemaker/broadcast_tables.h new file mode 100644 index 0000000..5e351c5 --- /dev/null +++ b/components/tablemaker/broadcast_tables.h @@ -0,0 +1,32 @@ +/** + * @file broadcast_tables.h + * @brief Premodulated waveform lookup tables for NTSC/PAL RF broadcast + * + * These tables contain 1408-bit patterns per color, chosen as an exact harmonic + * of both NTSC chroma (3.579545 MHz) and Channel 3 luma (61.25 MHz). + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#ifndef BROADCAST_TABLES_H +#define BROADCAST_TABLES_H + +#include + +#define PREMOD_ENTRIES 44 +#define PREMOD_ENTRIES_WITH_SPILL 51 +#define PREMOD_SIZE 18 + +// Color level indices for the premodulated table +#define SYNC_LEVEL 17 +#define COLORBURST_LEVEL 16 +#define BLACK_LEVEL 0 +#define GRAY_LEVEL 1 +#define WHITE_LEVEL 10 + +// Premodulated table: 918 entries (51 * 18) +// Each entry is a 32-bit word containing the precomputed RF waveform +extern const uint32_t premodulated_table[918]; + +#endif // BROADCAST_TABLES_H diff --git a/flash.ps1 b/flash.ps1 new file mode 100644 index 0000000..20b5795 --- /dev/null +++ b/flash.ps1 @@ -0,0 +1,26 @@ +$ErrorActionPreference = "Continue" + +# Clear MSYS environment variables +Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue +$env:MSYSTEM = "" + +# Set ESP-IDF environment +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" + +$toolPaths = @( + "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", + "C:\Espressif\tools\cmake\3.30.2\bin", + "C:\Espressif\tools\ninja\1.12.1", + "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin" +) +$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH + +Set-Location "C:\git\channel3\esp32_channel3" + +$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" +$idfpy = "$env:IDF_PATH\tools\idf.py" + +Write-Host "Flashing to COM5..." +& $python $idfpy -p COM5 flash monitor diff --git a/flash_only.ps1 b/flash_only.ps1 new file mode 100644 index 0000000..ca3309f --- /dev/null +++ b/flash_only.ps1 @@ -0,0 +1,10 @@ +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:MSYSTEM = $null +$env:SHELL = $null +$env:SHLVL = $null +$env:TERM = $null +Set-Location "C:\git\channel3\esp32_channel3" +. "C:\Espressif\Initialize-Idf.ps1" +idf.py -p COM5 flash diff --git a/main/3d.c b/main/3d.c new file mode 100644 index 0000000..c0563ef --- /dev/null +++ b/main/3d.c @@ -0,0 +1,602 @@ +/** + * @file 3d.c + * @brief Fixed-point 3D graphics engine implementation + * + * Provides matrix-based 3D transformations and rendering primitives. + * Uses 256 = 1.0 fixed-point math (8-bit fractional part). + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#include "3d.h" +#include +#include + +// Matrix element indices +#define m00 0 +#define m01 1 +#define m02 2 +#define m03 3 +#define m10 4 +#define m11 5 +#define m12 6 +#define m13 7 +#define m20 8 +#define m21 9 +#define m22 10 +#define m23 11 +#define m30 12 +#define m31 13 +#define m32 14 +#define m33 15 + +// Global state +uint8_t *frontframe; +int16_t ModelviewMatrix[16]; +int16_t ProjectionMatrix[16]; +uint8_t CNFGBGColor; +uint8_t CNFGLastColor; +uint16_t LTW = FBW; +uint8_t CNFGDialogColor; +int CNFGPenX, CNFGPenY; + +// Function pointer for pixel plotting +void (*CNFGTackPixel)(int x, int y); + +// Sine lookup table (0-127 = 0 to pi) +static const uint8_t sintable[128] = { + 0, 6, 12, 18, 25, 31, 37, 43, 49, 55, 62, 68, 74, 80, 86, 91, + 97, 103, 109, 114, 120, 125, 131, 136, 141, 147, 152, 157, 162, 166, 171, 176, + 180, 185, 189, 193, 197, 201, 205, 208, 212, 215, 219, 222, 225, 228, 230, 233, + 236, 238, 240, 242, 244, 246, 247, 249, 250, 251, 252, 253, 254, 254, 255, 255, + 255, 255, 255, 254, 254, 253, 252, 251, 250, 249, 247, 246, 244, 242, 240, 238, + 236, 233, 230, 228, 225, 222, 219, 215, 212, 208, 205, 201, 197, 193, 189, 185, + 180, 176, 171, 166, 162, 157, 152, 147, 141, 136, 131, 125, 120, 114, 109, 103, + 97, 91, 86, 80, 74, 68, 62, 55, 49, 43, 37, 31, 25, 18, 12, 6, +}; + +int16_t tdSIN(uint8_t iv) +{ + if (iv > 127) { + return -sintable[iv - 128]; + } else { + return sintable[iv]; + } +} + +int16_t tdCOS(uint8_t iv) +{ + return tdSIN(iv + 64); +} + +void MakeXRotationMatrix(uint8_t angle, int16_t *f) +{ + f[0] = 256; f[1] = 0; f[2] = 0; f[3] = 0; + f[4] = 0; f[5] = tdCOS(angle); f[6] = -tdSIN(angle); f[7] = 0; + f[8] = 0; f[9] = tdSIN(angle); f[10] = tdCOS(angle); f[11] = 0; + f[12] = 0; f[13] = 0; f[14] = 0; f[15] = 256; +} + +void MakeYRotationMatrix(uint8_t angle, int16_t *f) +{ + f[0] = tdCOS(angle); f[1] = 0; f[2] = tdSIN(angle); f[3] = 0; + f[4] = 0; f[5] = 256; f[6] = 0; f[7] = 0; + f[8] = -tdSIN(angle); f[9] = 0; f[10] = tdCOS(angle); f[11] = 0; + f[12] = 0; f[13] = 0; f[14] = 0; f[15] = 256; +} + +void tdIdentity(int16_t *matrix) +{ + matrix[0] = 256; matrix[1] = 0; matrix[2] = 0; matrix[3] = 0; + matrix[4] = 0; matrix[5] = 256; matrix[6] = 0; matrix[7] = 0; + matrix[8] = 0; matrix[9] = 0; matrix[10] = 256; matrix[11] = 0; + matrix[12] = 0; matrix[13] = 0; matrix[14] = 0; matrix[15] = 256; +} + +void Perspective(int fovx, int aspect, int zNear, int zFar, int16_t *out) +{ + int16_t f = fovx; + out[0] = f * 256 / aspect; out[1] = 0; out[2] = 0; out[3] = 0; + out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; + out[8] = 0; out[9] = 0; + out[10] = 256 * (zFar + zNear) / (zNear - zFar); + out[11] = 2 * zFar * zNear / (zNear - zFar); + out[12] = 0; out[13] = 0; out[14] = -256; out[15] = 0; +} + +void MakeTranslate(int x, int y, int z, int16_t *out) +{ + tdIdentity(out); + out[m03] += x; + out[m13] += y; + out[m23] += z; +} + +void tdTranslate(int16_t *f, int16_t x, int16_t y, int16_t z) +{ + int16_t ftmp[16]; + tdIdentity(ftmp); + ftmp[m03] += x; + ftmp[m13] += y; + ftmp[m23] += z; + tdMultiply(f, ftmp, f); +} + +void tdScale(int16_t *f, int16_t x, int16_t y, int16_t z) +{ + f[m00] = (f[m00] * x) >> 8; + f[m01] = (f[m01] * x) >> 8; + f[m02] = (f[m02] * x) >> 8; + f[m03] = (f[m03] * x) >> 8; + + f[m10] = (f[m10] * y) >> 8; + f[m11] = (f[m11] * y) >> 8; + f[m12] = (f[m12] * y) >> 8; + f[m13] = (f[m13] * y) >> 8; + + f[m20] = (f[m20] * z) >> 8; + f[m21] = (f[m21] * z) >> 8; + f[m22] = (f[m22] * z) >> 8; + f[m23] = (f[m23] * z) >> 8; +} + +void tdRotateEA(int16_t *f, int16_t x, int16_t y, int16_t z) +{ + int16_t ftmp[16]; + + int16_t cx = tdCOS(x); + int16_t sx = tdSIN(x); + int16_t cy = tdCOS(y); + int16_t sy = tdSIN(y); + int16_t cz = tdCOS(z); + int16_t sz = tdSIN(z); + + // Row major, manually transposed + ftmp[m00] = (cy * cz) >> 8; + ftmp[m10] = ((((sx * sy) >> 8) * cz) - (cx * sz)) >> 8; + ftmp[m20] = ((((cx * sy) >> 8) * cz) + (sx * sz)) >> 8; + ftmp[m30] = 0; + + ftmp[m01] = (cy * sz) >> 8; + ftmp[m11] = ((((sx * sy) >> 8) * sz) + (cx * cz)) >> 8; + ftmp[m21] = ((((cx * sy) >> 8) * sz) - (sx * cz)) >> 8; + ftmp[m31] = 0; + + ftmp[m02] = -sy; + ftmp[m12] = (sx * cy) >> 8; + ftmp[m22] = (cx * cy) >> 8; + ftmp[m32] = 0; + + ftmp[m03] = 0; + ftmp[m13] = 0; + ftmp[m23] = 0; + ftmp[m33] = 1; + + tdMultiply(f, ftmp, f); +} + +void tdMultiply(int16_t *fin1, int16_t *fin2, int16_t *fout) +{ + int16_t fotmp[16]; + + fotmp[m00] = ((int32_t)fin1[m00] * (int32_t)fin2[m00] + (int32_t)fin1[m01] * (int32_t)fin2[m10] + (int32_t)fin1[m02] * (int32_t)fin2[m20] + (int32_t)fin1[m03] * (int32_t)fin2[m30]) >> 8; + fotmp[m01] = ((int32_t)fin1[m00] * (int32_t)fin2[m01] + (int32_t)fin1[m01] * (int32_t)fin2[m11] + (int32_t)fin1[m02] * (int32_t)fin2[m21] + (int32_t)fin1[m03] * (int32_t)fin2[m31]) >> 8; + fotmp[m02] = ((int32_t)fin1[m00] * (int32_t)fin2[m02] + (int32_t)fin1[m01] * (int32_t)fin2[m12] + (int32_t)fin1[m02] * (int32_t)fin2[m22] + (int32_t)fin1[m03] * (int32_t)fin2[m32]) >> 8; + fotmp[m03] = ((int32_t)fin1[m00] * (int32_t)fin2[m03] + (int32_t)fin1[m01] * (int32_t)fin2[m13] + (int32_t)fin1[m02] * (int32_t)fin2[m23] + (int32_t)fin1[m03] * (int32_t)fin2[m33]) >> 8; + + fotmp[m10] = ((int32_t)fin1[m10] * (int32_t)fin2[m00] + (int32_t)fin1[m11] * (int32_t)fin2[m10] + (int32_t)fin1[m12] * (int32_t)fin2[m20] + (int32_t)fin1[m13] * (int32_t)fin2[m30]) >> 8; + fotmp[m11] = ((int32_t)fin1[m10] * (int32_t)fin2[m01] + (int32_t)fin1[m11] * (int32_t)fin2[m11] + (int32_t)fin1[m12] * (int32_t)fin2[m21] + (int32_t)fin1[m13] * (int32_t)fin2[m31]) >> 8; + fotmp[m12] = ((int32_t)fin1[m10] * (int32_t)fin2[m02] + (int32_t)fin1[m11] * (int32_t)fin2[m12] + (int32_t)fin1[m12] * (int32_t)fin2[m22] + (int32_t)fin1[m13] * (int32_t)fin2[m32]) >> 8; + fotmp[m13] = ((int32_t)fin1[m10] * (int32_t)fin2[m03] + (int32_t)fin1[m11] * (int32_t)fin2[m13] + (int32_t)fin1[m12] * (int32_t)fin2[m23] + (int32_t)fin1[m13] * (int32_t)fin2[m33]) >> 8; + + fotmp[m20] = ((int32_t)fin1[m20] * (int32_t)fin2[m00] + (int32_t)fin1[m21] * (int32_t)fin2[m10] + (int32_t)fin1[m22] * (int32_t)fin2[m20] + (int32_t)fin1[m23] * (int32_t)fin2[m30]) >> 8; + fotmp[m21] = ((int32_t)fin1[m20] * (int32_t)fin2[m01] + (int32_t)fin1[m21] * (int32_t)fin2[m11] + (int32_t)fin1[m22] * (int32_t)fin2[m21] + (int32_t)fin1[m23] * (int32_t)fin2[m31]) >> 8; + fotmp[m22] = ((int32_t)fin1[m20] * (int32_t)fin2[m02] + (int32_t)fin1[m21] * (int32_t)fin2[m12] + (int32_t)fin1[m22] * (int32_t)fin2[m22] + (int32_t)fin1[m23] * (int32_t)fin2[m32]) >> 8; + fotmp[m23] = ((int32_t)fin1[m20] * (int32_t)fin2[m03] + (int32_t)fin1[m21] * (int32_t)fin2[m13] + (int32_t)fin1[m22] * (int32_t)fin2[m23] + (int32_t)fin1[m23] * (int32_t)fin2[m33]) >> 8; + + fotmp[m30] = ((int32_t)fin1[m30] * (int32_t)fin2[m00] + (int32_t)fin1[m31] * (int32_t)fin2[m10] + (int32_t)fin1[m32] * (int32_t)fin2[m20] + (int32_t)fin1[m33] * (int32_t)fin2[m30]) >> 8; + fotmp[m31] = ((int32_t)fin1[m30] * (int32_t)fin2[m01] + (int32_t)fin1[m31] * (int32_t)fin2[m11] + (int32_t)fin1[m32] * (int32_t)fin2[m21] + (int32_t)fin1[m33] * (int32_t)fin2[m31]) >> 8; + fotmp[m32] = ((int32_t)fin1[m30] * (int32_t)fin2[m02] + (int32_t)fin1[m31] * (int32_t)fin2[m12] + (int32_t)fin1[m32] * (int32_t)fin2[m22] + (int32_t)fin1[m33] * (int32_t)fin2[m32]) >> 8; + fotmp[m33] = ((int32_t)fin1[m30] * (int32_t)fin2[m03] + (int32_t)fin1[m31] * (int32_t)fin2[m13] + (int32_t)fin1[m32] * (int32_t)fin2[m23] + (int32_t)fin1[m33] * (int32_t)fin2[m33]) >> 8; + + memcpy(fout, fotmp, sizeof(fotmp)); +} + +void tdPTransform(int16_t *pin, int16_t *f, int16_t *pout) +{ + int16_t ptmp[2]; + ptmp[0] = ((pin[0] * f[m00] + pin[1] * f[m01] + pin[2] * f[m02]) >> 8) + f[m03]; + ptmp[1] = ((pin[0] * f[m10] + pin[1] * f[m11] + pin[2] * f[m12]) >> 8) + f[m13]; + pout[2] = ((pin[0] * f[m20] + pin[1] * f[m21] + pin[2] * f[m22]) >> 8) + f[m23]; + pout[0] = ptmp[0]; + pout[1] = ptmp[1]; +} + +void td4Transform(int16_t *pin, int16_t *f, int16_t *pout) +{ + int16_t ptmp[3]; + ptmp[0] = (pin[0] * f[m00] + pin[1] * f[m01] + pin[2] * f[m02] + pin[3] * f[m03]) >> 8; + ptmp[1] = (pin[0] * f[m10] + pin[1] * f[m11] + pin[2] * f[m12] + pin[3] * f[m13]) >> 8; + ptmp[2] = (pin[0] * f[m20] + pin[1] * f[m21] + pin[2] * f[m22] + pin[3] * f[m23]) >> 8; + pout[3] = (pin[0] * f[m30] + pin[1] * f[m31] + pin[2] * f[m32] + pin[3] * f[m33]) >> 8; + pout[0] = ptmp[0]; + pout[1] = ptmp[1]; + pout[2] = ptmp[2]; +} + +void LocalToScreenspace(int16_t *coords_3v, int16_t *o1, int16_t *o2) +{ + int16_t tmppt[4] = { coords_3v[0], coords_3v[1], coords_3v[2], 256 }; + td4Transform(tmppt, ModelviewMatrix, tmppt); + td4Transform(tmppt, ProjectionMatrix, tmppt); + if (tmppt[3] >= 0) { + *o1 = -1; + *o2 = -1; + return; + } + + if (CNFGLastColor > 15) { + // Half-height mode + *o1 = (256 * tmppt[0] / tmppt[3]) / 8 + (FBW / 2); + *o2 = (256 * tmppt[1] / tmppt[3]) / 8 + (FBH / 2); + } else { + *o1 = ((256 * tmppt[0] / tmppt[3]) / 8 + (FBW / 2)) / 2; + *o2 = ((256 * tmppt[1] / tmppt[3]) / 8 + (FBH / 2)); + } +} + +static void CNFGTackPixelW(int x, int y) +{ + frontframe[(x + y * FBW) >> 2] |= 2 << ((x & 3) << 1); +} + +static void CNFGTackPixelB(int x, int y) +{ + frontframe[(x + y * FBW) >> 2] &= ~(2 << ((x & 3) << 1)); +} + +static void CNFGTackPixelG(int x, int y) +{ + uint8_t *ffs = &frontframe[(x + y * FBW2) >> 1]; + if (x & 1) { + *ffs = (*ffs & 0x0f) | (CNFGLastColor << 4); + } else { + *ffs = (*ffs & 0xf0) | CNFGLastColor; + } +} + +void CNFGColor(uint8_t col) +{ + CNFGLastColor = col; + if (col == 16) { + LTW = FBW; + CNFGTackPixel = CNFGTackPixelB; + } else if (col == 17) { + LTW = FBW; + CNFGTackPixel = CNFGTackPixelW; + } else { + LTW = FBW / 2; + CNFGTackPixel = CNFGTackPixelG; + } +} + +int LABS(int x) +{ + return (x < 0) ? -x : x; +} + +// Bresenham's line algorithm +void CNFGTackSegment(int x0, int y0, int x1, int y1) +{ + int deltax = x1 - x0; + int deltay = y1 - y0; + int error = 0; + int x; + int sy = LABS(deltay); + int ysg = (y0 > y1) ? -1 : 1; + int y = y0; + + if (x0 < 0 || x0 >= LTW) return; + if (y0 < 0 || y0 >= FBH) return; + if (x1 < 0 || x1 >= LTW) return; + if (y1 < 0 || y1 >= FBH) return; + + if (CNFGLastColor) { + if (deltax == 0) { + if (y1 == y0) { + CNFGTackPixel(x1, y); + return; + } + for (; y != y1 + ysg; y += ysg) + CNFGTackPixel(x1, y); + return; + } + + int deltaerr = LABS((deltay * 256) / deltax); + int xsg = (x0 > x1) ? -1 : 1; + + for (x = x0; x != x1; x += xsg) { + CNFGTackPixel(x, y); + error = error + deltaerr; + while (error >= 128 && y >= 0 && y < FBH) { + y = y + ysg; + CNFGTackPixel(x, y); + error = error - 256; + } + } + CNFGTackPixel(x1, y1); + } else { + if (deltax == 0) { + if (y1 == y0) { + CNFGTackPixel(x1, y); + return; + } + for (; y != y1 + ysg; y += ysg) + CNFGTackPixel(x1, y); + return; + } + + int deltaerr = LABS((deltay * 256) / deltax); + int xsg = (x0 > x1) ? -1 : 1; + + for (x = x0; x != x1; x += xsg) { + CNFGTackPixel(x, y); + error = error + deltaerr; + while (error >= 128 && y >= 0 && y < FBH) { + y = y + ysg; + CNFGTackPixel(x, y); + error = error - 256; + } + } + CNFGTackPixel(x1, y1); + } +} + +// Geodesic sphere vertices +static const int16_t verts[] = { + 0, -256, 0, + 185, -114, 134, -70, -114, 217, -228, -114, 0, -70, -114, -217, + 185, -114, -134, 70, 114, 217, -185, 114, 134, -185, 114, -134, + 70, 114, -217, 228, 114, 0, 0, 256, 0, 108, -217, 79, + -41, -217, 127, 67, -134, 207, 108, -217, -79, 217, -134, 0, + -134, -217, 0, -176, -134, 127, -41, -217, -127, -176, -134, -127, + 67, -134, -207, 243, 0, -79, 243, 0, 79, 150, 0, 207, + 0, 0, 256, -150, 0, 207, -243, 0, 79, -243, 0, -79, + -150, 0, -207, 0, 0, -256, 150, 0, -207, 176, 134, 127, + -67, 134, 207, -217, 134, 0, -67, 134, -207, 176, 134, -127, + 134, 217, 0, 41, 217, 127, -108, 217, 79, -108, 217, -79, + 41, 217, -127 +}; + +// Geodesic sphere edge indices +static const uint16_t indices[] = { + 42, 36, 36, 3, 3, 42, 42, 39, 39, 36, 6, 39, 42, 6, 39, 0, + 0, 36, 48, 3, 36, 48, 36, 45, 45, 48, 15, 48, 45, 15, 0, 45, + 54, 39, 6, 54, 54, 51, 51, 39, 9, 51, 54, 9, 51, 0, 60, 51, + 9, 60, 60, 57, 57, 51, 12, 57, 60, 12, 57, 0, 63, 57, 12, 63, + 63, 45, 45, 57, 63, 15, 69, 3, 48, 69, 48, 66, 66, 69, 30, 69, + 66, 30, 15, 66, 75, 6, 42, 75, 42, 72, 72, 75, 18, 75, 72, 18, + 3, 72, 81, 9, 54, 81, 54, 78, 78, 81, 21, 81, 78, 21, 6, 78, + 87, 12, 60, 87, 60, 84, 84, 87, 24, 87, 84, 24, 9, 84, 93, 15, + 63, 93, 63, 90, 90, 93, 27, 93, 90, 27, 12, 90, 96, 69, 30, 96, + 96, 72, 72, 69, 96, 18, 99, 75, 18, 99, 99, 78, 78, 75, 99, 21, + 102, 81, 21, 102, 102, 84, 84, 81, 102, 24, 105, 87, 24, 105, 105, 90, + 90, 87, 105, 27, 108, 93, 27, 108, 108, 66, 66, 93, 108, 30, 114, 18, + 96, 114, 96, 111, 111, 114, 33, 114, 111, 33, 30, 111, 117, 21, 99, 117, + 99, 114, 114, 117, 33, 117, 120, 24, 102, 120, 102, 117, 117, 120, 33, 120, + 123, 27, 105, 123, 105, 120, 120, 123, 33, 123, 108, 111, 108, 123, 123, 111, +}; + +void Draw3DSegment(int16_t *c1, int16_t *c2) +{ + int16_t sx0, sy0, sx1, sy1; + LocalToScreenspace(c1, &sx0, &sy0); + LocalToScreenspace(c2, &sx1, &sy1); + CNFGTackSegment(sx0, sy0, sx1, sy1); +} + +void DrawGeoSphere(void) +{ + int i; + int nrv = sizeof(indices) / sizeof(uint16_t); + for (i = 0; i < nrv; i += 2) { + int16_t *c1 = (int16_t*)&verts[indices[i]]; + int16_t *c2 = (int16_t*)&verts[indices[i + 1]]; + Draw3DSegment(c1, c2); + } +} + +// Font character map and data +static const unsigned short FontCharMap[128] = { + 65535, 0, 10, 20, 32, 44, 56, 68, 70, 65535, 65535, 80, 92, 65535, 104, 114, + 126, 132, 138, 148, 156, 166, 180, 188, 200, 206, 212, 218, 224, 228, 238, 244, + 65535, 250, 254, 258, 266, 278, 288, 302, 304, 310, 316, 324, 328, 226, 252, 330, + 332, 342, 348, 358, 366, 372, 382, 392, 400, 410, 420, 424, 428, 262, 432, 436, + 446, 460, 470, 486, 496, 508, 516, 522, 536, 542, 548, 556, 568, 572, 580, 586, + 598, 608, 622, 634, 644, 648, 654, 662, 670, 682, 692, 700, 706, 708, 492, 198, + 714, 716, 726, 734, 742, 750, 760, 768, 782, 790, 794, 802, 204, 810, 820, 384, + 828, 836, 844, 850, 860, 864, 872, 880, 890, 894, 902, 908, 916, 920, 928, 934, +}; + +static const unsigned char FontCharData[949] = { + 0x00, 0x01, 0x20, 0x21, 0x03, 0x23, 0x23, 0x14, 0x14, 0x83, 0x00, 0x01, 0x20, 0x21, 0x04, 0x24, + 0x24, 0x13, 0x13, 0x84, 0x01, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x11, 0x92, + 0x11, 0x22, 0x22, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x02, 0x02, 0x91, 0x01, 0x21, 0x21, 0x23, + 0x23, 0x01, 0x03, 0x21, 0x03, 0x01, 0x12, 0x94, 0x03, 0x23, 0x13, 0x14, 0x23, 0x22, 0x22, 0x11, + 0x11, 0x02, 0x02, 0x83, 0x12, 0x92, 0x12, 0x12, 0x01, 0x21, 0x21, 0x23, 0x23, 0x03, 0x03, 0x81, + 0x03, 0x21, 0x21, 0x22, 0x21, 0x11, 0x03, 0x14, 0x14, 0x23, 0x23, 0x92, 0x01, 0x10, 0x10, 0x21, + 0x21, 0x12, 0x12, 0x01, 0x12, 0x14, 0x03, 0xa3, 0x02, 0x03, 0x03, 0x13, 0x02, 0x12, 0x13, 0x10, + 0x10, 0xa1, 0x01, 0x23, 0x03, 0x21, 0x02, 0x11, 0x11, 0x22, 0x22, 0x13, 0x13, 0x82, 0x00, 0x22, + 0x22, 0x04, 0x04, 0x80, 0x20, 0x02, 0x02, 0x24, 0x24, 0xa0, 0x01, 0x10, 0x10, 0x21, 0x10, 0x14, + 0x14, 0x03, 0x14, 0xa3, 0x00, 0x03, 0x04, 0x04, 0x20, 0x23, 0x24, 0xa4, 0x00, 0x20, 0x00, 0x02, + 0x02, 0x22, 0x10, 0x14, 0x20, 0xa4, 0x01, 0x21, 0x21, 0x23, 0x23, 0x03, 0x03, 0x01, 0x20, 0x10, + 0x10, 0x14, 0x14, 0x84, 0x03, 0x23, 0x23, 0x24, 0x24, 0x04, 0x04, 0x83, 0x01, 0x10, 0x10, 0x21, + 0x10, 0x14, 0x14, 0x03, 0x14, 0x23, 0x04, 0xa4, 0x01, 0x10, 0x21, 0x10, 0x10, 0x94, 0x03, 0x14, + 0x23, 0x14, 0x10, 0x94, 0x02, 0x22, 0x22, 0x11, 0x22, 0x93, 0x02, 0x22, 0x02, 0x11, 0x02, 0x93, + 0x01, 0x02, 0x02, 0xa2, 0x02, 0x22, 0x22, 0x11, 0x11, 0x02, 0x02, 0x13, 0x13, 0xa2, 0x11, 0x22, + 0x22, 0x02, 0x02, 0x91, 0x02, 0x13, 0x13, 0x22, 0x22, 0x82, 0x10, 0x13, 0x14, 0x94, 0x10, 0x01, + 0x20, 0x91, 0x10, 0x14, 0x20, 0x24, 0x01, 0x21, 0x03, 0xa3, 0x21, 0x10, 0x10, 0x01, 0x01, 0x23, + 0x23, 0x14, 0x14, 0x03, 0x10, 0x94, 0x00, 0x01, 0x23, 0x24, 0x04, 0x03, 0x03, 0x21, 0x21, 0xa0, + 0x21, 0x10, 0x10, 0x01, 0x01, 0x12, 0x12, 0x03, 0x03, 0x14, 0x14, 0x23, 0x02, 0xa4, 0x10, 0x91, + 0x10, 0x01, 0x01, 0x03, 0x03, 0x94, 0x10, 0x21, 0x21, 0x23, 0x23, 0x94, 0x01, 0x23, 0x11, 0x13, + 0x21, 0x03, 0x02, 0xa2, 0x02, 0x22, 0x11, 0x93, 0x31, 0xc0, 0x03, 0xa1, 0x00, 0x20, 0x20, 0x24, + 0x24, 0x04, 0x04, 0x00, 0x12, 0x92, 0x01, 0x10, 0x10, 0x14, 0x04, 0xa4, 0x01, 0x10, 0x10, 0x21, + 0x21, 0x22, 0x22, 0x04, 0x04, 0xa4, 0x00, 0x20, 0x20, 0x24, 0x24, 0x04, 0x12, 0xa2, 0x00, 0x02, + 0x02, 0x22, 0x20, 0xa4, 0x20, 0x00, 0x00, 0x02, 0x02, 0x22, 0x22, 0x24, 0x24, 0x84, 0x20, 0x02, + 0x02, 0x22, 0x22, 0x24, 0x24, 0x04, 0x04, 0x82, 0x00, 0x20, 0x20, 0x21, 0x21, 0x12, 0x12, 0x94, + 0x00, 0x04, 0x00, 0x20, 0x20, 0x24, 0x04, 0x24, 0x02, 0xa2, 0x00, 0x02, 0x02, 0x22, 0x22, 0x20, + 0x20, 0x00, 0x22, 0x84, 0x11, 0x11, 0x13, 0x93, 0x11, 0x11, 0x13, 0x84, 0x20, 0x02, 0x02, 0xa4, + 0x00, 0x22, 0x22, 0x84, 0x01, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x13, 0x14, 0x94, 0x21, 0x01, + 0x01, 0x04, 0x04, 0x24, 0x24, 0x22, 0x22, 0x12, 0x12, 0x13, 0x13, 0xa3, 0x04, 0x01, 0x01, 0x10, + 0x10, 0x21, 0x21, 0x24, 0x02, 0xa2, 0x00, 0x04, 0x04, 0x14, 0x14, 0x23, 0x23, 0x12, 0x12, 0x02, + 0x12, 0x21, 0x21, 0x10, 0x10, 0x80, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x01, 0x10, 0x10, 0xa1, + 0x00, 0x10, 0x10, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x04, 0x04, 0x80, 0x00, 0x04, 0x04, 0x24, + 0x00, 0x20, 0x02, 0x92, 0x00, 0x04, 0x00, 0x20, 0x02, 0x92, 0x21, 0x10, 0x10, 0x01, 0x01, 0x03, + 0x03, 0x14, 0x14, 0x23, 0x23, 0x22, 0x22, 0x92, 0x00, 0x04, 0x20, 0x24, 0x02, 0xa2, 0x00, 0x20, + 0x10, 0x14, 0x04, 0xa4, 0x00, 0x20, 0x20, 0x23, 0x23, 0x14, 0x14, 0x83, 0x00, 0x04, 0x02, 0x12, + 0x12, 0x21, 0x21, 0x20, 0x12, 0x23, 0x23, 0xa4, 0x00, 0x04, 0x04, 0xa4, 0x04, 0x00, 0x00, 0x11, + 0x11, 0x20, 0x20, 0xa4, 0x04, 0x00, 0x00, 0x22, 0x20, 0xa4, 0x01, 0x10, 0x10, 0x21, 0x21, 0x23, + 0x23, 0x14, 0x14, 0x03, 0x03, 0x81, 0x00, 0x04, 0x00, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x82, + 0x01, 0x10, 0x10, 0x21, 0x21, 0x23, 0x23, 0x14, 0x14, 0x03, 0x03, 0x01, 0x04, 0x93, 0x00, 0x04, + 0x00, 0x10, 0x10, 0x21, 0x21, 0x12, 0x12, 0x02, 0x02, 0xa4, 0x21, 0x10, 0x10, 0x01, 0x01, 0x23, + 0x23, 0x14, 0x14, 0x83, 0x00, 0x20, 0x10, 0x94, 0x00, 0x04, 0x04, 0x24, 0x24, 0xa0, 0x00, 0x03, + 0x03, 0x14, 0x14, 0x23, 0x23, 0xa0, 0x00, 0x04, 0x04, 0x24, 0x14, 0x13, 0x24, 0xa0, 0x00, 0x01, + 0x01, 0x23, 0x23, 0x24, 0x04, 0x03, 0x03, 0x21, 0x21, 0xa0, 0x00, 0x01, 0x01, 0x12, 0x12, 0x14, + 0x12, 0x21, 0x21, 0xa0, 0x00, 0x20, 0x20, 0x02, 0x02, 0x04, 0x04, 0xa4, 0x10, 0x00, 0x00, 0x04, + 0x04, 0x94, 0x01, 0xa3, 0x10, 0x20, 0x20, 0x24, 0x24, 0x94, 0x00, 0x91, 0x02, 0x04, 0x04, 0x24, + 0x24, 0x22, 0x23, 0x12, 0x12, 0x82, 0x00, 0x04, 0x04, 0x24, 0x24, 0x22, 0x22, 0x82, 0x24, 0x04, + 0x04, 0x03, 0x03, 0x12, 0x12, 0xa2, 0x20, 0x24, 0x24, 0x04, 0x04, 0x02, 0x02, 0xa2, 0x24, 0x04, + 0x04, 0x02, 0x02, 0x22, 0x22, 0x23, 0x23, 0x93, 0x04, 0x01, 0x02, 0x12, 0x01, 0x10, 0x10, 0xa1, + 0x23, 0x12, 0x12, 0x03, 0x03, 0x14, 0x14, 0x23, 0x23, 0x24, 0x24, 0x15, 0x15, 0x84, 0x00, 0x04, + 0x03, 0x12, 0x12, 0x23, 0x23, 0xa4, 0x11, 0x11, 0x12, 0x94, 0x22, 0x22, 0x23, 0x24, 0x24, 0x15, + 0x15, 0x84, 0x00, 0x04, 0x03, 0x13, 0x13, 0x22, 0x13, 0xa4, 0x02, 0x04, 0x02, 0x13, 0x12, 0x14, + 0x12, 0x23, 0x23, 0xa4, 0x02, 0x04, 0x03, 0x12, 0x12, 0x23, 0x23, 0xa4, 0x02, 0x05, 0x04, 0x24, + 0x24, 0x22, 0x22, 0x82, 0x02, 0x04, 0x04, 0x24, 0x25, 0x22, 0x22, 0x82, 0x02, 0x04, 0x03, 0x12, + 0x12, 0xa2, 0x22, 0x02, 0x02, 0x03, 0x03, 0x23, 0x23, 0x24, 0x24, 0x84, 0x11, 0x14, 0x02, 0xa2, + 0x02, 0x04, 0x04, 0x14, 0x14, 0x23, 0x24, 0xa2, 0x02, 0x03, 0x03, 0x14, 0x14, 0x23, 0x23, 0xa2, + 0x02, 0x03, 0x03, 0x14, 0x14, 0x12, 0x13, 0x24, 0x24, 0xa2, 0x02, 0x24, 0x04, 0xa2, 0x02, 0x03, + 0x03, 0x14, 0x22, 0x23, 0x23, 0x85, 0x02, 0x22, 0x22, 0x04, 0x04, 0xa4, 0x20, 0x10, 0x10, 0x14, + 0x14, 0x24, 0x12, 0x82, 0x10, 0x11, 0x13, 0x94, 0x00, 0x10, 0x10, 0x14, 0x14, 0x04, 0x12, 0xa2, + 0x01, 0x10, 0x10, 0x11, 0x11, 0xa0, 0x03, 0x04, 0x04, 0x24, 0x24, 0x23, 0x23, 0x12, 0x12, 0x83, + 0x10, 0x10, 0x11, 0x94, 0x21 +}; + +void CNFGDrawText(const char *text, int scale) +{ + const unsigned char *lmap; + int16_t iox = (int16_t)CNFGPenX; + int16_t ioy = (int16_t)CNFGPenY; + + int place = 0; + unsigned short index; + int bQuit = 0; + while (text[place]) { + unsigned char c = text[place]; + + switch (c) { + case 9: + iox += 12 * scale; + break; + case 10: + iox = (int16_t)CNFGPenX; + ioy += 6 * scale; + break; + default: + index = FontCharMap[c & 0x7f]; + if (index == 65535) { + iox += 3 * scale; + break; + } + + lmap = &FontCharData[index]; + do { + int x1 = (int)((((*lmap) & 0x70) >> 4) * scale + iox); + int y1 = (int)(((*lmap) & 0x0f) * scale + ioy); + int x2 = (int)((((*(lmap + 1)) & 0x70) >> 4) * scale + iox); + int y2 = (int)(((*(lmap + 1)) & 0x0f) * scale + ioy); + lmap++; + CNFGTackSegment(x1, y1, x2, y2); + bQuit = *lmap & 0x80; + lmap++; + } while (!bQuit); + + iox += 3 * scale; + } + place++; + } +} + +void CNFGDrawBox(int x1, int y1, int x2, int y2) +{ + unsigned lc = CNFGLastColor; + CNFGColor(CNFGDialogColor); + CNFGTackRectangle(x1, y1, x2, y2); + CNFGColor(lc); + CNFGTackSegment(x1, y1, x2, y1); + CNFGTackSegment(x2, y1, x2, y2); + CNFGTackSegment(x2, y2, x1, y2); + CNFGTackSegment(x1, y2, x1, y1); +} + +void CNFGTackRectangle(short x1, short y1, short x2, short y2) +{ + short ly = 0, my = 0, lx = 0, mx = 0; + short y, x; + if (y1 < y2) { ly = y1; my = y2; } + else { ly = y2; my = y1; } + if (x1 < x2) { lx = x1; mx = x2; } + else { lx = x2; mx = x1; } + + for (y = ly; y <= my; y++) + for (x = lx; x <= mx; x++) + CNFGTackPixel(x >> 1, y); +} + +// Perlin noise functions +int16_t tdNoiseAt(int16_t x, int16_t y) +{ + return ((x * 13244321 + y * 33442927)); +} + +static inline int16_t tdFade(int16_t f) +{ + return f; +} + +int16_t tdFLerp(int16_t a, int16_t b, int16_t t) +{ + int16_t fr = tdFade(t); + return (a * (256 - fr) + b * fr) >> 8; +} + +static inline int16_t tdFNoiseAt(int16_t x, int16_t y) +{ + int ix = x; + int iy = y; + int16_t fx = x - ix; + int16_t fy = y - iy; + + int16_t a = tdNoiseAt(ix, iy); + int16_t b = tdNoiseAt(ix + 1, iy); + int16_t c = tdNoiseAt(ix, iy + 1); + int16_t d = tdNoiseAt(ix + 1, iy + 1); + + int16_t top = tdFLerp(a, b, fx); + int16_t bottom = tdFLerp(c, d, fx); + + return tdFLerp(top, bottom, fy); +} + +int16_t tdPerlin2D(int16_t x, int16_t y) +{ + int ndepth = 5; + int depth; + int16_t ret = 0; + for (depth = 0; depth < ndepth; depth++) { + int16_t nx = (x * 256) / (256 << (ndepth - depth - 1)); + int16_t ny = (y * 256) / (256 << (ndepth - depth - 1)); + ret += tdFNoiseAt(nx, ny) / (256 << (depth + 1)); + } + return ret; +} diff --git a/main/3d.h b/main/3d.h new file mode 100644 index 0000000..e6f7820 --- /dev/null +++ b/main/3d.h @@ -0,0 +1,76 @@ +/** + * @file 3d.h + * @brief Fixed-point 3D graphics engine + * + * Provides matrix-based 3D transformations and rendering primitives + * for the Channel3 video output. Uses 256 = 1.0 fixed-point math. + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#ifndef _3D_H +#define _3D_H + +#include +#include "video_broadcast.h" + +// External references +extern int gframe; +extern uint8_t *frontframe; +extern int16_t ProjectionMatrix[16]; +extern int16_t ModelviewMatrix[16]; +extern int CNFGPenX, CNFGPenY; +extern uint8_t CNFGBGColor; +extern uint8_t CNFGLastColor; +extern uint8_t CNFGDialogColor; + +// Drawing primitives +void CNFGTackSegment(int x0, int y0, int x1, int y1); +int LABS(int x); + +// Pixel plotting function pointer (set by CNFGColor) +extern void (*CNFGTackPixel)(int x, int y); + +// Coordinate transformation +void LocalToScreenspace(int16_t *coords_3v, int16_t *o1, int16_t *o2); + +// Trigonometry (lookup table based) +int16_t tdSIN(uint8_t iv); +int16_t tdCOS(uint8_t iv); + +/** + * @brief Set drawing color + * @param col Color value: + * 0-15: Standard density colors + * 16: Black, double-density + * 17: White, double-density + */ +void CNFGColor(uint8_t col); + +// Matrix operations +void tdTranslate(int16_t *f, int16_t x, int16_t y, int16_t z); +void tdScale(int16_t *f, int16_t x, int16_t y, int16_t z); +void tdRotateEA(int16_t *f, int16_t x, int16_t y, int16_t z); +void tdMultiply(int16_t *fin1, int16_t *fin2, int16_t *fout); +void tdPTransform(int16_t *pin, int16_t *f, int16_t *pout); +void td4Transform(int16_t *pin, int16_t *f, int16_t *pout); +void MakeTranslate(int x, int y, int z, int16_t *out); +void Perspective(int fovx, int aspect, int zNear, int zFar, int16_t *out); +void tdIdentity(int16_t *matrix); +void MakeYRotationMatrix(uint8_t angle, int16_t *f); +void MakeXRotationMatrix(uint8_t angle, int16_t *f); + +// High-level drawing +void DrawGeoSphere(void); +void Draw3DSegment(int16_t *c1, int16_t *c2); +void CNFGDrawText(const char *text, int scale); +void CNFGDrawBox(int x1, int y1, int x2, int y2); +void CNFGTackRectangle(short x1, short y1, short x2, short y2); + +// Perlin noise +int16_t tdPerlin2D(int16_t x, int16_t y); +int16_t tdFLerp(int16_t a, int16_t b, int16_t t); +int16_t tdNoiseAt(int16_t x, int16_t y); + +#endif // _3D_H diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..a3cf568 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,21 @@ +idf_component_register( + SRCS + "user_main.c" + "video_broadcast.c" + "3d.c" + INCLUDE_DIRS + "." + REQUIRES + driver + esp_timer + esp_wifi + nvs_flash + esp_netif + esp_http_server + esp_http_client + mqtt + PRIV_REQUIRES + tablemaker + json + mbedtls +) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..581d5ed --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,63 @@ +menu "Channel3 Configuration" + + choice VIDEO_STANDARD + prompt "Video Standard" + default VIDEO_NTSC + help + Select the video standard for broadcast output. + + config VIDEO_NTSC + bool "NTSC (North America, Japan)" + config VIDEO_PAL + bool "PAL (Europe, Australia)" + endchoice + + config I2S_DATA_GPIO + int "I2S Data Output GPIO" + default 22 + range 0 39 + help + GPIO pin for I2S data output (RF broadcast). + This pin will output the 80MHz modulated signal. + + choice WIFI_MODE + prompt "WiFi Mode" + default WIFI_MODE_STATION + help + Select WiFi operation mode. + + config WIFI_MODE_STATION + bool "Station (connect to existing network)" + config WIFI_MODE_SOFTAP + bool "SoftAP (create own access point)" + endchoice + + config WIFI_STA_SSID + string "WiFi Network SSID" + default "MyNetwork" + depends on WIFI_MODE_STATION + help + SSID of the WiFi network to connect to. + + config WIFI_STA_PASS + string "WiFi Network Password" + default "MyPassword" + depends on WIFI_MODE_STATION + help + Password for the WiFi network. + + config WIFI_SOFTAP_SSID + string "WiFi SoftAP SSID" + default "Channel3" + depends on WIFI_MODE_SOFTAP + help + SSID for the ESP32 SoftAP mode. + + config WIFI_SOFTAP_PASS + string "WiFi SoftAP Password" + default "channel3tv" + depends on WIFI_MODE_SOFTAP + help + Password for the ESP32 SoftAP mode. Leave empty for open network. + +endmenu diff --git a/main/user_main.c b/main/user_main.c new file mode 100644 index 0000000..074b965 --- /dev/null +++ b/main/user_main.c @@ -0,0 +1,4272 @@ +/** + * @file user_main.c + * @brief ESP32 Channel3 Main Application + * + * Main entry point and demo screens for the Channel3 video broadcast system. + * Provides various demonstration states showing text, graphics, and 3D rendering. + * + * Original Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#include +#include +#include +#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.h" +#include "esp_timer.h" +#include "esp_http_server.h" +#include "esp_http_client.h" +#include "lwip/sockets.h" +#include "lwip/netdb.h" +#include "esp_sntp.h" +#include "esp_crt_bundle.h" +#include "cJSON.h" +#include "mqtt_client.h" + +#include "video_broadcast.h" +#include "3d.h" + +static const char *TAG = "channel3"; + +// From video_broadcast.c - for jam_color control +extern int8_t jam_color; + +// Video broadcast state +static bool video_running = false; + +// WiFi connection state +static bool wifi_connected = false; +static char wifi_ip_str[16] = "0.0.0.0"; + +// Demo state machine +#define INITIAL_SHOW_STATE 13 // Start with weather display + +extern int gframe; +extern uint32_t last_internal_frametime; +static char lastct[256]; +static uint8_t showstate = INITIAL_SHOW_STATE; +static uint8_t showallowadvance = 1; +static int framessostate = 0; +static int showtemp = 0; + +// HTTP Server handle +static httpd_handle_t http_server = NULL; + +// Uploaded image buffer (116x220 at 4bpp = 12760 bytes) +#define IMG_WIDTH 116 +#define IMG_HEIGHT 220 +#define IMG_BUFFER_SIZE ((IMG_WIDTH * IMG_HEIGHT) / 2) +static uint8_t uploaded_image[IMG_BUFFER_SIZE]; +static bool has_uploaded_image = false; + +// Video streaming server +#define STREAM_PORT 5000 +static bool streaming_active = false; + +// Weather data +static char weather_city[24] = "Loading..."; +static char weather_condition[24] = ""; +static char weather_temp[8] = "--"; +static char weather_humidity[8] = "--"; +static char weather_wind[16] = "--"; +static uint32_t last_weather_fetch = 0; +static uint32_t last_weather_screen_toggle = 0; +static int weather_screen_page = 0; // 0 = current conditions, 1 = forecast +#define WEATHER_FETCH_INTERVAL_MS 300000 // 5 minutes +#define WEATHER_SCREEN_TOGGLE_MS 5000 // 5 seconds per screen + +// Cedar Park, TX location (78613) +#define WEATHER_CITY_NAME "Cedar Park" +#define WEATHER_LAT "30.51" +#define WEATHER_LON "-97.82" + +// Hourly forecast (6 entries, every 2 hours) +#define FORECAST_ENTRIES 6 +typedef struct { + char time[8]; // "2pm", "4pm", etc. + char temp[8]; // "56F" + char cond[12]; // "Rain", "Clear", etc. + char humidity[8]; // "100%" + char wind[12]; // "3mph" +} forecast_entry_t; +static forecast_entry_t hourly_forecast[FORECAST_ENTRIES]; + +// Screen calibration margins (stored in NVS) +static int8_t margin_left = 0; +static int8_t margin_top = 0; +static int8_t margin_right = 0; +static int8_t margin_bottom = 0; +#define NVS_NAMESPACE "channel3" +#define NVS_KEY_MARGIN_L "margin_l" +#define NVS_KEY_MARGIN_T "margin_t" +#define NVS_KEY_MARGIN_R "margin_r" +#define NVS_KEY_MARGIN_B "margin_b" + +// MQTT Configuration (stored in NVS) +#define ALERT_DURATION_MS 5000 +#define MAX_ALERTS 4 +#define NVS_KEY_MQTT_BROKER "mqtt_broker" +#define NVS_KEY_MQTT_PORT "mqtt_port" +#define NVS_KEY_MQTT_USER "mqtt_user" +#define NVS_KEY_MQTT_PASS "mqtt_pass" +#define NVS_KEY_ALERT_TOPIC "alert_topic" +#define NVS_KEY_ALERT_MSG "alert_msg" + +static char mqtt_broker[64] = "10.0.0.18"; +static uint16_t mqtt_port = 1883; +static char mqtt_username[64] = "homeassistant"; +static char mqtt_password[128] = "oes5gohng9gau1Quei2ohpixashi4Thidoon1shohGai2mae0ru2zaph2vooshai"; +static esp_mqtt_client_handle_t mqtt_client = NULL; + +// Alert configurations (topic -> message mapping) +typedef struct { + char topic[64]; + char message[24]; // Short message for TV display +} alert_config_t; + +static alert_config_t alerts[MAX_ALERTS] = { + { "channel3/intruder", "INTRUDER!" }, + { "channel3/door", "DOOR OPENED!" }, + { "", "" }, + { "", "" } +}; + +// Alert state +static volatile bool alert_active = false; +static uint32_t alert_start_time = 0; +static char current_alert_message[64] = "ALERT!"; + +// MQTT debug state +static volatile bool mqtt_connected = false; +static volatile bool mqtt_needs_restart = false; +static int mqtt_subscribe_msg_ids[MAX_ALERTS] = {0}; +static int mqtt_subscribed_count = 0; +static char mqtt_last_topic[64] = ""; +static char mqtt_last_data[32] = ""; +static uint32_t mqtt_last_msg_time = 0; +static uint32_t mqtt_connect_count = 0; + +// HTTP client response buffer (Open-Meteo JSON is ~1.5KB) +static char http_response_buffer[2048]; +static int http_response_len = 0; + +// ============================================================================ +// Home Assistant Integration +// ============================================================================ + +// HA Connection Config (stored in NVS) +#define NVS_KEY_HA_URL "ha_url" +#define NVS_KEY_HA_TOKEN "ha_token" +#define NVS_KEY_HA_INTERVAL "ha_interval" +#define NVS_KEY_IMAGE "uploaded_img" + +static char ha_url[96] = ""; // e.g., "http://10.0.0.5:8123" +static char ha_token[256] = ""; // Long-lived access token +static uint32_t ha_poll_interval_ms = 60000; + +// Sensor Config (max 8 sensors) +#define MAX_HA_SENSORS 8 +#define HA_DISPLAY_TEXT 0 +#define HA_DISPLAY_GAUGE 1 + +typedef struct { + char entity_id[64]; // e.g., "sensor.outdoor_temperature" + char attribute[32]; // e.g., "humidity" or empty for state + char name[16]; // User-friendly display name + uint8_t display_type; // HA_DISPLAY_TEXT or HA_DISPLAY_GAUGE + int16_t min_value; // Gauge minimum + int16_t max_value; // Gauge maximum + uint8_t enabled; // Include in rotation + char cached_value[16]; // Runtime: cached display value + uint32_t last_update; // Runtime: last fetch timestamp +} ha_sensor_config_t; + +static ha_sensor_config_t ha_sensors[MAX_HA_SENSORS] = {0}; +static int ha_sensor_count = 0; +static int ha_current_sensor = 0; +static uint32_t last_ha_fetch = 0; + +// Screen Rotation Configuration +#define MAX_ROTATION_SLOTS 12 +#define SCREEN_TYPE_WEATHER 0 +#define SCREEN_TYPE_CLOCK 1 +#define SCREEN_TYPE_HA_SENSOR 2 +#define SCREEN_TYPE_IMAGE 3 + +typedef struct { + uint8_t screen_type; // 0=Weather, 1=Clock, 2=HA Sensor, 3=Image + uint8_t sensor_idx; // For HA sensors: which sensor (0-7) + uint8_t enabled; // Include in rotation + uint16_t duration_sec; // How long to display (5-300 seconds) +} rotation_slot_t; + +static rotation_slot_t rotation_slots[MAX_ROTATION_SLOTS] = { + {SCREEN_TYPE_WEATHER, 0, 1, 30}, // Weather, 30 sec + {SCREEN_TYPE_CLOCK, 0, 1, 15}, // Clock, 15 sec + {0, 0, 0, 0}, // Empty slots +}; +static uint8_t rotation_count = 2; +static uint8_t current_rotation_idx = 0; +static uint32_t rotation_slot_start_time = 0; + +// Screen Transition Configuration +#define TRANS_NONE 0 +#define TRANS_FADE 1 +#define TRANS_WIPE_L 2 // New slides in from right +#define TRANS_WIPE_R 3 // New slides in from left +#define TRANS_WIPE_D 4 // New slides in from top +#define TRANS_WIPE_U 5 // New slides in from bottom +#define TRANS_DISSOLVE 6 + +#define NVS_KEY_TRANS_TYPE "trans_type" +#define NVS_KEY_TRANS_SPEED "trans_speed" + +// Transition state +static uint8_t transition_active = 0; +static uint8_t transition_type = TRANS_NONE; +static uint8_t transition_progress = 0; // 0-255 (0=start, 255=complete) +static uint8_t transition_speed = 12; // Progress increment per frame (~20 frames) +static uint8_t prev_frame[(FBW2/2) * FBH]; // Store previous frame for blending + +// Global default transition (NVS persisted) +static uint8_t default_transition = TRANS_FADE; +static uint8_t default_trans_speed = 12; + +/** + * @brief Load screen margins from NVS + */ +static void load_margins(void) +{ + nvs_handle_t nvs; + if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) { + nvs_get_i8(nvs, NVS_KEY_MARGIN_L, &margin_left); + nvs_get_i8(nvs, NVS_KEY_MARGIN_T, &margin_top); + nvs_get_i8(nvs, NVS_KEY_MARGIN_R, &margin_right); + nvs_get_i8(nvs, NVS_KEY_MARGIN_B, &margin_bottom); + nvs_close(nvs); + ESP_LOGI(TAG, "Loaded margins: L=%d T=%d R=%d B=%d", + margin_left, margin_top, margin_right, margin_bottom); + } +} + +/** + * @brief Save screen margins to NVS + */ +static void save_margins(void) +{ + nvs_handle_t nvs; + video_broadcast_pause(); + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_i8(nvs, NVS_KEY_MARGIN_L, margin_left); + nvs_set_i8(nvs, NVS_KEY_MARGIN_T, margin_top); + nvs_set_i8(nvs, NVS_KEY_MARGIN_R, margin_right); + nvs_set_i8(nvs, NVS_KEY_MARGIN_B, margin_bottom); + nvs_commit(nvs); + nvs_close(nvs); + ESP_LOGI(TAG, "Saved margins: L=%d T=%d R=%d B=%d", + margin_left, margin_top, margin_right, margin_bottom); + } + video_broadcast_resume(); +} + +/** + * @brief Save uploaded image to NVS + */ +static void save_uploaded_image(void) +{ + nvs_handle_t nvs; + video_broadcast_pause(); + + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS for image save: %s", esp_err_to_name(err)); + video_broadcast_resume(); + return; + } + + // Get NVS stats to check available space + nvs_stats_t nvs_stats; + if (nvs_get_stats(NULL, &nvs_stats) == ESP_OK) { + ESP_LOGI(TAG, "NVS stats: used=%d free=%d total=%d", + nvs_stats.used_entries, nvs_stats.free_entries, nvs_stats.total_entries); + } + + err = nvs_set_blob(nvs, NVS_KEY_IMAGE, uploaded_image, IMG_BUFFER_SIZE); + if (err == ESP_OK) { + err = nvs_commit(nvs); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Saved uploaded image to NVS (%d bytes)", IMG_BUFFER_SIZE); + } else { + ESP_LOGE(TAG, "Failed to commit image to NVS: %s", esp_err_to_name(err)); + } + } else { + ESP_LOGE(TAG, "Failed to save image to NVS: %s (size=%d)", esp_err_to_name(err), IMG_BUFFER_SIZE); + } + nvs_close(nvs); + video_broadcast_resume(); +} + +/** + * @brief Load uploaded image from NVS + */ +static void load_uploaded_image(void) +{ + nvs_handle_t nvs; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to open NVS for image load: %s", esp_err_to_name(err)); + return; + } + + size_t len = IMG_BUFFER_SIZE; + err = nvs_get_blob(nvs, NVS_KEY_IMAGE, uploaded_image, &len); + if (err == ESP_OK && len == IMG_BUFFER_SIZE) { + has_uploaded_image = true; + ESP_LOGI(TAG, "Loaded uploaded image from NVS (%d bytes)", len); + } else if (err == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGI(TAG, "No saved image found in NVS"); + } else { + ESP_LOGE(TAG, "Failed to load image from NVS: %s (len=%d, expected=%d)", + esp_err_to_name(err), len, IMG_BUFFER_SIZE); + } + nvs_close(nvs); +} + +/** + * @brief Load MQTT configuration from NVS + */ +static void load_mqtt_config(void) +{ + nvs_handle_t nvs; + if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(mqtt_broker); + nvs_get_str(nvs, NVS_KEY_MQTT_BROKER, mqtt_broker, &len); + nvs_get_u16(nvs, NVS_KEY_MQTT_PORT, &mqtt_port); + len = sizeof(mqtt_username); + nvs_get_str(nvs, NVS_KEY_MQTT_USER, mqtt_username, &len); + len = sizeof(mqtt_password); + nvs_get_str(nvs, NVS_KEY_MQTT_PASS, mqtt_password, &len); + + // Load alert configurations (only overwrite if NVS has non-empty value) + for (int i = 0; i < MAX_ALERTS; i++) { + char key[16]; + char temp[64]; + snprintf(key, sizeof(key), "%s%d", NVS_KEY_ALERT_TOPIC, i); + len = sizeof(temp); + if (nvs_get_str(nvs, key, temp, &len) == ESP_OK && temp[0]) { + strncpy(alerts[i].topic, temp, sizeof(alerts[i].topic) - 1); + alerts[i].topic[sizeof(alerts[i].topic) - 1] = '\0'; + } + snprintf(key, sizeof(key), "%s%d", NVS_KEY_ALERT_MSG, i); + len = sizeof(temp); + if (nvs_get_str(nvs, key, temp, &len) == ESP_OK && temp[0]) { + strncpy(alerts[i].message, temp, sizeof(alerts[i].message) - 1); + alerts[i].message[sizeof(alerts[i].message) - 1] = '\0'; + } + } + + nvs_close(nvs); + ESP_LOGI(TAG, "MQTT config: %s:%d user=%s", mqtt_broker, mqtt_port, mqtt_username); + for (int i = 0; i < MAX_ALERTS; i++) { + if (alerts[i].topic[0]) { + ESP_LOGI(TAG, "Alert %d: %s -> %s", i, alerts[i].topic, alerts[i].message); + } + } + } +} + +/** + * @brief Save MQTT configuration to NVS + */ +static void save_mqtt_config(void) +{ + nvs_handle_t nvs; + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_str(nvs, NVS_KEY_MQTT_BROKER, mqtt_broker); + nvs_set_u16(nvs, NVS_KEY_MQTT_PORT, mqtt_port); + nvs_set_str(nvs, NVS_KEY_MQTT_USER, mqtt_username); + nvs_set_str(nvs, NVS_KEY_MQTT_PASS, mqtt_password); + + // Save alert configurations + for (int i = 0; i < MAX_ALERTS; i++) { + char key[16]; + snprintf(key, sizeof(key), "%s%d", NVS_KEY_ALERT_TOPIC, i); + nvs_set_str(nvs, key, alerts[i].topic); + snprintf(key, sizeof(key), "%s%d", NVS_KEY_ALERT_MSG, i); + nvs_set_str(nvs, key, alerts[i].message); + } + + nvs_commit(nvs); + nvs_close(nvs); + } +} + +/** + * @brief Load Home Assistant configuration from NVS + */ +static void load_ha_config(void) +{ + nvs_handle_t nvs; + if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) { + size_t len = sizeof(ha_url); + nvs_get_str(nvs, NVS_KEY_HA_URL, ha_url, &len); + len = sizeof(ha_token); + nvs_get_str(nvs, NVS_KEY_HA_TOKEN, ha_token, &len); + nvs_get_u32(nvs, NVS_KEY_HA_INTERVAL, &ha_poll_interval_ms); + + // Load sensor configurations + ha_sensor_count = 0; + for (int i = 0; i < MAX_HA_SENSORS; i++) { + char key[16]; + char temp[64]; + + // Load entity_id + snprintf(key, sizeof(key), "ha_ent%d", i); + len = sizeof(temp); + if (nvs_get_str(nvs, key, temp, &len) == ESP_OK && temp[0]) { + strncpy(ha_sensors[i].entity_id, temp, sizeof(ha_sensors[i].entity_id) - 1); + + // Load attribute + snprintf(key, sizeof(key), "ha_attr%d", i); + len = sizeof(ha_sensors[i].attribute); + nvs_get_str(nvs, key, ha_sensors[i].attribute, &len); + + // Load display name + snprintf(key, sizeof(key), "ha_name%d", i); + len = sizeof(ha_sensors[i].name); + nvs_get_str(nvs, key, ha_sensors[i].name, &len); + + // Load display type + snprintf(key, sizeof(key), "ha_type%d", i); + uint8_t dtype = 0; + nvs_get_u8(nvs, key, &dtype); + ha_sensors[i].display_type = dtype; + + // Load gauge range + snprintf(key, sizeof(key), "ha_min%d", i); + nvs_get_i16(nvs, key, &ha_sensors[i].min_value); + snprintf(key, sizeof(key), "ha_max%d", i); + nvs_get_i16(nvs, key, &ha_sensors[i].max_value); + + // Load enabled flag + snprintf(key, sizeof(key), "ha_en%d", i); + uint8_t en = 1; + nvs_get_u8(nvs, key, &en); + ha_sensors[i].enabled = en; + + ha_sensor_count++; + ESP_LOGI(TAG, "HA Sensor %d: %s (attr=%s, name=%s, type=%d)", + i, ha_sensors[i].entity_id, ha_sensors[i].attribute, + ha_sensors[i].name, ha_sensors[i].display_type); + } + } + + // Load rotation configuration + uint8_t rot_count = 0; + if (nvs_get_u8(nvs, "rot_count", &rot_count) == ESP_OK && rot_count > 0) { + rotation_count = (rot_count > MAX_ROTATION_SLOTS) ? MAX_ROTATION_SLOTS : rot_count; + for (int i = 0; i < rotation_count; i++) { + char key[16]; + snprintf(key, sizeof(key), "rot_type%d", i); + nvs_get_u8(nvs, key, &rotation_slots[i].screen_type); + snprintf(key, sizeof(key), "rot_sens%d", i); + nvs_get_u8(nvs, key, &rotation_slots[i].sensor_idx); + snprintf(key, sizeof(key), "rot_en%d", i); + nvs_get_u8(nvs, key, &rotation_slots[i].enabled); + snprintf(key, sizeof(key), "rot_dur%d", i); + nvs_get_u16(nvs, key, &rotation_slots[i].duration_sec); + } + } + + // Load transition configuration + nvs_get_u8(nvs, NVS_KEY_TRANS_TYPE, &default_transition); + nvs_get_u8(nvs, NVS_KEY_TRANS_SPEED, &default_trans_speed); + if (default_trans_speed < 1) default_trans_speed = 1; + if (default_trans_speed > 50) default_trans_speed = 50; + ESP_LOGI(TAG, "Transition config: type=%d, speed=%d", default_transition, default_trans_speed); + + nvs_close(nvs); + ESP_LOGI(TAG, "HA config loaded: URL=%s, interval=%lums, %d sensors, %d rotation slots", + ha_url, (unsigned long)ha_poll_interval_ms, ha_sensor_count, rotation_count); + } + + // Initialize rotation timer + rotation_slot_start_time = xTaskGetTickCount() * portTICK_PERIOD_MS; + current_rotation_idx = 0; +} + +/** + * @brief Save Home Assistant configuration to NVS + */ +static void save_ha_config(void) +{ + nvs_handle_t nvs; + video_broadcast_pause(); // Pause video to prevent flash cache conflicts + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_str(nvs, NVS_KEY_HA_URL, ha_url); + nvs_set_str(nvs, NVS_KEY_HA_TOKEN, ha_token); + nvs_set_u32(nvs, NVS_KEY_HA_INTERVAL, ha_poll_interval_ms); + nvs_commit(nvs); + nvs_close(nvs); + ESP_LOGI(TAG, "HA config saved"); + } + video_broadcast_resume(); +} + +/** + * @brief Save a single HA sensor configuration to NVS + */ +static void save_ha_sensor(int idx) +{ + if (idx < 0 || idx >= MAX_HA_SENSORS) return; + + nvs_handle_t nvs; + video_broadcast_pause(); + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + char key[16]; + + snprintf(key, sizeof(key), "ha_ent%d", idx); + nvs_set_str(nvs, key, ha_sensors[idx].entity_id); + + snprintf(key, sizeof(key), "ha_attr%d", idx); + nvs_set_str(nvs, key, ha_sensors[idx].attribute); + + snprintf(key, sizeof(key), "ha_name%d", idx); + nvs_set_str(nvs, key, ha_sensors[idx].name); + + snprintf(key, sizeof(key), "ha_type%d", idx); + nvs_set_u8(nvs, key, ha_sensors[idx].display_type); + + snprintf(key, sizeof(key), "ha_min%d", idx); + nvs_set_i16(nvs, key, ha_sensors[idx].min_value); + + snprintf(key, sizeof(key), "ha_max%d", idx); + nvs_set_i16(nvs, key, ha_sensors[idx].max_value); + + snprintf(key, sizeof(key), "ha_en%d", idx); + nvs_set_u8(nvs, key, ha_sensors[idx].enabled); + + nvs_commit(nvs); + nvs_close(nvs); + } + video_broadcast_resume(); +} + +/** + * @brief Save rotation configuration to NVS + */ +static void save_rotation_config(void) +{ + nvs_handle_t nvs; + video_broadcast_pause(); + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_u8(nvs, "rot_count", rotation_count); + + for (int i = 0; i < rotation_count; i++) { + char key[16]; + snprintf(key, sizeof(key), "rot_type%d", i); + nvs_set_u8(nvs, key, rotation_slots[i].screen_type); + snprintf(key, sizeof(key), "rot_sens%d", i); + nvs_set_u8(nvs, key, rotation_slots[i].sensor_idx); + snprintf(key, sizeof(key), "rot_en%d", i); + nvs_set_u8(nvs, key, rotation_slots[i].enabled); + snprintf(key, sizeof(key), "rot_dur%d", i); + nvs_set_u16(nvs, key, rotation_slots[i].duration_sec); + } + + nvs_commit(nvs); + nvs_close(nvs); + ESP_LOGI(TAG, "Rotation config saved: %d slots", rotation_count); + } + video_broadcast_resume(); +} + +/** + * @brief Save transition configuration to NVS + */ +static void save_transition_config(void) +{ + nvs_handle_t nvs; + video_broadcast_pause(); + if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_u8(nvs, NVS_KEY_TRANS_TYPE, default_transition); + nvs_set_u8(nvs, NVS_KEY_TRANS_SPEED, default_trans_speed); + nvs_commit(nvs); + nvs_close(nvs); + ESP_LOGI(TAG, "Transition config saved: type=%d, speed=%d", default_transition, default_trans_speed); + } + video_broadcast_resume(); +} + +/** + * @brief Get showstate value for a rotation screen type + */ +static int rotation_type_to_state(uint8_t screen_type) +{ + switch (screen_type) { + case SCREEN_TYPE_WEATHER: return 13; + case SCREEN_TYPE_CLOCK: return 16; + case SCREEN_TYPE_HA_SENSOR: return 17; + case SCREEN_TYPE_IMAGE: return 12; + default: return 13; + } +} + +/** + * @brief Advance to next enabled rotation slot + * @return The showstate for the new slot + */ +static int advance_rotation(void) +{ + if (rotation_count == 0) return 13; // Default to weather + + // Find next enabled slot + for (int tries = 0; tries < rotation_count; tries++) { + current_rotation_idx = (current_rotation_idx + 1) % rotation_count; + if (rotation_slots[current_rotation_idx].enabled) { + rotation_slot_start_time = xTaskGetTickCount() * portTICK_PERIOD_MS; + + // For HA sensor type, set which sensor to display + if (rotation_slots[current_rotation_idx].screen_type == SCREEN_TYPE_HA_SENSOR) { + ha_current_sensor = rotation_slots[current_rotation_idx].sensor_idx; + } + + return rotation_type_to_state(rotation_slots[current_rotation_idx].screen_type); + } + } + + return 13; // Default to weather if nothing enabled +} + +/** + * @brief Check if current rotation slot duration has expired + */ +static bool rotation_duration_expired(void) +{ + if (rotation_count == 0) return false; + + uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS; + uint32_t duration_ms = rotation_slots[current_rotation_idx].duration_sec * 1000; + + return (now - rotation_slot_start_time) >= duration_ms; +} + +/** + * @brief MQTT event handler + */ +static void mqtt_event_handler(void *args, esp_event_base_t base, + int32_t event_id, void *event_data) +{ + esp_mqtt_event_handle_t event = event_data; + + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT connected to broker"); + mqtt_connected = true; + mqtt_connect_count++; + mqtt_subscribed_count = 0; + // Subscribe to channel3/# wildcard - any message payload becomes the alert text + { + int msg_id = esp_mqtt_client_subscribe(event->client, "channel3/#", 0); + mqtt_subscribe_msg_ids[0] = msg_id; + ESP_LOGI(TAG, "Subscribed to 'channel3/#' msg_id=%d", msg_id); + } + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT subscribed, msg_id=%d", event->msg_id); + mqtt_subscribed_count++; + break; + + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT data: topic_len=%d data_len=%d", event->topic_len, event->data_len); + mqtt_last_msg_time = xTaskGetTickCount() * portTICK_PERIOD_MS; + // Store last received topic/data for debug + if (event->topic_len > 0) { + int len = event->topic_len < 63 ? event->topic_len : 63; + memcpy(mqtt_last_topic, event->topic, len); + mqtt_last_topic[len] = '\0'; + } + if (event->data_len > 0) { + int len = event->data_len < 31 ? event->data_len : 31; + memcpy(mqtt_last_data, event->data, len); + mqtt_last_data[len] = '\0'; + } + + // Use the message payload as the alert text + if (event->data_len > 0) { + int len = event->data_len < sizeof(current_alert_message) - 1 ? event->data_len : sizeof(current_alert_message) - 1; + memcpy(current_alert_message, event->data, len); + current_alert_message[len] = '\0'; + + alert_active = true; + framessostate = 0; // Reset frame counter for alert duration + showstate = 15; // Switch to alert screen + showallowadvance = 0; + ESP_LOGI(TAG, "ALERT triggered: %s", current_alert_message); + } + break; + + case MQTT_EVENT_DISCONNECTED: + ESP_LOGW(TAG, "MQTT disconnected"); + mqtt_connected = false; + break; + + default: + break; + } +} + +/** + * @brief Start MQTT client with current configuration + */ +static void start_mqtt_client(void) +{ + char uri[128]; + snprintf(uri, sizeof(uri), "mqtt://%s:%d", mqtt_broker, mqtt_port); + + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = uri, + }; + + // Add credentials if configured + if (mqtt_username[0]) { + mqtt_cfg.credentials.username = mqtt_username; + } + if (mqtt_password[0]) { + mqtt_cfg.credentials.authentication.password = mqtt_password; + } + + mqtt_client = esp_mqtt_client_init(&mqtt_cfg); + esp_mqtt_client_register_event(mqtt_client, ESP_EVENT_ANY_ID, + mqtt_event_handler, NULL); + esp_mqtt_client_start(mqtt_client); + ESP_LOGI(TAG, "MQTT client started: %s user=%s", uri, mqtt_username[0] ? mqtt_username : "(none)"); +} + +/** + * @brief HTTP event handler - accumulates response data + */ +static esp_err_t http_event_handler(esp_http_client_event_t *evt) +{ + switch (evt->event_id) { + case HTTP_EVENT_ON_DATA: + if (http_response_len + evt->data_len < sizeof(http_response_buffer) - 1) { + memcpy(http_response_buffer + http_response_len, evt->data, evt->data_len); + http_response_len += evt->data_len; + http_response_buffer[http_response_len] = '\0'; + } else { + ESP_LOGW(TAG, "HTTP response buffer overflow! Current: %d, incoming: %d, max: %d", + http_response_len, evt->data_len, sizeof(http_response_buffer)); + } + break; + default: + break; + } + return ESP_OK; +} + + +/** + * @brief Helper to make HTTP request and get response + */ +static bool http_get(const char *url) +{ + http_response_len = 0; + http_response_buffer[0] = '\0'; + + esp_http_client_config_t config = { + .url = url, + .event_handler = http_event_handler, + .timeout_ms = 15000, + .crt_bundle_attach = esp_crt_bundle_attach, // Enable HTTPS with CA bundle + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) return false; + + esp_http_client_set_header(client, "User-Agent", "curl/7.0"); + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + esp_http_client_cleanup(client); + + return (err == ESP_OK && status == 200 && http_response_len > 0); +} + + +/** + * @brief Map WMO weather code to short condition string + * WMO codes: https://open-meteo.com/en/docs (weather_code description) + */ +static const char* wmo_to_condition(int code) +{ + switch (code) { + case 0: return "Clear"; + case 1: return "Mostly Clr"; + case 2: return "PtCloud"; + case 3: return "Cloudy"; + case 45: + case 48: return "Fog"; + case 51: + case 53: + case 55: return "Drizzle"; + case 56: + case 57: return "FrzDrzl"; + case 61: + case 63: + case 65: return "Rain"; + case 66: + case 67: return "FrzRain"; + case 71: + case 73: + case 75: return "Snow"; + case 77: return "SnowGrn"; + case 80: + case 81: + case 82: return "Showers"; + case 85: + case 86: return "SnowShwr"; + case 95: return "TStorm"; + case 96: + case 99: return "TStorm+"; + default: return "Unknown"; + } +} + +/** + * @brief Format hour from ISO timestamp to 12-hour format + * Input: "2025-01-21T14:00" -> Output: "2pm" + */ +static void format_hour_from_iso(const char *iso_time, char *out, size_t out_size) +{ + // Extract hour from "YYYY-MM-DDTHH:MM" + const char *t = strchr(iso_time, 'T'); + if (!t || strlen(t) < 3) { + strncpy(out, "??", out_size); + return; + } + int hour = atoi(t + 1); + // Ensure valid hour range 0-23 + if (hour < 0) hour = 0; + if (hour > 23) hour = 23; + + if (hour == 0) { + snprintf(out, out_size, "12am"); + } else if (hour < 12) { + snprintf(out, out_size, "%dam", hour); // max "11am" = 4 chars + } else if (hour == 12) { + snprintf(out, out_size, "12pm"); + } else { + snprintf(out, out_size, "%dpm", hour - 12); // max "11pm" = 4 chars + } +} + +/** + * @brief Fetch weather data from Open-Meteo API + * Uses ~1.5KB JSON with proper hourly forecasts + */ +static void fetch_weather(void) +{ + if (!wifi_connected) { + ESP_LOGW(TAG, "WiFi not connected, skipping weather fetch"); + return; + } + + // Open-Meteo API URL for Cedar Park, TX + const char *url = "https://api.open-meteo.com/v1/forecast?" + "latitude=" WEATHER_LAT "&longitude=" WEATHER_LON + "¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m" + "&hourly=temperature_2m,weather_code,relative_humidity_2m,wind_speed_10m" + "&temperature_unit=fahrenheit&wind_speed_unit=mph" + "&timezone=America/Chicago&forecast_hours=12"; + + ESP_LOGI(TAG, "Fetching weather from Open-Meteo..."); + + if (!http_get(url)) { + ESP_LOGE(TAG, "Failed to fetch weather"); + strcpy(weather_city, "Fetch fail"); + return; + } + + ESP_LOGI(TAG, "Response length: %d bytes", http_response_len); + + // Parse JSON + cJSON *root = cJSON_Parse(http_response_buffer); + if (!root) { + ESP_LOGE(TAG, "JSON parse failed"); + strcpy(weather_city, "Parse fail"); + return; + } + + // Set city name (static - Open-Meteo doesn't return location name) + strncpy(weather_city, WEATHER_CITY_NAME, sizeof(weather_city) - 1); + weather_city[sizeof(weather_city) - 1] = '\0'; + + // Parse current conditions + cJSON *current = cJSON_GetObjectItem(root, "current"); + if (current) { + cJSON *temp = cJSON_GetObjectItem(current, "temperature_2m"); + cJSON *humidity = cJSON_GetObjectItem(current, "relative_humidity_2m"); + cJSON *weather_code = cJSON_GetObjectItem(current, "weather_code"); + cJSON *wind = cJSON_GetObjectItem(current, "wind_speed_10m"); + + if (temp && cJSON_IsNumber(temp)) { + snprintf(weather_temp, sizeof(weather_temp), "%.0fF", temp->valuedouble); + } + if (humidity && cJSON_IsNumber(humidity)) { + snprintf(weather_humidity, sizeof(weather_humidity), "%.0f%%", humidity->valuedouble); + } + if (weather_code && cJSON_IsNumber(weather_code)) { + strncpy(weather_condition, wmo_to_condition((int)weather_code->valuedouble), + sizeof(weather_condition) - 1); + weather_condition[sizeof(weather_condition) - 1] = '\0'; + } + if (wind && cJSON_IsNumber(wind)) { + snprintf(weather_wind, sizeof(weather_wind), "%.0fmph", wind->valuedouble); + } + } + + // Parse hourly forecast + cJSON *hourly = cJSON_GetObjectItem(root, "hourly"); + if (hourly) { + cJSON *times = cJSON_GetObjectItem(hourly, "time"); + cJSON *temps = cJSON_GetObjectItem(hourly, "temperature_2m"); + cJSON *codes = cJSON_GetObjectItem(hourly, "weather_code"); + cJSON *humids = cJSON_GetObjectItem(hourly, "relative_humidity_2m"); + cJSON *winds = cJSON_GetObjectItem(hourly, "wind_speed_10m"); + + if (times && temps && codes && humids && winds && + cJSON_IsArray(times) && cJSON_IsArray(temps)) { + + int array_size = cJSON_GetArraySize(times); + // Get 6 forecasts at 2-hour intervals (indices 0, 2, 4, 6, 8, 10) + for (int i = 0; i < FORECAST_ENTRIES && (i * 2) < array_size; i++) { + int idx = i * 2; // Every 2 hours + + cJSON *time_item = cJSON_GetArrayItem(times, idx); + cJSON *temp_item = cJSON_GetArrayItem(temps, idx); + cJSON *code_item = cJSON_GetArrayItem(codes, idx); + cJSON *humid_item = cJSON_GetArrayItem(humids, idx); + cJSON *wind_item = cJSON_GetArrayItem(winds, idx); + + if (time_item && cJSON_IsString(time_item)) { + format_hour_from_iso(time_item->valuestring, + hourly_forecast[i].time, + sizeof(hourly_forecast[i].time)); + } + if (temp_item && cJSON_IsNumber(temp_item)) { + snprintf(hourly_forecast[i].temp, sizeof(hourly_forecast[i].temp), + "%.0fF", temp_item->valuedouble); + } + if (code_item && cJSON_IsNumber(code_item)) { + strncpy(hourly_forecast[i].cond, + wmo_to_condition((int)code_item->valuedouble), + sizeof(hourly_forecast[i].cond) - 1); + hourly_forecast[i].cond[sizeof(hourly_forecast[i].cond) - 1] = '\0'; + } + if (humid_item && cJSON_IsNumber(humid_item)) { + snprintf(hourly_forecast[i].humidity, sizeof(hourly_forecast[i].humidity), + "%.0f%%", humid_item->valuedouble); + } + if (wind_item && cJSON_IsNumber(wind_item)) { + snprintf(hourly_forecast[i].wind, sizeof(hourly_forecast[i].wind), + "%.0fmph", wind_item->valuedouble); + } + } + } + } + + cJSON_Delete(root); + + last_weather_fetch = xTaskGetTickCount() * portTICK_PERIOD_MS; + ESP_LOGI(TAG, "Weather updated: %s - %s %s, Humidity: %s, Wind: %s", + weather_city, weather_temp, weather_condition, weather_humidity, weather_wind); +} + +/** + * @brief Fetch a single Home Assistant entity state + * @param entity_id Entity ID (e.g., "sensor.temperature") + * @param attribute Attribute name (empty string for state) + * @param out_value Output buffer for the value + * @param out_size Size of output buffer + * @return true on success + */ +static bool fetch_ha_entity(const char *entity_id, const char *attribute, + char *out_value, size_t out_size) +{ + if (!wifi_connected || !ha_url[0] || !ha_token[0]) { + return false; + } + + // Build URL: {ha_url}/api/states/{entity_id} + char url[256]; + snprintf(url, sizeof(url), "%s/api/states/%s", ha_url, entity_id); + + http_response_len = 0; + http_response_buffer[0] = '\0'; + + esp_http_client_config_t config = { + .url = url, + .event_handler = http_event_handler, + .timeout_ms = 10000, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) return false; + + // Add authorization header + char auth_header[280]; + snprintf(auth_header, sizeof(auth_header), "Bearer %s", ha_token); + esp_http_client_set_header(client, "Authorization", auth_header); + esp_http_client_set_header(client, "Content-Type", "application/json"); + + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + esp_http_client_cleanup(client); + + if (err != ESP_OK || status != 200 || http_response_len == 0) { + ESP_LOGW(TAG, "HA fetch failed: %s status=%d", entity_id, status); + return false; + } + + // Parse JSON response + cJSON *root = cJSON_Parse(http_response_buffer); + if (!root) { + ESP_LOGW(TAG, "HA JSON parse failed for %s", entity_id); + return false; + } + + bool success = false; + + if (attribute && attribute[0]) { + // Get attribute from attributes object + cJSON *attrs = cJSON_GetObjectItem(root, "attributes"); + if (attrs) { + cJSON *attr_val = cJSON_GetObjectItem(attrs, attribute); + if (attr_val) { + if (cJSON_IsNumber(attr_val)) { + snprintf(out_value, out_size, "%.1f", attr_val->valuedouble); + success = true; + } else if (cJSON_IsString(attr_val)) { + strncpy(out_value, attr_val->valuestring, out_size - 1); + out_value[out_size - 1] = '\0'; + success = true; + } + } + } + } else { + // Get state value + cJSON *state = cJSON_GetObjectItem(root, "state"); + if (state && cJSON_IsString(state)) { + strncpy(out_value, state->valuestring, out_size - 1); + out_value[out_size - 1] = '\0'; + success = true; + } + } + + cJSON_Delete(root); + return success; +} + +/** + * @brief Fetch all enabled HA sensors and update cached values + */ +static void fetch_all_ha_sensors(void) +{ + if (!wifi_connected || !ha_url[0] || !ha_token[0]) { + return; + } + + uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS; + + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (ha_sensors[i].entity_id[0] && ha_sensors[i].enabled) { + char value[16]; + if (fetch_ha_entity(ha_sensors[i].entity_id, ha_sensors[i].attribute, + value, sizeof(value))) { + strncpy(ha_sensors[i].cached_value, value, sizeof(ha_sensors[i].cached_value) - 1); + ha_sensors[i].cached_value[sizeof(ha_sensors[i].cached_value) - 1] = '\0'; + ha_sensors[i].last_update = now; + ESP_LOGI(TAG, "HA sensor %s = %s", ha_sensors[i].entity_id, value); + } + } + } + + last_ha_fetch = now; +} + +/** + * @brief TCP streaming server task + * Receives raw 4bpp frames (12760 bytes each) and displays them + */ +// Static buffer for streaming to avoid stack overflow +static uint8_t stream_frame_buffer[IMG_BUFFER_SIZE]; + +static void stream_server_task(void *arg) +{ + struct sockaddr_in server_addr, client_addr; + socklen_t client_len = sizeof(client_addr); + int listen_sock, client_sock; + + ESP_LOGI(TAG, "Starting video stream server on port %d", STREAM_PORT); + + // Create socket + listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listen_sock < 0) { + ESP_LOGE(TAG, "Failed to create stream socket"); + vTaskDelete(NULL); + return; + } + + // Allow socket reuse + int opt = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Bind + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(STREAM_PORT); + + if (bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + ESP_LOGE(TAG, "Failed to bind stream socket"); + close(listen_sock); + vTaskDelete(NULL); + return; + } + + // Listen + if (listen(listen_sock, 1) < 0) { + ESP_LOGE(TAG, "Failed to listen on stream socket"); + close(listen_sock); + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "Stream server listening on port %d", STREAM_PORT); + + while (1) { + // Accept connection + client_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_len); + if (client_sock < 0) { + ESP_LOGW(TAG, "Accept failed"); + continue; + } + + char addr_str[16]; + inet_ntoa_r(client_addr.sin_addr, addr_str, sizeof(addr_str)); + ESP_LOGI(TAG, "Stream client connected from %s", addr_str); + + streaming_active = true; + showstate = 12; // Switch to uploaded image display + showallowadvance = 0; // Disable auto-advance during streaming + + // Set socket timeout + struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 }; + setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + // Receive frames + while (1) { + int total_received = 0; + int remaining = IMG_BUFFER_SIZE; + + // Receive one complete frame + while (remaining > 0) { + int received = recv(client_sock, stream_frame_buffer + total_received, remaining, 0); + if (received <= 0) { + if (received == 0) { + ESP_LOGI(TAG, "Stream client disconnected"); + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + ESP_LOGW(TAG, "Stream recv error: %d", errno); + } + goto disconnect; + } + total_received += received; + remaining -= received; + } + + // Copy frame to display buffer + memcpy(uploaded_image, stream_frame_buffer, IMG_BUFFER_SIZE); + has_uploaded_image = true; + } + +disconnect: + close(client_sock); + streaming_active = false; + ESP_LOGI(TAG, "Stream ended, received frames displayed"); + } + + close(listen_sock); + vTaskDelete(NULL); +} + +// HTML page for web interface +static const char *html_page = +"Channel3 ESP32" +"" +"
" +"

// CHANNEL3 ESP32

[ RF BROADCAST TERMINAL v1.0 ]
" +"
> INITIALIZING...
" +"
" +"

> TX_CONTROL

" +"
" +"" +"" +"
" +"
Jam: " +"
" +"
" +"

> CALIBRATION

" +"
" +"" +"" +"
" +"
" +"
" +"
Backup/Restore:
" +"
" +"" +"
" +"
" +"
" +"

> IMAGE_UPLOAD

" +"
" +"
" +"
" +"
" +"

> DEMO_SCREENS

" +"" +"" +"" +"" +"" +"" +"" +"" +"
" +"

> ROTATION

" +"
" +"
" +"" +"" +"
" +"
Transition:
" +"
" +"" +"
" +"
" +"

> MQTT_ALERTS

" +"
" +"
" +"
" +"
" +"
Topics:
" +"
" +"
" +"
" +"
" +"
" +"
" +"
" +"

> HOME_ASSISTANT

" +"
" +"
" +"
" +"
" +"
" +"
Sensors:
" +"
Add:
" +"
" +"
" +"
" +"
" +"
" +"
" +"
" +"
" +""; + +/** + * @brief Handler for GET / - main page + */ +static esp_err_t root_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, html_page, strlen(html_page)); + return ESP_OK; +} + +/** + * @brief Handler for GET /status - JSON status + */ +static esp_err_t status_handler(httpd_req_t *req) +{ + char json[420]; + snprintf(json, sizeof(json), + "{\"frame\":%d,\"frametime\":%lu,\"screen\":%d,\"auto\":%s,\"jam\":%d,\"wifi\":%s,\"ip\":\"%s\"," + "\"video\":%s,\"margins\":{\"left\":%d,\"top\":%d,\"right\":%d,\"bottom\":%d}}", + gframe, (unsigned long)last_internal_frametime, showstate, + showallowadvance ? "true" : "false", (int)jam_color, + wifi_connected ? "true" : "false", wifi_ip_str, + video_running ? "true" : "false", + margin_left, margin_top, margin_right, margin_bottom); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + return ESP_OK; +} + +/** + * @brief Handler for GET /screen?n=X - change demo screen (legacy) + */ +static esp_err_t screen_handler(httpd_req_t *req) +{ + char buf[32]; + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + char param[8]; + if (httpd_query_key_value(buf, "n", param, sizeof(param)) == ESP_OK) { + int screen = atoi(param); + if (screen >= 0 && screen <= 14) { + showstate = screen; + showallowadvance = (screen == 7) ? 1 : 0; + framessostate = 0; + showtemp = 0; + ESP_LOGI(TAG, "Screen changed to %d", screen); + if (screen == 13) { + fetch_weather(); // Refresh weather when switching to weather screen + } + } + } + } + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /control?screen=X&auto=Y - full control + */ +static esp_err_t control_handler(httpd_req_t *req) +{ + char buf[64]; + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + char param[8]; + if (httpd_query_key_value(buf, "screen", param, sizeof(param)) == ESP_OK) { + int screen = atoi(param); + if (screen >= 0 && screen <= 14) { + showstate = screen; + framessostate = 0; + showtemp = 0; + ESP_LOGI(TAG, "Screen changed to %d", screen); + if (screen == 13) { + fetch_weather(); // Refresh weather when switching to weather screen + } + } + } + if (httpd_query_key_value(buf, "auto", param, sizeof(param)) == ESP_OK) { + showallowadvance = atoi(param) ? 1 : 0; + ESP_LOGI(TAG, "Auto-advance: %s", showallowadvance ? "ON" : "OFF"); + } + } + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /jam?c=X - set jam color for RF testing + * c=-1 disables jam, c=0-15 sets a specific color + */ +static esp_err_t jam_handler(httpd_req_t *req) +{ + char buf[32]; + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + char param[8]; + if (httpd_query_key_value(buf, "c", param, sizeof(param)) == ESP_OK) { + int color = atoi(param); + if (color >= -1 && color <= 15) { + jam_color = (int8_t)color; + if (color >= 0) { + ESP_LOGI(TAG, "Jam color set to %d - RF test mode", color); + } else { + ESP_LOGI(TAG, "Jam color disabled - normal mode"); + } + } + } + } + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /video?on=0|1 - toggle video broadcast on/off + */ +static esp_err_t video_toggle_handler(httpd_req_t *req) +{ + char buf[32]; + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + char param[8]; + if (httpd_query_key_value(buf, "on", param, sizeof(param)) == ESP_OK) { + int on = atoi(param); + if (on && !video_running) { + video_broadcast_init(); + video_running = true; + ESP_LOGI(TAG, "Video broadcast started"); + } else if (!on && video_running) { + video_broadcast_stop(); + video_running = false; + ESP_LOGI(TAG, "Video broadcast stopped"); + } + } + } + httpd_resp_send(req, video_running ? "ON" : "OFF", video_running ? 2 : 3); + return ESP_OK; +} + +/** + * @brief Parse margin values from query string into variables + */ +static bool parse_margin_params(const char *buf, int8_t *l, int8_t *t, int8_t *r, int8_t *b) +{ + bool changed = false; + char param[8]; + + if (httpd_query_key_value(buf, "l", param, sizeof(param)) == ESP_OK) { + int val = atoi(param); + if (val >= 0 && val <= 50) { + *l = (int8_t)val; + changed = true; + } + } + if (httpd_query_key_value(buf, "t", param, sizeof(param)) == ESP_OK) { + int val = atoi(param); + if (val >= 0 && val <= 50) { + *t = (int8_t)val; + changed = true; + } + } + if (httpd_query_key_value(buf, "r", param, sizeof(param)) == ESP_OK) { + int val = atoi(param); + if (val >= 0 && val <= 50) { + *r = (int8_t)val; + changed = true; + } + } + if (httpd_query_key_value(buf, "b", param, sizeof(param)) == ESP_OK) { + int val = atoi(param); + if (val >= 0 && val <= 50) { + *b = (int8_t)val; + changed = true; + } + } + return changed; +} + +/** + * @brief Handler for GET /margins/preview - preview margins without saving to NVS + * Updates margin values in memory only for real-time preview + */ +static esp_err_t margins_preview_handler(httpd_req_t *req) +{ + char buf[64]; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + parse_margin_params(buf, &margin_left, &margin_top, &margin_right, &margin_bottom); + } + + // Return current margins as JSON (no save) + char json[128]; + snprintf(json, sizeof(json), + "{\"left\":%d,\"top\":%d,\"right\":%d,\"bottom\":%d}", + margin_left, margin_top, margin_right, margin_bottom); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + return ESP_OK; +} + +/** + * @brief Handler for GET /margins - set and SAVE screen calibration margins to NVS + */ +static esp_err_t margins_handler(httpd_req_t *req) +{ + char buf[64]; + bool changed = false; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + changed = parse_margin_params(buf, &margin_left, &margin_top, &margin_right, &margin_bottom); + } + + if (changed) { + save_margins(); + ESP_LOGI(TAG, "Margins saved: L=%d T=%d R=%d B=%d", + margin_left, margin_top, margin_right, margin_bottom); + } + + // Return current margins as JSON + char json[128]; + snprintf(json, sizeof(json), + "{\"left\":%d,\"top\":%d,\"right\":%d,\"bottom\":%d}", + margin_left, margin_top, margin_right, margin_bottom); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + return ESP_OK; +} + +/** + * @brief Handler for POST /upload - receive image data + */ +static esp_err_t upload_handler(httpd_req_t *req) +{ + if (req->content_len != IMG_BUFFER_SIZE) { + ESP_LOGE(TAG, "Invalid image size: %d (expected %d)", req->content_len, IMG_BUFFER_SIZE); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid image size"); + return ESP_FAIL; + } + + int received = 0; + while (received < IMG_BUFFER_SIZE) { + int ret = httpd_req_recv(req, (char*)&uploaded_image[received], IMG_BUFFER_SIZE - received); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) continue; + ESP_LOGE(TAG, "Image receive error"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive failed"); + return ESP_FAIL; + } + received += ret; + } + + has_uploaded_image = true; + ESP_LOGI(TAG, "Image uploaded: %d bytes", received); + save_uploaded_image(); // Save to NVS for persistence + httpd_resp_send(req, "Image saved!", -1); + return ESP_OK; +} + +/** + * @brief Handler for GET /mqtt/status - Return current MQTT config as JSON + */ +static esp_err_t mqtt_status_handler(httpd_req_t *req) +{ + char response[512]; + int len = snprintf(response, sizeof(response), + "{\"broker\":\"%s\",\"port\":%d,\"user\":\"%s\",\"connected\":%s,\"alerts\":[", + mqtt_broker, mqtt_port, mqtt_username, + mqtt_client ? "true" : "false"); + + for (int i = 0; i < MAX_ALERTS; i++) { + if (i > 0) len += snprintf(response + len, sizeof(response) - len, ","); + len += snprintf(response + len, sizeof(response) - len, + "{\"topic\":\"%s\",\"message\":\"%s\"}", + alerts[i].topic, alerts[i].message); + } + snprintf(response + len, sizeof(response) - len, "]}"); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + return ESP_OK; +} + +/** + * @brief Handler for GET /mqtt - Save MQTT config with credentials and alerts + */ +static esp_err_t mqtt_config_handler(httpd_req_t *req) +{ + char buf[256]; + char param[64]; + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + // Parse user + if (httpd_query_key_value(buf, "user", param, sizeof(param)) == ESP_OK) { + strncpy(mqtt_username, param, sizeof(mqtt_username)-1); + } + // Parse pass + if (httpd_query_key_value(buf, "pass", param, sizeof(param)) == ESP_OK) { + strncpy(mqtt_password, param, sizeof(mqtt_password)-1); + } + // Parse broker + if (httpd_query_key_value(buf, "broker", param, sizeof(param)) == ESP_OK) { + strncpy(mqtt_broker, param, sizeof(mqtt_broker)-1); + } + // Parse port + if (httpd_query_key_value(buf, "port", param, sizeof(param)) == ESP_OK) { + mqtt_port = atoi(param); + } + httpd_resp_send(req, "PARSED", -1); + return ESP_OK; + } + httpd_resp_send(req, "NO QUERY", -1); + return ESP_OK; +} + +/** + * @brief Handler for GET /mqtt/test - Trigger test alert + * Use ?n=0 through ?n=3 to test specific alert, default tests alert 0 + */ +static esp_err_t mqtt_test_handler(httpd_req_t *req) +{ + int idx = 0; + char buf[32]; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + char param[8]; + if (httpd_query_key_value(buf, "n", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + if (idx < 0 || idx >= MAX_ALERTS) idx = 0; + } + } + + // Use message from alerts config, or default + const char *msg = alerts[idx].message[0] ? alerts[idx].message : "ALERT!"; + strncpy(current_alert_message, msg, sizeof(current_alert_message)-1); + current_alert_message[sizeof(current_alert_message)-1] = '\0'; + + alert_active = true; + framessostate = 0; // Reset frame counter for alert duration + showstate = 15; + showallowadvance = 0; + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /mqtt/debug - Show MQTT debug info + */ +static esp_err_t mqtt_debug_handler(httpd_req_t *req) +{ + char response[768]; + uint32_t now_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; + uint32_t last_msg_ago = mqtt_last_msg_time ? (now_ms - mqtt_last_msg_time) / 1000 : 0; + + int len = snprintf(response, sizeof(response), + "MQTT Debug Info\n" + "===============\n" + "Broker: %s:%d\n" + "Connected: %s\n" + "Connect count: %lu\n" + "Subscriptions confirmed: %d\n\n" + "Subscribe msg_ids: [%d, %d, %d, %d]\n\n" + "Configured topics:\n" + " 0: '%s' -> '%s'\n" + " 1: '%s' -> '%s'\n" + " 2: '%s' -> '%s'\n" + " 3: '%s' -> '%s'\n\n" + "Last message:\n" + " Topic: '%s'\n" + " Data: '%s'\n" + " %lu seconds ago\n", + mqtt_broker, mqtt_port, + mqtt_connected ? "YES" : "NO", + (unsigned long)mqtt_connect_count, + mqtt_subscribed_count, + mqtt_subscribe_msg_ids[0], mqtt_subscribe_msg_ids[1], + mqtt_subscribe_msg_ids[2], mqtt_subscribe_msg_ids[3], + alerts[0].topic, alerts[0].message, + alerts[1].topic, alerts[1].message, + alerts[2].topic, alerts[2].message, + alerts[3].topic, alerts[3].message, + mqtt_last_topic, + mqtt_last_data, + (unsigned long)last_msg_ago); + + httpd_resp_set_type(req, "text/plain"); + httpd_resp_send(req, response, len); + return ESP_OK; +} + +// ============================================================================ +// Home Assistant HTTP Handlers +// ============================================================================ + +/** + * @brief Handler for GET /ha/status - Return HA config + all sensor states as JSON + */ +static esp_err_t ha_status_handler(httpd_req_t *req) +{ + char *response = malloc(2048); + if (!response) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + int len = snprintf(response, 2048, + "{\"url\":\"%s\",\"token_set\":%s,\"interval\":%lu,\"sensors\":[", + ha_url, ha_token[0] ? "true" : "false", (unsigned long)ha_poll_interval_ms); + + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (i > 0) len += snprintf(response + len, 2048 - len, ","); + len += snprintf(response + len, 2048 - len, + "{\"entity_id\":\"%s\",\"attribute\":\"%s\",\"name\":\"%s\"," + "\"type\":%d,\"min\":%d,\"max\":%d,\"enabled\":%d,\"value\":\"%s\"}", + ha_sensors[i].entity_id, ha_sensors[i].attribute, ha_sensors[i].name, + ha_sensors[i].display_type, ha_sensors[i].min_value, ha_sensors[i].max_value, + ha_sensors[i].enabled, ha_sensors[i].cached_value); + } + snprintf(response + len, 2048 - len, "]}"); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + free(response); + return ESP_OK; +} + +/** + * @brief Simple URL decode (handles %XX sequences) + */ +static void url_decode(char *dst, const char *src, size_t dst_size) +{ + size_t i = 0, j = 0; + while (src[i] && j < dst_size - 1) { + if (src[i] == '%' && src[i+1] && src[i+2]) { + char hex[3] = {src[i+1], src[i+2], 0}; + dst[j++] = (char)strtol(hex, NULL, 16); + i += 3; + } else if (src[i] == '+') { + dst[j++] = ' '; + i++; + } else { + dst[j++] = src[i++]; + } + } + dst[j] = '\0'; +} + +/** + * @brief Handler for GET /ha/config - Save HA connection settings + * Query params: url, token, interval + */ +static esp_err_t ha_config_handler(httpd_req_t *req) +{ + // Use heap allocation to avoid stack overflow + char *buf = malloc(512); + char *param = malloc(300); + if (!buf || !param) { + free(buf); + free(param); + httpd_resp_send_500(req); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "HA config handler called"); + + if (httpd_req_get_url_query_str(req, buf, 512) == ESP_OK) { + if (httpd_query_key_value(buf, "url", param, 300) == ESP_OK) { + url_decode(ha_url, param, sizeof(ha_url)); + ESP_LOGI(TAG, "HA URL set: %s", ha_url); + } + if (httpd_query_key_value(buf, "token", param, 300) == ESP_OK) { + url_decode(ha_token, param, sizeof(ha_token)); + ESP_LOGI(TAG, "HA Token set (len=%d)", (int)strlen(ha_token)); + } + if (httpd_query_key_value(buf, "interval", param, 300) == ESP_OK) { + int interval = atoi(param); + if (interval >= 5000 && interval <= 3600000) { + ha_poll_interval_ms = interval; + } + } + save_ha_config(); + ESP_LOGI(TAG, "HA config saved: url=%s interval=%lu token_len=%d", + ha_url, (unsigned long)ha_poll_interval_ms, (int)strlen(ha_token)); + } + + free(buf); + free(param); + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /ha/entity - Proxy to HA API, return entity state + attributes + * Query params: entity_id + */ +static esp_err_t ha_entity_handler(httpd_req_t *req) +{ + char buf[128]; + char entity_id[64] = ""; + + ESP_LOGI(TAG, "HA entity fetch request received"); + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + httpd_query_key_value(buf, "entity_id", entity_id, sizeof(entity_id)); + } + + ESP_LOGI(TAG, "HA entity_id: %s", entity_id); + + if (!entity_id[0]) { + ESP_LOGW(TAG, "Missing entity_id in request"); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing entity_id"); + return ESP_FAIL; + } + + if (!ha_url[0] || !ha_token[0]) { + ESP_LOGW(TAG, "HA not configured: url=%s token_len=%d", ha_url, (int)strlen(ha_token)); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "HA not configured"); + return ESP_FAIL; + } + + // Build URL: {ha_url}/api/states/{entity_id} + char url[256]; + snprintf(url, sizeof(url), "%s/api/states/%s", ha_url, entity_id); + ESP_LOGI(TAG, "Fetching HA entity from: %s", url); + + http_response_len = 0; + http_response_buffer[0] = '\0'; + + esp_http_client_config_t config = { + .url = url, + .event_handler = http_event_handler, + .timeout_ms = 10000, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + ESP_LOGE(TAG, "Failed to init HTTP client"); + httpd_resp_send_500(req); + return ESP_FAIL; + } + + char auth_header[280]; + snprintf(auth_header, sizeof(auth_header), "Bearer %s", ha_token); + esp_http_client_set_header(client, "Authorization", auth_header); + esp_http_client_set_header(client, "Content-Type", "application/json"); + + ESP_LOGI(TAG, "Performing HA API request..."); + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + esp_http_client_cleanup(client); + + ESP_LOGI(TAG, "HA API response: err=%d status=%d len=%d", err, status, http_response_len); + + if (err != ESP_OK || status != 200) { + char errmsg[64]; + snprintf(errmsg, sizeof(errmsg), "HA API error: %d", status); + ESP_LOGW(TAG, "%s", errmsg); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errmsg); + return ESP_FAIL; + } + + // Return raw JSON from HA + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, http_response_buffer, http_response_len); + return ESP_OK; +} + +/** + * @brief Handler for GET /ha/sensor/add - Add/update sensor config + * Query params: idx, entity_id, attr, name, type, min, max, enabled + */ +static esp_err_t ha_sensor_add_handler(httpd_req_t *req) +{ + char buf[384]; + char param[64]; + int idx = -1; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing params"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "idx", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + } + + // If idx not specified, find first empty slot + if (idx < 0) { + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (!ha_sensors[i].entity_id[0]) { + idx = i; + break; + } + } + } + + if (idx < 0 || idx >= MAX_HA_SENSORS) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "No available slot"); + return ESP_FAIL; + } + + // Parse sensor config (URL decode text fields) + if (httpd_query_key_value(buf, "entity_id", param, sizeof(param)) == ESP_OK) { + url_decode(ha_sensors[idx].entity_id, param, sizeof(ha_sensors[idx].entity_id)); + } + if (httpd_query_key_value(buf, "attr", param, sizeof(param)) == ESP_OK) { + url_decode(ha_sensors[idx].attribute, param, sizeof(ha_sensors[idx].attribute)); + } + if (httpd_query_key_value(buf, "name", param, sizeof(param)) == ESP_OK) { + url_decode(ha_sensors[idx].name, param, sizeof(ha_sensors[idx].name)); + } + if (httpd_query_key_value(buf, "type", param, sizeof(param)) == ESP_OK) { + ha_sensors[idx].display_type = atoi(param); + } + if (httpd_query_key_value(buf, "min", param, sizeof(param)) == ESP_OK) { + ha_sensors[idx].min_value = atoi(param); + } + if (httpd_query_key_value(buf, "max", param, sizeof(param)) == ESP_OK) { + ha_sensors[idx].max_value = atoi(param); + } + if (httpd_query_key_value(buf, "enabled", param, sizeof(param)) == ESP_OK) { + ha_sensors[idx].enabled = atoi(param) ? 1 : 0; + } else { + ha_sensors[idx].enabled = 1; // Default enabled + } + + // Recount sensors + ha_sensor_count = 0; + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (ha_sensors[i].entity_id[0]) ha_sensor_count++; + } + + save_ha_sensor(idx); + ESP_LOGI(TAG, "HA sensor %d added: %s", idx, ha_sensors[idx].entity_id); + + char response[32]; + snprintf(response, sizeof(response), "{\"idx\":%d}", idx); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + return ESP_OK; +} + +/** + * @brief Handler for GET /ha/sensor/remove - Remove sensor config + * Query params: idx + */ +static esp_err_t ha_sensor_remove_handler(httpd_req_t *req) +{ + char buf[32]; + char param[8]; + int idx = -1; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + if (httpd_query_key_value(buf, "idx", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + } + } + + if (idx < 0 || idx >= MAX_HA_SENSORS) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid idx"); + return ESP_FAIL; + } + + // Clear sensor + memset(&ha_sensors[idx], 0, sizeof(ha_sensor_config_t)); + + // Recount sensors + ha_sensor_count = 0; + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (ha_sensors[i].entity_id[0]) ha_sensor_count++; + } + + save_ha_sensor(idx); + ESP_LOGI(TAG, "HA sensor %d removed", idx); + + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /ha/sensor/rename - Rename a sensor + * Query params: idx, name + */ +static esp_err_t ha_sensor_rename_handler(httpd_req_t *req) +{ + char buf[128]; + char param[64]; + int idx = -1; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing params"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "idx", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + } + + if (idx < 0 || idx >= MAX_HA_SENSORS || !ha_sensors[idx].entity_id[0]) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid sensor idx"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "name", param, sizeof(param)) == ESP_OK) { + url_decode(ha_sensors[idx].name, param, sizeof(ha_sensors[idx].name)); + save_ha_sensor(idx); + ESP_LOGI(TAG, "HA sensor %d renamed to: %s", idx, ha_sensors[idx].name); + } + + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +// ============================================================================ +// Rotation HTTP Handlers +// ============================================================================ + +/** + * @brief Handler for GET /rotation/status - Return all rotation slots as JSON + */ +static esp_err_t rotation_status_handler(httpd_req_t *req) +{ + char *response = malloc(1024); + if (!response) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + int len = snprintf(response, 1024, "{\"count\":%d,\"current\":%d,\"slots\":[", rotation_count, current_rotation_idx); + + for (int i = 0; i < MAX_ROTATION_SLOTS; i++) { + if (i > 0) len += snprintf(response + len, 1024 - len, ","); + len += snprintf(response + len, 1024 - len, + "{\"type\":%d,\"sensor_idx\":%d,\"enabled\":%d,\"duration\":%d}", + rotation_slots[i].screen_type, rotation_slots[i].sensor_idx, + rotation_slots[i].enabled, rotation_slots[i].duration_sec); + } + snprintf(response + len, 1024 - len, "]}"); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + free(response); + return ESP_OK; +} + +/** + * @brief Handler for GET /rotation/set - Update a rotation slot + * Query params: idx, type, sensor, enabled, duration + */ +static esp_err_t rotation_set_handler(httpd_req_t *req) +{ + char buf[128]; + char param[16]; + int idx = -1; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing params"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "idx", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + } + + if (idx < 0 || idx >= MAX_ROTATION_SLOTS) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid idx"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "type", param, sizeof(param)) == ESP_OK) { + rotation_slots[idx].screen_type = atoi(param); + } + if (httpd_query_key_value(buf, "sensor", param, sizeof(param)) == ESP_OK) { + rotation_slots[idx].sensor_idx = atoi(param); + } + if (httpd_query_key_value(buf, "enabled", param, sizeof(param)) == ESP_OK) { + rotation_slots[idx].enabled = atoi(param) ? 1 : 0; + } + if (httpd_query_key_value(buf, "duration", param, sizeof(param)) == ESP_OK) { + int dur = atoi(param); + if (dur >= 5 && dur <= 300) { + rotation_slots[idx].duration_sec = dur; + } + } + + save_rotation_config(); + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /rotation/add - Add new rotation slot + * Query params: type, sensor, duration + */ +static esp_err_t rotation_add_handler(httpd_req_t *req) +{ + if (rotation_count >= MAX_ROTATION_SLOTS) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Max slots reached"); + return ESP_FAIL; + } + + char buf[64]; + char param[16]; + int idx = rotation_count; + + rotation_slots[idx].enabled = 1; + rotation_slots[idx].duration_sec = 15; // Default + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + if (httpd_query_key_value(buf, "type", param, sizeof(param)) == ESP_OK) { + rotation_slots[idx].screen_type = atoi(param); + } + if (httpd_query_key_value(buf, "sensor", param, sizeof(param)) == ESP_OK) { + rotation_slots[idx].sensor_idx = atoi(param); + } + if (httpd_query_key_value(buf, "duration", param, sizeof(param)) == ESP_OK) { + int dur = atoi(param); + if (dur >= 5 && dur <= 300) { + rotation_slots[idx].duration_sec = dur; + } + } + } + + rotation_count++; + save_rotation_config(); + + char response[32]; + snprintf(response, sizeof(response), "{\"idx\":%d}", idx); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + return ESP_OK; +} + +/** + * @brief Handler for GET /rotation/remove - Remove rotation slot + * Query params: idx + */ +static esp_err_t rotation_remove_handler(httpd_req_t *req) +{ + char buf[32]; + char param[8]; + int idx = -1; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + if (httpd_query_key_value(buf, "idx", param, sizeof(param)) == ESP_OK) { + idx = atoi(param); + } + } + + if (idx < 0 || idx >= rotation_count) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid idx"); + return ESP_FAIL; + } + + // Shift remaining slots down + for (int i = idx; i < rotation_count - 1; i++) { + rotation_slots[i] = rotation_slots[i + 1]; + } + rotation_count--; + memset(&rotation_slots[rotation_count], 0, sizeof(rotation_slot_t)); + + // Adjust current index if needed + if (current_rotation_idx >= rotation_count && rotation_count > 0) { + current_rotation_idx = 0; + } + + save_rotation_config(); + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /rotation/reorder - Reorder rotation slots + * Query params: order (comma-separated indices, e.g., "0,2,1,3") + */ +static esp_err_t rotation_reorder_handler(httpd_req_t *req) +{ + char buf[128]; + char param[64]; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) != ESP_OK) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing order param"); + return ESP_FAIL; + } + + if (httpd_query_key_value(buf, "order", param, sizeof(param)) != ESP_OK) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing order param"); + return ESP_FAIL; + } + + // Parse order and create new arrangement + rotation_slot_t temp_slots[MAX_ROTATION_SLOTS]; + memcpy(temp_slots, rotation_slots, sizeof(temp_slots)); + + char *token = strtok(param, ","); + int new_idx = 0; + while (token && new_idx < rotation_count) { + int old_idx = atoi(token); + if (old_idx >= 0 && old_idx < rotation_count) { + rotation_slots[new_idx] = temp_slots[old_idx]; + new_idx++; + } + token = strtok(NULL, ","); + } + + save_rotation_config(); + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +// ============================================================================ +// Transition HTTP Handlers +// ============================================================================ + +/** + * @brief Handler for GET /transition/status - Return transition settings as JSON + */ +static esp_err_t transition_status_handler(httpd_req_t *req) +{ + char response[64]; + snprintf(response, sizeof(response), "{\"type\":%d,\"speed\":%d,\"active\":%d}", + default_transition, default_trans_speed, transition_active); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response, strlen(response)); + return ESP_OK; +} + +/** + * @brief Handler for GET /transition/set - Set transition type and/or speed + * Query params: type (0-6), speed (1-50) + */ +static esp_err_t transition_set_handler(httpd_req_t *req) +{ + char buf[64]; + char param[8]; + + if (httpd_req_get_url_query_str(req, buf, sizeof(buf)) == ESP_OK) { + if (httpd_query_key_value(buf, "type", param, sizeof(param)) == ESP_OK) { + int t = atoi(param); + if (t >= 0 && t <= 6) { + default_transition = t; + } + } + if (httpd_query_key_value(buf, "speed", param, sizeof(param)) == ESP_OK) { + int s = atoi(param); + if (s >= 1 && s <= 50) { + default_trans_speed = s; + } + } + } + + save_transition_config(); + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +/** + * @brief Handler for GET /transition/test - Trigger a test transition + * Forces a transition by jumping to a different rotation screen + */ +static esp_err_t transition_test_handler(httpd_req_t *req) +{ + // Only test if we have a transition type set + if (default_transition == TRANS_NONE) { + httpd_resp_send(req, "No transition set", -1); + return ESP_OK; + } + + // Save current frame for transition + memcpy(prev_frame, frontframe, sizeof(prev_frame)); + transition_active = 1; + transition_type = default_transition; + transition_speed = default_trans_speed; + transition_progress = 0; + + // Advance to next rotation slot to show the transition + int next = advance_rotation(); + showstate = next; + framessostate = 0; + showtemp = 0; + + httpd_resp_send(req, "OK", 2); + return ESP_OK; +} + +// ============================================================================ +// Settings Export/Import Handlers +// ============================================================================ + +// Base64 encoding table +static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** + * @brief Encode binary data to base64 + */ +static size_t base64_encode(const uint8_t *src, size_t src_len, char *dst, size_t dst_len) +{ + size_t i, j; + size_t needed = ((src_len + 2) / 3) * 4 + 1; + if (dst_len < needed) return 0; + + for (i = 0, j = 0; i < src_len; i += 3) { + uint32_t v = src[i] << 16; + if (i + 1 < src_len) v |= src[i + 1] << 8; + if (i + 2 < src_len) v |= src[i + 2]; + + dst[j++] = b64_table[(v >> 18) & 0x3F]; + dst[j++] = b64_table[(v >> 12) & 0x3F]; + dst[j++] = (i + 1 < src_len) ? b64_table[(v >> 6) & 0x3F] : '='; + dst[j++] = (i + 2 < src_len) ? b64_table[v & 0x3F] : '='; + } + dst[j] = '\0'; + return j; +} + +/** + * @brief Decode base64 to binary data + */ +static size_t base64_decode(const char *src, uint8_t *dst, size_t dst_len) +{ + size_t src_len = strlen(src); + size_t i, j; + int8_t dtable[256]; + + memset(dtable, -1, sizeof(dtable)); + for (i = 0; i < 64; i++) dtable[(uint8_t)b64_table[i]] = i; + + for (i = 0, j = 0; i < src_len && j < dst_len; i += 4) { + int8_t a = dtable[(uint8_t)src[i]]; + int8_t b = (i + 1 < src_len) ? dtable[(uint8_t)src[i + 1]] : -1; + int8_t c = (i + 2 < src_len) ? dtable[(uint8_t)src[i + 2]] : -1; + int8_t d = (i + 3 < src_len) ? dtable[(uint8_t)src[i + 3]] : -1; + + if (a < 0 || b < 0) break; + dst[j++] = (a << 2) | (b >> 4); + if (c >= 0 && j < dst_len) dst[j++] = (b << 4) | (c >> 2); + if (d >= 0 && j < dst_len) dst[j++] = (c << 6) | d; + } + return j; +} + +/** + * @brief Handler for GET /settings/export - Export all settings as JSON + */ +static esp_err_t settings_export_handler(httpd_req_t *req) +{ + // Allocate buffer for JSON response (settings + base64 image) + // Image is 12760 bytes, base64 is ~17KB, plus ~2KB for other settings + size_t buf_size = 22000; + char *response = malloc(buf_size); + if (!response) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + int len = 0; + + // Start JSON + len += snprintf(response + len, buf_size - len, "{"); + + // Margins + len += snprintf(response + len, buf_size - len, + "\"margins\":{\"left\":%d,\"top\":%d,\"right\":%d,\"bottom\":%d},", + margin_left, margin_top, margin_right, margin_bottom); + + // Transition + len += snprintf(response + len, buf_size - len, + "\"transition\":{\"type\":%d,\"speed\":%d},", + default_transition, default_trans_speed); + + // MQTT + len += snprintf(response + len, buf_size - len, + "\"mqtt\":{\"broker\":\"%s\",\"port\":%d,\"user\":\"%s\",\"pass\":\"%s\",\"alerts\":[", + mqtt_broker, mqtt_port, mqtt_username, mqtt_password); + for (int i = 0; i < MAX_ALERTS; i++) { + if (i > 0) len += snprintf(response + len, buf_size - len, ","); + len += snprintf(response + len, buf_size - len, + "{\"topic\":\"%s\",\"message\":\"%s\"}", + alerts[i].topic, alerts[i].message); + } + len += snprintf(response + len, buf_size - len, "]},"); + + // Home Assistant + len += snprintf(response + len, buf_size - len, + "\"ha\":{\"url\":\"%s\",\"token\":\"%s\",\"interval\":%lu,\"sensors\":[", + ha_url, ha_token, (unsigned long)ha_poll_interval_ms); + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (i > 0) len += snprintf(response + len, buf_size - len, ","); + len += snprintf(response + len, buf_size - len, + "{\"entity_id\":\"%s\",\"attribute\":\"%s\",\"name\":\"%s\"," + "\"display_type\":%d,\"min_value\":%d,\"max_value\":%d,\"enabled\":%d}", + ha_sensors[i].entity_id, ha_sensors[i].attribute, ha_sensors[i].name, + ha_sensors[i].display_type, ha_sensors[i].min_value, + ha_sensors[i].max_value, ha_sensors[i].enabled); + } + len += snprintf(response + len, buf_size - len, "]},"); + + // Rotation + len += snprintf(response + len, buf_size - len, + "\"rotation\":{\"count\":%d,\"slots\":[", rotation_count); + for (int i = 0; i < MAX_ROTATION_SLOTS; i++) { + if (i > 0) len += snprintf(response + len, buf_size - len, ","); + len += snprintf(response + len, buf_size - len, + "{\"type\":%d,\"sensor_idx\":%d,\"enabled\":%d,\"duration\":%d}", + rotation_slots[i].screen_type, rotation_slots[i].sensor_idx, + rotation_slots[i].enabled, rotation_slots[i].duration_sec); + } + len += snprintf(response + len, buf_size - len, "]},"); + + // Image (base64 encoded) + len += snprintf(response + len, buf_size - len, "\"image\":{\"has_image\":%s", + has_uploaded_image ? "true" : "false"); + if (has_uploaded_image) { + len += snprintf(response + len, buf_size - len, ",\"data\":\""); + len += base64_encode(uploaded_image, IMG_BUFFER_SIZE, response + len, buf_size - len); + len += snprintf(response + len, buf_size - len, "\""); + } + len += snprintf(response + len, buf_size - len, "}"); + + // End JSON + len += snprintf(response + len, buf_size - len, "}"); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"channel3_settings.json\""); + httpd_resp_send(req, response, len); + free(response); + return ESP_OK; +} + +/** + * @brief Handler for POST /settings/import - Import settings from JSON + */ +static esp_err_t settings_import_handler(httpd_req_t *req) +{ + // Allocate buffer for receiving JSON + size_t buf_size = 22000; + char *buf = malloc(buf_size); + if (!buf) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + // Receive the JSON data + int received = 0; + int remaining = req->content_len; + while (remaining > 0) { + int ret = httpd_req_recv(req, buf + received, remaining); + if (ret <= 0) { + free(buf); + httpd_resp_send_500(req); + return ESP_FAIL; + } + received += ret; + remaining -= ret; + } + buf[received] = '\0'; + + // Parse JSON using cJSON + cJSON *root = cJSON_Parse(buf); + if (!root) { + free(buf); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); + return ESP_FAIL; + } + + // Import margins + cJSON *margins = cJSON_GetObjectItem(root, "margins"); + if (margins) { + cJSON *item; + if ((item = cJSON_GetObjectItem(margins, "left"))) margin_left = item->valueint; + if ((item = cJSON_GetObjectItem(margins, "top"))) margin_top = item->valueint; + if ((item = cJSON_GetObjectItem(margins, "right"))) margin_right = item->valueint; + if ((item = cJSON_GetObjectItem(margins, "bottom"))) margin_bottom = item->valueint; + save_margins(); + } + + // Import transition + cJSON *trans = cJSON_GetObjectItem(root, "transition"); + if (trans) { + cJSON *item; + if ((item = cJSON_GetObjectItem(trans, "type"))) default_transition = item->valueint; + if ((item = cJSON_GetObjectItem(trans, "speed"))) default_trans_speed = item->valueint; + save_transition_config(); + } + + // Import MQTT + cJSON *mqtt = cJSON_GetObjectItem(root, "mqtt"); + if (mqtt) { + cJSON *item; + if ((item = cJSON_GetObjectItem(mqtt, "broker"))) strncpy(mqtt_broker, item->valuestring, sizeof(mqtt_broker) - 1); + if ((item = cJSON_GetObjectItem(mqtt, "port"))) mqtt_port = item->valueint; + if ((item = cJSON_GetObjectItem(mqtt, "user"))) strncpy(mqtt_username, item->valuestring, sizeof(mqtt_username) - 1); + if ((item = cJSON_GetObjectItem(mqtt, "pass"))) strncpy(mqtt_password, item->valuestring, sizeof(mqtt_password) - 1); + + cJSON *alerts_arr = cJSON_GetObjectItem(mqtt, "alerts"); + if (alerts_arr && cJSON_IsArray(alerts_arr)) { + int i = 0; + cJSON *alert; + cJSON_ArrayForEach(alert, alerts_arr) { + if (i >= MAX_ALERTS) break; + cJSON *topic = cJSON_GetObjectItem(alert, "topic"); + cJSON *msg = cJSON_GetObjectItem(alert, "message"); + if (topic) strncpy(alerts[i].topic, topic->valuestring, sizeof(alerts[i].topic) - 1); + if (msg) strncpy(alerts[i].message, msg->valuestring, sizeof(alerts[i].message) - 1); + i++; + } + } + // Note: save_mqtt_config is defined but we need to trigger MQTT restart + } + + // Import Home Assistant + cJSON *ha = cJSON_GetObjectItem(root, "ha"); + if (ha) { + cJSON *item; + if ((item = cJSON_GetObjectItem(ha, "url"))) strncpy(ha_url, item->valuestring, sizeof(ha_url) - 1); + if ((item = cJSON_GetObjectItem(ha, "token"))) strncpy(ha_token, item->valuestring, sizeof(ha_token) - 1); + if ((item = cJSON_GetObjectItem(ha, "interval"))) ha_poll_interval_ms = item->valueint; + + cJSON *sensors_arr = cJSON_GetObjectItem(ha, "sensors"); + if (sensors_arr && cJSON_IsArray(sensors_arr)) { + int i = 0; + ha_sensor_count = 0; + cJSON *sensor; + cJSON_ArrayForEach(sensor, sensors_arr) { + if (i >= MAX_HA_SENSORS) break; + cJSON *eid = cJSON_GetObjectItem(sensor, "entity_id"); + if (eid && eid->valuestring[0]) { + strncpy(ha_sensors[i].entity_id, eid->valuestring, sizeof(ha_sensors[i].entity_id) - 1); + cJSON *attr = cJSON_GetObjectItem(sensor, "attribute"); + cJSON *name = cJSON_GetObjectItem(sensor, "name"); + cJSON *dtype = cJSON_GetObjectItem(sensor, "display_type"); + cJSON *minv = cJSON_GetObjectItem(sensor, "min_value"); + cJSON *maxv = cJSON_GetObjectItem(sensor, "max_value"); + cJSON *en = cJSON_GetObjectItem(sensor, "enabled"); + if (attr) strncpy(ha_sensors[i].attribute, attr->valuestring, sizeof(ha_sensors[i].attribute) - 1); + if (name) strncpy(ha_sensors[i].name, name->valuestring, sizeof(ha_sensors[i].name) - 1); + if (dtype) ha_sensors[i].display_type = dtype->valueint; + if (minv) ha_sensors[i].min_value = minv->valueint; + if (maxv) ha_sensors[i].max_value = maxv->valueint; + if (en) ha_sensors[i].enabled = en->valueint; + ha_sensor_count++; + save_ha_sensor(i); + } + i++; + } + } + save_ha_config(); + } + + // Import rotation + cJSON *rotation = cJSON_GetObjectItem(root, "rotation"); + if (rotation) { + cJSON *count = cJSON_GetObjectItem(rotation, "count"); + if (count) rotation_count = count->valueint; + + cJSON *slots_arr = cJSON_GetObjectItem(rotation, "slots"); + if (slots_arr && cJSON_IsArray(slots_arr)) { + int i = 0; + cJSON *slot; + cJSON_ArrayForEach(slot, slots_arr) { + if (i >= MAX_ROTATION_SLOTS) break; + cJSON *type = cJSON_GetObjectItem(slot, "type"); + cJSON *sidx = cJSON_GetObjectItem(slot, "sensor_idx"); + cJSON *en = cJSON_GetObjectItem(slot, "enabled"); + cJSON *dur = cJSON_GetObjectItem(slot, "duration"); + if (type) rotation_slots[i].screen_type = type->valueint; + if (sidx) rotation_slots[i].sensor_idx = sidx->valueint; + if (en) rotation_slots[i].enabled = en->valueint; + if (dur) rotation_slots[i].duration_sec = dur->valueint; + i++; + } + } + save_rotation_config(); + } + + // Import image + cJSON *image = cJSON_GetObjectItem(root, "image"); + if (image) { + cJSON *has_img = cJSON_GetObjectItem(image, "has_image"); + cJSON *data = cJSON_GetObjectItem(image, "data"); + if (has_img && cJSON_IsTrue(has_img) && data && data->valuestring) { + size_t decoded = base64_decode(data->valuestring, uploaded_image, IMG_BUFFER_SIZE); + if (decoded == IMG_BUFFER_SIZE) { + has_uploaded_image = true; + save_uploaded_image(); + ESP_LOGI(TAG, "Imported image: %d bytes", decoded); + } else { + ESP_LOGW(TAG, "Image decode size mismatch: %d vs %d", decoded, IMG_BUFFER_SIZE); + } + } + } + + cJSON_Delete(root); + free(buf); + + httpd_resp_send(req, "Settings imported successfully", -1); + return ESP_OK; +} + +/** + * @brief Start the HTTP server + */ +static void start_webserver(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = 16384; + config.max_uri_handlers = 40; + + if (httpd_start(&http_server, &config) == ESP_OK) { + // Register URI handlers + httpd_uri_t root_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = root_handler + }; + httpd_register_uri_handler(http_server, &root_uri); + + httpd_uri_t status_uri = { + .uri = "/status", + .method = HTTP_GET, + .handler = status_handler + }; + httpd_register_uri_handler(http_server, &status_uri); + + httpd_uri_t screen_uri = { + .uri = "/screen", + .method = HTTP_GET, + .handler = screen_handler + }; + httpd_register_uri_handler(http_server, &screen_uri); + + httpd_uri_t control_uri = { + .uri = "/control", + .method = HTTP_GET, + .handler = control_handler + }; + httpd_register_uri_handler(http_server, &control_uri); + + httpd_uri_t jam_uri = { + .uri = "/jam", + .method = HTTP_GET, + .handler = jam_handler + }; + httpd_register_uri_handler(http_server, &jam_uri); + + httpd_uri_t video_toggle_uri = { + .uri = "/video", + .method = HTTP_GET, + .handler = video_toggle_handler + }; + httpd_register_uri_handler(http_server, &video_toggle_uri); + + httpd_uri_t margins_uri = { + .uri = "/margins", + .method = HTTP_GET, + .handler = margins_handler + }; + httpd_register_uri_handler(http_server, &margins_uri); + + httpd_uri_t margins_preview_uri = { + .uri = "/margins/preview", + .method = HTTP_GET, + .handler = margins_preview_handler + }; + httpd_register_uri_handler(http_server, &margins_preview_uri); + + httpd_uri_t upload_uri = { + .uri = "/upload", + .method = HTTP_POST, + .handler = upload_handler + }; + httpd_register_uri_handler(http_server, &upload_uri); + + httpd_uri_t mqtt_status_uri = { + .uri = "/mqtt/status", + .method = HTTP_GET, + .handler = mqtt_status_handler + }; + httpd_register_uri_handler(http_server, &mqtt_status_uri); + + httpd_uri_t mqtt_config_uri = { + .uri = "/mqtt/config", + .method = HTTP_GET, + .handler = mqtt_config_handler + }; + httpd_register_uri_handler(http_server, &mqtt_config_uri); + + httpd_uri_t mqtt_test_uri = { + .uri = "/mqtt/test", + .method = HTTP_GET, + .handler = mqtt_test_handler + }; + httpd_register_uri_handler(http_server, &mqtt_test_uri); + + httpd_uri_t mqtt_debug_uri = { + .uri = "/mqtt/debug", + .method = HTTP_GET, + .handler = mqtt_debug_handler + }; + httpd_register_uri_handler(http_server, &mqtt_debug_uri); + + // Home Assistant endpoints + httpd_uri_t ha_status_uri = { + .uri = "/ha/status", + .method = HTTP_GET, + .handler = ha_status_handler + }; + httpd_register_uri_handler(http_server, &ha_status_uri); + + httpd_uri_t ha_config_uri = { + .uri = "/ha/config", + .method = HTTP_GET, + .handler = ha_config_handler + }; + httpd_register_uri_handler(http_server, &ha_config_uri); + + httpd_uri_t ha_entity_uri = { + .uri = "/ha/entity", + .method = HTTP_GET, + .handler = ha_entity_handler + }; + httpd_register_uri_handler(http_server, &ha_entity_uri); + + httpd_uri_t ha_sensor_add_uri = { + .uri = "/ha/sensor/add", + .method = HTTP_GET, + .handler = ha_sensor_add_handler + }; + httpd_register_uri_handler(http_server, &ha_sensor_add_uri); + + httpd_uri_t ha_sensor_remove_uri = { + .uri = "/ha/sensor/remove", + .method = HTTP_GET, + .handler = ha_sensor_remove_handler + }; + httpd_register_uri_handler(http_server, &ha_sensor_remove_uri); + + httpd_uri_t ha_sensor_rename_uri = { + .uri = "/ha/sensor/rename", + .method = HTTP_GET, + .handler = ha_sensor_rename_handler + }; + httpd_register_uri_handler(http_server, &ha_sensor_rename_uri); + + // Rotation endpoints + httpd_uri_t rotation_status_uri = { + .uri = "/rotation/status", + .method = HTTP_GET, + .handler = rotation_status_handler + }; + httpd_register_uri_handler(http_server, &rotation_status_uri); + + httpd_uri_t rotation_set_uri = { + .uri = "/rotation/set", + .method = HTTP_GET, + .handler = rotation_set_handler + }; + httpd_register_uri_handler(http_server, &rotation_set_uri); + + httpd_uri_t rotation_add_uri = { + .uri = "/rotation/add", + .method = HTTP_GET, + .handler = rotation_add_handler + }; + httpd_register_uri_handler(http_server, &rotation_add_uri); + + httpd_uri_t rotation_remove_uri = { + .uri = "/rotation/remove", + .method = HTTP_GET, + .handler = rotation_remove_handler + }; + httpd_register_uri_handler(http_server, &rotation_remove_uri); + + httpd_uri_t rotation_reorder_uri = { + .uri = "/rotation/reorder", + .method = HTTP_GET, + .handler = rotation_reorder_handler + }; + httpd_register_uri_handler(http_server, &rotation_reorder_uri); + + // Transition endpoints + httpd_uri_t trans_status_uri = { + .uri = "/transition/status", + .method = HTTP_GET, + .handler = transition_status_handler + }; + httpd_register_uri_handler(http_server, &trans_status_uri); + + httpd_uri_t trans_set_uri = { + .uri = "/transition/set", + .method = HTTP_GET, + .handler = transition_set_handler + }; + httpd_register_uri_handler(http_server, &trans_set_uri); + + httpd_uri_t trans_test_uri = { + .uri = "/transition/test", + .method = HTTP_GET, + .handler = transition_test_handler + }; + httpd_register_uri_handler(http_server, &trans_test_uri); + + // Settings export/import endpoints + httpd_uri_t settings_export_uri = { + .uri = "/settings/export", + .method = HTTP_GET, + .handler = settings_export_handler + }; + httpd_register_uri_handler(http_server, &settings_export_uri); + + httpd_uri_t settings_import_uri = { + .uri = "/settings/import", + .method = HTTP_POST, + .handler = settings_import_handler + }; + httpd_register_uri_handler(http_server, &settings_import_uri); + + ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port); + } else { + ESP_LOGE(TAG, "Failed to start HTTP server"); + } +} + +/** + * @brief Render transition effect + * Called at end of DrawFrame() when transition_active is true. + * Blends prev_frame with current frontframe based on transition_progress. + */ +static void render_transition(void) +{ + if (!transition_active) return; + + // Advance progress + int new_progress = transition_progress + transition_speed; + if (new_progress >= 255) { + transition_active = 0; + transition_progress = 255; + return; + } + transition_progress = new_progress; + + uint8_t p = transition_progress; // 0-255 progress + + switch (transition_type) { + case TRANS_FADE: + // Blend prev_frame and frontframe + for (int i = 0; i < sizeof(prev_frame); i++) { + uint8_t old_lo = prev_frame[i] & 0x0F; + uint8_t old_hi = (prev_frame[i] >> 4) & 0x0F; + uint8_t new_lo = frontframe[i] & 0x0F; + uint8_t new_hi = (frontframe[i] >> 4) & 0x0F; + // Linear interpolation between old and new colors + uint8_t blend_lo = (old_lo * (255 - p) + new_lo * p) / 255; + uint8_t blend_hi = (old_hi * (255 - p) + new_hi * p) / 255; + frontframe[i] = (blend_hi << 4) | blend_lo; + } + break; + + case TRANS_WIPE_L: + // New screen slides in from right, boundary moves left + { + int boundary_x = (FBW2 * (255 - p)) / 255; // pixels from left to keep old + for (int y = 0; y < FBH; y++) { + for (int x = 0; x < boundary_x; x++) { + int byte_idx = y * (FBW2 / 2) + x / 2; + if (x % 2 == 0) { + // High nibble + frontframe[byte_idx] = (frontframe[byte_idx] & 0x0F) | + (prev_frame[byte_idx] & 0xF0); + } else { + // Low nibble + frontframe[byte_idx] = (frontframe[byte_idx] & 0xF0) | + (prev_frame[byte_idx] & 0x0F); + } + } + } + } + break; + + case TRANS_WIPE_R: + // New screen slides in from left, boundary moves right + { + int boundary_x = (FBW2 * p) / 255; // pixels from left showing new + for (int y = 0; y < FBH; y++) { + for (int x = boundary_x; x < FBW2; x++) { + int byte_idx = y * (FBW2 / 2) + x / 2; + if (x % 2 == 0) { + frontframe[byte_idx] = (frontframe[byte_idx] & 0x0F) | + (prev_frame[byte_idx] & 0xF0); + } else { + frontframe[byte_idx] = (frontframe[byte_idx] & 0xF0) | + (prev_frame[byte_idx] & 0x0F); + } + } + } + } + break; + + case TRANS_WIPE_D: + // New screen slides in from top, boundary moves down + { + int boundary_y = (FBH * (255 - p)) / 255; // rows from top to keep old + for (int y = 0; y < boundary_y; y++) { + memcpy(&frontframe[y * (FBW2 / 2)], + &prev_frame[y * (FBW2 / 2)], + FBW2 / 2); + } + } + break; + + case TRANS_WIPE_U: + // New screen slides in from bottom, boundary moves up + { + int boundary_y = (FBH * p) / 255; // rows from top showing new + for (int y = boundary_y; y < FBH; y++) { + memcpy(&frontframe[y * (FBW2 / 2)], + &prev_frame[y * (FBW2 / 2)], + FBW2 / 2); + } + } + break; + + case TRANS_DISSOLVE: + // Random pixel replacement using LCG + { + uint32_t seed = 12345; // Fixed seed for deterministic pattern + for (int i = 0; i < (int)sizeof(prev_frame); i++) { + // Two pixels per byte, handle each nibble + seed = seed * 1103515245 + 12345; + uint8_t rand1 = (seed >> 16) & 0xFF; + seed = seed * 1103515245 + 12345; + uint8_t rand2 = (seed >> 16) & 0xFF; + + uint8_t result = frontframe[i]; + // High nibble (first pixel) + if (rand1 > p) { + result = (result & 0x0F) | (prev_frame[i] & 0xF0); + } + // Low nibble (second pixel) + if (rand2 > p) { + result = (result & 0xF0) | (prev_frame[i] & 0x0F); + } + frontframe[i] = result; + } + } + break; + + default: + // TRANS_NONE or unknown - instant switch, no blending + transition_active = 0; + break; + } +} + +/** + * @brief Setup projection and modelview matrices + */ +static void SetupMatrix(void) +{ + tdIdentity(ProjectionMatrix); + tdIdentity(ModelviewMatrix); + Perspective(600, 250, 50, 8192, ProjectionMatrix); +} + +/** + * @brief Calculate terrain height for mesh demo + */ +static int16_t Height(int x, int y, int l) +{ + return tdCOS((x * x + y * y) + l); +} + +/** + * @brief Draw the current demo frame + */ +static void DrawFrame(void) +{ + char *ctx = &lastct[0]; + int x = 0; + int y = 0; + int i; + int newstate = showstate; + + // Apply calibration margins to default pen position + CNFGPenX = 14 + margin_left; + CNFGPenY = 20 + margin_top; + memset(frontframe, 0x00, ((FBW / 4) * FBH)); + tdIdentity(ModelviewMatrix); + tdIdentity(ProjectionMatrix); + CNFGColor(17); + + switch (showstate) { + case 14: // Calibration screen + { + // Draw border rectangle at current margins + // This shows the "safe area" that will be visible on the TV + int left = margin_left; + int top = margin_top; + int right = FBW2 - 1 - margin_right; + int bottom = FBH - 1 - margin_bottom; + + // Draw white border rectangle + CNFGColor(15); // White + // Top edge + CNFGTackSegment(left, top, right, top); + // Bottom edge + CNFGTackSegment(left, bottom, right, bottom); + // Left edge + CNFGTackSegment(left, top, left, bottom); + // Right edge + CNFGTackSegment(right, top, right, bottom); + + // Draw corner markers (more visible) + CNFGColor(14); // Yellow + // Top-left corner + CNFGTackSegment(left, top, left + 10, top); + CNFGTackSegment(left, top, left, top + 10); + // Top-right corner + CNFGTackSegment(right - 10, top, right, top); + CNFGTackSegment(right, top, right, top + 10); + // Bottom-left corner + CNFGTackSegment(left, bottom - 10, left, bottom); + CNFGTackSegment(left, bottom, left + 10, bottom); + // Bottom-right corner + CNFGTackSegment(right - 10, bottom, right, bottom); + CNFGTackSegment(right, bottom - 10, right, bottom); + + // Draw crosshair in center + int cx = FBW2 / 2; + int cy = FBH / 2; + CNFGColor(10); // Green + CNFGTackSegment(cx - 15, cy, cx + 15, cy); + CNFGTackSegment(cx, cy - 15, cx, cy + 15); + + // Display margin values + char cal_text[64]; + CNFGColor(15); // White + CNFGPenX = 10; + CNFGPenY = 80; + CNFGDrawText("CALIBRATION", 3); + + CNFGColor(11); // Cyan + CNFGPenX = 10; + CNFGPenY = 115; + snprintf(cal_text, sizeof(cal_text), "L:%d R:%d", margin_left, margin_right); + CNFGDrawText(cal_text, 2); + + CNFGPenX = 10; + CNFGPenY = 135; + snprintf(cal_text, sizeof(cal_text), "T:%d B:%d", margin_top, margin_bottom); + CNFGDrawText(cal_text, 2); + + CNFGColor(8); // Gray + CNFGPenX = 10; + CNFGPenY = 165; + CNFGDrawText("Adjust via", 2); + CNFGPenX = 10; + CNFGPenY = 183; + CNFGDrawText("web interface", 2); + break; + } + + case 15: // ALERT - Flashing warning (grayscale friendly) + { + // Check if alert should expire + if (framessostate >= 150) { // ~5 seconds at 30fps + alert_active = false; + newstate = 13; // Return to weather + showallowadvance = 1; // Restore normal rotation + break; + } + + // Flash effect: invert colors every 15 frames + int inverted = (framessostate / 15) % 2; + + if (inverted) { + // White background, black text + CNFGColor(15); // White + CNFGTackRectangle(0, 0, FBW - 1, FBH - 1); // FBW for rect (divides by 2 internally) + CNFGColor(0); // Black text + } else { + // Black background (already cleared), white text + CNFGColor(15); // White + } + + // Word wrap and center the alert message + { + const int scale = 3; + const int char_width = 3 * scale; // 6 pixels per char + const int line_height = 6 * scale; // 12 pixels per line + const int margin = 4; + const int screen_width = FBW2; // Use FBW2 (116) for actual visible width + const int max_width = screen_width - 2 * margin; + const int max_chars = max_width / char_width; // ~18 chars per line + + // Build wrapped lines + char lines[8][24]; // Up to 8 lines, 24 chars each + int line_lengths[8] = {0}; + int num_lines = 0; + + const char *src = current_alert_message; + int line_pos = 0; + + while (*src && num_lines < 8) { + // Skip leading spaces + while (*src == ' ') src++; + if (!*src) break; + + // Find word end + const char *word_start = src; + while (*src && *src != ' ') src++; + int word_len = src - word_start; + + // Check if word fits on current line + if (line_pos > 0 && line_pos + 1 + word_len > max_chars) { + // Start new line + lines[num_lines][line_pos] = '\0'; + line_lengths[num_lines] = line_pos; + num_lines++; + line_pos = 0; + if (num_lines >= 8) break; + } + + // Add space if not at line start + if (line_pos > 0) { + lines[num_lines][line_pos++] = ' '; + } + + // Add word (truncate if too long) + for (int i = 0; i < word_len && line_pos < 23; i++) { + lines[num_lines][line_pos++] = word_start[i]; + } + } + + // Finish last line + if (line_pos > 0 && num_lines < 8) { + lines[num_lines][line_pos] = '\0'; + line_lengths[num_lines] = line_pos; + num_lines++; + } + + // Calculate vertical centering + int total_height = num_lines * line_height; + int start_y = (FBH - total_height) / 2; + + // Draw each line centered horizontally + for (int i = 0; i < num_lines; i++) { + int text_width = line_lengths[i] * char_width; + int start_x = (screen_width - text_width) / 2; + CNFGPenX = start_x; + CNFGPenY = start_y + i * line_height; + CNFGDrawText(lines[i], scale); + } + } + + break; + } + + case 16: // Digital clock display + { + time_t now_time; + struct tm timeinfo; + time(&now_time); + localtime_r(&now_time, &timeinfo); + + // Get time components + int hour = timeinfo.tm_hour; + int minute = timeinfo.tm_min; + int second = timeinfo.tm_sec; + const char *ampm = (hour >= 12) ? "PM" : "AM"; + if (hour == 0) hour = 12; + else if (hour > 12) hour -= 12; + + // Format date as "April 1st 2026" + static const char *months[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + }; + + // Get ordinal suffix for day + const char *suffix; + int day = timeinfo.tm_mday; + if (day >= 11 && day <= 13) { + suffix = "th"; + } else { + switch (day % 10) { + case 1: suffix = "st"; break; + case 2: suffix = "nd"; break; + case 3: suffix = "rd"; break; + default: suffix = "th"; break; + } + } + + char date_str[32]; + snprintf(date_str, sizeof(date_str), "%s %d%s %d", + months[timeinfo.tm_mon], day, suffix, timeinfo.tm_year + 1900); + + // Draw time with custom kerning - colons are narrower + const int time_scale = 4; + const int char_w = 3 * time_scale; // 12 pixels for digits + const int colon_w = 2 * time_scale; // 8 pixels for colons (tighter) + const int space_w = 2 * time_scale; // 8 pixels for space before AM/PM + const int ampm_scale = 2; // Smaller AM/PM + const int ampm_char_w = 3 * ampm_scale; // 6 pixels for AM/PM chars + + // Calculate total width: HH:MM:SS AM (2+colon+2+colon+2+space+2 for AM/PM) + int time_width = (6 * char_w) + (2 * colon_w) + space_w + (2 * ampm_char_w); + int time_x = (FBW2 - time_width) / 2; + int time_y = 60 + margin_top; + + CNFGColor(15); // White + + // Draw hour + char buf[16]; + snprintf(buf, sizeof(buf), "%2d", hour); + CNFGPenX = time_x; + CNFGPenY = time_y; + CNFGDrawText(buf, time_scale); + time_x += 2 * char_w; + + // Draw colon + CNFGPenX = time_x; + CNFGPenY = time_y; + CNFGDrawText(":", time_scale); + time_x += colon_w; + + // Draw minute + snprintf(buf, sizeof(buf), "%02d", minute); + CNFGPenX = time_x; + CNFGPenY = time_y; + CNFGDrawText(buf, time_scale); + time_x += 2 * char_w; + + // Draw colon + CNFGPenX = time_x; + CNFGPenY = time_y; + CNFGDrawText(":", time_scale); + time_x += colon_w; + + // Draw second + snprintf(buf, sizeof(buf), "%02d", second); + CNFGPenX = time_x; + CNFGPenY = time_y; + CNFGDrawText(buf, time_scale); + time_x += 2 * char_w + space_w; + + // Draw AM/PM smaller + CNFGColor(15); // White for AM/PM + CNFGPenX = time_x; + CNFGPenY = time_y + (time_scale - ampm_scale) * 3; // Align to bottom + CNFGDrawText(ampm, ampm_scale); + + // Draw date - smaller centered below + const int date_scale = 2; + int date_width = strlen(date_str) * 3 * date_scale; + int date_x = (FBW2 - date_width) / 2; + CNFGColor(15); // White + CNFGPenX = date_x; + CNFGPenY = 120 + margin_top; + CNFGDrawText(date_str, date_scale); + + // Transition when rotation duration expires + if (rotation_duration_expired()) { + newstate = advance_rotation(); + } + break; + } + + case 17: // Home Assistant Sensor Display + { + // Get the specific sensor for this rotation slot + ha_sensor_config_t *sensor = NULL; + int enabled_count = 0; + + // Check if specified sensor is valid and enabled + if (ha_current_sensor >= 0 && ha_current_sensor < MAX_HA_SENSORS && + ha_sensors[ha_current_sensor].entity_id[0] && ha_sensors[ha_current_sensor].enabled) { + sensor = &ha_sensors[ha_current_sensor]; + } + + // Count enabled sensors for the "no sensors" check + for (int i = 0; i < MAX_HA_SENSORS; i++) { + if (ha_sensors[i].entity_id[0] && ha_sensors[i].enabled) { + enabled_count++; + } + } + + // If no sensors configured, advance to next rotation slot immediately + if (!sensor || enabled_count == 0) { + CNFGColor(8); // Gray + CNFGPenX = 10 + margin_left; + CNFGPenY = 90 + margin_top; + CNFGDrawText("No HA Sensors", 2); + CNFGPenX = 10 + margin_left; + CNFGPenY = 115 + margin_top; + CNFGDrawText("Configure via", 2); + CNFGPenX = 10 + margin_left; + CNFGPenY = 135 + margin_top; + CNFGDrawText("web interface", 2); + if (framessostate > 150) newstate = advance_rotation(); + break; + } + + // Get display name (use entity_id if name not set) + const char *display_name = sensor->name[0] ? sensor->name : sensor->entity_id; + + if (sensor->display_type == HA_DISPLAY_GAUGE) { + // GAUGE LAYOUT - Speedometer style (arc curves UP) + // Name at top, centered + CNFGColor(14); // Yellow + int name_width = strlen(display_name) * 3 * 2; + CNFGPenX = (FBW2 - name_width) / 2; + CNFGPenY = 5 + margin_top; + CNFGDrawText(display_name, 2); + + // Gauge parameters - taller gauge + int cx = FBW2 / 2; + int cy = 130 + margin_top; // Move center down for upward arc + int radius = 50; // Larger radius + + // Draw gauge arc (semi-circle from 180 to 0 degrees - top half) + CNFGColor(8); // Gray for arc + for (int angle = 0; angle <= 128; angle += 4) { + int x1 = cx + (radius * tdCOS(angle)) / 256; + int y1 = cy - (radius * tdSIN(angle)) / 256; + int x2 = cx + (radius * tdCOS(angle + 4)) / 256; + int y2 = cy - (radius * tdSIN(angle + 4)) / 256; + CNFGTackSegment(x1, y1, x2, y2); + } + + // Calculate value and range + float value = atof(sensor->cached_value); + int range = sensor->max_value - sensor->min_value; + if (range <= 0) range = 100; + + // Draw tick marks and intermediate values (0%, 25%, 50%, 75%, 100%) + // For upward arc: min at left (angle 128), max at right (angle 0) + CNFGColor(7); // Light gray + char tick_label[8]; + int tick_radius = radius + 8; + for (int i = 0; i <= 4; i++) { + int tick_angle = 128 - (i * 32); // 128, 96, 64, 32, 0 + int tick_value = sensor->min_value + (range * i) / 4; + + // Draw tick mark (short line outward from arc) + int tx1 = cx + ((radius - 4) * tdCOS(tick_angle)) / 256; + int ty1 = cy - ((radius - 4) * tdSIN(tick_angle)) / 256; + int tx2 = cx + ((radius + 4) * tdCOS(tick_angle)) / 256; + int ty2 = cy - ((radius + 4) * tdSIN(tick_angle)) / 256; + CNFGTackSegment(tx1, ty1, tx2, ty2); + + // Draw tick label + snprintf(tick_label, sizeof(tick_label), "%d", tick_value); + int label_len = strlen(tick_label); + int lx = cx + (tick_radius * tdCOS(tick_angle)) / 256; + int ly = cy - (tick_radius * tdSIN(tick_angle)) / 256; + + // Adjust position based on angle (for upward arc) + if (i == 0) { // Left (min) - angle 128 + CNFGPenX = lx - (label_len * 3 * 2) - 2; + CNFGPenY = ly - 4; + } else if (i == 4) { // Right (max) - angle 0 + CNFGPenX = lx + 3; + CNFGPenY = ly - 4; + } else if (i == 2) { // Top (middle) - angle 64 + CNFGPenX = lx - (label_len * 3); + CNFGPenY = ly - 14; + } else { // 25% and 75% + CNFGPenX = lx - (label_len * 3) + (i < 2 ? -8 : 8); + CNFGPenY = ly - 10; + } + CNFGDrawText(tick_label, 2); + } + + // Calculate needle angle (min=128/left, max=0/right for upward arc) + int needle_angle; + if (value <= sensor->min_value) { + needle_angle = 128; // Far left + } else if (value >= sensor->max_value) { + needle_angle = 0; // Far right + } else { + needle_angle = 128 - (int)(((value - sensor->min_value) * 128) / range); + } + + // Draw thick needle (multiple parallel lines) + CNFGColor(12); // Red for needle + int nx = cx + ((radius - 5) * tdCOS(needle_angle)) / 256; + int ny = cy - ((radius - 5) * tdSIN(needle_angle)) / 256; + // Main needle + CNFGTackSegment(cx, cy, nx, ny); + // Parallel lines for thickness + CNFGTackSegment(cx - 1, cy, nx - 1, ny); + CNFGTackSegment(cx + 1, cy, nx + 1, ny); + CNFGTackSegment(cx, cy - 1, nx, ny - 1); + CNFGTackSegment(cx, cy + 1, nx, ny + 1); + + // Draw current value below gauge - large and centered + CNFGColor(15); // White + int val_width = strlen(sensor->cached_value) * 3 * 4; + CNFGPenX = (FBW2 - val_width) / 2; + CNFGPenY = 150 + margin_top; + CNFGDrawText(sensor->cached_value, 4); + } else { + // TEXT LAYOUT (default) + // Name centered at top + CNFGColor(14); // Yellow + int name_width = strlen(display_name) * 3 * 2; + CNFGPenX = (FBW2 - name_width) / 2; + CNFGPenY = 40 + margin_top; + CNFGDrawText(display_name, 2); + + // Value large and centered + CNFGColor(15); // White + int val_width = strlen(sensor->cached_value) * 3 * 5; + CNFGPenX = (FBW2 - val_width) / 2; + CNFGPenY = 90 + margin_top; + CNFGDrawText(sensor->cached_value, 5); + } + + // Transition when rotation duration expires + if (rotation_duration_expired()) { + newstate = advance_rotation(); + } + break; + } + + case 13: // Weather display - 3 pages: current, forecast 1-3, forecast 4-6 + { + char weather_text[64]; + uint32_t now_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; + + // Toggle between 3 screens every 5 seconds + // Page 0: Current conditions + // Page 1: Forecast hours 1-3 + // Page 2: Forecast hours 4-6 + if (now_ms - last_weather_screen_toggle >= WEATHER_SCREEN_TOGGLE_MS) { + weather_screen_page = (weather_screen_page + 1) % 3; + last_weather_screen_toggle = now_ms; + } + + // Get current date for display + time_t now_time; + struct tm timeinfo; + time(&now_time); + localtime_r(&now_time, &timeinfo); + char date_str[16]; + strftime(date_str, sizeof(date_str), "%m/%d/%y", &timeinfo); + + if (weather_screen_page == 0) { + // PAGE 0: Current conditions + + // City name - top left + CNFGColor(14); // Yellow + CNFGPenX = 2 + margin_left; + CNFGPenY = 2 + margin_top; + CNFGDrawText(weather_city, 2); + + // Date - top right + CNFGColor(7); // Light gray + CNFGPenX = 170 + margin_left; + CNFGPenY = 2 + margin_top; + CNFGDrawText(date_str, 2); + + // Current temperature - large + CNFGColor(15); // White + CNFGPenX = 2 + margin_left; + CNFGPenY = 28 + margin_top; + CNFGDrawText(weather_temp, 5); + + // Current condition + CNFGColor(11); // Cyan + CNFGPenX = 2 + margin_left; + CNFGPenY = 80 + margin_top; + CNFGDrawText(weather_condition, 3); + + // Humidity + CNFGColor(9); // Light blue + CNFGPenX = 2 + margin_left; + CNFGPenY = 120 + margin_top; + snprintf(weather_text, sizeof(weather_text), "Humidity: %s", weather_humidity); + CNFGDrawText(weather_text, 2); + + // Wind + CNFGColor(10); // Green + CNFGPenX = 2 + margin_left; + CNFGPenY = 150 + margin_top; + snprintf(weather_text, sizeof(weather_text), "Wind: %s", weather_wind); + CNFGDrawText(weather_text, 2); + + } else { + // PAGE 1 or 2: Hourly Forecast (3 entries per page) + // Page 1 shows entries 0-2, Page 2 shows entries 3-5 + + int start_idx = (weather_screen_page - 1) * 3; + int forecast_y = 4 + margin_top; + + for (int i = 0; i < 3; i++) { + int idx = start_idx + i; + if (idx >= FORECAST_ENTRIES || hourly_forecast[idx].time[0] == '\0') continue; + + int base_y = forecast_y + (i * 70); + + // Line 1: Hour + CNFGColor(15); // White + CNFGPenX = 2 + margin_left; + CNFGPenY = base_y; + CNFGDrawText(hourly_forecast[idx].time, 2); + + // Line 2: Temperature and condition + CNFGColor(11); // Cyan + CNFGPenX = 2 + margin_left; + CNFGPenY = base_y + 20; + snprintf(weather_text, sizeof(weather_text), "%s %s", + hourly_forecast[idx].temp, hourly_forecast[idx].cond); + CNFGDrawText(weather_text, 2); + + // Line 3: Humidity and wind + CNFGColor(10); // Green + CNFGPenX = 2 + margin_left; + CNFGPenY = base_y + 40; + snprintf(weather_text, sizeof(weather_text), "H:%s Wind:%s", + hourly_forecast[idx].humidity, hourly_forecast[idx].wind); + CNFGDrawText(weather_text, 2); + } + } + + // Transition when rotation duration expires + if (rotation_duration_expired()) { + newstate = advance_rotation(); + } + break; + } + + case 12: // Uploaded image display + { + if (has_uploaded_image) { + // Copy uploaded image to framebuffer + memcpy(frontframe, uploaded_image, IMG_BUFFER_SIZE); + } else { + CNFGDrawText("No image uploaded.\nUse web interface\nto upload an image.", 2); + } + // Check rotation + if (rotation_duration_expired()) { + newstate = advance_rotation(); + } + break; + } + + case 11: // Color test pattern - 16 colored boxes + { + // Calculate available area after margins + int avail_w = FBW2 - margin_left - margin_right; + int avail_h = FBH - margin_top - margin_bottom; + int box_w = avail_w / 4; + int box_h = avail_h / 4; + + for (i = 0; i < 16; i++) { + x = (i % 4) * box_w + margin_left; + y = (i / 4) * box_h + margin_top; + CNFGColor(i); + CNFGTackRectangle(x, y, x + box_w - 1, y + box_h - 1); + } + break; + } + + case 10: // Combined demo - text, colors, and 3D spheres + { + for (i = 0; i < 16; i++) { + CNFGPenX = 14 + margin_left; + CNFGPenY = (i + 1) * 12 + margin_top; + CNFGColor(i); + CNFGDrawText("Hello", 3); + CNFGTackRectangle(120 + margin_left, (i + 1) * 12 + margin_top, + 180 + margin_left, (i + 1) * 12 + 12 + margin_top); + } + + SetupMatrix(); + tdRotateEA(ProjectionMatrix, -20, 0, 0); + tdRotateEA(ModelviewMatrix, framessostate, 0, 0); + + for (y = 3; y >= 0; y--) { + for (x = 0; x < 4; x++) { + CNFGColor(x + y * 4); + ModelviewMatrix[11] = 1000 + tdSIN((x + y) * 40 + framessostate * 2); + ModelviewMatrix[3] = 600 * x - 850; + ModelviewMatrix[7] = 600 * y + 800 - 850; + DrawGeoSphere(); + } + } + + if (framessostate > 500) newstate = 9; + break; + } + + case 9: // Credits text + { + const char *s = "ESP32 RF Broadcast\nDMA through I2S!\nTry it yourself!\n\ngithub.com/cnlohr/\nchannel3\n"; + + i = strlen(s); + if (i > framessostate) i = framessostate; + memcpy(lastct, s, i); + lastct[i] = 0; + CNFGDrawText(lastct, 3); + if (framessostate > 500) newstate = 0; + break; + } + + case 8: // Dynamic 3D terrain mesh + { + CNFGColor(15); // Use white from 16-color palette + CNFGDrawText("3D Meshes", 2); + SetupMatrix(); + tdRotateEA(ProjectionMatrix, -20, 0, 0); + tdRotateEA(ModelviewMatrix, 0, 0, framessostate); + + for (y = -18; y < 18; y++) { + for (x = -18; x < 18; x++) { + int o = -framessostate * 2; + int t = Height(x, y, o) * 2 + 2000; + CNFGColor(((t / 100) % 15) + 1); + int nx = Height(x + 1, y, o) * 2 + 2000; + int ny = Height(x, y + 1, o) * 2 + 2000; + int16_t p0[3] = { x * 140, y * 140, t }; + int16_t p1[3] = { (x + 1) * 140, y * 140, nx }; + int16_t p2[3] = { x * 140, (y + 1) * 140, ny }; + Draw3DSegment(p0, p1); + Draw3DSegment(p0, p2); + } + } + + if (framessostate > 400) newstate = 10; + break; + } + + case 7: // Multiple rotating geodesic spheres + { + CNFGDrawText("Matrix-based 3D engine.", 3); + SetupMatrix(); + tdRotateEA(ProjectionMatrix, -20, 0, 0); + tdRotateEA(ModelviewMatrix, framessostate, 0, 0); + + int sphereset = (framessostate / 120); + if (sphereset > 2) sphereset = 2; + + if (framessostate > 400) { + newstate = 8; + } + + for (y = -sphereset; y <= sphereset; y++) { + for (x = -sphereset; x <= sphereset; x++) { + if (y == 2) continue; + ModelviewMatrix[11] = 1000 + tdSIN((x + y) * 40 + framessostate * 2); + ModelviewMatrix[3] = 500 * x; + ModelviewMatrix[7] = 500 * y + 800; + DrawGeoSphere(); + } + } + break; + } + + case 6: // Random colored lines + { + CNFGDrawText("Lines on double-buffered 232x220.", 2); + if (framessostate > 60) { + int avail_w = FBW2 - margin_left - margin_right; + int avail_h = FBH - margin_top - margin_bottom - 30; + for (i = 0; i < 350; i++) { + CNFGColor(rand() % 16); + CNFGTackSegment(rand() % avail_w + margin_left, + rand() % avail_h + 30 + margin_top, + rand() % avail_w + margin_left, + rand() % avail_h + 30 + margin_top); + } + } + if (framessostate > 240) { + newstate = 7; + } + break; + } + + case 5: // Memory visualization (simplified for ESP32) + CNFGColor(17); + CNFGTackRectangle(70 + margin_left, 110 + margin_top, + 180 + margin_left, 150 + margin_top); + CNFGColor(16); + if (framessostate > 160) newstate = 6; + // Fall through to case 4 + __attribute__((fallthrough)); + + case 4: // Text mode demo + CNFGPenY += 14 * 7; + CNFGPenX += 60; + CNFGDrawText("38x14 TEXT MODE", 2); + + CNFGPenY += 14; + CNFGPenX -= 5; + CNFGDrawText("...on 232x220 gfx", 2); + + if (framessostate > 60 && showstate == 4) { + newstate = 5; + } + break; + + case 3: // Scrolling character demo + for (y = 0; y < 14; y++) { + for (x = 0; x < 38; x++) { + i = x + y + 1; + if (i < framessostate && i > framessostate - 60) + lastct[x] = (i != 10 && i != 9) ? i : ' '; + else + lastct[x] = ' '; + } + if (y == 7) { + memcpy(lastct + 10, "36x12 TEXT MODE", 15); + } + lastct[x] = 0; + CNFGDrawText(lastct, 2); + CNFGPenY += 14; + if (framessostate > 120) newstate = 4; + } + break; + + case 2: // ESP32 features list + ctx += sprintf(ctx, "ESP32 Features:\n 240 MHz Xtensa LX6\n 520kB SRAM\n WiFi 802.11 b/g/n\n Bluetooth 4.2\n" + " GPIO, SPI, I2C, UART\n PWM, ADC, DAC\n I2S with DMA\n\n Analog Broadcast TV\n"); + { + int il = ctx - lastct; + if (framessostate / 2 < il) + lastct[framessostate / 2] = 0; + else + showtemp++; + } + CNFGDrawText(lastct, 2); + if (showtemp == 60) newstate = 3; + break; + + case 1: // Transition out + i = strlen(lastct); + lastct[i - framessostate] = 0; + if (i - framessostate == 1) newstate = 2; + // Fall through to case 0 + __attribute__((fallthrough)); + + case 0: // Main status display + { + CNFGDrawText(lastct, 2); + + ctx += sprintf(ctx, "Channel 3 Broadcasting.\nframe: %d\n", gframe); + +#ifdef CONFIG_WIFI_MODE_STATION + // Get WiFi status in station mode + wifi_ap_record_t ap_info; + esp_err_t err = esp_wifi_sta_get_ap_info(&ap_info); + int rssi = (err == ESP_OK) ? ap_info.rssi : 0; + ctx += sprintf(ctx, "rssi: %d\n", rssi); + if (wifi_connected) { + ctx += sprintf(ctx, "IP: %s\n", wifi_ip_str); + } else { + ctx += sprintf(ctx, "Connecting...\n"); + } +#else + ctx += sprintf(ctx, "AP IP: %s\n", wifi_ip_str); +#endif + + ctx += sprintf(ctx, "ESP32 Online\n"); + showtemp++; + if (showtemp == 30) newstate = 1; + break; + } + } + + // Handle state transitions + if (showstate != newstate && showallowadvance) { + // Start transition for rotation-based screen changes + // (Weather=13, Clock=16, HA Sensor=17, Image=12) + if (default_transition != TRANS_NONE && !transition_active && + (showstate == 13 || showstate == 16 || showstate == 17 || showstate == 12) && + (newstate == 13 || newstate == 16 || newstate == 17 || newstate == 12)) { + memcpy(prev_frame, frontframe, sizeof(prev_frame)); + transition_active = 1; + transition_type = default_transition; + transition_speed = default_trans_speed; + transition_progress = 0; + } + showstate = newstate; + framessostate = 0; + showtemp = 0; + } else { + framessostate++; + } + + // Apply transition effect if active (blends prev_frame with current frontframe) + render_transition(); +} + +/** + * @brief Video rendering task + * + * Runs on core 1 to handle frame rendering without interfering with WiFi + */ +static void video_task(void *arg) +{ + uint8_t lastframe = 0; + + ESP_LOGI(TAG, "Video task started on core %d", xPortGetCoreID()); + + while (1) { + uint8_t tbuffer = !(gframe & 1); + + if (lastframe != tbuffer) { + frontframe = (uint8_t*)&framebuffer[((FBW2 / 4) * FBH) * tbuffer]; + DrawFrame(); + lastframe = tbuffer; + } + + // Small delay to prevent watchdog issues + vTaskDelay(1); + } +} + +#ifdef CONFIG_WIFI_MODE_STATION + +static int retry_count = 0; + +/** + * @brief WiFi event handler for station mode + */ +static void wifi_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) { + // Start a scan first, then connect after scan completes + wifi_scan_config_t scan_config = { .show_hidden = true }; + esp_wifi_scan_start(&scan_config, false); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { + ESP_LOGI(TAG, "Scan complete, connecting..."); + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + ESP_LOGI(TAG, "WiFi connected, waiting for IP..."); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + wifi_connected = false; + retry_count++; + if (retry_count < 10) { + ESP_LOGI(TAG, "Disconnected, retry %d...", retry_count); + vTaskDelay(pdMS_TO_TICKS(2000)); // Wait before retry + esp_wifi_connect(); + } else { + ESP_LOGE(TAG, "Failed to connect after %d retries", retry_count); + } + } 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; + snprintf(wifi_ip_str, sizeof(wifi_ip_str), IPSTR, IP2STR(&event->ip_info.ip)); + wifi_connected = true; + retry_count = 0; + ESP_LOGI(TAG, "Connected! IP: %s", wifi_ip_str); + + // Initialize SNTP for time sync + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "pool.ntp.org"); + esp_sntp_init(); + ESP_LOGI(TAG, "SNTP initialized, syncing time..."); + + // Set timezone to Central Time (Austin, TX) + setenv("TZ", "CST6CDT,M3.2.0,M11.1.0", 1); + tzset(); + + // Load MQTT config and start client + load_mqtt_config(); + start_mqtt_client(); + + // Load Home Assistant config + load_ha_config(); + } +} + +/** + * @brief Initialize WiFi in Station mode + */ +static void wifi_init_station(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &wifi_event_handler, NULL, NULL)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &wifi_event_handler, NULL, NULL)); + + wifi_config_t wifi_config = { + .sta = { + .ssid = CONFIG_WIFI_STA_SSID, + .password = CONFIG_WIFI_STA_PASS, + .threshold.authmode = WIFI_AUTH_OPEN, // Accept any auth mode + .sae_pwe_h2e = WPA3_SAE_PWE_BOTH, + }, + }; + + 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, "WiFi Station mode initialized. Connecting to: %s", CONFIG_WIFI_STA_SSID); +} +#else +/** + * @brief Initialize WiFi in SoftAP mode + */ +static void wifi_init_softap(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + wifi_config_t wifi_config = { + .ap = { + .ssid = CONFIG_WIFI_SOFTAP_SSID, + .ssid_len = strlen(CONFIG_WIFI_SOFTAP_SSID), + .channel = 1, + .password = CONFIG_WIFI_SOFTAP_PASS, + .max_connection = 4, + .authmode = WIFI_AUTH_WPA_WPA2_PSK + }, + }; + + if (strlen(CONFIG_WIFI_SOFTAP_PASS) == 0) { + wifi_config.ap.authmode = WIFI_AUTH_OPEN; + } + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + wifi_connected = true; + snprintf(wifi_ip_str, sizeof(wifi_ip_str), "192.168.4.1"); + ESP_LOGI(TAG, "WiFi SoftAP initialized. SSID: %s", CONFIG_WIFI_SOFTAP_SSID); +} +#endif + +/** + * @brief Application entry point + */ +void app_main(void) +{ + ESP_LOGI(TAG, "================================="); + ESP_LOGI(TAG, " Channel3 ESP32 - RF Broadcast "); + ESP_LOGI(TAG, "================================="); + + // Initialize NVS + 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); + + // Load calibration margins from NVS + load_margins(); + + // Load uploaded image from NVS if available + load_uploaded_image(); + + // Initialize WiFi +#ifdef CONFIG_WIFI_MODE_STATION + wifi_init_station(); + + // Wait for WiFi connection before starting video + // This avoids the cache conflict during WiFi handshake + ESP_LOGI(TAG, "Waiting for WiFi connection before starting video..."); + int wait_count = 0; + while (!wifi_connected && wait_count < 30) { // Wait up to 30 seconds + vTaskDelay(pdMS_TO_TICKS(1000)); + wait_count++; + ESP_LOGI(TAG, "Waiting for WiFi... (%d/30)", wait_count); + } + + if (wifi_connected) { + ESP_LOGI(TAG, "WiFi connected! IP: %s", wifi_ip_str); + start_webserver(); + // Start video streaming server + xTaskCreate(stream_server_task, "stream_server", 8192, NULL, 4, NULL); + // Initial weather fetch + ESP_LOGI(TAG, "Fetching initial weather data..."); + fetch_weather(); + } else { + ESP_LOGW(TAG, "WiFi connection timeout, starting video anyway"); + } +#else + wifi_init_softap(); + start_webserver(); + // Start video streaming server + xTaskCreate(stream_server_task, "stream_server", 8192, NULL, 4, NULL); +#endif + + // Setup 3D matrices + SetupMatrix(); + + // Initialize video broadcast + video_broadcast_init(); + video_running = true; + + // Create video rendering task on core 1 + // Core 0 is used for WiFi, so we use core 1 for video + xTaskCreatePinnedToCore(video_task, "video_task", 8192, NULL, 5, NULL, 1); + + ESP_LOGI(TAG, "Channel3 ESP32 initialized"); +#ifdef CONFIG_WIFI_MODE_STATION + ESP_LOGI(TAG, "Connected to WiFi network '%s'", CONFIG_WIFI_STA_SSID); +#else + ESP_LOGI(TAG, "Connect to WiFi AP '%s' to access web interface", CONFIG_WIFI_SOFTAP_SSID); +#endif + ESP_LOGI(TAG, "Tune analog TV to Channel 3 to view broadcast"); + ESP_LOGI(TAG, "Video stream server on port %d", STREAM_PORT); + + // Main loop - monitor status and refresh weather + while (1) { + vTaskDelay(pdMS_TO_TICKS(1000)); + ESP_LOGI(TAG, "Frame: %d, Frame time: %lu us", gframe, (unsigned long)last_internal_frametime); + + // Check if MQTT needs restart (set by web config handler) + if (mqtt_needs_restart) { + mqtt_needs_restart = false; + ESP_LOGI(TAG, "Restarting MQTT client with new config..."); + if (mqtt_client) { + esp_mqtt_client_stop(mqtt_client); + esp_mqtt_client_destroy(mqtt_client); + mqtt_client = NULL; + } + start_mqtt_client(); + } + + // Periodic weather refresh + uint32_t now = xTaskGetTickCount() * portTICK_PERIOD_MS; + if (wifi_connected && (now - last_weather_fetch) > WEATHER_FETCH_INTERVAL_MS) { + ESP_LOGI(TAG, "Refreshing weather data..."); + fetch_weather(); + } + + // Periodic Home Assistant sensor refresh + if (wifi_connected && ha_url[0] && ha_token[0] && + (now - last_ha_fetch) > ha_poll_interval_ms) { + ESP_LOGI(TAG, "Refreshing HA sensor data..."); + fetch_all_ha_sensors(); + } + } +} diff --git a/main/video_broadcast.c b/main/video_broadcast.c new file mode 100644 index 0000000..4789244 --- /dev/null +++ b/main/video_broadcast.c @@ -0,0 +1,497 @@ +/** + * @file video_broadcast.c + * @brief ESP32 Video Broadcast - RF modulation via I2S DMA + * + * This module generates analog NTSC/PAL television signals by outputting + * an 80 MHz bitstream through I2S DMA. The premodulated waveforms create + * harmonics at 61.25 MHz (Channel 3 luma) for RF broadcast. + * + * Key differences from ESP8266 version: + * - Uses ESP32 I2S LCD mode for parallel output + * - Uses GDMA (lldesc_t) instead of SLC (sdio_queue) + * - Uses esp_intr_alloc() instead of ets_isr_attach() + * - Uses APLL for clock generation + * + * Original ESP8266 version: Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#include "video_broadcast.h" +#include "broadcast_tables.h" +#include "CbTable.h" +#include "sdkconfig.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/i2s_std.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_attr.h" +#include "esp_timer.h" +#include "soc/i2s_reg.h" +#include "soc/i2s_struct.h" +#include "soc/gpio_sig_map.h" +#include "hal/gpio_ll.h" +#include "rom/lldesc.h" +#include "esp_rom_gpio.h" +#include "esp_private/periph_ctrl.h" +#include "esp_intr_alloc.h" +#include "soc/rtc.h" + +static const char *TAG = "video_broadcast"; + +// Video timing constants +#ifdef CONFIG_VIDEO_PAL + #define LINE_BUFFER_LENGTH 160 + #define SHORT_SYNC_INTERVAL 5 + #define LONG_SYNC_INTERVAL 75 + #define NORMAL_SYNC_INTERVAL 10 + #define LINE_SIGNAL_INTERVAL 150 + #define COLORBURST_INTERVAL 10 +#else + #define LINE_BUFFER_LENGTH 159 + #define SHORT_SYNC_INTERVAL 6 + #define LONG_SYNC_INTERVAL 73 + #define SERRATION_PULSE_INT 67 + #define NORMAL_SYNC_INTERVAL 12 + #define LINE_SIGNAL_INTERVAL 147 + #define COLORBURST_INTERVAL 4 +#endif + +#define I2SDMABUFLEN LINE_BUFFER_LENGTH +#define LINE32LEN I2SDMABUFLEN + +// I2S configuration for 80 MHz output +// ESP32 can use APLL or divide from 240 MHz +// Target: 80 MHz bit clock +// Using I2S LCD mode: clock = source / (N + b/a) +// For 80 MHz from 240 MHz APB: divider = 3 + +// Global state +int8_t jam_color = -1; +int gframe = 0; +uint16_t framebuffer[((FBW2/4)*(FBH))*2]; +uint32_t last_internal_frametime = 0; + +// Internal state +static int gline = 0; +static int linescratch; +static uint8_t pixline; + +// Premodulation table pointers +static const uint32_t *tablestart; +static const uint32_t *tablept; +static const uint32_t *tableend; +static uint32_t *curdma; + +// DMA descriptors and buffers +static lldesc_t dma_desc[DMABUFFERDEPTH] __attribute__((aligned(4))); +static uint32_t dma_buffer[I2SDMABUFLEN * DMABUFFERDEPTH] __attribute__((aligned(4))); + +// I2S interrupt handle +static intr_handle_t i2s_intr_handle = NULL; + +// Timing measurement +static uint32_t systimex = 0; +static uint32_t systimein = 0; + +/** + * @brief Fill DMA buffer with premodulated waveform data + * @param qty Number of 32-bit words to fill + * @param color Color/level index into premodulation table + */ +static inline void IRAM_ATTR fillwith(uint16_t qty, uint8_t color) +{ + if (qty & 1) { + *(curdma++) = tablept[color]; + tablept += PREMOD_SIZE; + } + qty >>= 1; + for (linescratch = 0; linescratch < qty; linescratch++) { + *(curdma++) = tablept[color]; + tablept += PREMOD_SIZE; + *(curdma++) = tablept[color]; + tablept += PREMOD_SIZE; + if (tablept >= tableend) { + tablept = tablept - tableend + tablestart; + } + } +} + +/** + * @brief Short sync pulse (FT_STA) + */ +static void IRAM_ATTR FT_STA(void) +{ + pixline = 0; // Reset framebuffer line counter + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); +} + +/** + * @brief Long sync pulse (FT_STB) + */ +static void IRAM_ATTR FT_STB(void) +{ +#ifdef CONFIG_VIDEO_PAL + #define FT_STB_BLACK_INTERVAL SHORT_SYNC_INTERVAL +#else + #define FT_STB_BLACK_INTERVAL NORMAL_SYNC_INTERVAL +#endif + fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(FT_STB_BLACK_INTERVAL, BLACK_LEVEL); + fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (LONG_SYNC_INTERVAL + FT_STB_BLACK_INTERVAL + LONG_SYNC_INTERVAL), BLACK_LEVEL); +} + +/** + * @brief Black/blanking line (FT_B) + */ +static void IRAM_ATTR FT_B(void) +{ + fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(2, BLACK_LEVEL); + fillwith(COLORBURST_INTERVAL, COLORBURST_LEVEL); + fillwith(LINE32LEN - NORMAL_SYNC_INTERVAL - 2 - COLORBURST_INTERVAL, + (pixline < 1) ? GRAY_LEVEL : BLACK_LEVEL); +} + +/** + * @brief Short to long sync transition (FT_SRA) + */ +static void IRAM_ATTR FT_SRA(void) +{ + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); +#ifdef CONFIG_VIDEO_PAL + fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + LONG_SYNC_INTERVAL), BLACK_LEVEL); +#else + fillwith(SERRATION_PULSE_INT, SYNC_LEVEL); + fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SERRATION_PULSE_INT), BLACK_LEVEL); +#endif +} + +/** + * @brief Long to short sync transition (FT_SRB) + */ +static void IRAM_ATTR FT_SRB(void) +{ +#ifdef CONFIG_VIDEO_PAL + fillwith(LONG_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(SHORT_SYNC_INTERVAL, BLACK_LEVEL); + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); +#else + fillwith(SERRATION_PULSE_INT, SYNC_LEVEL); + fillwith(NORMAL_SYNC_INTERVAL, BLACK_LEVEL); + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (SERRATION_PULSE_INT + NORMAL_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); +#endif +} + +/** + * @brief Active video line (FT_LIN) + */ +static void IRAM_ATTR FT_LIN(void) +{ + fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(1, BLACK_LEVEL); + fillwith(COLORBURST_INTERVAL, COLORBURST_LEVEL); + fillwith(11, BLACK_LEVEL); +#define HDR_SPD (NORMAL_SYNC_INTERVAL + 1 + COLORBURST_INTERVAL + 11) + + int fframe = gframe & 1; + uint16_t *fbs = (uint16_t*)(&framebuffer[((pixline * (FBW2/2)) + (((FBW2/2)*(FBH)) * fframe)) / 2]); + + for (linescratch = 0; linescratch < FBW2/4; linescratch++) { + uint16_t fbb = fbs[linescratch]; + *(curdma++) = tablept[(fbb >> 0) & 15]; tablept += PREMOD_SIZE; + *(curdma++) = tablept[(fbb >> 4) & 15]; tablept += PREMOD_SIZE; + *(curdma++) = tablept[(fbb >> 8) & 15]; tablept += PREMOD_SIZE; + *(curdma++) = tablept[(fbb >> 12) & 15]; tablept += PREMOD_SIZE; + if (tablept >= tableend) { + tablept = tablept - tableend + tablestart; + } + } + + fillwith(LINE32LEN - (HDR_SPD + FBW2), BLACK_LEVEL); + pixline++; +} + +/** + * @brief End frame marker (FT_CLOSE_M) + */ +static void IRAM_ATTR FT_CLOSE_M(void) +{ +#ifdef CONFIG_VIDEO_PAL + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LONG_SYNC_INTERVAL, BLACK_LEVEL); + fillwith(SHORT_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(LINE32LEN - (SHORT_SYNC_INTERVAL + LONG_SYNC_INTERVAL + SHORT_SYNC_INTERVAL), BLACK_LEVEL); +#else + fillwith(NORMAL_SYNC_INTERVAL, SYNC_LEVEL); + fillwith(2, BLACK_LEVEL); + fillwith(4, COLORBURST_LEVEL); + fillwith(LINE32LEN - NORMAL_SYNC_INTERVAL - 6, WHITE_LEVEL); +#endif + gline = -1; + gframe++; + + last_internal_frametime = systimex; + systimex = 0; + systimein = (uint32_t)esp_timer_get_time(); +} + +// Line type function table +static void (*CbTable[FT_MAX_d])(void) = { + FT_STA, FT_STB, FT_B, FT_SRA, FT_SRB, FT_LIN, FT_CLOSE_M +}; + +/** + * @brief I2S DMA interrupt handler + * + * Called when a DMA buffer completes transmission. + * Fills the completed buffer with the next line's data. + */ +static void IRAM_ATTR i2s_isr(void *arg) +{ + // Clear interrupt + typeof(I2S0.int_st) status = I2S0.int_st; + I2S0.int_clr.val = status.val; + + if (status.out_eof) { + // Get the descriptor that just finished + lldesc_t *finish_desc = (lldesc_t*)I2S0.out_eof_des_addr; + curdma = (uint32_t*)finish_desc->buf; + + if (jam_color < 0) { + // Normal operation - generate video line + int lk = 0; + if (gline & 1) { + lk = (CbLookup[gline >> 1] >> 4) & 0x0f; + } else { + lk = CbLookup[gline >> 1] & 0x0f; + } + + systimein = (uint32_t)esp_timer_get_time(); + CbTable[lk](); + systimex += (uint32_t)esp_timer_get_time() - systimein; + gline++; + } else { + // Jam mode - fill with single color for RF testing + fillwith(LINE32LEN, jam_color); + } + } +} + +/** + * @brief Configure I2S for 80 MHz serial bitstream output + * + * This uses standard I2S mode (not LCD mode) to output a serial bitstream + * on the data pin at 80 MHz. The premodulated patterns create RF harmonics + * at Channel 3 frequency (61.25 MHz). + */ +static void configure_i2s(void) +{ + // Enable I2S peripheral + periph_module_enable(PERIPH_I2S0_MODULE); + + // Reset I2S + I2S0.conf.tx_reset = 1; + I2S0.conf.tx_reset = 0; + I2S0.conf.rx_reset = 1; + I2S0.conf.rx_reset = 0; + + // Reset FIFO + I2S0.conf.tx_fifo_reset = 1; + I2S0.conf.tx_fifo_reset = 0; + I2S0.conf.rx_fifo_reset = 1; + I2S0.conf.rx_fifo_reset = 0; + + // Reset DMA + I2S0.lc_conf.in_rst = 1; + I2S0.lc_conf.in_rst = 0; + I2S0.lc_conf.out_rst = 1; + I2S0.lc_conf.out_rst = 0; + + // Disable LCD mode - use standard serial I2S + I2S0.conf2.lcd_en = 0; + I2S0.conf2.lcd_tx_wrx2_en = 0; + I2S0.conf2.lcd_tx_sdx2_en = 0; + I2S0.conf2.camera_en = 0; + + // Configure for serial transmission (like ESP8266) + I2S0.conf.tx_msb_right = 0; + I2S0.conf.tx_right_first = 0; + I2S0.conf.tx_slave_mod = 0; // Master mode + I2S0.conf.tx_mono = 1; // Mono - single channel + I2S0.conf.tx_short_sync = 0; + I2S0.conf.tx_msb_shift = 0; // No shift, output raw bits + + // Configure FIFO for 32-bit mono + I2S0.fifo_conf.tx_fifo_mod = 3; // 32-bit single channel + I2S0.fifo_conf.tx_fifo_mod_force_en = 1; + I2S0.fifo_conf.dscr_en = 1; // Enable DMA + + // Configure channel - mono mode + I2S0.conf_chan.tx_chan_mod = 3; // Single channel on data out + + // Configure sample bits - 32 bits per sample + I2S0.sample_rate_conf.tx_bits_mod = 32; + + // Configure clock for 80 MHz bit clock + // PLL_D2_CLK = 160 MHz (when CPU at 240 MHz) + // We want BCK = 80 MHz + // Master clock divider: 160 / 2 = 80 MHz + // BCK divider: 1 (pass through) + + I2S0.clkm_conf.clkm_div_num = 2; // Divide 160 MHz by 2 = 80 MHz + I2S0.clkm_conf.clkm_div_b = 0; + I2S0.clkm_conf.clkm_div_a = 1; + I2S0.clkm_conf.clk_en = 1; + I2S0.clkm_conf.clka_en = 0; // Use PLL_D2_CLK (160 MHz) + + // BCK = MCLK / tx_bck_div_num + // We want BCK = 80 MHz, MCLK = 80 MHz, so div = 1 + I2S0.sample_rate_conf.tx_bck_div_num = 1; + + // Don't start yet + I2S0.conf.tx_start = 0; + + ESP_LOGI(TAG, "I2S configured: serial mode, 80 MHz target bit clock"); +} + +/** + * @brief Setup DMA descriptors in circular configuration + */ +static void setup_dma_descriptors(void) +{ + for (int i = 0; i < DMABUFFERDEPTH; i++) { + dma_desc[i].size = I2SDMABUFLEN * 4; + dma_desc[i].length = I2SDMABUFLEN * 4; + dma_desc[i].buf = (uint8_t*)&dma_buffer[i * I2SDMABUFLEN]; + dma_desc[i].owner = 1; + dma_desc[i].sosf = 0; + dma_desc[i].eof = 1; + dma_desc[i].qe.stqe_next = (i < DMABUFFERDEPTH - 1) ? + &dma_desc[i + 1] : &dma_desc[0]; + + // Initialize buffer to black + memset((void*)dma_desc[i].buf, 0, I2SDMABUFLEN * 4); + } +} + +/** + * @brief Configure GPIO for I2S data output + */ +static void configure_gpio(void) +{ + int gpio_num = CONFIG_I2S_DATA_GPIO; + + // Configure GPIO as high-speed output + gpio_set_direction(gpio_num, GPIO_MODE_OUTPUT); + gpio_set_pull_mode(gpio_num, GPIO_FLOATING); + + // Set high drive strength for better RF output + gpio_set_drive_capability(gpio_num, GPIO_DRIVE_CAP_3); + + // Route I2S0 serial data to GPIO + // In standard I2S mode, DATA_OUT23 is the serial data (MSB first) + esp_rom_gpio_connect_out_signal(gpio_num, I2S0O_DATA_OUT23_IDX, false, false); + + ESP_LOGI(TAG, "I2S serial data output on GPIO %d (high drive)", gpio_num); +} + +void video_broadcast_init(void) +{ + ESP_LOGI(TAG, "Initializing video broadcast system"); + + // Initialize table pointers + tablestart = &premodulated_table[0]; + tablept = &premodulated_table[0]; + tableend = &premodulated_table[PREMOD_ENTRIES * PREMOD_SIZE]; + + // Initialize state + jam_color = -1; + gframe = 0; + gline = 0; + pixline = 0; + memset(framebuffer, 0, sizeof(framebuffer)); + + // Configure I2S peripheral + configure_i2s(); + + // Setup DMA descriptors + setup_dma_descriptors(); + + // Configure GPIO + configure_gpio(); + + // Register interrupt handler + esp_intr_alloc(ETS_I2S0_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, + i2s_isr, NULL, &i2s_intr_handle); + + // Enable interrupt on TX EOF + I2S0.int_ena.out_eof = 1; + + // Link DMA descriptor + I2S0.out_link.addr = (uint32_t)&dma_desc[0]; + I2S0.out_link.start = 1; + + // Start transmission + I2S0.conf.tx_start = 1; + + ESP_LOGI(TAG, "Video broadcast started"); +#ifdef CONFIG_VIDEO_PAL + ESP_LOGI(TAG, "Video standard: PAL (%d lines)", VIDEO_LINES); +#else + ESP_LOGI(TAG, "Video standard: NTSC (%d lines)", VIDEO_LINES); +#endif + ESP_LOGI(TAG, "Framebuffer: %dx%d pixels", FBW2, FBH); +} + +void video_broadcast_stop(void) +{ + // Only stop if video was ever initialized + if (!i2s_intr_handle) { + ESP_LOGI(TAG, "Video broadcast not running"); + return; + } + + ESP_LOGI(TAG, "Stopping video broadcast"); + + // Stop transmission + I2S0.conf.tx_start = 0; + I2S0.out_link.stop = 1; + + // Disable interrupt + I2S0.int_ena.out_eof = 0; + + // Free interrupt + esp_intr_free(i2s_intr_handle); + i2s_intr_handle = NULL; + + // Disable I2S peripheral + periph_module_disable(PERIPH_I2S0_MODULE); + + ESP_LOGI(TAG, "Video broadcast stopped"); +} + +void video_broadcast_pause(void) +{ + if (i2s_intr_handle) { + esp_intr_disable(i2s_intr_handle); + } +} + +void video_broadcast_resume(void) +{ + if (i2s_intr_handle) { + esp_intr_enable(i2s_intr_handle); + } +} diff --git a/main/video_broadcast.h b/main/video_broadcast.h new file mode 100644 index 0000000..fa23fc3 --- /dev/null +++ b/main/video_broadcast.h @@ -0,0 +1,70 @@ +/** + * @file video_broadcast.h + * @brief ESP32 Video Broadcast - RF modulation via I2S DMA + * + * This module generates analog NTSC/PAL television signals by outputting + * an 80 MHz bitstream through I2S DMA. The GPIO pin acts as an antenna, + * radiating RF directly on Channel 3 (61.25 MHz). + * + * Original ESP8266 version: Copyright 2015 <>< Charles Lohr + * ESP32 Port 2024 + */ + +#ifndef VIDEO_BROADCAST_H +#define VIDEO_BROADCAST_H + +#include +#include "sdkconfig.h" + +// Framebuffer dimensions +// FBW is "double-pixels" for double-resolution monochrome width +#define FBW 232 +#define FBW2 (FBW / 2) // Actual width in true pixels + +#ifdef CONFIG_VIDEO_PAL + #define FBH 264 +#else + #define FBH 220 +#endif + +// DMA buffer configuration +#define DMABUFFERDEPTH 3 + +// Global variables +extern int gframe; // Current frame number +extern uint16_t framebuffer[((FBW2/4)*(FBH))*2]; // Double-buffered framebuffer +extern uint32_t last_internal_frametime; // Last frame rendering time +extern int8_t jam_color; // Color jam for RF testing (-1 = disabled) + +/** + * @brief Initialize the I2S DMA video broadcast system + * + * Sets up: + * - APLL clock for 80 MHz output + * - I2S in LCD/parallel mode + * - DMA descriptors in circular buffer configuration + * - ISR for line-by-line rendering + * - GPIO routing for RF output + */ +void video_broadcast_init(void); + +/** + * @brief Stop video broadcast + * + * Disables I2S output and releases resources + */ +void video_broadcast_stop(void); + +/** + * @brief Temporarily pause video broadcast for flash operations + * + * Disables the I2S interrupt to prevent cache conflicts during NVS writes + */ +void video_broadcast_pause(void); + +/** + * @brief Resume video broadcast after pause + */ +void video_broadcast_resume(void); + +#endif // VIDEO_BROADCAST_H diff --git a/monitor.ps1 b/monitor.ps1 new file mode 100644 index 0000000..fa05a5d --- /dev/null +++ b/monitor.ps1 @@ -0,0 +1,10 @@ +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:MSYSTEM = $null +$env:SHELL = $null +$env:SHLVL = $null +$env:TERM = $null +Set-Location "C:\git\channel3\esp32_channel3" +. "C:\Espressif\Initialize-Idf.ps1" +idf.py -p COM5 monitor diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..d7ea0dd --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Custom partition table with larger NVS for image storage +nvs, data, nvs, 0x9000, 0x10000, +phy_init, data, phy, 0x19000, 0x1000, +factory, app, factory, 0x20000, 0x100000, diff --git a/rebuild.ps1 b/rebuild.ps1 new file mode 100644 index 0000000..3a27892 --- /dev/null +++ b/rebuild.ps1 @@ -0,0 +1,26 @@ +$ErrorActionPreference = "Continue" + +# Clear MSYS environment variables +Remove-Item Env:MSYSTEM -ErrorAction SilentlyContinue +$env:MSYSTEM = "" + +# Set ESP-IDF environment +$env:IDF_PATH = "C:\Espressif\frameworks\esp-idf-v5.5.2" +$env:IDF_TOOLS_PATH = "C:\Espressif" +$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\python_env\idf5.5_py3.11_env" + +$toolPaths = @( + "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts", + "C:\Espressif\tools\cmake\3.30.2\bin", + "C:\Espressif\tools\ninja\1.12.1", + "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin" +) +$env:PATH = ($toolPaths -join ";") + ";" + $env:PATH + +Set-Location "C:\git\channel3\esp32_channel3" + +$python = "C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe" +$idfpy = "$env:IDF_PATH\tools\idf.py" + +Write-Host "Building..." +& $python $idfpy build diff --git a/run_build.bat b/run_build.bat new file mode 100644 index 0000000..468422a --- /dev/null +++ b/run_build.bat @@ -0,0 +1,19 @@ +@echo off +REM Clear MSYS environment variables that confuse ESP-IDF +set MSYSTEM= +set MSYSTEM_PREFIX= +set MSYSTEM_CARCH= +set MSYSTEM_CHOST= +set MINGW_CHOST= +set MINGW_PREFIX= +set MINGW_PACKAGE_PREFIX= + +set IDF_PATH=C:\Espressif\frameworks\esp-idf-v5.5.2 +set IDF_TOOLS_PATH=C:\Espressif +set IDF_PYTHON_ENV_PATH=C:\Espressif\python_env\idf5.5_py3.11_env +set PATH=C:\Espressif\python_env\idf5.5_py3.11_env\Scripts;C:\Espressif\tools\cmake\3.30.2\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20251107\xtensa-esp-elf\bin;C:\Espressif\tools\esp32ulp-elf\2.38_20240113\esp32ulp-elf\bin;C:\Espressif\tools\idf-git\2.44.0\cmd;%PATH% + +cd /d C:\git\channel3\esp32_channel3 +echo Starting build... +C:\Espressif\python_env\idf5.5_py3.11_env\Scripts\python.exe C:\Espressif\frameworks\esp-idf-v5.5.2\tools\idf.py build > build_output.txt 2>&1 +echo Build exit code: %ERRORLEVEL% diff --git a/run_build.cmd b/run_build.cmd new file mode 100644 index 0000000..421cc3a --- /dev/null +++ b/run_build.cmd @@ -0,0 +1,5 @@ +@echo off +echo Running PowerShell build script... +powershell.exe -ExecutionPolicy Bypass -NoProfile -Command "& {cd 'C:\git\channel3\esp32_channel3'; .\build.ps1}" > C:\git\channel3\esp32_channel3\ps_output.txt 2>&1 +echo Done. Exit code: %ERRORLEVEL% +type C:\git\channel3\esp32_channel3\ps_output.txt diff --git a/sdkconfig b/sdkconfig new file mode 100644 index 0000000..7532de6 --- /dev/null +++ b/sdkconfig @@ -0,0 +1,2231 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) 5.5.2 Project Configuration +# +CONFIG_SOC_CAPS_ECO_VER_MAX=301 +CONFIG_SOC_ADC_SUPPORTED=y +CONFIG_SOC_DAC_SUPPORTED=y +CONFIG_SOC_UART_SUPPORTED=y +CONFIG_SOC_MCPWM_SUPPORTED=y +CONFIG_SOC_GPTIMER_SUPPORTED=y +CONFIG_SOC_SDMMC_HOST_SUPPORTED=y +CONFIG_SOC_BT_SUPPORTED=y +CONFIG_SOC_PCNT_SUPPORTED=y +CONFIG_SOC_PHY_SUPPORTED=y +CONFIG_SOC_WIFI_SUPPORTED=y +CONFIG_SOC_SDIO_SLAVE_SUPPORTED=y +CONFIG_SOC_TWAI_SUPPORTED=y +CONFIG_SOC_EFUSE_SUPPORTED=y +CONFIG_SOC_EMAC_SUPPORTED=y +CONFIG_SOC_ULP_SUPPORTED=y +CONFIG_SOC_CCOMP_TIMER_SUPPORTED=y +CONFIG_SOC_RTC_FAST_MEM_SUPPORTED=y +CONFIG_SOC_RTC_SLOW_MEM_SUPPORTED=y +CONFIG_SOC_RTC_MEM_SUPPORTED=y +CONFIG_SOC_I2S_SUPPORTED=y +CONFIG_SOC_RMT_SUPPORTED=y +CONFIG_SOC_SDM_SUPPORTED=y +CONFIG_SOC_GPSPI_SUPPORTED=y +CONFIG_SOC_LEDC_SUPPORTED=y +CONFIG_SOC_I2C_SUPPORTED=y +CONFIG_SOC_SUPPORT_COEXISTENCE=y +CONFIG_SOC_AES_SUPPORTED=y +CONFIG_SOC_MPI_SUPPORTED=y +CONFIG_SOC_SHA_SUPPORTED=y +CONFIG_SOC_FLASH_ENC_SUPPORTED=y +CONFIG_SOC_SECURE_BOOT_SUPPORTED=y +CONFIG_SOC_TOUCH_SENSOR_SUPPORTED=y +CONFIG_SOC_BOD_SUPPORTED=y +CONFIG_SOC_ULP_FSM_SUPPORTED=y +CONFIG_SOC_CLK_TREE_SUPPORTED=y +CONFIG_SOC_MPU_SUPPORTED=y +CONFIG_SOC_WDT_SUPPORTED=y +CONFIG_SOC_SPI_FLASH_SUPPORTED=y +CONFIG_SOC_RNG_SUPPORTED=y +CONFIG_SOC_LIGHT_SLEEP_SUPPORTED=y +CONFIG_SOC_DEEP_SLEEP_SUPPORTED=y +CONFIG_SOC_LP_PERIPH_SHARE_INTERRUPT=y +CONFIG_SOC_PM_SUPPORTED=y +CONFIG_SOC_DPORT_WORKAROUND_DIS_INTERRUPT_LVL=5 +CONFIG_SOC_XTAL_SUPPORT_26M=y +CONFIG_SOC_XTAL_SUPPORT_40M=y +CONFIG_SOC_XTAL_SUPPORT_AUTO_DETECT=y +CONFIG_SOC_ADC_RTC_CTRL_SUPPORTED=y +CONFIG_SOC_ADC_DIG_CTRL_SUPPORTED=y +CONFIG_SOC_ADC_DMA_SUPPORTED=y +CONFIG_SOC_ADC_PERIPH_NUM=2 +CONFIG_SOC_ADC_MAX_CHANNEL_NUM=10 +CONFIG_SOC_ADC_ATTEN_NUM=4 +CONFIG_SOC_ADC_DIGI_CONTROLLER_NUM=2 +CONFIG_SOC_ADC_PATT_LEN_MAX=16 +CONFIG_SOC_ADC_DIGI_MIN_BITWIDTH=9 +CONFIG_SOC_ADC_DIGI_MAX_BITWIDTH=12 +CONFIG_SOC_ADC_DIGI_RESULT_BYTES=2 +CONFIG_SOC_ADC_DIGI_DATA_BYTES_PER_CONV=4 +CONFIG_SOC_ADC_DIGI_MONITOR_NUM=0 +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_HIGH=2 +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_LOW=20 +CONFIG_SOC_ADC_RTC_MIN_BITWIDTH=9 +CONFIG_SOC_ADC_RTC_MAX_BITWIDTH=12 +CONFIG_SOC_ADC_SHARED_POWER=y +CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y +CONFIG_SOC_SHARED_IDCACHE_SUPPORTED=y +CONFIG_SOC_IDCACHE_PER_CORE=y +CONFIG_SOC_CPU_CORES_NUM=2 +CONFIG_SOC_CPU_INTR_NUM=32 +CONFIG_SOC_CPU_HAS_FPU=y +CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y +CONFIG_SOC_CPU_BREAKPOINTS_NUM=2 +CONFIG_SOC_CPU_WATCHPOINTS_NUM=2 +CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40 +CONFIG_SOC_DAC_CHAN_NUM=2 +CONFIG_SOC_DAC_RESOLUTION=8 +CONFIG_SOC_DAC_DMA_16BIT_ALIGN=y +CONFIG_SOC_GPIO_PORT=1 +CONFIG_SOC_GPIO_PIN_COUNT=40 +CONFIG_SOC_GPIO_VALID_GPIO_MASK=0xFFFFFFFFFF +CONFIG_SOC_GPIO_IN_RANGE_MAX=39 +CONFIG_SOC_GPIO_OUT_RANGE_MAX=33 +CONFIG_SOC_GPIO_VALID_DIGITAL_IO_PAD_MASK=0xEF0FEA +CONFIG_SOC_GPIO_CLOCKOUT_BY_IO_MUX=y +CONFIG_SOC_GPIO_CLOCKOUT_CHANNEL_NUM=3 +CONFIG_SOC_I2C_NUM=2 +CONFIG_SOC_HP_I2C_NUM=2 +CONFIG_SOC_I2C_FIFO_LEN=32 +CONFIG_SOC_I2C_CMD_REG_NUM=16 +CONFIG_SOC_I2C_SUPPORT_SLAVE=y +CONFIG_SOC_I2C_SUPPORT_APB=y +CONFIG_SOC_I2C_SUPPORT_10BIT_ADDR=y +CONFIG_SOC_I2C_STOP_INDEPENDENT=y +CONFIG_SOC_I2S_NUM=2 +CONFIG_SOC_I2S_HW_VERSION_1=y +CONFIG_SOC_I2S_SUPPORTS_APLL=y +CONFIG_SOC_I2S_SUPPORTS_PLL_F160M=y +CONFIG_SOC_I2S_SUPPORTS_PDM=y +CONFIG_SOC_I2S_SUPPORTS_PDM_TX=y +CONFIG_SOC_I2S_SUPPORTS_PCM2PDM=y +CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y +CONFIG_SOC_I2S_SUPPORTS_PDM2PCM=y +CONFIG_SOC_I2S_PDM_MAX_TX_LINES=1 +CONFIG_SOC_I2S_PDM_MAX_RX_LINES=1 +CONFIG_SOC_I2S_SUPPORTS_ADC_DAC=y +CONFIG_SOC_I2S_SUPPORTS_ADC=y +CONFIG_SOC_I2S_SUPPORTS_DAC=y +CONFIG_SOC_I2S_SUPPORTS_LCD_CAMERA=y +CONFIG_SOC_I2S_MAX_DATA_WIDTH=24 +CONFIG_SOC_I2S_TRANS_SIZE_ALIGN_WORD=y +CONFIG_SOC_I2S_LCD_I80_VARIANT=y +CONFIG_SOC_LCD_I80_SUPPORTED=y +CONFIG_SOC_LCD_I80_BUSES=2 +CONFIG_SOC_LCD_I80_BUS_WIDTH=24 +CONFIG_SOC_LEDC_HAS_TIMER_SPECIFIC_MUX=y +CONFIG_SOC_LEDC_SUPPORT_APB_CLOCK=y +CONFIG_SOC_LEDC_SUPPORT_REF_TICK=y +CONFIG_SOC_LEDC_SUPPORT_HS_MODE=y +CONFIG_SOC_LEDC_TIMER_NUM=4 +CONFIG_SOC_LEDC_CHANNEL_NUM=8 +CONFIG_SOC_LEDC_TIMER_BIT_WIDTH=20 +CONFIG_SOC_MCPWM_GROUPS=2 +CONFIG_SOC_MCPWM_TIMERS_PER_GROUP=3 +CONFIG_SOC_MCPWM_OPERATORS_PER_GROUP=3 +CONFIG_SOC_MCPWM_COMPARATORS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_GENERATORS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_TRIGGERS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_GPIO_FAULTS_PER_GROUP=3 +CONFIG_SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP=y +CONFIG_SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER=3 +CONFIG_SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP=3 +CONFIG_SOC_MMU_PERIPH_NUM=2 +CONFIG_SOC_MMU_LINEAR_ADDRESS_REGION_NUM=3 +CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000 +CONFIG_SOC_MPU_REGIONS_MAX_NUM=8 +CONFIG_SOC_PCNT_GROUPS=1 +CONFIG_SOC_PCNT_UNITS_PER_GROUP=8 +CONFIG_SOC_PCNT_CHANNELS_PER_UNIT=2 +CONFIG_SOC_PCNT_THRES_POINT_PER_UNIT=2 +CONFIG_SOC_RMT_GROUPS=1 +CONFIG_SOC_RMT_TX_CANDIDATES_PER_GROUP=8 +CONFIG_SOC_RMT_RX_CANDIDATES_PER_GROUP=8 +CONFIG_SOC_RMT_CHANNELS_PER_GROUP=8 +CONFIG_SOC_RMT_MEM_WORDS_PER_CHANNEL=64 +CONFIG_SOC_RMT_SUPPORT_REF_TICK=y +CONFIG_SOC_RMT_SUPPORT_APB=y +CONFIG_SOC_RMT_CHANNEL_CLK_INDEPENDENT=y +CONFIG_SOC_RTCIO_PIN_COUNT=18 +CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y +CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y +CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y +CONFIG_SOC_SDM_GROUPS=1 +CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8 +CONFIG_SOC_SDM_CLK_SUPPORT_APB=y +CONFIG_SOC_SPI_HD_BOTH_INOUT_SUPPORTED=y +CONFIG_SOC_SPI_AS_CS_SUPPORTED=y +CONFIG_SOC_SPI_PERIPH_NUM=3 +CONFIG_SOC_SPI_DMA_CHAN_NUM=2 +CONFIG_SOC_SPI_MAX_CS_NUM=3 +CONFIG_SOC_SPI_SUPPORT_CLK_APB=y +CONFIG_SOC_SPI_MAXIMUM_BUFFER_SIZE=64 +CONFIG_SOC_SPI_MAX_PRE_DIVIDER=8192 +CONFIG_SOC_MEMSPI_SRC_FREQ_80M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_40M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_26M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_20M_SUPPORTED=y +CONFIG_SOC_TIMER_GROUPS=2 +CONFIG_SOC_TIMER_GROUP_TIMERS_PER_GROUP=2 +CONFIG_SOC_TIMER_GROUP_COUNTER_BIT_WIDTH=64 +CONFIG_SOC_TIMER_GROUP_TOTAL_TIMERS=4 +CONFIG_SOC_TIMER_GROUP_SUPPORT_APB=y +CONFIG_SOC_LP_TIMER_BIT_WIDTH_LO=32 +CONFIG_SOC_LP_TIMER_BIT_WIDTH_HI=16 +CONFIG_SOC_TOUCH_SENSOR_VERSION=1 +CONFIG_SOC_TOUCH_SENSOR_NUM=10 +CONFIG_SOC_TOUCH_MIN_CHAN_ID=0 +CONFIG_SOC_TOUCH_MAX_CHAN_ID=9 +CONFIG_SOC_TOUCH_SUPPORT_SLEEP_WAKEUP=y +CONFIG_SOC_TOUCH_SAMPLE_CFG_NUM=1 +CONFIG_SOC_TWAI_CONTROLLER_NUM=1 +CONFIG_SOC_TWAI_MASK_FILTER_NUM=1 +CONFIG_SOC_TWAI_BRP_MIN=2 +CONFIG_SOC_TWAI_CLK_SUPPORT_APB=y +CONFIG_SOC_TWAI_SUPPORT_MULTI_ADDRESS_LAYOUT=y +CONFIG_SOC_UART_NUM=3 +CONFIG_SOC_UART_HP_NUM=3 +CONFIG_SOC_UART_SUPPORT_APB_CLK=y +CONFIG_SOC_UART_SUPPORT_REF_TICK=y +CONFIG_SOC_UART_FIFO_LEN=128 +CONFIG_SOC_UART_BITRATE_MAX=5000000 +CONFIG_SOC_UART_WAKEUP_SUPPORT_ACTIVE_THRESH_MODE=y +CONFIG_SOC_SPIRAM_SUPPORTED=y +CONFIG_SOC_SPI_MEM_SUPPORT_CONFIG_GPIO_BY_EFUSE=y +CONFIG_SOC_SHA_SUPPORT_PARALLEL_ENG=y +CONFIG_SOC_SHA_ENDIANNESS_BE=y +CONFIG_SOC_SHA_SUPPORT_SHA1=y +CONFIG_SOC_SHA_SUPPORT_SHA256=y +CONFIG_SOC_SHA_SUPPORT_SHA384=y +CONFIG_SOC_SHA_SUPPORT_SHA512=y +CONFIG_SOC_MPI_MEM_BLOCKS_NUM=4 +CONFIG_SOC_MPI_OPERATIONS_NUM=1 +CONFIG_SOC_RSA_MAX_BIT_LEN=4096 +CONFIG_SOC_AES_SUPPORT_AES_128=y +CONFIG_SOC_AES_SUPPORT_AES_192=y +CONFIG_SOC_AES_SUPPORT_AES_256=y +CONFIG_SOC_SECURE_BOOT_V1=y +CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS=1 +CONFIG_SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX=32 +CONFIG_SOC_PHY_DIG_REGS_MEM_SIZE=21 +CONFIG_SOC_PM_SUPPORT_EXT0_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_EXT1_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_EXT_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_TOUCH_SENSOR_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_RTC_PERIPH_PD=y +CONFIG_SOC_PM_SUPPORT_RTC_FAST_MEM_PD=y +CONFIG_SOC_PM_SUPPORT_RTC_SLOW_MEM_PD=y +CONFIG_SOC_PM_SUPPORT_RC_FAST_PD=y +CONFIG_SOC_PM_SUPPORT_VDDSDIO_PD=y +CONFIG_SOC_PM_SUPPORT_MODEM_PD=y +CONFIG_SOC_CONFIGURABLE_VDDSDIO_SUPPORTED=y +CONFIG_SOC_PM_MODEM_PD_BY_SW=y +CONFIG_SOC_CLK_APLL_SUPPORTED=y +CONFIG_SOC_CLK_RC_FAST_D256_SUPPORTED=y +CONFIG_SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256=y +CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y +CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y +CONFIG_SOC_CLK_LP_FAST_SUPPORT_XTAL_D4=y +CONFIG_SOC_SDMMC_USE_IOMUX=y +CONFIG_SOC_SDMMC_NUM_SLOTS=2 +CONFIG_SOC_WIFI_WAPI_SUPPORT=y +CONFIG_SOC_WIFI_CSI_SUPPORT=y +CONFIG_SOC_WIFI_MESH_SUPPORT=y +CONFIG_SOC_WIFI_SUPPORT_VARIABLE_BEACON_WINDOW=y +CONFIG_SOC_WIFI_NAN_SUPPORT=y +CONFIG_SOC_BLE_SUPPORTED=y +CONFIG_SOC_BLE_MESH_SUPPORTED=y +CONFIG_SOC_BT_CLASSIC_SUPPORTED=y +CONFIG_SOC_BLUFI_SUPPORTED=y +CONFIG_SOC_BT_H2C_ENC_KEY_CTRL_ENH_VSC_SUPPORTED=y +CONFIG_SOC_BLE_MULTI_CONN_OPTIMIZATION=y +CONFIG_SOC_ULP_HAS_ADC=y +CONFIG_SOC_PHY_COMBO_MODULE=y +CONFIG_SOC_EMAC_RMII_CLK_OUT_INTERNAL_LOOPBACK=y +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TOOLCHAIN="gcc" +CONFIG_IDF_TOOLCHAIN_GCC=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET_ARCH="xtensa" +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_INIT_VERSION="5.5.2" +CONFIG_IDF_TARGET_ESP32=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 + +# +# Build type +# +CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y +# CONFIG_APP_BUILD_TYPE_RAM is not set +CONFIG_APP_BUILD_GENERATE_BINARIES=y +CONFIG_APP_BUILD_BOOTLOADER=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y +# CONFIG_APP_REPRODUCIBLE_BUILD is not set +# CONFIG_APP_NO_BLOBS is not set +# CONFIG_APP_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_APP_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# end of Build type + +# +# Bootloader config +# + +# +# Bootloader manager +# +CONFIG_BOOTLOADER_COMPILE_TIME_DATE=y +CONFIG_BOOTLOADER_PROJECT_VER=1 +# end of Bootloader manager + +# +# Application Rollback +# +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set +# end of Application Rollback + +# +# Recovery Bootloader and Rollback +# +# end of Recovery Bootloader and Rollback + +CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x1000 +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set + +# +# Log +# +CONFIG_BOOTLOADER_LOG_VERSION_1=y +CONFIG_BOOTLOADER_LOG_VERSION=1 +# CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set +CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y +# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set +CONFIG_BOOTLOADER_LOG_LEVEL=3 + +# +# Format +# +# CONFIG_BOOTLOADER_LOG_COLORS is not set +CONFIG_BOOTLOADER_LOG_TIMESTAMP_SOURCE_CPU_TICKS=y +# end of Format + +# +# Settings +# +CONFIG_BOOTLOADER_LOG_MODE_TEXT_EN=y +CONFIG_BOOTLOADER_LOG_MODE_TEXT=y +# end of Settings +# end of Log + +# +# Serial Flash Configurations +# +# CONFIG_BOOTLOADER_FLASH_DC_AWARE is not set +CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y +# end of Serial Flash Configurations + +# CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set +CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# CONFIG_BOOTLOADER_APP_TEST is not set +CONFIG_BOOTLOADER_REGION_PROTECTION_ENABLE=y +CONFIG_BOOTLOADER_WDT_ENABLE=y +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set +CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0 +# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set +# end of Bootloader config + +# +# Security features +# +CONFIG_SECURE_BOOT_V1_SUPPORTED=y +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# CONFIG_SECURE_BOOT is not set +# CONFIG_SECURE_FLASH_ENC_ENABLED is not set +# end of Security features + +# +# Application manager +# +CONFIG_APP_COMPILE_TIME_DATE=y +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=9 +# end of Application manager + +CONFIG_ESP_ROM_HAS_CRC_LE=y +CONFIG_ESP_ROM_HAS_CRC_BE=y +CONFIG_ESP_ROM_HAS_MZ_CRC32=y +CONFIG_ESP_ROM_HAS_JPEG_DECODE=y +CONFIG_ESP_ROM_HAS_UART_BUF_SWITCH=y +CONFIG_ESP_ROM_NEEDS_SWSETUP_WORKAROUND=y +CONFIG_ESP_ROM_HAS_NEWLIB=y +CONFIG_ESP_ROM_HAS_NEWLIB_NANO_FORMAT=y +CONFIG_ESP_ROM_HAS_NEWLIB_32BIT_TIME=y +CONFIG_ESP_ROM_HAS_SW_FLOAT=y +CONFIG_ESP_ROM_USB_OTG_NUM=-1 +CONFIG_ESP_ROM_USB_SERIAL_DEVICE_NUM=-1 +CONFIG_ESP_ROM_SUPPORT_DEEP_SLEEP_WAKEUP_STUB=y +CONFIG_ESP_ROM_HAS_OUTPUT_PUTC_FUNC=y + +# +# Serial flasher config +# +# CONFIG_ESPTOOLPY_NO_STUB is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set +CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +CONFIG_ESPTOOLPY_FLASHFREQ="40m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +CONFIG_ESPTOOLPY_BEFORE="default_reset" +CONFIG_ESPTOOLPY_AFTER_RESET=y +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +CONFIG_ESPTOOLPY_AFTER="hard_reset" +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 +# end of Serial flasher config + +# +# Partition Table +# +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# Channel3 Configuration +# +CONFIG_VIDEO_NTSC=y +# CONFIG_VIDEO_PAL is not set +CONFIG_I2S_DATA_GPIO=22 +CONFIG_WIFI_MODE_STATION=y +# CONFIG_WIFI_MODE_SOFTAP is not set +CONFIG_WIFI_STA_SSID="Super Exmodiar Lvl2.5" +CONFIG_WIFI_STA_PASS="Commodore" +# end of Channel3 Configuration + +# +# Compiler options +# +# CONFIG_COMPILER_OPTIMIZATION_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_SIZE is not set +CONFIG_COMPILER_OPTIMIZATION_PERF=y +# CONFIG_COMPILER_OPTIMIZATION_NONE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set +CONFIG_COMPILER_ASSERT_NDEBUG_EVALUATE=y +CONFIG_COMPILER_FLOAT_LIB_FROM_GCCLIB=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2 +# CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set +CONFIG_COMPILER_HIDE_PATHS_MACROS=y +# CONFIG_COMPILER_CXX_EXCEPTIONS is not set +# CONFIG_COMPILER_CXX_RTTI is not set +CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y +# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set +# CONFIG_COMPILER_NO_MERGE_CONSTANTS is not set +# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set +CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y +# CONFIG_COMPILER_DISABLE_GCC12_WARNINGS is not set +# CONFIG_COMPILER_DISABLE_GCC13_WARNINGS is not set +# CONFIG_COMPILER_DISABLE_GCC14_WARNINGS is not set +# CONFIG_COMPILER_DUMP_RTL_FILES is not set +CONFIG_COMPILER_RT_LIB_GCCLIB=y +CONFIG_COMPILER_RT_LIB_NAME="gcc" +CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y +# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set +# CONFIG_COMPILER_STATIC_ANALYZER is not set +# end of Compiler options + +# +# Component config +# + +# +# Application Level Tracing +# +# CONFIG_APPTRACE_DEST_JTAG is not set +CONFIG_APPTRACE_DEST_NONE=y +# CONFIG_APPTRACE_DEST_UART1 is not set +# CONFIG_APPTRACE_DEST_UART2 is not set +CONFIG_APPTRACE_DEST_UART_NONE=y +CONFIG_APPTRACE_UART_TASK_PRIO=1 +CONFIG_APPTRACE_LOCK_ENABLE=y +# end of Application Level Tracing + +# +# Bluetooth +# +# CONFIG_BT_ENABLED is not set + +# +# Common Options +# + +# +# BLE Log +# +# CONFIG_BLE_LOG_ENABLED is not set +# end of BLE Log + +# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set +# CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set +# CONFIG_BT_LE_USED_MEM_STATISTICS_ENABLED is not set +# end of Common Options +# end of Bluetooth + +# +# Console Library +# +# CONFIG_CONSOLE_SORTED_HELP is not set +# end of Console Library + +# +# Driver Configurations +# + +# +# Legacy TWAI Driver Configurations +# +# CONFIG_TWAI_SKIP_LEGACY_CONFLICT_CHECK is not set +CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC=y +CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST=y +CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID=y +CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT=y +CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y +# end of Legacy TWAI Driver Configurations + +# +# Legacy ADC Driver Configuration +# +CONFIG_ADC_DISABLE_DAC=y +# CONFIG_ADC_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_ADC_SKIP_LEGACY_CONFLICT_CHECK is not set + +# +# Legacy ADC Calibration Configuration +# +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y +CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CAL_LUT_ENABLE=y +# CONFIG_ADC_CALI_SUPPRESS_DEPRECATE_WARN is not set +# end of Legacy ADC Calibration Configuration +# end of Legacy ADC Driver Configuration + +# +# Legacy DAC Driver Configurations +# +# CONFIG_DAC_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_DAC_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy DAC Driver Configurations + +# +# Legacy MCPWM Driver Configurations +# +# CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_MCPWM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy MCPWM Driver Configurations + +# +# Legacy Timer Group Driver Configurations +# +# CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_GPTIMER_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Timer Group Driver Configurations + +# +# Legacy RMT Driver Configurations +# +# CONFIG_RMT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_RMT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy RMT Driver Configurations + +# +# Legacy I2S Driver Configurations +# +# CONFIG_I2S_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_I2S_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2S Driver Configurations + +# +# Legacy I2C Driver Configurations +# +# CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2C Driver Configurations + +# +# Legacy PCNT Driver Configurations +# +# CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_PCNT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy PCNT Driver Configurations + +# +# Legacy SDM Driver Configurations +# +# CONFIG_SDM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_SDM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy SDM Driver Configurations + +# +# Legacy Touch Sensor Driver Configurations +# +# CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_TOUCH_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Touch Sensor Driver Configurations +# end of Driver Configurations + +# +# eFuse Bit Manager +# +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# CONFIG_EFUSE_VIRTUAL is not set +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE is not set +CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4=y +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set +CONFIG_EFUSE_MAX_BLK_LEN=192 +# end of eFuse Bit Manager + +# +# ESP-TLS +# +CONFIG_ESP_TLS_USING_MBEDTLS=y +# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set +# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK is not set +# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set +# CONFIG_ESP_TLS_PSK_VERIFICATION is not set +# CONFIG_ESP_TLS_INSECURE is not set +CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y +# end of ESP-TLS + +# +# ADC and ADC Calibration +# +# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set +# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set + +# +# ADC Calibration Configurations +# +CONFIG_ADC_CALI_EFUSE_TP_ENABLE=y +CONFIG_ADC_CALI_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CALI_LUT_ENABLE=y +# end of ADC Calibration Configurations + +CONFIG_ADC_DISABLE_DAC_OUTPUT=y +# CONFIG_ADC_ENABLE_DEBUG_LOG is not set +# end of ADC and ADC Calibration + +# +# Wireless Coexistence +# +CONFIG_ESP_COEX_ENABLED=y +# CONFIG_ESP_COEX_GPIO_DEBUG is not set +# end of Wireless Coexistence + +# +# Common ESP-related +# +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +# end of Common ESP-related + +# +# ESP-Driver:DAC Configurations +# +# CONFIG_DAC_CTRL_FUNC_IN_IRAM is not set +# CONFIG_DAC_ISR_IRAM_SAFE is not set +# CONFIG_DAC_ENABLE_DEBUG_LOG is not set +CONFIG_DAC_DMA_AUTO_16BIT_ALIGN=y +# end of ESP-Driver:DAC Configurations + +# +# ESP-Driver:GPIO Configurations +# +# CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL is not set +# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:GPIO Configurations + +# +# ESP-Driver:GPTimer Configurations +# +CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y +# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set +# CONFIG_GPTIMER_ISR_CACHE_SAFE is not set +CONFIG_GPTIMER_OBJ_CACHE_SAFE=y +# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:GPTimer Configurations + +# +# ESP-Driver:I2C Configurations +# +# CONFIG_I2C_ISR_IRAM_SAFE is not set +# CONFIG_I2C_ENABLE_DEBUG_LOG is not set +# CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2 is not set +CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y +# end of ESP-Driver:I2C Configurations + +# +# ESP-Driver:I2S Configurations +# +CONFIG_I2S_ISR_IRAM_SAFE=y +# CONFIG_I2S_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:I2S Configurations + +# +# ESP-Driver:LEDC Configurations +# +# CONFIG_LEDC_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:LEDC Configurations + +# +# ESP-Driver:MCPWM Configurations +# +CONFIG_MCPWM_ISR_HANDLER_IN_IRAM=y +# CONFIG_MCPWM_ISR_CACHE_SAFE is not set +# CONFIG_MCPWM_CTRL_FUNC_IN_IRAM is not set +CONFIG_MCPWM_OBJ_CACHE_SAFE=y +# CONFIG_MCPWM_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:MCPWM Configurations + +# +# ESP-Driver:PCNT Configurations +# +# CONFIG_PCNT_CTRL_FUNC_IN_IRAM is not set +# CONFIG_PCNT_ISR_IRAM_SAFE is not set +# CONFIG_PCNT_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:PCNT Configurations + +# +# ESP-Driver:RMT Configurations +# +CONFIG_RMT_ENCODER_FUNC_IN_IRAM=y +CONFIG_RMT_TX_ISR_HANDLER_IN_IRAM=y +CONFIG_RMT_RX_ISR_HANDLER_IN_IRAM=y +# CONFIG_RMT_RECV_FUNC_IN_IRAM is not set +# CONFIG_RMT_TX_ISR_CACHE_SAFE is not set +# CONFIG_RMT_RX_ISR_CACHE_SAFE is not set +CONFIG_RMT_OBJ_CACHE_SAFE=y +# CONFIG_RMT_ENABLE_DEBUG_LOG is not set +# CONFIG_RMT_ISR_IRAM_SAFE is not set +# end of ESP-Driver:RMT Configurations + +# +# ESP-Driver:Sigma Delta Modulator Configurations +# +# CONFIG_SDM_CTRL_FUNC_IN_IRAM is not set +# CONFIG_SDM_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:Sigma Delta Modulator Configurations + +# +# ESP-Driver:SPI Configurations +# +# CONFIG_SPI_MASTER_IN_IRAM is not set +CONFIG_SPI_MASTER_ISR_IN_IRAM=y +# CONFIG_SPI_SLAVE_IN_IRAM is not set +CONFIG_SPI_SLAVE_ISR_IN_IRAM=y +# end of ESP-Driver:SPI Configurations + +# +# ESP-Driver:Touch Sensor Configurations +# +# CONFIG_TOUCH_CTRL_FUNC_IN_IRAM is not set +# CONFIG_TOUCH_ISR_IRAM_SAFE is not set +# CONFIG_TOUCH_ENABLE_DEBUG_LOG is not set +# CONFIG_TOUCH_SKIP_FSM_CHECK is not set +# end of ESP-Driver:Touch Sensor Configurations + +# +# ESP-Driver:TWAI Configurations +# +# CONFIG_TWAI_ISR_IN_IRAM is not set +# CONFIG_TWAI_IO_FUNC_IN_IRAM is not set +# CONFIG_TWAI_ISR_CACHE_SAFE is not set +# CONFIG_TWAI_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:TWAI Configurations + +# +# ESP-Driver:UART Configurations +# +# CONFIG_UART_ISR_IN_IRAM is not set +# end of ESP-Driver:UART Configurations + +# +# ESP-Driver:UHCI Configurations +# +# CONFIG_UHCI_ISR_HANDLER_IN_IRAM is not set +# CONFIG_UHCI_ISR_CACHE_SAFE is not set +# CONFIG_UHCI_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:UHCI Configurations + +# +# Ethernet +# +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_RMII_CLK_INPUT=y +# CONFIG_ETH_RMII_CLK_OUTPUT is not set +CONFIG_ETH_RMII_CLK_IN_GPIO=0 +CONFIG_ETH_DMA_BUFFER_SIZE=512 +CONFIG_ETH_DMA_RX_BUFFER_NUM=10 +CONFIG_ETH_DMA_TX_BUFFER_NUM=10 +# CONFIG_ETH_IRAM_OPTIMIZATION is not set +CONFIG_ETH_USE_SPI_ETHERNET=y +# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set +# CONFIG_ETH_SPI_ETHERNET_W5500 is not set +# CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set +# CONFIG_ETH_USE_OPENETH is not set +# CONFIG_ETH_TRANSMIT_MUTEX is not set +# end of Ethernet + +# +# Event Loop Library +# +# CONFIG_ESP_EVENT_LOOP_PROFILING is not set +CONFIG_ESP_EVENT_POST_FROM_ISR=y +CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y +# end of Event Loop Library + +# +# GDB Stub +# +CONFIG_ESP_GDBSTUB_ENABLED=y +# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set +CONFIG_ESP_GDBSTUB_SUPPORT_TASKS=y +CONFIG_ESP_GDBSTUB_MAX_TASKS=32 +# end of GDB Stub + +# +# ESP HID +# +CONFIG_ESPHID_TASK_SIZE_BT=2048 +CONFIG_ESPHID_TASK_SIZE_BLE=4096 +# end of ESP HID + +# +# ESP HTTP client +# +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set +# CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH is not set +# CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT is not set +CONFIG_ESP_HTTP_CLIENT_EVENT_POST_TIMEOUT=2000 +# end of ESP HTTP client + +# +# HTTP Server +# +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +# CONFIG_HTTPD_WS_SUPPORT is not set +# CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set +CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 +# end of HTTP Server + +# +# ESP HTTPS OTA +# +# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set +# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set +CONFIG_ESP_HTTPS_OTA_EVENT_POST_TIMEOUT=2000 +# end of ESP HTTPS OTA + +# +# ESP HTTPS server +# +# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set +CONFIG_ESP_HTTPS_SERVER_EVENT_POST_TIMEOUT=2000 +# CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK is not set +# end of ESP HTTPS server + +# +# Hardware Settings +# + +# +# Chip revision +# +CONFIG_ESP32_REV_MIN_0=y +# CONFIG_ESP32_REV_MIN_1 is not set +# CONFIG_ESP32_REV_MIN_1_1 is not set +# CONFIG_ESP32_REV_MIN_2 is not set +# CONFIG_ESP32_REV_MIN_3 is not set +# CONFIG_ESP32_REV_MIN_3_1 is not set +CONFIG_ESP32_REV_MIN=0 +CONFIG_ESP32_REV_MIN_FULL=0 +CONFIG_ESP_REV_MIN_FULL=0 + +# +# Maximum Supported ESP32 Revision (Rev v3.99) +# +CONFIG_ESP32_REV_MAX_FULL=399 +CONFIG_ESP_REV_MAX_FULL=399 +CONFIG_ESP_EFUSE_BLOCK_REV_MIN_FULL=0 +CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL=99 + +# +# Maximum Supported ESP32 eFuse Block Revision (eFuse Block Rev v0.99) +# +# end of Chip revision + +# +# MAC Config +# +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES=4 +# CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_TWO is not set +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES=4 +# CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR is not set +# CONFIG_ESP_MAC_USE_CUSTOM_MAC_AS_BASE_MAC is not set +# end of MAC Config + +# +# Sleep Config +# +# CONFIG_ESP_SLEEP_POWER_DOWN_FLASH is not set +CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y +# CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU is not set +CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y +# CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set +CONFIG_ESP_SLEEP_WAIT_FLASH_READY_EXTRA_DELAY=2000 +# CONFIG_ESP_SLEEP_CACHE_SAFE_ASSERTION is not set +# CONFIG_ESP_SLEEP_DEBUG is not set +CONFIG_ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS=y +# end of Sleep Config + +# +# RTC Clock Config +# +CONFIG_RTC_CLK_SRC_INT_RC=y +# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_RTC_CLK_SRC_INT_8MD256 is not set +CONFIG_RTC_CLK_CAL_CYCLES=1024 +# end of RTC Clock Config + +# +# Peripheral Control +# +CONFIG_ESP_PERIPH_CTRL_FUNC_IN_IRAM=y +CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM=y +# end of Peripheral Control + +# +# Main XTAL Config +# +# CONFIG_XTAL_FREQ_26 is not set +# CONFIG_XTAL_FREQ_32 is not set +CONFIG_XTAL_FREQ_40=y +# CONFIG_XTAL_FREQ_AUTO is not set +CONFIG_XTAL_FREQ=40 +# end of Main XTAL Config + +# +# Power Supplier +# + +# +# Brownout Detector +# +# CONFIG_ESP_BROWNOUT_DET is not set +CONFIG_ESP_BROWNOUT_USE_INTR=y +# end of Brownout Detector +# end of Power Supplier + +CONFIG_ESP_SPI_BUS_LOCK_ISR_FUNCS_IN_IRAM=y +CONFIG_ESP_INTR_IN_IRAM=y +# end of Hardware Settings + +# +# ESP-Driver:LCD Controller Configurations +# +# CONFIG_LCD_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:LCD Controller Configurations + +# +# ESP-MM: Memory Management Configurations +# +# end of ESP-MM: Memory Management Configurations + +# +# ESP NETIF Adapter +# +CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 +# CONFIG_ESP_NETIF_PROVIDE_CUSTOM_IMPLEMENTATION is not set +CONFIG_ESP_NETIF_TCPIP_LWIP=y +# CONFIG_ESP_NETIF_LOOPBACK is not set +CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y +CONFIG_ESP_NETIF_REPORT_DATA_TRAFFIC=y +# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set +# CONFIG_ESP_NETIF_L2_TAP is not set +# CONFIG_ESP_NETIF_BRIDGE_EN is not set +# CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set +# end of ESP NETIF Adapter + +# +# Partition API Configuration +# +# end of Partition API Configuration + +# +# PHY +# +CONFIG_ESP_PHY_ENABLED=y +CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP_PHY_MAX_TX_POWER=20 +# CONFIG_ESP_PHY_ENABLE_CERT_TEST is not set +CONFIG_ESP_PHY_RF_CAL_PARTIAL=y +# CONFIG_ESP_PHY_RF_CAL_NONE is not set +# CONFIG_ESP_PHY_RF_CAL_FULL is not set +CONFIG_ESP_PHY_CALIBRATION_MODE=0 +CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000 +# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set +# CONFIG_ESP_PHY_RECORD_USED_TIME is not set +CONFIG_ESP_PHY_IRAM_OPT=y +# CONFIG_ESP_PHY_DEBUG is not set +# end of PHY + +# +# Power Management +# +CONFIG_PM_SLEEP_FUNC_IN_IRAM=y +# CONFIG_PM_ENABLE is not set +CONFIG_PM_SLP_IRAM_OPT=y +# end of Power Management + +# +# ESP PSRAM +# +# CONFIG_SPIRAM is not set +# end of ESP PSRAM + +# +# ESP Ringbuf +# +# CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH is not set +# end of ESP Ringbuf + +# +# ESP-ROM +# +CONFIG_ESP_ROM_PRINT_IN_IRAM=y +# end of ESP-ROM + +# +# ESP Security Specific +# +# end of ESP Security Specific + +# +# ESP System Settings +# +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 + +# +# Memory +# +# CONFIG_ESP32_USE_FIXED_STATIC_RAM_SIZE is not set + +# +# Non-backward compatible options +# +# CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM is not set +# end of Non-backward compatible options +# end of Memory + +# +# Trace memory +# +# CONFIG_ESP32_TRAX is not set +CONFIG_ESP32_TRACEMEM_RESERVE_DRAM=0x0 +# end of Trace memory + +CONFIG_ESP_SYSTEM_IN_IRAM=y +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y +# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set +CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0 + +# +# Memory protection +# +# end of Memory protection + +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y +# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set +# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 +CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_UART=y +CONFIG_ESP_CONSOLE_UART_NUM=0 +CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0 +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +CONFIG_ESP_INT_WDT=y +CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 +CONFIG_ESP_INT_WDT_CHECK_CPU1=y +CONFIG_ESP_TASK_WDT_EN=y +CONFIG_ESP_TASK_WDT_INIT=y +# CONFIG_ESP_TASK_WDT_PANIC is not set +CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_ESP_PANIC_HANDLER_IRAM is not set +# CONFIG_ESP_DEBUG_STUBS_ENABLE is not set +CONFIG_ESP_DEBUG_OCDAWARE=y +# CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 is not set +CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y +# CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE is not set +# end of ESP System Settings + +# +# IPC (Inter-Processor Call) +# +CONFIG_ESP_IPC_ENABLE=y +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y +CONFIG_ESP_IPC_ISR_ENABLE=y +# end of IPC (Inter-Processor Call) + +# +# ESP Timer (High Resolution Timer) +# +CONFIG_ESP_TIMER_IN_IRAM=y +# CONFIG_ESP_TIMER_PROFILING is not set +CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y +CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y +CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1 +# CONFIG_ESP_TIMER_SHOW_EXPERIMENTAL is not set +CONFIG_ESP_TIMER_TASK_AFFINITY=0x0 +CONFIG_ESP_TIMER_TASK_AFFINITY_CPU0=y +CONFIG_ESP_TIMER_ISR_AFFINITY_CPU0=y +# CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set +CONFIG_ESP_TIMER_IMPL_TG0_LAC=y +# end of ESP Timer (High Resolution Timer) + +# +# Wi-Fi +# +CONFIG_ESP_WIFI_ENABLED=y +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set +CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0 +CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5 +# CONFIG_ESP_WIFI_CSI_ENABLED is not set +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP_WIFI_TX_BA_WIN=6 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_RX_BA_WIN=6 +CONFIG_ESP_WIFI_NVS_ENABLED=y +CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP_WIFI_IRAM_OPT=y +# CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set +CONFIG_ESP_WIFI_RX_IRAM_OPT=y +CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP_WIFI_ENABLE_SAE_PK=y +CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y +CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y +CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y +# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set +CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 +# CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT is not set +CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10 +CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y +CONFIG_ESP_WIFI_GMAC_SUPPORT=y +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y +# CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set +CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7 +# CONFIG_ESP_WIFI_NAN_ENABLE is not set +CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y +CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y +# CONFIG_ESP_WIFI_WAPI_PSK is not set +# CONFIG_ESP_WIFI_11KV_SUPPORT is not set +# CONFIG_ESP_WIFI_MBO_SUPPORT is not set +# CONFIG_ESP_WIFI_DPP_SUPPORT is not set +# CONFIG_ESP_WIFI_11R_SUPPORT is not set +# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set + +# +# WPS Configuration Options +# +# CONFIG_ESP_WIFI_WPS_STRICT is not set +# CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set +# CONFIG_ESP_WIFI_WPS_RECONNECT_ON_FAIL is not set +# end of WPS Configuration Options + +# CONFIG_ESP_WIFI_DEBUG_PRINT is not set +CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y +# CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set +# end of Wi-Fi + +# +# Core dump +# +# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set +# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set +CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y +# end of Core dump + +# +# FAT Filesystem support +# +CONFIG_FATFS_VOLUME_COUNT=2 +CONFIG_FATFS_LFN_NONE=y +# CONFIG_FATFS_LFN_HEAP is not set +# CONFIG_FATFS_LFN_STACK is not set +# CONFIG_FATFS_SECTOR_512 is not set +CONFIG_FATFS_SECTOR_4096=y +# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set +CONFIG_FATFS_CODEPAGE_437=y +# CONFIG_FATFS_CODEPAGE_720 is not set +# CONFIG_FATFS_CODEPAGE_737 is not set +# CONFIG_FATFS_CODEPAGE_771 is not set +# CONFIG_FATFS_CODEPAGE_775 is not set +# CONFIG_FATFS_CODEPAGE_850 is not set +# CONFIG_FATFS_CODEPAGE_852 is not set +# CONFIG_FATFS_CODEPAGE_855 is not set +# CONFIG_FATFS_CODEPAGE_857 is not set +# CONFIG_FATFS_CODEPAGE_860 is not set +# CONFIG_FATFS_CODEPAGE_861 is not set +# CONFIG_FATFS_CODEPAGE_862 is not set +# CONFIG_FATFS_CODEPAGE_863 is not set +# CONFIG_FATFS_CODEPAGE_864 is not set +# CONFIG_FATFS_CODEPAGE_865 is not set +# CONFIG_FATFS_CODEPAGE_866 is not set +# CONFIG_FATFS_CODEPAGE_869 is not set +# CONFIG_FATFS_CODEPAGE_932 is not set +# CONFIG_FATFS_CODEPAGE_936 is not set +# CONFIG_FATFS_CODEPAGE_949 is not set +# CONFIG_FATFS_CODEPAGE_950 is not set +CONFIG_FATFS_CODEPAGE=437 +CONFIG_FATFS_FS_LOCK=0 +CONFIG_FATFS_TIMEOUT_MS=10000 +CONFIG_FATFS_PER_FILE_CACHE=y +# CONFIG_FATFS_USE_FASTSEEK is not set +CONFIG_FATFS_USE_STRFUNC_NONE=y +# CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set +# CONFIG_FATFS_USE_STRFUNC_WITH_CRLF_CONV is not set +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=0 +# CONFIG_FATFS_IMMEDIATE_FSYNC is not set +# CONFIG_FATFS_USE_LABEL is not set +CONFIG_FATFS_LINK_LOCK=y +# CONFIG_FATFS_USE_DYN_BUFFERS is not set + +# +# File system free space calculation behavior +# +CONFIG_FATFS_DONT_TRUST_FREE_CLUSTER_CNT=0 +CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0 +# end of File system free space calculation behavior +# end of FAT Filesystem support + +# +# FreeRTOS +# + +# +# Kernel +# +# CONFIG_FREERTOS_SMP is not set +# CONFIG_FREERTOS_UNICORE is not set +CONFIG_FREERTOS_HZ=100 +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 +# CONFIG_FREERTOS_USE_IDLE_HOOK is not set +# CONFIG_FREERTOS_USE_TICK_HOOK is not set +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +# CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY is not set +CONFIG_FREERTOS_USE_TIMERS=y +CONFIG_FREERTOS_TIMER_SERVICE_TASK_NAME="Tmr Svc" +# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU0 is not set +# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU1 is not set +CONFIG_FREERTOS_TIMER_TASK_NO_AFFINITY=y +CONFIG_FREERTOS_TIMER_SERVICE_TASK_CORE_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set +# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set +# end of Kernel + +# +# Port +# +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS=y +# CONFIG_FREERTOS_TASK_PRE_DELETION_HOOK is not set +# CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +# CONFIG_FREERTOS_FPU_IN_ISR is not set +CONFIG_FREERTOS_TICK_SUPPORT_CORETIMER=y +CONFIG_FREERTOS_CORETIMER_0=y +# CONFIG_FREERTOS_CORETIMER_1 is not set +CONFIG_FREERTOS_SYSTICK_USES_CCOUNT=y +# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +# end of Port + +# +# Extra +# +# end of Extra + +CONFIG_FREERTOS_PORT=y +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y +CONFIG_FREERTOS_DEBUG_OCDAWARE=y +CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y +CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y +CONFIG_FREERTOS_NUMBER_OF_CORES=2 +CONFIG_FREERTOS_IN_IRAM=y +# end of FreeRTOS + +# +# Hardware Abstraction Layer (HAL) and Low Level (LL) +# +CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y +# CONFIG_HAL_ASSERTION_DISABLE is not set +# CONFIG_HAL_ASSERTION_SILENT is not set +# CONFIG_HAL_ASSERTION_ENABLE is not set +CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 +# end of Hardware Abstraction Layer (HAL) and Low Level (LL) + +# +# Heap memory debugging +# +CONFIG_HEAP_POISONING_DISABLED=y +# CONFIG_HEAP_POISONING_LIGHT is not set +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +CONFIG_HEAP_TRACING_OFF=y +# CONFIG_HEAP_TRACING_STANDALONE is not set +# CONFIG_HEAP_TRACING_TOHOST is not set +# CONFIG_HEAP_USE_HOOKS is not set +# CONFIG_HEAP_TASK_TRACKING is not set +# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set +# CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH is not set +# end of Heap memory debugging + +# +# Log +# +CONFIG_LOG_VERSION_1=y +# CONFIG_LOG_VERSION_2 is not set +CONFIG_LOG_VERSION=1 + +# +# Log Level +# +# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y +# CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set +# CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set +CONFIG_LOG_MAXIMUM_LEVEL=3 + +# +# Level Settings +# +# CONFIG_LOG_MASTER_LEVEL is not set +CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=y +# CONFIG_LOG_TAG_LEVEL_IMPL_NONE is not set +# CONFIG_LOG_TAG_LEVEL_IMPL_LINKED_LIST is not set +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_AND_LINKED_LIST=y +# CONFIG_LOG_TAG_LEVEL_CACHE_ARRAY is not set +CONFIG_LOG_TAG_LEVEL_CACHE_BINARY_MIN_HEAP=y +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_SIZE=31 +# end of Level Settings +# end of Log Level + +# +# Format +# +# CONFIG_LOG_COLORS is not set +CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y +# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set +# end of Format + +# +# Settings +# +CONFIG_LOG_MODE_TEXT_EN=y +CONFIG_LOG_MODE_TEXT=y +# end of Settings + +CONFIG_LOG_IN_IRAM=y +# end of Log + +# +# LWIP +# +CONFIG_LWIP_ENABLE=y +CONFIG_LWIP_LOCAL_HOSTNAME="espressif" +CONFIG_LWIP_TCPIP_TASK_PRIO=18 +# CONFIG_LWIP_TCPIP_CORE_LOCKING is not set +# CONFIG_LWIP_CHECK_THREAD_SAFETY is not set +CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y +# CONFIG_LWIP_L2_TO_L3_COPY is not set +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +# CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_TIMERS_ONDEMAND=y +CONFIG_LWIP_ND6=y +# CONFIG_LWIP_FORCE_ROUTER_FORWARDING is not set +CONFIG_LWIP_MAX_SOCKETS=10 +# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set +# CONFIG_LWIP_SO_LINGER is not set +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_REUSE_RXTOALL=y +# CONFIG_LWIP_SO_RCVBUF is not set +# CONFIG_LWIP_NETBUF_RECVINFO is not set +CONFIG_LWIP_IP_DEFAULT_TTL=64 +CONFIG_LWIP_IP4_FRAG=y +CONFIG_LWIP_IP6_FRAG=y +# CONFIG_LWIP_IP4_REASSEMBLY is not set +# CONFIG_LWIP_IP6_REASSEMBLY is not set +CONFIG_LWIP_IP_REASS_MAX_PBUFS=10 +# CONFIG_LWIP_IP_FORWARD is not set +# CONFIG_LWIP_STATS is not set +CONFIG_LWIP_ESP_GRATUITOUS_ARP=y +CONFIG_LWIP_GARP_TMR_INTERVAL=60 +CONFIG_LWIP_ESP_MLDV6_REPORT=y +CONFIG_LWIP_MLDV6_TMR_INTERVAL=40 +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set +# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set +# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set +CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set +CONFIG_LWIP_DHCP_OPTIONS_LEN=69 +CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 +CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 + +# +# DHCP server +# +CONFIG_LWIP_DHCPS=y +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y +CONFIG_LWIP_DHCPS_ADD_DNS=y +# end of DHCP server + +# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_IPV4=y +CONFIG_LWIP_IPV6=y +# CONFIG_LWIP_IPV6_AUTOCONFIG is not set +CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 +# CONFIG_LWIP_IPV6_FORWARD is not set +# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 + +# +# TCP +# +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y +CONFIG_LWIP_TCP_MAXRTX=12 +CONFIG_LWIP_TCP_SYNMAXRTX=12 +CONFIG_LWIP_TCP_MSS=1440 +CONFIG_LWIP_TCP_TMR_INTERVAL=250 +CONFIG_LWIP_TCP_MSL=60000 +CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=20000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5760 +CONFIG_LWIP_TCP_WND_DEFAULT=5760 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 +CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=6 +CONFIG_LWIP_TCP_QUEUE_OOSEQ=y +CONFIG_LWIP_TCP_OOSEQ_TIMEOUT=6 +CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 +# CONFIG_LWIP_TCP_SACK_OUT is not set +CONFIG_LWIP_TCP_OVERSIZE_MSS=y +# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +CONFIG_LWIP_TCP_RTO_TIME=1500 +# end of TCP + +# +# UDP +# +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 +# end of UDP + +# +# Checksums +# +# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set +# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set +CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y +# end of Checksums + +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF +CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 +CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 +CONFIG_LWIP_IPV6_ND6_NUM_PREFIXES=5 +CONFIG_LWIP_IPV6_ND6_NUM_ROUTERS=3 +CONFIG_LWIP_IPV6_ND6_NUM_DESTINATIONS=10 +# CONFIG_LWIP_IPV6_ND6_ROUTE_INFO_OPTION_SUPPORT is not set +# CONFIG_LWIP_PPP_SUPPORT is not set +# CONFIG_LWIP_SLIP_SUPPORT is not set + +# +# ICMP +# +CONFIG_LWIP_ICMP=y +# CONFIG_LWIP_MULTICAST_PING is not set +# CONFIG_LWIP_BROADCAST_PING is not set +# end of ICMP + +# +# LWIP RAW API +# +CONFIG_LWIP_MAX_RAW_PCBS=16 +# end of LWIP RAW API + +# +# SNTP +# +CONFIG_LWIP_SNTP_MAX_SERVERS=1 +# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +CONFIG_LWIP_SNTP_STARTUP_DELAY=y +CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000 +# end of SNTP + +# +# DNS +# +CONFIG_LWIP_DNS_MAX_HOST_IP=1 +CONFIG_LWIP_DNS_MAX_SERVERS=3 +# CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set +# CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set +# CONFIG_LWIP_USE_ESP_GETADDRINFO is not set +# end of DNS + +CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 +CONFIG_LWIP_ESP_LWIP_ASSERT=y + +# +# Hooks +# +# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set +CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y +# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y +# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set +CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y +# CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set +# CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_NONE=y +# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM is not set +CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_NONE=y +# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_DEFAULT is not set +# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_CUSTOM is not set +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set +CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_CUSTOM is not set +# CONFIG_LWIP_HOOK_IP6_INPUT_NONE is not set +CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y +# CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM is not set +# end of Hooks + +# CONFIG_LWIP_DEBUG is not set +# end of LWIP + +# +# mbedTLS +# +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 +# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set +# CONFIG_MBEDTLS_DEBUG is not set + +# +# mbedTLS v3.x related +# +# CONFIG_MBEDTLS_SSL_PROTO_TLS1_3 is not set +# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set +# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set +# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set +CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y +# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set +CONFIG_MBEDTLS_PKCS7_C=y +# end of mbedTLS v3.x related + +# +# Certificate Bundle +# +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEPRECATED_LIST is not set +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=200 +# end of Certificate Bundle + +# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set +CONFIG_MBEDTLS_CMAC_C=y +CONFIG_MBEDTLS_HARDWARE_AES=y +CONFIG_MBEDTLS_GCM_SUPPORT_NON_AES_CIPHER=y +CONFIG_MBEDTLS_HARDWARE_MPI=y +# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set +CONFIG_MBEDTLS_HARDWARE_SHA=y +CONFIG_MBEDTLS_ROM_MD5=y +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_MBEDTLS_HAVE_TIME=y +# CONFIG_MBEDTLS_PLATFORM_TIME_ALT is not set +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y +CONFIG_MBEDTLS_SHA1_C=y +CONFIG_MBEDTLS_SHA512_C=y +# CONFIG_MBEDTLS_SHA3_C is not set +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# CONFIG_MBEDTLS_TLS_DISABLED is not set +CONFIG_MBEDTLS_TLS_SERVER=y +CONFIG_MBEDTLS_TLS_CLIENT=y +CONFIG_MBEDTLS_TLS_ENABLED=y + +# +# TLS Key Exchange Methods +# +# CONFIG_MBEDTLS_PSK_MODES is not set +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y +# end of TLS Key Exchange Methods + +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set +CONFIG_MBEDTLS_SSL_ALPN=y +CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y +CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y + +# +# Symmetric Ciphers +# +CONFIG_MBEDTLS_AES_C=y +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# CONFIG_MBEDTLS_DES_C is not set +# CONFIG_MBEDTLS_BLOWFISH_C is not set +# CONFIG_MBEDTLS_XTEA_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y +# CONFIG_MBEDTLS_NIST_KW_C is not set +# end of Symmetric Ciphers + +# CONFIG_MBEDTLS_RIPEMD160_C is not set + +# +# Certificates +# +CONFIG_MBEDTLS_PEM_PARSE_C=y +CONFIG_MBEDTLS_PEM_WRITE_C=y +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +# end of Certificates + +CONFIG_MBEDTLS_ECP_C=y +CONFIG_MBEDTLS_PK_PARSE_EC_EXTENDED=y +CONFIG_MBEDTLS_PK_PARSE_EC_COMPRESSED=y +# CONFIG_MBEDTLS_DHM_C is not set +CONFIG_MBEDTLS_ECDH_C=y +CONFIG_MBEDTLS_ECDSA_C=y +# CONFIG_MBEDTLS_ECJPAKE_C is not set +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +# CONFIG_MBEDTLS_ECP_FIXED_POINT_OPTIM is not set +# CONFIG_MBEDTLS_POLY1305_C is not set +# CONFIG_MBEDTLS_CHACHA20_C is not set +# CONFIG_MBEDTLS_HKDF_C is not set +# CONFIG_MBEDTLS_THREADING_C is not set +CONFIG_MBEDTLS_ERROR_STRINGS=y +CONFIG_MBEDTLS_FS_IO=y +# CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION is not set +# end of mbedTLS + +# +# ESP-MQTT Configurations +# +CONFIG_MQTT_PROTOCOL_311=y +# CONFIG_MQTT_PROTOCOL_5 is not set +CONFIG_MQTT_TRANSPORT_SSL=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y +# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set +# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set +# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set +# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set +# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set +# CONFIG_MQTT_CUSTOM_OUTBOX is not set +# end of ESP-MQTT Configurations + +# +# LibC +# +CONFIG_LIBC_NEWLIB=y +CONFIG_LIBC_MISC_IN_IRAM=y +CONFIG_LIBC_LOCKS_PLACE_IN_IRAM=y +CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_LIBC_STDOUT_LINE_ENDING_LF is not set +# CONFIG_LIBC_STDOUT_LINE_ENDING_CR is not set +# CONFIG_LIBC_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_LIBC_STDIN_LINE_ENDING_LF is not set +CONFIG_LIBC_STDIN_LINE_ENDING_CR=y +# CONFIG_LIBC_NEWLIB_NANO_FORMAT is not set +CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y +# CONFIG_LIBC_TIME_SYSCALL_USE_RTC is not set +# CONFIG_LIBC_TIME_SYSCALL_USE_HRT is not set +# CONFIG_LIBC_TIME_SYSCALL_USE_NONE is not set +# end of LibC + +# +# NVS +# +# CONFIG_NVS_ASSERT_ERROR_CHECK is not set +# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set +# end of NVS + +# +# OpenThread +# +# CONFIG_OPENTHREAD_ENABLED is not set + +# +# OpenThread Spinel +# +# CONFIG_OPENTHREAD_SPINEL_ONLY is not set +# end of OpenThread Spinel + +# CONFIG_OPENTHREAD_DEBUG is not set +# end of OpenThread + +# +# Protocomm +# +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION=y +# end of Protocomm + +# +# PThreads +# +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_PTHREAD_STACK_MIN=768 +CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y +# CONFIG_PTHREAD_DEFAULT_CORE_0 is not set +# CONFIG_PTHREAD_DEFAULT_CORE_1 is not set +CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" +# end of PThreads + +# +# MMU Config +# +CONFIG_MMU_PAGE_SIZE_64KB=y +CONFIG_MMU_PAGE_MODE="64KB" +CONFIG_MMU_PAGE_SIZE=0x10000 +# end of MMU Config + +# +# Main Flash configuration +# + +# +# SPI Flash behavior when brownout +# +CONFIG_SPI_FLASH_BROWNOUT_RESET_XMC=y +CONFIG_SPI_FLASH_BROWNOUT_RESET=y +# end of SPI Flash behavior when brownout + +# +# Optional and Experimental Features (READ DOCS FIRST) +# + +# +# Features here require specific hardware (READ DOCS FIRST!) +# +CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50 +# CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set +# CONFIG_SPI_FLASH_FORCE_ENABLE_C6_H2_SUSPEND is not set +CONFIG_SPI_FLASH_PLACE_FUNCTIONS_IN_IRAM=y +# end of Optional and Experimental Features (READ DOCS FIRST) +# end of Main Flash configuration + +# +# SPI Flash driver +# +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y +CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set +# CONFIG_SPI_FLASH_SHARE_SPI1_BUS is not set +# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 +# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set +# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set +# CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set + +# +# Auto-detect flash chips +# +CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_VENDOR_GD_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_VENDOR_ISSI_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_VENDOR_MXIC_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_VENDOR_WINBOND_SUPPORT_ENABLED=y +CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y +# CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP is not set +# CONFIG_SPI_FLASH_SUPPORT_TH_CHIP is not set +# end of Auto-detect flash chips + +CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y +# end of SPI Flash driver + +# +# SPIFFS Configuration +# +CONFIG_SPIFFS_MAX_PARTITIONS=3 + +# +# SPIFFS Cache Configuration +# +CONFIG_SPIFFS_CACHE=y +CONFIG_SPIFFS_CACHE_WR=y +# CONFIG_SPIFFS_CACHE_STATS is not set +# end of SPIFFS Cache Configuration + +CONFIG_SPIFFS_PAGE_CHECK=y +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# CONFIG_SPIFFS_GC_STATS is not set +CONFIG_SPIFFS_PAGE_SIZE=256 +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set +CONFIG_SPIFFS_USE_MAGIC=y +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +CONFIG_SPIFFS_META_LENGTH=4 +CONFIG_SPIFFS_USE_MTIME=y + +# +# Debug Configuration +# +# CONFIG_SPIFFS_DBG is not set +# CONFIG_SPIFFS_API_DBG is not set +# CONFIG_SPIFFS_GC_DBG is not set +# CONFIG_SPIFFS_CACHE_DBG is not set +# CONFIG_SPIFFS_CHECK_DBG is not set +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +# end of Debug Configuration +# end of SPIFFS Configuration + +# +# TCP Transport +# + +# +# Websocket +# +CONFIG_WS_TRANSPORT=y +CONFIG_WS_BUFFER_SIZE=1024 +# CONFIG_WS_DYNAMIC_BUFFER is not set +# end of Websocket +# end of TCP Transport + +# +# Ultra Low Power (ULP) Co-processor +# +# CONFIG_ULP_COPROC_ENABLED is not set + +# +# ULP Debugging Options +# +# end of ULP Debugging Options +# end of Ultra Low Power (ULP) Co-processor + +# +# Unity unit testing library +# +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +# CONFIG_UNITY_ENABLE_64BIT is not set +# CONFIG_UNITY_ENABLE_COLOR is not set +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +# CONFIG_UNITY_ENABLE_FIXTURE is not set +# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set +# CONFIG_UNITY_TEST_ORDER_BY_FILE_PATH_AND_LINE is not set +# end of Unity unit testing library + +# +# Virtual file system +# +CONFIG_VFS_SUPPORT_IO=y +CONFIG_VFS_SUPPORT_DIR=y +CONFIG_VFS_SUPPORT_SELECT=y +CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y +# CONFIG_VFS_SELECT_IN_RAM is not set +CONFIG_VFS_SUPPORT_TERMIOS=y +CONFIG_VFS_MAX_COUNT=8 + +# +# Host File System I/O (Semihosting) +# +CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# end of Host File System I/O (Semihosting) + +CONFIG_VFS_INITIALIZE_DEV_NULL=y +# end of Virtual file system + +# +# Wear Levelling +# +# CONFIG_WL_SECTOR_SIZE_512 is not set +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_WL_SECTOR_SIZE=4096 +# end of Wear Levelling + +# +# Wi-Fi Provisioning Manager +# +CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 +CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 +CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y +# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set +# end of Wi-Fi Provisioning Manager +# end of Component config + +# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set + +# Deprecated options for backward compatibility +# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set +# CONFIG_NO_BLOBS is not set +# CONFIG_ESP32_NO_BLOBS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set +CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=3 +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +# CONFIG_FLASHMODE_QIO is not set +# CONFIG_FLASHMODE_QOUT is not set +CONFIG_FLASHMODE_DIO=y +# CONFIG_FLASHMODE_DOUT is not set +CONFIG_MONITOR_BAUD=115200 +# CONFIG_OPTIMIZATION_LEVEL_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG is not set +# CONFIG_COMPILER_OPTIMIZATION_DEFAULT is not set +# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set +CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set +CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 +# CONFIG_CXX_EXCEPTIONS is not set +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set +CONFIG_ESP32_APPTRACE_DEST_NONE=y +CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y +CONFIG_ADC2_DISABLE_DAC=y +# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set +# CONFIG_MCPWM_ISR_IRAM_SAFE is not set +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_POST_EVENTS_FROM_ISR=y +CONFIG_POST_EVENTS_FROM_IRAM_ISR=y +CONFIG_GDBSTUB_SUPPORT_TASKS=y +CONFIG_GDBSTUB_MAX_TASKS=32 +# CONFIG_OTA_ALLOW_HTTP is not set +# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set +CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y +CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 +# CONFIG_ESP_SYSTEM_PD_FLASH is not set +CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP32_RTC_CLK_SRC_INT_RC=y +CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y +# CONFIG_ESP32_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set +# CONFIG_ESP32_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set +# CONFIG_ESP32_RTC_CLK_SRC_INT_8MD256 is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set +CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 +CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y +# CONFIG_ESP32_XTAL_FREQ_26 is not set +CONFIG_ESP32_XTAL_FREQ_40=y +# CONFIG_ESP32_XTAL_FREQ_AUTO is not set +CONFIG_ESP32_XTAL_FREQ=40 +# CONFIG_BROWNOUT_DET is not set +# CONFIG_ESP32_BROWNOUT_DET is not set +CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# CONFIG_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set +# CONFIG_ESP32_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=240 +CONFIG_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_ESP32_PANIC_PRINT_HALT is not set +CONFIG_ESP32_PANIC_PRINT_REBOOT=y +# CONFIG_ESP32_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP32_PANIC_GDBSTUB is not set +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_CONSOLE_UART_DEFAULT=y +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_CONSOLE_UART_NONE is not set +# CONFIG_ESP_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART=y +CONFIG_CONSOLE_UART_NUM=0 +CONFIG_CONSOLE_UART_BAUDRATE=115200 +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=300 +CONFIG_INT_WDT_CHECK_CPU1=y +CONFIG_TASK_WDT=y +CONFIG_ESP_TASK_WDT=y +# CONFIG_TASK_WDT_PANIC is not set +CONFIG_TASK_WDT_TIMEOUT_S=10 +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set +CONFIG_ESP32_DEBUG_OCDAWARE=y +# CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP32_WIFI_ENABLED=y +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP32_WIFI_TX_BA_WIN=6 +CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP32_WIFI_RX_BA_WIN=6 +CONFIG_ESP32_WIFI_NVS_ENABLED=y +CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y +CONFIG_WPA_MBEDTLS_CRYPTO=y +CONFIG_WPA_MBEDTLS_TLS_CLIENT=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# CONFIG_WPA_MBO_SUPPORT is not set +# CONFIG_WPA_DPP_SUPPORT is not set +# CONFIG_WPA_11R_SUPPORT is not set +# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_DEBUG_PRINT is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set +CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set +# CONFIG_HAL_ASSERTION_SILIENT is not set +# CONFIG_L2_TO_L3_COPY is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 +CONFIG_TCPIP_RECVMBOX_SIZE=32 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=12 +CONFIG_TCP_MSS=1440 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=5760 +CONFIG_TCP_WND_DEFAULT=5760 +CONFIG_TCP_RECVMBOX_SIZE=6 +CONFIG_TCP_QUEUE_OOSEQ=y +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_UDP_RECVMBOX_SIZE=6 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_PPP_SUPPORT is not set +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_HRT=y +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y +# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_HRT is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_ESP32_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +# CONFIG_ESP32_ULP_COPROC_ENABLED is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_SUPPORT_TERMIOS=y +CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# End of deprecated options diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..ee30f12 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,38 @@ +# Channel3 ESP32 - Default SDK Configuration + +# Use 240MHz CPU frequency for maximum performance +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y + +# Enable APLL for precise audio clock generation +CONFIG_ESP32_APLL_ENABLED=y + +# Increase task watchdog timeout for video generation +CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 + +# Place frequently called functions in IRAM +CONFIG_COMPILER_OPTIMIZATION_PERF=y + +# Enable WiFi +CONFIG_ESP_WIFI_ENABLED=y + +# WiFi Station Mode - connect to existing network +# Video starts AFTER WiFi connects to avoid cache conflict +CONFIG_WIFI_MODE_STATION=y +CONFIG_WIFI_STA_SSID="Super Exmodiar Lvl2.5" +CONFIG_WIFI_STA_PASS="Commodore" + +# I2S configuration +CONFIG_I2S_ISR_IRAM_SAFE=y + +# Console output +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 + +# Flash size (adjust as needed) +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Partition table +CONFIG_PARTITION_TABLE_SINGLE_APP=y + +# Disable brownout detector during RF operations +CONFIG_ESP_BROWNOUT_DET=n diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..a72160d --- /dev/null +++ b/tools/README.md @@ -0,0 +1,84 @@ +# Channel3 ESP32 Streaming Tools + +## stream_video.py + +Stream video files to the ESP32 Channel3 RF Broadcast over WiFi. + +### Requirements + +- Python 3.6+ +- NumPy (`pip install numpy`) +- FFmpeg installed and in PATH + +### Basic Usage + +```bash +ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py +``` + +### Examples + +**Stream a video file (with correct aspect ratio):** +```bash +ffmpeg -i myvideo.mp4 -vf "scale=293:220:force_original_aspect_ratio=decrease,pad=293:220:(ow-iw)/2:(oh-ih)/2,scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 +``` + +> **Note:** The TV has wide pixels (PAR ~2.53:1). We first scale to 293x220 (116×2.53=293) +> to preserve aspect ratio, then squash to 116x220 for the physical pixels. + +**Stream at a specific frame rate (e.g., 15 fps):** +```bash +ffmpeg -i myvideo.mp4 -vf "scale=116:220,fps=15" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 -f 15 +``` + +**Stream webcam (Windows):** +```bash +ffmpeg -f dshow -i video="Your Webcam Name" -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 +``` + +**Stream webcam (Linux):** +```bash +ffmpeg -f v4l2 -i /dev/video0 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 +``` + +**Stream desktop (Windows):** +```bash +ffmpeg -f gdigrab -i desktop -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 +``` + +**Stream desktop (Linux):** +```bash +ffmpeg -f x11grab -i :0.0 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 +``` + +### Options + +| Option | Description | +|--------|-------------| +| `-p, --port PORT` | TCP port (default: 5000) | +| `-f, --fps FPS` | Target frame rate (default: 30) | +| `--no-dither` | Disable Floyd-Steinberg dithering (faster but lower quality) | + +### How it Works + +1. FFmpeg decodes the video and scales it to 116x220 pixels, outputting raw RGB24 frames +2. The Python script reads each frame, applies Floyd-Steinberg dithering to reduce to 16 colors +3. Frames are packed as 4 bits per pixel (two pixels per byte) +4. Packed frames (12,760 bytes each) are sent to the ESP32 over TCP +5. The ESP32 displays each frame on the analog TV broadcast + +### Troubleshooting + +**"Connection refused"** +- Make sure the ESP32 is powered on and connected to WiFi +- Check that you're using the correct IP address +- The stream server runs on port 5000 by default + +**Low frame rate** +- Try `--no-dither` for faster processing +- Reduce target FPS with `-f 15` +- Make sure your WiFi connection is stable + +**Video looks stretched** +- Use the aspect ratio preservation ffmpeg filter shown above +- The display is 116x220 pixels with approximately 2.5:1 pixel aspect ratio diff --git a/tools/stream_lcars.bat b/tools/stream_lcars.bat new file mode 100644 index 0000000..4481859 --- /dev/null +++ b/tools/stream_lcars.bat @@ -0,0 +1,8 @@ +@echo off +REM Stream lcars.mp4 to ESP32 Channel3 +REM Make sure ffmpeg is in your PATH and numpy is installed (pip install numpy) +REM +REM The TV has a pixel aspect ratio of ~2.53:1 (wide pixels) +REM So we scale to 293x220 first (116*2.53=293), then squash to 116x220 + +ffmpeg -f lavfi -i color=black:293x220 -i "%~dp0lcars.mp4" -filter_complex "[1:v]scale=293:220:force_original_aspect_ratio=decrease[vid];[0:v][vid]overlay=(W-w)/2:(H-h)/2,scale=116:220" -map 0:a? -f rawvideo -pix_fmt rgb24 -shortest - | python "%~dp0stream_video.py" 10.0.0.59 -f 60 --grayscale diff --git a/tools/stream_video.py b/tools/stream_video.py new file mode 100644 index 0000000..2d72f8e --- /dev/null +++ b/tools/stream_video.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +""" +Stream video to ESP32 Channel3 RF Broadcast + +This script takes RGB24 frames from ffmpeg via stdin, dithers them to 16 colors, +packs them as 4bpp, and streams them to the ESP32 over TCP. + +Usage: + ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py + +Options: + -p, --port Port number (default: 5000) + -f, --fps Target frame rate (default: 30) + --no-dither Disable Floyd-Steinberg dithering (faster but lower quality) +""" + +import sys +import socket +import argparse +import time +import numpy as np + +# Image dimensions +WIDTH = 116 +HEIGHT = 220 +FRAME_SIZE_RGB = WIDTH * HEIGHT * 3 +FRAME_SIZE_4BPP = WIDTH * HEIGHT // 2 + +# CGA-like 16 color palette (RGB values) +PALETTE = np.array([ + [0, 0, 0], # 0: Black + [0, 0, 170], # 1: Blue + [0, 170, 0], # 2: Green + [0, 170, 170], # 3: Cyan + [170, 0, 0], # 4: Red + [170, 0, 170], # 5: Magenta + [170, 85, 0], # 6: Brown + [170, 170, 170], # 7: Light Gray + [85, 85, 85], # 8: Dark Gray + [85, 85, 255], # 9: Light Blue + [85, 255, 85], # 10: Light Green + [85, 255, 255], # 11: Light Cyan + [255, 85, 85], # 12: Light Red + [255, 85, 255], # 13: Light Magenta + [255, 255, 85], # 14: Yellow + [255, 255, 255], # 15: White +], dtype=np.float32) + +# Luminance values for each palette color (ITU-R BT.601) +# Y = 0.299*R + 0.587*G + 0.114*B +PALETTE_LUMINANCE = np.array([ + 0.299*0 + 0.587*0 + 0.114*0, # 0: Black = 0 + 0.299*0 + 0.587*0 + 0.114*170, # 1: Blue = 19.4 + 0.299*0 + 0.587*170 + 0.114*0, # 2: Green = 99.8 + 0.299*0 + 0.587*170 + 0.114*170, # 3: Cyan = 119.2 + 0.299*170 + 0.587*0 + 0.114*0, # 4: Red = 50.8 + 0.299*170 + 0.587*0 + 0.114*170, # 5: Magenta = 70.2 + 0.299*170 + 0.587*85 + 0.114*0, # 6: Brown = 100.7 + 0.299*170 + 0.587*170 + 0.114*170, # 7: Light Gray = 170 + 0.299*85 + 0.587*85 + 0.114*85, # 8: Dark Gray = 85 + 0.299*85 + 0.587*85 + 0.114*255, # 9: Light Blue = 104.4 + 0.299*85 + 0.587*255 + 0.114*85, # 10: Light Green = 185.3 + 0.299*85 + 0.587*255 + 0.114*255, # 11: Light Cyan = 204.6 + 0.299*255 + 0.587*85 + 0.114*85, # 12: Light Red = 135.9 + 0.299*255 + 0.587*85 + 0.114*255, # 13: Light Magenta = 155.2 + 0.299*255 + 0.587*255 + 0.114*85, # 14: Yellow = 235.6 + 0.299*255 + 0.587*255 + 0.114*255, # 15: White = 255 +], dtype=np.float32) + +# Palette indices sorted by luminance (darkest to brightest) +GRAYSCALE_ORDER = np.argsort(PALETTE_LUMINANCE) # [0,1,4,5,8,2,6,9,3,12,13,7,10,11,14,15] +SORTED_LUMINANCE = PALETTE_LUMINANCE[GRAYSCALE_ORDER] + + +def find_nearest_grayscale_fast(img): + """ + Convert RGB image to grayscale and map to palette by luminance. + This gives 16 distinct gray levels on a B&W TV. + """ + # Convert to grayscale using luminance formula + gray = 0.299 * img[:,:,0] + 0.587 * img[:,:,1] + 0.114 * img[:,:,2] + + # Find nearest luminance level + gray_expanded = gray[:, :, np.newaxis] # (H, W, 1) + lum_expanded = SORTED_LUMINANCE[np.newaxis, np.newaxis, :] # (1, 1, 16) + distances = np.abs(gray_expanded - lum_expanded) + nearest_idx = np.argmin(distances, axis=2) + + # Map back to actual palette index + return GRAYSCALE_ORDER[nearest_idx].astype(np.uint8) + + +def find_nearest_colors_fast(img): + """ + Find nearest palette color for each pixel using vectorized operations. + + Args: + img: numpy array of shape (H, W, 3) with RGB values (float32) + + Returns: + numpy array of shape (H, W) with palette indices 0-15 + """ + # Reshape for broadcasting: (H, W, 3) -> (H, W, 1, 3) + img_expanded = img[:, :, np.newaxis, :] + # PALETTE shape: (16, 3) -> (1, 1, 16, 3) + palette_expanded = PALETTE[np.newaxis, np.newaxis, :, :] + + # Calculate squared distances to all palette colors + # Result shape: (H, W, 16) + distances = np.sum((img_expanded - palette_expanded) ** 2, axis=3) + + # Find index of minimum distance for each pixel + return np.argmin(distances, axis=2).astype(np.uint8) + + +def dither_frame_fast(frame): + """ + Convert RGB frame to 16-color indexed using optimized Floyd-Steinberg dithering. + Uses vectorized row operations for better performance. + + Args: + frame: numpy array of shape (HEIGHT, WIDTH, 3) with RGB values + + Returns: + numpy array of shape (HEIGHT, WIDTH) with palette indices 0-15 + """ + img = frame.astype(np.float32) + output = np.zeros((HEIGHT, WIDTH), dtype=np.uint8) + + for y in range(HEIGHT): + # Process entire row at once for color matching + row = np.clip(img[y], 0, 255) + + # Find nearest colors for entire row + row_expanded = row[:, np.newaxis, :] # (W, 1, 3) + palette_expanded = PALETTE[np.newaxis, :, :] # (1, 16, 3) + distances = np.sum((row_expanded - palette_expanded) ** 2, axis=2) # (W, 16) + indices = np.argmin(distances, axis=1).astype(np.uint8) + output[y] = indices + + # Calculate errors for entire row + chosen_colors = PALETTE[indices] # (W, 3) + errors = row - chosen_colors # (W, 3) + + # Distribute errors (Floyd-Steinberg) + # Right pixel: 7/16 + if y < HEIGHT: + img[y, 1:, :] += errors[:-1, :] * (7.0 / 16.0) + + # Next row + if y + 1 < HEIGHT: + # Bottom-left: 3/16 + img[y + 1, :-1, :] += errors[1:, :] * (3.0 / 16.0) + # Bottom: 5/16 + img[y + 1, :, :] += errors * (5.0 / 16.0) + # Bottom-right: 1/16 + img[y + 1, 1:, :] += errors[:-1, :] * (1.0 / 16.0) + + return output + + +def dither_frame_none(frame): + """ + Convert RGB frame to 16-color indexed without dithering (fastest). + + Args: + frame: numpy array of shape (HEIGHT, WIDTH, 3) with RGB values + + Returns: + numpy array of shape (HEIGHT, WIDTH) with palette indices 0-15 + """ + return find_nearest_colors_fast(frame.astype(np.float32)) + + +def pack_4bpp_fast(indexed_frame): + """ + Pack indexed frame (0-15 values) into 4bpp format using vectorized operations. + Two pixels per byte: high nibble = first pixel, low nibble = second pixel. + + Args: + indexed_frame: numpy array of shape (HEIGHT, WIDTH) with values 0-15 + + Returns: + bytes object of length FRAME_SIZE_4BPP + """ + flat = indexed_frame.flatten() + # Take pairs of pixels and pack them + high_nibbles = flat[0::2].astype(np.uint8) << 4 + low_nibbles = flat[1::2].astype(np.uint8) + packed = high_nibbles | low_nibbles + return packed.tobytes() + + +def main(): + parser = argparse.ArgumentParser( + description='Stream video to ESP32 Channel3 RF Broadcast', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + Basic usage (with PAR correction for wide TV pixels): + ffmpeg -f lavfi -i color=black:293x220 -i video.mp4 -filter_complex "[1:v]scale=293:220:force_original_aspect_ratio=decrease[vid];[0:v][vid]overlay=(W-w)/2:(H-h)/2,scale=116:220" -f rawvideo -pix_fmt rgb24 -shortest - | python stream_video.py 192.168.1.100 + + Stream webcam: + ffmpeg -f dshow -i video="Your Webcam" -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 + + Fast mode (no dithering): + ffmpeg -i video.mp4 -vf "scale=116:220" -f rawvideo -pix_fmt rgb24 - | python stream_video.py 192.168.1.100 --no-dither -f 60 +""" + ) + parser.add_argument('host', help='ESP32 IP address') + parser.add_argument('-p', '--port', type=int, default=5000, help='Port number (default: 5000)') + parser.add_argument('-f', '--fps', type=float, default=30, help='Target frame rate (default: 30)') + parser.add_argument('--no-dither', action='store_true', help='Disable dithering (faster)') + parser.add_argument('--grayscale', '--bw', action='store_true', help='Grayscale mode for B&W TVs (16 distinct gray levels)') + + args = parser.parse_args() + + # Calculate frame timing + frame_interval = 1.0 / args.fps + + print(f"Connecting to {args.host}:{args.port}...") + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle's algorithm + sock.connect((args.host, args.port)) + print(f"Connected! Streaming at {args.fps} fps target...") + print("Press Ctrl+C to stop") + except Exception as e: + print(f"Connection failed: {e}") + sys.exit(1) + + frame_count = 0 + start_time = time.time() + + # Select processing function based on mode + if args.grayscale: + # Grayscale mode: map by luminance for 16 distinct gray levels on B&W TV + dither_func = lambda f: find_nearest_grayscale_fast(f.astype(np.float32)) + print("Grayscale mode: mapping to 16 luminance levels") + elif args.no_dither: + dither_func = dither_frame_none + else: + dither_func = dither_frame_fast + + try: + while True: + frame_start = time.time() + + # Read one RGB24 frame from stdin + raw_data = sys.stdin.buffer.read(FRAME_SIZE_RGB) + if len(raw_data) < FRAME_SIZE_RGB: + print(f"\nEnd of stream after {frame_count} frames") + break + + # Convert to numpy array + frame = np.frombuffer(raw_data, dtype=np.uint8).reshape((HEIGHT, WIDTH, 3)) + + # Dither to 16 colors + indexed = dither_func(frame) + + # Pack to 4bpp + packed = pack_4bpp_fast(indexed) + + # Send to ESP32 + try: + sock.sendall(packed) + except Exception as e: + print(f"\nSend error: {e}") + break + + frame_count += 1 + + # Frame rate limiting + elapsed = time.time() - frame_start + if elapsed < frame_interval: + time.sleep(frame_interval - elapsed) + + # Progress indicator + if frame_count % 30 == 0: + actual_fps = frame_count / (time.time() - start_time) + print(f"\rFrames: {frame_count}, FPS: {actual_fps:.1f} ", end='', flush=True) + + except KeyboardInterrupt: + print(f"\nStopped after {frame_count} frames") + + finally: + sock.close() + elapsed = time.time() - start_time + if elapsed > 0: + print(f"Average FPS: {frame_count / elapsed:.1f}") + + +if __name__ == '__main__': + main()