Last active
January 30, 2026 07:05
-
-
Save ice09/a87fb327ecbbd3c2ae7ac57fee3ffd1a to your computer and use it in GitHub Desktop.
Works with Waveshare ESP32-S3-Matrix Development Board, Onboard 8×8 RGB LED Matrix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <WiFi.h> | |
| #include <WebServer.h> | |
| #include <FastLED.h> | |
| // ===================== LED CONFIG ===================== | |
| #define LED_PIN 14 | |
| #define NUM_LEDS 64 | |
| #define LED_TYPE WS2812B | |
| #define COLOR_ORDER RGB | |
| #define BRIGHTNESS 120 | |
| #define W 8 | |
| #define H 8 | |
| // 5x7 font geometry | |
| #define CHAR_W 5 | |
| #define CHAR_H 7 | |
| #define CHAR_SP 1 // 1 column spacing | |
| CRGB leds[NUM_LEDS]; | |
| WebServer server(80); | |
| // ===================== WIFI ===================== | |
| const char* ssid = "ice09"; | |
| const char* pass = "acstmb1810"; | |
| // ===================== TEXT STATE ===================== | |
| String gText = "HELLO"; | |
| float gSpeed = 1.5f; // pixels per frame accumulator | |
| uint8_t gMode = 1; // 1=Rainbow, 2=Fire, 3=Ice, 4=Neon | |
| uint8_t gBri = BRIGHTNESS; | |
| float gScroll = 0.0f; // 0 = fully offscreen right (blank) | |
| unsigned long gLastFrame = 0; | |
| const uint16_t FRAME_MS = 25; // ~40 FPS | |
| // small gap before text appears | |
| const int START_GAP = 2; // extra empty columns offscreen to the right | |
| // ===================== MATRIX MAPPING ===================== | |
| // Calibration: | |
| // index 0 bottom-right, then moves left, no snake -> rotate 180 + straight rows | |
| int xyToIndex(int x, int y) { | |
| int rx = (W - 1 - x); | |
| int ry = (H - 1 - y); | |
| return ry * W + rx; | |
| } | |
| inline void setXY(int x, int y, const CRGB& c) { | |
| if (x < 0 || x >= W || y < 0 || y >= H) return; | |
| leds[xyToIndex(x, y)] = c; | |
| } | |
| // ===================== 5x7 FONT ===================== | |
| struct Glyph { char ch; uint8_t col[5]; }; | |
| const Glyph font[] = { | |
| {' ', {0,0,0,0,0}}, | |
| {'A', {0x7E,0x11,0x11,0x7E,0}}, | |
| {'B', {0x7F,0x49,0x49,0x36,0}}, | |
| {'C', {0x3E,0x41,0x41,0x22,0}}, | |
| {'D', {0x7F,0x41,0x41,0x3E,0}}, | |
| {'E', {0x7F,0x49,0x49,0x41,0}}, | |
| {'F', {0x7F,0x09,0x09,0x01,0}}, | |
| {'G', {0x3E,0x41,0x51,0x32,0}}, | |
| {'H', {0x7F,0x08,0x08,0x7F,0}}, | |
| {'I', {0x41,0x7F,0x41,0,0}}, | |
| {'J', {0x20,0x40,0x41,0x3F,0}}, | |
| {'K', {0x7F,0x08,0x14,0x63,0}}, | |
| {'L', {0x7F,0x40,0x40,0x40,0}}, | |
| {'M', {0x7F,0x02,0x04,0x02,0x7F}}, | |
| {'N', {0x7F,0x04,0x08,0x7F,0}}, | |
| {'O', {0x3E,0x41,0x41,0x3E,0}}, | |
| {'P', {0x7F,0x09,0x09,0x06,0}}, | |
| {'R', {0x7F,0x09,0x19,0x66,0}}, | |
| {'S', {0x26,0x49,0x49,0x32,0}}, | |
| {'T', {0x01,0x7F,0x01,0x01,0}}, | |
| {'U', {0x3F,0x40,0x40,0x3F,0}}, | |
| {'V', {0x1F,0x20,0x40,0x20,0x1F}}, | |
| {'W', {0x7F,0x20,0x10,0x20,0x7F}}, | |
| {'X', {0x63,0x14,0x08,0x14,0x63}}, | |
| {'Y', {0x07,0x08,0x70,0x08,0x07}}, | |
| {'Z', {0x61,0x51,0x49,0x45,0x43}}, | |
| {'0', {0x3E,0x45,0x49,0x51,0x3E}}, | |
| {'1', {0x00,0x21,0x7F,0x01,0x00}}, | |
| {'2', {0x23,0x45,0x49,0x51,0x21}}, | |
| {'3', {0x22,0x41,0x49,0x49,0x36}}, | |
| {'4', {0x0C,0x14,0x24,0x7F,0x04}}, | |
| {'5', {0x72,0x51,0x51,0x51,0x4E}}, | |
| {'6', {0x3E,0x49,0x49,0x49,0x26}}, | |
| {'7', {0x40,0x47,0x48,0x50,0x60}}, | |
| {'8', {0x36,0x49,0x49,0x49,0x36}}, | |
| {'9', {0x32,0x49,0x49,0x49,0x3E}}, | |
| {'!', {0x00,0x00,0x5F,0x00,0x00}}, | |
| {'-', {0x08,0x08,0x08,0x08,0x08}}, | |
| }; | |
| const uint8_t* glyphCols(char c) { | |
| c = toupper((unsigned char)c); | |
| for (auto &g : font) if (g.ch == c) return g.col; | |
| return font[0].col; | |
| } | |
| // ===================== TEXT LOGIC ===================== | |
| int textWidthPx(const String& s) { | |
| return s.length() * (CHAR_W + CHAR_SP); | |
| } | |
| bool textPixelAt(const String& s, int px, int py) { | |
| if (py < 0 || py >= CHAR_H) return false; | |
| int ci = px / (CHAR_W + CHAR_SP); | |
| int col = px % (CHAR_W + CHAR_SP); | |
| if (ci < 0 || ci >= (int)s.length()) return false; | |
| if (col >= CHAR_W) return false; // spacing column | |
| const uint8_t* cols = glyphCols(s.charAt(ci)); | |
| return ((cols[col] >> py) & 1) == 1; | |
| } | |
| // Readable lively colors (no blur/trails) | |
| CRGB colorForLitPixel(int x, int y, uint8_t mode) { | |
| uint8_t t = millis() / 8; | |
| switch (mode) { | |
| case 2: { // Fire | |
| uint8_t heat = qadd8((uint8_t)(y * 20), (uint8_t)(t + x * 10)); | |
| return CHSV(10 + heat / 12, 255, 255); | |
| } | |
| case 3: { // Ice | |
| uint8_t wave = sin8(t + x * 18 + y * 10); | |
| return CHSV(145 + wave / 20, 200, 255); | |
| } | |
| case 4: { // Neon | |
| uint8_t wave = sin8(t + x * 24); | |
| uint8_t hue = (wave > 127) ? 200 : 170; | |
| return CHSV(hue + (uint8_t)(y * 2), 255, 255); | |
| } | |
| default: { // Rainbow | |
| uint8_t hue = t + x * 18 + y * 7; | |
| return CHSV(hue, 255, 255); | |
| } | |
| } | |
| } | |
| void renderTextFrame() { | |
| fill_solid(leds, NUM_LEDS, CRGB::Black); | |
| int w = textWidthPx(gText); | |
| // startX is the left edge of the text in screen coordinates. | |
| // At gScroll = 0, we want the text fully offscreen right -> startX = W + START_GAP | |
| int startX = (W + START_GAP) - (int)gScroll; | |
| // KEY FIX: | |
| // Don't draw ANYTHING until the first character (5 columns) can be fully visible. | |
| // That happens when startX <= W - CHAR_W. | |
| if (startX > (W - CHAR_W)) { | |
| FastLED.show(); | |
| } else { | |
| // draw visible pixels | |
| for (int y = 0; y < CHAR_H; y++) { | |
| for (int x = 0; x < W; x++) { | |
| int px = x - startX; // position within text | |
| if (textPixelAt(gText, px, y)) { | |
| setXY(x, y, colorForLitPixel(x, y, gMode)); | |
| } | |
| } | |
| } | |
| FastLED.show(); | |
| } | |
| // advance | |
| gScroll += gSpeed; | |
| // when text fully passed left (with some padding), restart from fully offscreen right again | |
| if (gScroll > (float)(w + W + START_GAP)) { | |
| gScroll = 0.0f; | |
| } | |
| } | |
| // ===================== HTTP ===================== | |
| void handleHealth() { | |
| server.send(200, "text/plain", "ok"); | |
| } | |
| // POST /text {"text":"HELLO","speed":1.5,"mode":1,"bri":120} | |
| void handleText() { | |
| String body = server.arg("plain"); | |
| body.trim(); | |
| int ti = body.indexOf("\"text\""); | |
| if (ti >= 0) { | |
| int q1 = body.indexOf('"', body.indexOf(':', ti)); | |
| int q2 = body.indexOf('"', q1 + 1); | |
| if (q1 > 0 && q2 > q1) { | |
| gText = body.substring(q1 + 1, q2); | |
| if (gText.length() == 0) gText = " "; | |
| gScroll = 0.0f; // restart clean: blank -> first char appears fully | |
| } | |
| } | |
| int si = body.indexOf("\"speed\""); | |
| if (si >= 0) { | |
| int colon = body.indexOf(':', si); | |
| float v = body.substring(colon + 1).toFloat(); | |
| gSpeed = constrain(v, 0.25f, 6.0f); | |
| } | |
| int mi = body.indexOf("\"mode\""); | |
| if (mi >= 0) { | |
| int colon = body.indexOf(':', mi); | |
| int v = body.substring(colon + 1).toInt(); | |
| gMode = (uint8_t)constrain(v, 1, 4); | |
| } | |
| int bi = body.indexOf("\"bri\""); | |
| if (bi >= 0) { | |
| int colon = body.indexOf(':', bi); | |
| int v = body.substring(colon + 1).toInt(); | |
| gBri = (uint8_t)constrain(v, 1, 255); | |
| FastLED.setBrightness(gBri); | |
| } | |
| server.send(200, "application/json", "{\"ok\":true}"); | |
| } | |
| // ===================== SETUP / LOOP ===================== | |
| void setup() { | |
| delay(200); | |
| FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS); | |
| FastLED.setBrightness(gBri); | |
| fill_solid(leds, NUM_LEDS, CRGB::Black); | |
| FastLED.show(); | |
| gScroll = 0.0f; // blank start | |
| WiFi.mode(WIFI_STA); | |
| WiFi.begin(ssid, pass); | |
| unsigned long start = millis(); | |
| while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) { | |
| delay(200); | |
| } | |
| server.on("/health", HTTP_GET, handleHealth); | |
| server.on("/text", HTTP_POST, handleText); | |
| server.begin(); | |
| } | |
| void loop() { | |
| server.handleClient(); | |
| unsigned long now = millis(); | |
| if (now - gLastFrame < FRAME_MS) return; | |
| gLastFrame = now; | |
| renderTextFrame(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.net.URI; | |
| import java.net.http.*; | |
| import java.nio.charset.StandardCharsets; | |
| import java.time.Duration; | |
| import java.util.Scanner; | |
| public class SendLivelyText { | |
| static final String IP = "192.168.2.215"; | |
| static final String URL = "http://" + IP + "/text"; | |
| public static void main(String[] args) throws Exception { | |
| String text = null; | |
| double speed = 0.01; | |
| int mode = 1; // 1=Rainbow, 2=Fire, 3=Ice, 4=Neon | |
| int bri = 250; | |
| for (int i = 0; i < args.length; i++) { | |
| switch (args[i]) { | |
| case "--text" -> text = (i + 1 < args.length) ? args[++i] : ""; | |
| case "--speed" -> speed = Double.parseDouble(args[++i]); | |
| case "--mode" -> mode = Integer.parseInt(args[++i]); | |
| case "--bri" -> bri = Integer.parseInt(args[++i]); | |
| } | |
| } | |
| HttpClient client = HttpClient.newBuilder() | |
| .connectTimeout(Duration.ofSeconds(3)) | |
| .build(); | |
| if (text != null) { | |
| send(client, text, speed, mode, bri); | |
| return; | |
| } | |
| try (Scanner sc = new Scanner(System.in, StandardCharsets.UTF_8)) { | |
| System.out.println("Type text and press Enter (empty line quits)."); | |
| System.out.println("Modes: 1=Rainbow, 2=Fire, 3=Ice, 4=Neon"); | |
| while (true) { | |
| System.out.print("> "); | |
| String line = sc.nextLine(); | |
| if (line == null || line.isBlank()) break; | |
| send(client, line, speed, mode, bri); | |
| } | |
| } | |
| } | |
| static void send(HttpClient client, String text, double speed, int mode, int bri) throws Exception { | |
| String safe = text.replace("\"", ""); | |
| String json = """ | |
| {"text":"%s","speed":%.2f,"mode":%d,"bri":%d} | |
| """.formatted(safe, speed, mode, bri); | |
| HttpRequest req = HttpRequest.newBuilder() | |
| .uri(URI.create(URL)) | |
| .timeout(Duration.ofSeconds(3)) | |
| .header("Content-Type", "application/json") | |
| .POST(HttpRequest.BodyPublishers.ofString(json)) | |
| .build(); | |
| client.send(req, HttpResponse.BodyHandlers.discarding()); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment