diff --git a/.gitignore b/.gitignore index 1861224..45107d4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,36 @@ .claude/settings.backup .claude/current_mode .claude/agents/ + +# PlatformIO +.pio/ +.vscode/ +.pioenvs/ +.piolibdeps/ + +# Build artifacts +*.bin +*.elf +*.map + +# Python +__pycache__/ +*.pyc +*.pyo +.Python + +# IDE +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Tool state +.happy/ + +# Arduino CLI (legacy) +bin/arduino-cli diff --git a/CLAUDE.md b/CLAUDE.md index 50c31eb..e051ae1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,9 @@ ESP32‑S3 Matrix — Fast Path for New Games + Debug -This repo lets anyone build a new LED‑matrix game quickly, visualize it in a terminal, and keep hardware + tools in sync. Follow this flow and you’re productive on day one. +This repo lets anyone build a new LED‑matrix game quickly, visualize it in a terminal, and keep hardware + tools in sync. Follow this flow and you're productive on day one. 1) Configure The Board Once -- Edit `config/BoardConfig.h` (single source of truth): +- Edit `lib/BoardConfig/BoardConfig.h` (single source of truth): - `MATRIX_WIDTH/HEIGHT` (default 8x8) - `LED_PIN` (default 14) and `BRIGHTNESS_LIMIT` (≤ 60) - `COLOR_ORDER` (use `RGB` for Waveshare ESP32‑S3‑Matrix) @@ -11,9 +11,12 @@ This repo lets anyone build a new LED‑matrix game quickly, visualize it in a t - All games include this file; change it once and everything follows. 2) Use The Shared Helpers -- Include in your sketch: - - `#include "config/BoardConfig.h"` - - `#include "lib/MatrixUtil/MatrixUtil.h"` +- Include in your sketch (`src/main.cpp`): + ```cpp + #include + #include + #include + ``` - What you get: - `MU_XY(x,y)`: stable XY→index mapping honoring the board profile - `MU_ADD_LEDS(DATA_PIN, leds, count)`: FastLED init using shared `COLOR_ORDER` @@ -23,9 +26,10 @@ This repo lets anyone build a new LED‑matrix game quickly, visualize it in a t Minimal New‑Game Template ```cpp +// src/main.cpp #include -#include "config/BoardConfig.h" -#include "lib/MatrixUtil/MatrixUtil.h" +#include +#include #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) CRGB leds[NUM_LEDS]; @@ -57,36 +61,129 @@ void loop() { - Auto‑detect port, auto‑configure from META: - `python3 tools/led_matrix_viz.py --list-ports` - `python3 tools/led_matrix_viz.py -p /dev/ttyACM? -b 115200 --stats --verbose` -- If you don’t print META, pass flags: `--width/--height --input-order xy --wiring progressive --rotate ...` +- If you don't print META, pass flags: `--width/--height --input-order xy --wiring progressive --rotate ...` - Tips: `--ascii` for plain text, `--flip-x/--flip-y` for quick checks. -4) Build / Flash (ESP32‑S3) **IMPORTANT MAKE SURE TO FOLLOW** -- use USB CDC On Boot = Enabled (prevents Serial from blocking) if you need to debug Serial. -- Use the bundled CLI at `./bin/arduino-cli`: - - First‑time setup (once): - - `./bin/arduino-cli config init` - - `./bin/arduino-cli core update-index` - - Prepare the dependency for esp32 - - `./bin/arduino-cli config set network.connection_timeout 1000s` Increase the timeout for the big download, WARN USER this gonna take some time, possible (10-20min), so need to be patient. - - `GODEBUG=http2client=0 ./bin/arduino-cli --log-level debug core install esp32:esp32` (Force HTTP/1.1 bypasses the flaky HTTP/2 path) - - Wait for the download to finish, dont put it background, do this step linearly. - - Compile (example: Snake): - - `./bin/arduino-cli compile --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc examples/Snake` - - Upload: - - **IMPORTANT find the correct port by checking lsusb first** - - `./bin/arduino-cli upload --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc --port /dev/ttyACM0 examples/Snake` - - Monitor: - - `./bin/arduino-cli monitor --port /dev/ttyACM0 --config baudrate=115200` - - NOTE : you should not suggest user to use Arduino IDE to upload, they cli tool given to you should be very suffcient. +4) Build / Flash (ESP32‑S3) **PLATFORMIO** + +## Installing PlatformIO (CM5/Raspberry Pi) + +**Recommended: Use `uv` (fast & clean)** +```bash +# Install uv (modern Python package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh +source ~/.bashrc # Or restart terminal + +# Install PlatformIO +uv tool install platformio + +# Fix missing pip in uv environment (required for esptoolpy) +~/.local/share/uv/tools/platformio/bin/python -m ensurepip + +# Update PATH and verify +export PATH="/home/distiller/.local/bin:$PATH" +pio --version +``` + +**Alternative: Quick install (works immediately)** +```bash +pip3 install platformio --break-system-packages +``` + +**Why uv?** 10-100x faster than pip, handles PATH automatically, keeps Python environment clean. It's the modern standard for installing Python CLI tools. + +## Create New Game +```bash +# Create project structure +mkdir -p examples/MyGame/src +cd examples/MyGame +``` + +**Create platformio.ini:** +```ini +[env:waveshare-esp32s3-matrix] +platform = espressif32 +board = adafruit_feather_esp32s3 # 4MB flash (matches Waveshare hardware) +framework = arduino +lib_deps = + fastled/FastLED@^3.9.20 +lib_extra_dirs = ../../lib # Shared libs at repo root +monitor_speed = 115200 +upload_speed = 921600 +``` + +**Create src/main.cpp** - copy template above + +## Build & Upload +```bash +# Build (first time downloads ~1.5GB ESP32 toolchain, 5-10 min) +pio run + +# Upload (auto-detects port) +pio run -t upload + +# Upload to specific port +pio run -t upload --upload-port /dev/ttyACM0 + +# Clean +pio run -t clean +``` + +## Upload Troubleshooting + +### Normal Case (No Buttons Needed) +```bash +pio run -t upload # Usually just works +``` + +You should see connecting dots then progress bars: +``` +Connecting.... +Writing at 0x00010000... (10 %) +Writing at 0x00020000... (20 %) +``` +If this happens, you're done! No buttons needed. + +### If Upload Fails ("Connecting..." Forever) + +If you see endless dots with no progress: +``` +Connecting........._____....._____ +``` + +**Manual button sequence:** +1. Hold **BOOT** button +2. While holding BOOT, press **RESET** button once +3. Release both buttons +4. Run `pio run -t upload` again within 10 seconds + +**Still failing after button sequence?** +- Check USB cable is plugged in firmly +- Confirm device detected: `lsusb | grep -i esp` should show "Espressif USB JTAG/serial debug unit" +- Try different USB port on your computer +- Check cable quality (some cables are power-only, no data) + +### If Code Crashes or Acts Weird +- Press **RESET** button once (no BOOT needed) +- This restarts your code from the beginning + +### Quick Reference + +| Issue | Solution | +|-------|----------| +| Upload times out with dots | BOOT+RESET sequence → retry upload | +| Code frozen/not responding | Press RESET only | +| "Couldn't find board" | Check `lsusb`, check cable, try button sequence | +| First upload on new board | Often needs manual button sequence | 5) Proven Debug Workflow - Keep Serial optional: short wait, then guard prints with `if (Serial)`. - Use `MU_DrawCalibration(leds)` once to prove mapping (TL=G, TR=R, BL=B, BR=W). -- If colors are wrong on hardware, fix `COLOR_ORDER` in `BoardConfig.h` (Waveshare = `RGB`). +- If colors are wrong on hardware, fix `COLOR_ORDER` in `lib/BoardConfig/BoardConfig.h` (Waveshare = `RGB`). - If left/right swap on alternating rows, set `PANEL_WIRING_SERPENTINE` to `0` (progressive row‑major). 6) Quick Commands -- List ports: `python3 tools/led_matrix_viz.py --list-ports` +- List ports: `python3 tools/led_matrix_viz.py --list-ports` or `lsusb | grep -i esp` - Visualize: `python3 tools/led_matrix_viz.py -p /dev/ttyACM0 -b 115200 --stats` - Raw monitor: `python3 tools/monitor_pong.py -p /dev/ttyACM0` - Demo (no hardware): `python3 tools/led_matrix_viz.py --demo --width 8 --height 8` @@ -95,14 +192,75 @@ void loop() { - Start from the template; draw only via `MU_XY()`. - Never hardcode edges; use `MATRIX_WIDTH/HEIGHT`. - Limit debug frame rate (≈5–20 FPS) to keep serial stable. -- Update only `BoardConfig.h` for new panels/orientation; all games + tools follow. +- Update only `lib/BoardConfig/BoardConfig.h` for new panels/orientation; all games + tools follow. +- **Shared libs:** BoardConfig and MatrixUtil are at repo root (`lib/`), included as `` and `` + +## AGENT AUTOMATION INSTRUCTIONS + +**Agents MUST automate the complete workflow. Never ask users to run commands manually.** + +### Critical Rules +1. **PATH**: Always prefix pio with `export PATH="/home/distiller/.local/bin:$PATH" &&` +2. **Directory**: cd into project directory before running pio commands +3. **Timeouts**: 120000ms for builds (600000ms first time), check for `[SUCCESS]` in output +4. **Auto-upload**: Always run `pio run -t upload` after building + +### Standard Workflow +```bash +# User: "create X animation" +# Agent automatically does: +mkdir -p examples/X/src +# Write platformio.ini and src/main.cpp +cd /home/distiller/projects/esp32-agent-example/examples/X +export PATH="/home/distiller/.local/bin:$PATH" && pio run +lsusb | grep -i esp # Verify ESP32 (should show "Feather ESP32-S3" or "Espressif") +export PATH="/home/distiller/.local/bin:$PATH" && pio run -t upload +# Report: "✅ X uploaded successfully!" +``` + +### Upload Failure Recovery +If upload shows endless "Connecting..." dots: +1. Tell user: "Please press BOOT+RESET: Hold BOOT, press RESET once, release both" +2. Wait 15 seconds +3. Auto-retry: `export PATH="/home/distiller/.local/bin:$PATH" && pio run -t upload` + +### Quick Checks +- ✅ Success: Look for `Chip is ESP32-S3`, `Hash of data verified`, `Hard resetting via RTS pin` +- ❌ Wrong device: lsusb shows "MicroPython" or "Pico" → ask user to replug ESP32 USB +- ❌ PATH error: `pio: command not found` → missing PATH export Repo Highlights -- `config/BoardConfig.h` — board profile (geometry, color order, wiring/orientation, brightness) -- `lib/MatrixUtil/MatrixUtil.h` — mapping + serial frame helpers +- `lib/BoardConfig/` — board profile (geometry, color order, wiring/orientation, brightness) **SINGLE SOURCE OF TRUTH** +- `lib/MatrixUtil/` — mapping + serial frame helpers - `tools/led_matrix_viz.py` — terminal visualizer (reads META to auto‑configure) -- `examples/` — reference sketches (Snake, tilt‑demo, wifi‑slam) +- `examples/RotatingDonut/` — reference PlatformIO project + +DEBUGGING ISSUES +- **Flash size mismatch:** If you see "Detected size(4096k) smaller than (8192k)" on boot, use `board = adafruit_feather_esp32s3` in platformio.ini (NOT esp32-s3-devkitc-1) +- **Upload issues:** See "Upload Troubleshooting" section above for button sequences +- **Wrong device detected:** If `lsusb` shows "MicroPython" or "Pico" instead of "Espressif", that's the internal CM5 system. Replug the ESP32's USB cable. +- **Include errors:** Use `#include ` and `#include ` (angle brackets, not quotes or paths) +- **Shared libs not found:** Add `lib_extra_dirs = ../../lib` to platformio.ini -DEBUGING ISSUES -- when automatic upload fails, always ask for user to manual put device into bootloader mode, then wait for confirm before reflush again -- MicroPython Board in FS mode is a pico device (which belong to the internal system), you should see ESP devie when you try lsusb, if not remind user to try replug in usb +Project Structure +``` +esp32-agent-example/ +├── lib/ # SHARED LIBS (all games use these) +│ ├── BoardConfig/ +│ │ ├── BoardConfig.h # Single source of truth +│ │ └── library.json +│ └── MatrixUtil/ +│ ├── MatrixUtil.h +│ └── library.json +├── examples/ +│ ├── RotatingDonut/ # Example PlatformIO game +│ │ ├── platformio.ini +│ │ └── src/ +│ │ └── main.cpp +│ └── MyGame/ # Your new game +│ ├── platformio.ini # Copy from RotatingDonut +│ └── src/ +│ └── main.cpp # Your code here +└── tools/ + └── led_matrix_viz.py # Terminal visualizer +``` diff --git a/bin/arduino-cli b/bin/arduino-cli deleted file mode 100755 index 6d25a27..0000000 Binary files a/bin/arduino-cli and /dev/null differ diff --git a/examples/FallingStars/platformio.ini b/examples/FallingStars/platformio.ini new file mode 100644 index 0000000..9a33061 --- /dev/null +++ b/examples/FallingStars/platformio.ini @@ -0,0 +1,9 @@ +[env:waveshare-esp32s3-matrix] +platform = espressif32 +board = adafruit_feather_esp32s3 +framework = arduino +lib_deps = + fastled/FastLED@^3.9.20 +lib_extra_dirs = ../../lib +monitor_speed = 115200 +upload_speed = 921600 diff --git a/examples/FallingStars/src/main.cpp b/examples/FallingStars/src/main.cpp new file mode 100644 index 0000000..e85fb80 --- /dev/null +++ b/examples/FallingStars/src/main.cpp @@ -0,0 +1,122 @@ +// Falling Stars - Colorful particles with gravity and collision +#include +#include +#include + +#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) +#define MAX_PARTICLES 12 + +CRGB leds[NUM_LEDS]; + +struct Particle { + float x, y; + float vy; + CRGB color; + bool active; +}; + +Particle particles[MAX_PARTICLES]; +uint8_t grid[MATRIX_WIDTH][MATRIX_HEIGHT]; + +void setup() { + Serial.begin(115200); + unsigned long t0 = millis(); + while (!Serial && millis() - t0 < 1500) { delay(10); } + if (Serial) MU_PrintMeta(); + + MU_ADD_LEDS(LED_PIN, leds, NUM_LEDS); + FastLED.setBrightness(BRIGHTNESS_LIMIT); + FastLED.clear(); FastLED.show(); + + // Initialize particles + for (int i = 0; i < MAX_PARTICLES; i++) { + particles[i].active = false; + } + + // Clear grid + for (int x = 0; x < MATRIX_WIDTH; x++) { + for (int y = 0; y < MATRIX_HEIGHT; y++) { + grid[x][y] = 0; + } + } +} + +void spawnParticle() { + for (int i = 0; i < MAX_PARTICLES; i++) { + if (!particles[i].active) { + particles[i].x = random(0, MATRIX_WIDTH); + particles[i].y = 0; + particles[i].vy = 0; + particles[i].active = true; + + // Random vibrant colors + uint8_t colorChoice = random(0, 6); + switch(colorChoice) { + case 0: particles[i].color = CRGB(255, 0, 0); break; // Red + case 1: particles[i].color = CRGB(0, 255, 0); break; // Green + case 2: particles[i].color = CRGB(0, 0, 255); break; // Blue + case 3: particles[i].color = CRGB(255, 255, 0); break; // Yellow + case 4: particles[i].color = CRGB(255, 0, 255); break; // Magenta + case 5: particles[i].color = CRGB(0, 255, 255); break; // Cyan + } + break; + } + } +} + +void loop() { + // Spawn new particle randomly + if (random(0, 100) < 30) { + spawnParticle(); + } + + // Update physics + for (int i = 0; i < MAX_PARTICLES; i++) { + if (particles[i].active) { + particles[i].vy += 0.3; // Gravity + particles[i].y += particles[i].vy; + + int ix = (int)particles[i].x; + int iy = (int)particles[i].y; + + // Check collision with bottom or other particles + if (iy >= MATRIX_HEIGHT - 1) { + grid[ix][MATRIX_HEIGHT - 1] = i + 1; + particles[i].active = false; + } else if (iy >= 0 && iy < MATRIX_HEIGHT - 1) { + if (grid[ix][iy + 1] != 0) { + grid[ix][iy] = i + 1; + particles[i].active = false; + } + } + } + } + + // Render + FastLED.clear(); + + // Draw settled particles + for (int x = 0; x < MATRIX_WIDTH; x++) { + for (int y = 0; y < MATRIX_HEIGHT; y++) { + if (grid[x][y] != 0) { + uint8_t idx = grid[x][y] - 1; + leds[MU_XY(x, y)] = particles[idx].color; + } + } + } + + // Draw falling particles + for (int i = 0; i < MAX_PARTICLES; i++) { + if (particles[i].active) { + int ix = (int)particles[i].x; + int iy = (int)particles[i].y; + if (ix >= 0 && ix < MATRIX_WIDTH && iy >= 0 && iy < MATRIX_HEIGHT) { + leds[MU_XY(ix, iy)] = particles[i].color; + } + } + } + + FastLED.show(); + if (Serial) MU_SendFrameCSV(leds); + delay(50); +} diff --git a/examples/RotatingDonut/.gitignore b/examples/RotatingDonut/.gitignore new file mode 100644 index 0000000..03f4a3c --- /dev/null +++ b/examples/RotatingDonut/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/examples/RotatingDonut/RotatingDonut.ino b/examples/RotatingDonut/RotatingDonut.ino new file mode 100644 index 0000000..e0b1303 --- /dev/null +++ b/examples/RotatingDonut/RotatingDonut.ino @@ -0,0 +1,112 @@ +#include +#include "../config/BoardConfig.h" +#include "../lib/MatrixUtil/MatrixUtil.h" + +#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) +CRGB leds[NUM_LEDS]; + +// 3D donut parameters +float angleA = 0, angleB = 0; +const float R1 = 0.6; // Smaller tube = bigger hole +const float R2 = 2.2; // Overall donut radius +const float K1 = 6; // Screen distance (zoomed in) +const float K2 = 5; // Viewer distance + +// Z-buffer for depth +float zBuffer[NUM_LEDS]; + +void setup() { + MU_ADD_LEDS(LED_PIN, leds, NUM_LEDS); + FastLED.setBrightness(BRIGHTNESS_LIMIT); + FastLED.clear(); + FastLED.show(); +} + +void renderDonut() { + // Clear screen and z-buffer + FastLED.clear(); + for (int i = 0; i < NUM_LEDS; i++) { + zBuffer[i] = 0; + } + + // Precompute sines and cosines + float cosA = cos(angleA), sinA = sin(angleA); + float cosB = cos(angleB), sinB = sin(angleB); + + // Generate donut points + for (float theta = 0; theta < 2 * PI; theta += 0.3) { + float cosTheta = cos(theta), sinTheta = sin(theta); + + for (float phi = 0; phi < 2 * PI; phi += 0.2) { + float cosPhi = cos(phi), sinPhi = sin(phi); + + // 3D coordinates of the point on the torus + float circleX = R2 + R1 * cosTheta; + float circleY = R1 * sinTheta; + + // Rotate around Y axis (angleB) and X axis (angleA) + float x = circleX * (cosB * cosPhi + sinA * sinB * sinPhi) - circleY * cosA * sinB; + float y = circleX * (sinB * cosPhi - sinA * cosB * sinPhi) + circleY * cosA * cosB; + float z = K2 + cosA * circleX * sinPhi + circleY * sinA; + float ooz = 1 / z; // One over z + + // Project to 2D + int xp = (int)(MATRIX_WIDTH / 2 + K1 * ooz * x); + int yp = (int)(MATRIX_HEIGHT / 2 - K1 * ooz * y); + + // Check bounds + if (xp >= 0 && xp < MATRIX_WIDTH && yp >= 0 && yp < MATRIX_HEIGHT) { + int pixelIndex = MU_XY(xp, yp); + + // Only render if this point is closer than what's already there + if (ooz > zBuffer[pixelIndex]) { + zBuffer[pixelIndex] = ooz; + + // Calculate lighting based on surface normal + float L = cosPhi * cosTheta * sinB - cosA * cosTheta * sinPhi - sinA * sinTheta + + cosB * (cosA * sinTheta - cosTheta * sinA * sinPhi); + + if (L > 0) { + // Simple grayscale depth - far = dark grey, close = white + // This gives pure depth visualization + + // Normalize depth + float depth = (ooz - 0.1) * 7; + depth = max(0.0f, min(1.0f, depth)); + + // Apply lighting (surface angle affects brightness) + float lit = L * 0.7 + 0.3; // Never fully dark from lighting + + // Combine depth and lighting + float brightness = depth * lit; + + // Map to grayscale: dark grey (far) to white (close) + // Range from 15 (nearly black) to 255 (white) + uint8_t intensity = 15 + (uint8_t)(brightness * 240); + + // Pure grayscale - all RGB channels the same + leds[pixelIndex] = CRGB(intensity, intensity, intensity); + + // Optional: Add slight blue tint for style + // leds[pixelIndex] = CRGB( + // intensity * 0.9, // Slightly less red + // intensity * 0.95, // Slightly less green + // intensity // Full blue for cool tint + // ); + } + } + } + } + } +} + +void loop() { + renderDonut(); + FastLED.show(); + + // Rotate the donut + angleA += 0.07; + angleB += 0.13; + + delay(20); // 50 FPS +} \ No newline at end of file diff --git a/examples/RotatingDonut/include/README b/examples/RotatingDonut/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/examples/RotatingDonut/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/examples/RotatingDonut/platformio.ini b/examples/RotatingDonut/platformio.ini new file mode 100644 index 0000000..4cbd8e0 --- /dev/null +++ b/examples/RotatingDonut/platformio.ini @@ -0,0 +1,19 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:waveshare-esp32s3-matrix] +platform = espressif32 +board = adafruit_feather_esp32s3 +framework = arduino +lib_deps = + fastled/FastLED@^3.9.20 +lib_extra_dirs = ../../lib +monitor_speed = 115200 +upload_speed = 921600 diff --git a/examples/RotatingDonut/src/main.cpp b/examples/RotatingDonut/src/main.cpp new file mode 100644 index 0000000..b6f5141 --- /dev/null +++ b/examples/RotatingDonut/src/main.cpp @@ -0,0 +1,112 @@ +#include +#include +#include + +#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) +CRGB leds[NUM_LEDS]; + +// 3D donut parameters +float angleA = 0, angleB = 0; +const float R1 = 0.6; // Smaller tube = bigger hole +const float R2 = 2.2; // Overall donut radius +const float K1 = 6; // Screen distance (zoomed in) +const float K2 = 5; // Viewer distance + +// Z-buffer for depth +float zBuffer[NUM_LEDS]; + +void setup() { + MU_ADD_LEDS(LED_PIN, leds, NUM_LEDS); + FastLED.setBrightness(BRIGHTNESS_LIMIT); + FastLED.clear(); + FastLED.show(); +} + +void renderDonut() { + // Clear screen and z-buffer + FastLED.clear(); + for (int i = 0; i < NUM_LEDS; i++) { + zBuffer[i] = 0; + } + + // Precompute sines and cosines + float cosA = cos(angleA), sinA = sin(angleA); + float cosB = cos(angleB), sinB = sin(angleB); + + // Generate donut points + for (float theta = 0; theta < 2 * PI; theta += 0.3) { + float cosTheta = cos(theta), sinTheta = sin(theta); + + for (float phi = 0; phi < 2 * PI; phi += 0.2) { + float cosPhi = cos(phi), sinPhi = sin(phi); + + // 3D coordinates of the point on the torus + float circleX = R2 + R1 * cosTheta; + float circleY = R1 * sinTheta; + + // Rotate around Y axis (angleB) and X axis (angleA) + float x = circleX * (cosB * cosPhi + sinA * sinB * sinPhi) - circleY * cosA * sinB; + float y = circleX * (sinB * cosPhi - sinA * cosB * sinPhi) + circleY * cosA * cosB; + float z = K2 + cosA * circleX * sinPhi + circleY * sinA; + float ooz = 1 / z; // One over z + + // Project to 2D + int xp = (int)(MATRIX_WIDTH / 2 + K1 * ooz * x); + int yp = (int)(MATRIX_HEIGHT / 2 - K1 * ooz * y); + + // Check bounds + if (xp >= 0 && xp < MATRIX_WIDTH && yp >= 0 && yp < MATRIX_HEIGHT) { + int pixelIndex = MU_XY(xp, yp); + + // Only render if this point is closer than what's already there + if (ooz > zBuffer[pixelIndex]) { + zBuffer[pixelIndex] = ooz; + + // Calculate lighting based on surface normal + float L = cosPhi * cosTheta * sinB - cosA * cosTheta * sinPhi - sinA * sinTheta + + cosB * (cosA * sinTheta - cosTheta * sinA * sinPhi); + + if (L > 0) { + // Simple grayscale depth - far = dark grey, close = white + // This gives pure depth visualization + + // Normalize depth + float depth = (ooz - 0.1) * 7; + depth = max(0.0f, min(1.0f, depth)); + + // Apply lighting (surface angle affects brightness) + float lit = L * 0.7 + 0.3; // Never fully dark from lighting + + // Combine depth and lighting + float brightness = depth * lit; + + // Map to grayscale: dark grey (far) to white (close) + // Range from 15 (nearly black) to 255 (white) + uint8_t intensity = 15 + (uint8_t)(brightness * 240); + + // Pure grayscale - all RGB channels the same + leds[pixelIndex] = CRGB(intensity, intensity, intensity); + + // Optional: Add slight blue tint for style + // leds[pixelIndex] = CRGB( + // intensity * 0.9, // Slightly less red + // intensity * 0.95, // Slightly less green + // intensity // Full blue for cool tint + // ); + } + } + } + } + } +} + +void loop() { + renderDonut(); + FastLED.show(); + + // Rotate the donut + angleA += 0.07; + angleB += 0.13; + + delay(20); // 50 FPS +} \ No newline at end of file diff --git a/examples/RotatingDonut/test/README b/examples/RotatingDonut/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/examples/RotatingDonut/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/config/BoardConfig.h b/lib/BoardConfig/BoardConfig.h similarity index 100% rename from config/BoardConfig.h rename to lib/BoardConfig/BoardConfig.h diff --git a/lib/BoardConfig/library.json b/lib/BoardConfig/library.json new file mode 100644 index 0000000..4c55b9e --- /dev/null +++ b/lib/BoardConfig/library.json @@ -0,0 +1,11 @@ +{ + "name": "BoardConfig", + "version": "1.0.0", + "description": "ESP32-S3 Matrix board configuration - single source of truth for all games", + "keywords": "esp32, led, matrix, config", + "dependencies": { + "fastled/FastLED": "^3.9.20" + }, + "frameworks": "arduino", + "platforms": "espressif32" +} diff --git a/lib/MatrixUtil/library.json b/lib/MatrixUtil/library.json new file mode 100644 index 0000000..dac228f --- /dev/null +++ b/lib/MatrixUtil/library.json @@ -0,0 +1,12 @@ +{ + "name": "MatrixUtil", + "version": "1.0.0", + "description": "LED matrix utilities - XY mapping, calibration, serial output for ESP32-S3 games", + "keywords": "esp32, led, matrix, fastled, utilities", + "dependencies": { + "fastled/FastLED": "^3.9.20", + "BoardConfig": "*" + }, + "frameworks": "arduino", + "platforms": "espressif32" +}