Browse Source

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 <noreply@anthropic.com>
master
melancholytron 2 weeks ago
commit
0f692012a5
  1. 25
      .gitignore
  2. 12
      CMakeLists.txt
  3. 121
      README.md
  4. 10
      build.bat
  5. 43
      build.ps1
  6. 11
      build_helper.ps1
  7. 10
      build_only.ps1
  8. 7
      components/tablemaker/CMakeLists.txt
  9. 55
      components/tablemaker/CbTable.c
  10. 41
      components/tablemaker/CbTable.h
  11. 66
      components/tablemaker/broadcast_tables.c
  12. 32
      components/tablemaker/broadcast_tables.h
  13. 26
      flash.ps1
  14. 10
      flash_only.ps1
  15. 602
      main/3d.c
  16. 76
      main/3d.h
  17. 21
      main/CMakeLists.txt
  18. 63
      main/Kconfig.projbuild
  19. 4272
      main/user_main.c
  20. 497
      main/video_broadcast.c
  21. 70
      main/video_broadcast.h
  22. 10
      monitor.ps1
  23. 5
      partitions.csv
  24. 26
      rebuild.ps1
  25. 19
      run_build.bat
  26. 5
      run_build.cmd
  27. 2231
      sdkconfig
  28. 38
      sdkconfig.defaults
  29. 84
      tools/README.md
  30. 8
      tools/stream_lcars.bat
  31. 295
      tools/stream_video.py

25
.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

12
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)

121
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.

10
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

43
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

11
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

10
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

7
components/tablemaker/CMakeLists.txt

@ -0,0 +1,7 @@
idf_component_register(
SRCS
"broadcast_tables.c"
"CbTable.c"
INCLUDE_DIRS
"."
)

55
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,
};

41
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 <stdint.h>
#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

66
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,
};

32
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 <stdint.h>
#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

26
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

10
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

602
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 <string.h>
#include <stdio.h>
// 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;
}

76
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 <stdint.h>
#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

21
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
)

63
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

4272
main/user_main.c
File diff suppressed because it is too large
View File

497
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 <string.h>
#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);
}
}

70
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 <stdint.h>
#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

10
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

5
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,

26
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

19
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%

5
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

2231
sdkconfig
File diff suppressed because it is too large
View File

38
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

84
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 <ESP32_IP>
```
### 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

8
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

295
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 <ESP32_IP>
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()
Loading…
Cancel
Save