Skip to content

Instantly share code, notes, and snippets.

@ice09
Last active January 30, 2026 07:05
Show Gist options
  • Select an option

  • Save ice09/a87fb327ecbbd3c2ae7ac57fee3ffd1a to your computer and use it in GitHub Desktop.

Select an option

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
#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();
}
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