commit 40755ff5ef162a46634bf99b7da6bb47e248b650 Author: Steve Date: Thu Jul 16 03:37:10 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc9d5fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pio/ +.vscode/ +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..77962c7 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Baseball Display + +A starter ESP32 firmware project for a baseball scoreboard with a seven-segment-style LED display built from WS2811/SK6812-compatible LEDs. + +## Features +- Displays home score, away score, inning, outs, balls, strikes, current time, and elapsed game time +- Serves a small web control page from the ESP32 for remote updates +- Uses a configurable LED strip / pixel array as the display backend + +## Hardware +- ESP32 development board +- WS2811/SK6812-compatible LED strip or pixels +- Data pin: GPIO 13 +- Power: 5 V, with ground connected and a bulk capacitor near the LEDs + +## Build +```bash +pio run +``` + +## Flash +```bash +pio run --target upload +``` + +## Usage +1. Flash the firmware to the ESP32. +2. Connect to the Wi-Fi access point named `BaseballDisplay` with password `scoreboard`. +3. Open `http://192.168.4.1/` in a browser. +4. Use the controls to update the scoreboard state. + +## Next steps +- Replace the simple segment layout with your actual physical LED geometry. +- Add a real controller device or a more polished UI. +- Integrate real time sync or a scoreboard backend. diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..d8594ab --- /dev/null +++ b/platformio.ini @@ -0,0 +1,9 @@ +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +build_flags = -DCORE_DEBUG_LEVEL=0 +lib_deps = + fastled/FastLED@^3.9.0 + bblanchon/ArduinoJson@^7.0.0 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d61dd66 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include + +#define LED_PIN 13 +#define LED_TYPE WS2812B +#define COLOR_ORDER GRB +#define NUM_LEDS 120 +#define BRIGHTNESS 64 + +CRGB leds[NUM_LEDS]; +WebServer server(80); + +struct ScoreState { + int homeScore = 0; + int awayScore = 0; + int inning = 1; + int outs = 0; + int balls = 0; + int strikes = 0; + int hour = 12; + int minute = 0; + unsigned long gameStartMs = 0; +}; + +ScoreState state; + +void setPixel(int index, CRGB color) { + if (index >= 0 && index < NUM_LEDS) { + leds[index] = color; + } +} + +void renderDigit(int digit, int startIndex, CRGB color) { + static const bool segments[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, + {0, 1, 1, 0, 0, 0, 0}, + {1, 1, 0, 1, 1, 0, 1}, + {1, 1, 1, 1, 0, 0, 1}, + {0, 1, 1, 0, 0, 1, 1}, + {1, 0, 1, 1, 0, 1, 1}, + {1, 0, 1, 1, 1, 1, 1}, + {1, 1, 1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 1, 1}, + {1, 1, 1, 1, 0, 1, 1}, + }; + + for (int i = 0; i < 7; ++i) { + if (segments[digit][i]) { + setPixel(startIndex + i, color); + } else { + setPixel(startIndex + i, CRGB::Black); + } + } +} + +void renderScoreboard() { + fill_solid(leds, NUM_LEDS, CRGB::Black); + + int home = state.homeScore; + int away = state.awayScore; + int inning = state.inning; + int outs = state.outs; + int balls = state.balls; + int strikes = state.strikes; + + renderDigit(home / 10, 0, CRGB::Green); + renderDigit(home % 10, 7, CRGB::Green); + renderDigit(away / 10, 14, CRGB::Red); + renderDigit(away % 10, 21, CRGB::Red); + renderDigit(inning / 10, 28, CRGB::Blue); + renderDigit(inning % 10, 35, CRGB::Blue); + renderDigit(outs, 42, CRGB::Orange); + renderDigit(balls, 49, CRGB::Yellow); + renderDigit(strikes, 56, CRGB::Magenta); + + FastLED.show(); +} + +void handleRoot() { + String html = R"html( + + + + + + Baseball Display + + + +
+

Baseball Scoreboard

+
+ + + + + + + + + + + + + +
+
+ + +)html"; + server.send(200, "text/html", html); +} + +void handleUpdate() { + if (server.hasArg("homeScore")) state.homeScore = server.arg("homeScore").toInt(); + if (server.hasArg("awayScore")) state.awayScore = server.arg("awayScore").toInt(); + if (server.hasArg("inning")) state.inning = server.arg("inning").toInt(); + if (server.hasArg("outs")) state.outs = server.arg("outs").toInt(); + if (server.hasArg("balls")) state.balls = server.arg("balls").toInt(); + if (server.hasArg("strikes")) state.strikes = server.arg("strikes").toInt(); + + renderScoreboard(); + server.sendHeader("Location", "/"); + server.send(303); +} + +void setup() { + Serial.begin(115200); + delay(1000); + + FastLED.addLeds(leds, NUM_LEDS); + FastLED.setBrightness(BRIGHTNESS); + FastLED.clear(); + FastLED.show(); + + WiFi.mode(WIFI_AP); + WiFi.softAP("BaseballDisplay", "scoreboard"); + IPAddress ip = WiFi.softAPIP(); + Serial.print("AP IP: "); + Serial.println(ip); + + server.on("/", HTTP_GET, handleRoot); + server.on("/update", HTTP_POST, handleUpdate); + server.begin(); + + state.gameStartMs = millis(); + renderScoreboard(); +} + +void loop() { + server.handleClient(); + + static unsigned long lastTimeUpdate = 0; + if (millis() - lastTimeUpdate > 1000) { + lastTimeUpdate = millis(); + state.minute = (state.minute + 1) % 60; + if (state.minute == 0) { + state.hour = (state.hour + 1) % 24; + } + renderScoreboard(); + } +}