Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.pio/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <FastLED.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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(
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Baseball Display</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background: #111; color: #eee; padding: 24px; }
|
||||||
|
.card { max-width: 420px; margin: 0 auto; background: #222; padding: 20px; border-radius: 12px; }
|
||||||
|
label { display: block; margin-top: 10px; }
|
||||||
|
input, button { width: 100%; padding: 10px; margin-top: 6px; }
|
||||||
|
button { background: #4f46e5; color: white; border: none; border-radius: 8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Baseball Scoreboard</h1>
|
||||||
|
<form action="/update" method="post">
|
||||||
|
<label>Home score</label>
|
||||||
|
<input name="homeScore" value="0" />
|
||||||
|
<label>Away score</label>
|
||||||
|
<input name="awayScore" value="0" />
|
||||||
|
<label>Inning</label>
|
||||||
|
<input name="inning" value="1" />
|
||||||
|
<label>Outs</label>
|
||||||
|
<input name="outs" value="0" />
|
||||||
|
<label>Balls</label>
|
||||||
|
<input name="balls" value="0" />
|
||||||
|
<label>Strikes</label>
|
||||||
|
<input name="strikes" value="0" />
|
||||||
|
<button type="submit">Update display</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)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<LED_TYPE, LED_PIN, COLOR_ORDER>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user