@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
// ── WiFi ──────────────────────────────────────────────────────────────────────
|
||||
#define WIFI_SSID "YourSSID"
|
||||
#define WIFI_PASSWORD "YourPassword"
|
||||
|
||||
// ── Server ────────────────────────────────────────────────────────────────────
|
||||
// IP or hostname of your VFX Review server — no trailing slash
|
||||
#define SERVER_HOST "192.168.1.100"
|
||||
#define SERVER_PORT 3000
|
||||
#define DISPLAY_API_KEY "your-display-api-key-here" // must match .env DISPLAY_API_KEY
|
||||
|
||||
// ── MAX7219 hardware (hardware SPI) ──────────────────────────────────────────
|
||||
// Module: 4 × 8×8 MAX7219 daisy-chained = 8×32 display
|
||||
// DIN → GPIO 23 (MOSI)
|
||||
// CLK → GPIO 18 (SCK)
|
||||
// CS → GPIO 5 (SS)
|
||||
#define CS_PIN 5
|
||||
#define NUM_DEVICES 4 // number of 8×8 modules chained
|
||||
|
||||
// ── Display tuning ────────────────────────────────────────────────────────────
|
||||
#define BRIGHTNESS 4 // 0–15
|
||||
#define SCROLL_SPEED 35 // ms per pixel shift (lower = faster)
|
||||
#define NOTIFY_HOLD_MS 8000 // how long a notification stays before returning to stats
|
||||
|
||||
// ── Poll intervals ────────────────────────────────────────────────────────────
|
||||
#define STATS_INTERVAL_MS 30000 // fetch stats every 30 s
|
||||
#define EVENTS_INTERVAL_MS 5000 // check for new events every 5 s
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user