API Updates
Deploy / deploy (push) Successful in 3m3s

This commit is contained in:
twotalesanimation
2026-07-08 14:38:56 +02:00
parent d40ca8eb55
commit afcef02409
9 changed files with 399 additions and 4 deletions
+274
View File
@@ -0,0 +1,274 @@
/*
* 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();
}
}