Files
vfxreview/esp32/firmware/firmware.ino
T
twotalesanimation afcef02409
Deploy / deploy (push) Successful in 3m3s
API Updates
2026-07-08 14:38:56 +02:00

275 lines
8.8 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* VFX Review — ESP32 dot-matrix dashboard
*
* Hardware : ESP32 dev board + 4× MAX7219 8×8 modules chained (8×32 display)
* Libraries (install via Arduino Library Manager):
* - MD_Parola by majicDesigns (≥3.7)
* - MD_MAX72XX by majicDesigns (≥3.5)
* - ArduinoJson by Benoit Blanchon (≥7)
*
* Behaviour:
* Normal → scrolls "APR:142 CHG:12 TODO:38" on a loop
* Event → interrupts with e.g. "OK SH0010" / "REVISE SH0010" / "NOTE SH0010"
* held for NOTIFY_HOLD_MS then resumes stats scroll
*/
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include "config.h"
// ── Display setup ─────────────────────────────────────────────────────────────
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
MD_Parola display(HARDWARE_TYPE, CS_PIN, NUM_DEVICES);
// ── State ─────────────────────────────────────────────────────────────────────
Preferences prefs;
int approvedCount = 0;
int changesCount = 0;
int todoCount = 0;
int lastEventId = 0;
bool notifyActive = false;
uint32_t notifyUntil = 0;
char statsMsg[64]; // "APR:142 CHG:12 TODO:38"
char notifyMsg[64]; // interrupt message
uint32_t lastStatsFetch = 0;
uint32_t lastEventsFetch = 0;
// ── Helpers ───────────────────────────────────────────────────────────────────
void buildStatsMsg() {
snprintf(statsMsg, sizeof(statsMsg),
"APR:%d CHG:%d TODO:%d",
approvedCount, changesCount, todoCount);
}
// Truncate a string to maxLen chars, safe for display
void truncate(const char* src, char* dst, int maxLen) {
strncpy(dst, src, maxLen);
dst[maxLen] = '\0';
}
void connectWiFi() {
Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print('.');
}
Serial.printf("\n[WiFi] Connected — IP: %s\n", WiFi.localIP().toString().c_str());
}
// Make an authenticated GET request; fills `out` with the response body.
// Returns HTTP status code, or -1 on connection failure.
int apiGet(const char* path, String& out) {
if (WiFi.status() != WL_CONNECTED) connectWiFi();
char url[256];
snprintf(url, sizeof(url), "http://%s:%d%s", SERVER_HOST, SERVER_PORT, path);
HTTPClient http;
http.begin(url);
http.addHeader("x-display-key", DISPLAY_API_KEY);
http.setTimeout(8000);
int code = http.GET();
if (code > 0) out = http.getString();
http.end();
return code;
}
// ── Stats fetch ───────────────────────────────────────────────────────────────
void fetchStats() {
String body;
int code = apiGet("/api/dashboard/stats", body);
if (code != 200) {
Serial.printf("[Stats] HTTP %d\n", code);
return;
}
JsonDocument doc;
if (deserializeJson(doc, body)) { Serial.println("[Stats] JSON parse error"); return; }
approvedCount = doc["approved"] | approvedCount;
changesCount = doc["changes"] | changesCount;
todoCount = doc["todo"] | todoCount;
buildStatsMsg();
Serial.printf("[Stats] APR:%d CHG:%d TODO:%d\n", approvedCount, changesCount, todoCount);
}
// ── Events fetch ──────────────────────────────────────────────────────────────
void fetchEvents() {
char path[64];
snprintf(path, sizeof(path), "/api/display/events?after=%d", lastEventId);
String body;
int code = apiGet(path, body);
if (code != 200) {
Serial.printf("[Events] HTTP %d\n", code);
return;
}
JsonDocument doc;
if (deserializeJson(doc, body)) { Serial.println("[Events] JSON parse error"); return; }
JsonArray arr = doc.as<JsonArray>();
if (arr.isNull() || arr.size() == 0) return;
// Pick the most recent event to display, update lastEventId across all
const char* bestType = nullptr;
const char* bestShotCode = nullptr;
const char* bestBy = nullptr;
int newestId = lastEventId;
for (JsonObject ev : arr) {
int id = ev["id"] | 0;
const char* t = ev["type"] | "";
const char* s = ev["shotCode"] | "";
const char* b = ev["by"] | "";
if (id > newestId) {
newestId = id;
bestType = t;
bestShotCode = s;
bestBy = b;
}
}
if (newestId > lastEventId) {
lastEventId = newestId;
prefs.putInt("lastEvtId", lastEventId);
}
if (!bestType || !bestShotCode) return;
// Build notification message
char shotBuf[16];
truncate(bestShotCode, shotBuf, 12);
if (strcmp(bestType, "APPROVED") == 0) {
snprintf(notifyMsg, sizeof(notifyMsg), "OK: %s", shotBuf);
} else if (strcmp(bestType, "CHANGES") == 0) {
snprintf(notifyMsg, sizeof(notifyMsg), "REVISE: %s", shotBuf);
} else if (strcmp(bestType, "REJECTED") == 0) {
snprintf(notifyMsg, sizeof(notifyMsg), "REJECT: %s", shotBuf);
} else if (strcmp(bestType, "COMMENT") == 0) {
char byBuf[10];
truncate(bestBy, byBuf, 8);
snprintf(notifyMsg, sizeof(notifyMsg), "NOTE: %s", shotBuf);
} else {
snprintf(notifyMsg, sizeof(notifyMsg), "%s", shotBuf);
}
Serial.printf("[Events] id=%d type=%s shot=%s\n", newestId, bestType, bestShotCode);
notifyActive = true;
notifyUntil = millis() + NOTIFY_HOLD_MS;
display.displayClear();
display.displayText(notifyMsg, PA_CENTER, SCROLL_SPEED, 500, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
}
// ── Arduino setup ─────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(500);
// Restore last seen event ID from NVS so we don't replay old events on reboot
prefs.begin("vfxdisp", false);
lastEventId = prefs.getInt("lastEvtId", 0);
Serial.printf("[Init] lastEventId restored = %d\n", lastEventId);
// Display init
display.begin();
display.setIntensity(BRIGHTNESS);
display.displayClear();
display.setTextAlignment(PA_LEFT);
display.setScrollSpacing(4);
// Show connecting message
display.displayText("Connecting...", PA_LEFT, SCROLL_SPEED, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
while (!display.displayAnimate()) {}
connectWiFi();
// Initial data fetch
fetchStats();
// Prime the events endpoint — catch up to current without showing stale events
// We do a silent fetch just to advance lastEventId to now
{
char path[64];
snprintf(path, sizeof(path), "/api/display/events?after=%d", lastEventId);
String body;
int code = apiGet(path, body);
if (code == 200) {
JsonDocument doc;
if (!deserializeJson(doc, body)) {
JsonArray arr = doc.as<JsonArray>();
for (JsonObject ev : arr) {
int id = ev["id"] | 0;
if (id > lastEventId) lastEventId = id;
}
prefs.putInt("lastEvtId", lastEventId);
}
}
}
// Kick off the scrolling stats message
buildStatsMsg();
display.displayClear();
display.displayText(statsMsg, PA_LEFT, SCROLL_SPEED, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
lastStatsFetch = millis();
lastEventsFetch = millis();
}
// ── Arduino loop ──────────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// ── Timed fetches ──
if (now - lastStatsFetch >= STATS_INTERVAL_MS) {
lastStatsFetch = now;
fetchStats();
// If no notification is active, refresh the scrolling text immediately
if (!notifyActive) {
display.displayClear();
display.displayText(statsMsg, PA_LEFT, SCROLL_SPEED, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
}
}
if (now - lastEventsFetch >= EVENTS_INTERVAL_MS) {
lastEventsFetch = now;
if (!notifyActive) fetchEvents(); // don't overlap notifications
}
// ── Notification expiry ──
if (notifyActive && now >= notifyUntil) {
notifyActive = false;
display.displayClear();
display.displayText(statsMsg, PA_LEFT, SCROLL_SPEED, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
}
// ── Animate display ──
// When a scroll finishes, restart it (loops the message)
if (display.displayAnimate()) {
display.displayReset();
}
}