Files
ESP32_MatrixClock/ESP32_MatrixClock.ino
2026-07-10 14:49:12 +00:00

293 lines
9.9 KiB
Arduino

#include <MD_MAX72xx.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// --- WIFI & API CONFIG ---
const char* ssid = "SSID";
const char* password = "YOUR_PASSWORD";
const char* apiKey = "Weather_API_Key";
const char* city = "Locale";
// --- HARDWARE CONFIG ---
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define DATA_PIN 13
#define CS_PIN 14
#define CLK_PIN 15
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, DATA_PIN, CS_PIN, CLK_PIN, MAX_DEVICES);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
WebServer server(80);
// --- STATE VARIABLES ---
bool displayPower = true;
bool weatherMode = false;
bool autoNightEnabled = true;
bool manualNightOverride = false;
bool isDST = true;
int dayIntensity = 8;
int nightIntensity = 0;
int nightStartHour = 22;
int nightEndHour = 7;
String weatherText = "LOADING...";
unsigned long lastWeatherUpdate = 0;
unsigned long lastClockUpdate = 0;
unsigned long lastWeatherScroll = 0;
const unsigned long weatherInterval = 600000;
int prevDigits[4] = {-1, -1, -1, -1};
bool prevIsColon = false;
// --- BITMAP DATA ---
byte digits[10][8] = {
{0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00}, {0x08, 0x18, 0x28, 0x08, 0x08, 0x08, 0x3E, 0x00},
{0x3C, 0x42, 0x02, 0x1C, 0x20, 0x40, 0x7E, 0x00}, {0x3C, 0x42, 0x02, 0x1C, 0x02, 0x42, 0x3C, 0x00},
{0x22, 0x42, 0x42, 0x7E, 0x02, 0x02, 0x02, 0x00}, {0x7E, 0x40, 0x40, 0x7C, 0x02, 0x42, 0x3C, 0x00},
{0x3C, 0x40, 0x40, 0x7C, 0x42, 0x42, 0x3C, 0x00}, {0x7E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00},
{0x3C, 0x42, 0x42, 0x3C, 0x42, 0x42, 0x3C, 0x00}, {0x3C, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x3C, 0x00}
};
byte colon[8] = {0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00};
// --- WEB INTERFACE ---
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head><title>Matrix Control</title><meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; background: #121212; color: #e0e0e0; text-align: center; padding: 20px; }
.card { background: #1e1e1e; padding: 20px; border-radius: 15px; max-width: 350px; margin: 0 auto; box-shadow: 0 4px 10px rgba(0,0,0,0.5); }
.btn { display: block; width: 100%; margin: 10px 0; padding: 15px; border-radius: 10px; border: none; font-weight: bold; cursor: pointer; transition: 0.3s; color: white; }
.btn-blue { background: #03a9f4; }
.slider { width: 100%; height: 15px; margin: 20px 0; }
input[type=text] { width: 100%; padding: 12px; margin: 10px 0; border-radius: 8px; border: 1px solid #333; background: #222; color: #fff; box-sizing: border-box; }
</style></head>
<body onload="getStatus()">
<div class="card">
<h2>Matrix Control</h2>
<button id="btn-pwr" class="btn" onclick="toggle('/power', 'btn-pwr', 'Power')">Power: ...</button>
<label style="color:#888;">Brightness</label>
<input type="range" min="0" max="15" value="8" id="brightRange" class="slider" oninput="fetch('/brightness?value=' + this.value)">
<button id="btn-weather" class="btn" onclick="toggle('/toggleWeather', 'btn-weather', 'Weather Cycle')">Weather Cycle: ...</button>
<button id="btn-dst" class="btn" onclick="toggle('/dst', 'btn-dst', 'DST')">DST: ...</button>
<button id="btn-night" class="btn" onclick="toggle('/manual-night', 'btn-night', 'Force Night Mode')">Force Night Mode: ...</button>
<hr style="border: 0.5px solid #333; margin: 20px 0;">
<input type="text" id="msg" placeholder="Type custom message...">
<button class="btn btn-blue" onclick="sendMsg()">Send Message</button>
</div>
<script>
function updateBtn(id, name, isOn) {
let btn = document.getElementById(id);
btn.style.background = isOn ? "#4CAF50" : "#424242";
btn.innerText = name + (isOn ? ": ON" : ": OFF");
}
function toggle(url, id, name) {
fetch(url).then(r => r.text()).then(res => { updateBtn(id, name, res === "ON"); });
}
function getStatus() {
fetch('/state').then(r => r.json()).then(data => {
updateBtn('btn-pwr', 'Power', data.power);
updateBtn('btn-weather', 'Weather Cycle', data.weather);
updateBtn('btn-dst', 'DST', data.dst);
updateBtn('btn-night', 'Force Night Mode', data.night);
document.getElementById('brightRange').value = data.brightness;
});
}
function sendMsg() {
let txt = document.getElementById('msg').value;
if(txt) fetch('/message?text=' + encodeURIComponent(txt));
document.getElementById('msg').value = '';
}
</script>
</body></html>
)rawliteral";
// --- FUNCTION PROTOTYPES ---
void updateClock();
void updateWeather();
void scrollText(const char *pMsg);
void displayDigit(int dev, int digit);
void displayColon(int dev);
void setup() {
Serial.begin(115200);
mx.begin();
mx.control(MD_MAX72XX::INTENSITY, dayIntensity);
mx.clear();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
timeClient.begin();
server.on("/", []() { server.send(200, "text/html", index_html); });
server.on("/state", []() {
String json = "{";
json += "\"power\":" + String(displayPower ? "true" : "false") + ",";
json += "\"weather\":" + String(weatherMode ? "true" : "false") + ",";
json += "\"dst\":" + String(isDST ? "true" : "false") + ",";
json += "\"night\":" + String(manualNightOverride ? "true" : "false") + ",";
json += "\"brightness\":" + String(dayIntensity);
json += "}";
server.send(200, "application/json", json);
});
server.on("/power", []() {
displayPower = !displayPower;
mx.control(MD_MAX72XX::SHUTDOWN, !displayPower);
if(displayPower) { for(int i=0; i<4; i++) prevDigits[i] = -1; prevIsColon = false; }
server.send(200, "text/plain", displayPower ? "ON" : "OFF");
});
server.on("/brightness", []() {
if (server.hasArg("value")) {
dayIntensity = server.arg("value").toInt();
if (displayPower && !manualNightOverride) mx.control(MD_MAX72XX::INTENSITY, dayIntensity);
}
server.send(200, "text/plain", "OK");
});
server.on("/toggleWeather", []() {
weatherMode = !weatherMode;
server.send(200, "text/plain", weatherMode ? "ON" : "OFF");
});
server.on("/dst", []() {
isDST = !isDST;
for(int i=0; i<4; i++) prevDigits[i] = -1;
prevIsColon = false;
server.send(200, "text/plain", isDST ? "ON" : "OFF");
});
server.on("/manual-night", []() {
manualNightOverride = !manualNightOverride;
server.send(200, "text/plain", manualNightOverride ? "ON" : "OFF");
});
server.on("/message", []() {
if (server.hasArg("text") && displayPower) {
scrollText(server.arg("text").c_str());
mx.clear();
for(int i=0; i<4; i++) prevDigits[i] = -1;
prevIsColon = false;
lastClockUpdate = 0;
}
server.send(200, "text/plain", "Sent");
});
server.begin();
updateWeather();
}
void loop() {
server.handleClient();
if (!displayPower) return;
if (millis() - lastWeatherUpdate > weatherInterval) {
updateWeather();
}
if (weatherMode && (millis() - lastWeatherScroll > 60000)) {
lastWeatherScroll = millis();
scrollText(weatherText.c_str());
mx.clear();
for(int i=0; i<4; i++) prevDigits[i] = -1;
prevIsColon = false;
lastClockUpdate = 0;
}
if (millis() - lastClockUpdate >= 1000) {
lastClockUpdate = millis();
updateClock();
}
}
void updateWeather() {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(city) + "&appid=" + String(apiKey) + "&units=metric";
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
StaticJsonDocument<512> doc;
deserializeJson(doc, payload);
float temp = doc["main"]["temp"];
const char* desc = doc["weather"][0]["description"];
weatherText = String(temp, 1) + "C - " + String(desc);
weatherText.toUpperCase();
}
http.end();
}
lastWeatherUpdate = millis();
}
void updateClock() {
timeClient.update();
int currentOffset = -25200 + (isDST ? 3600 : 0);
timeClient.setTimeOffset(currentOffset);
int h24 = timeClient.getHours();
int mins = timeClient.getMinutes();
bool shouldBeDim = manualNightOverride || (autoNightEnabled && (h24 >= nightStartHour || h24 < nightEndHour));
mx.control(MD_MAX72XX::INTENSITY, shouldBeDim ? nightIntensity : dayIntensity);
int h12 = h24;
if (h12 == 0) h12 = 12; else if (h12 > 12) h12 -= 12;
int currentDigits[4];
bool isColon = false;
if (h12 < 10) { currentDigits[0] = h12; currentDigits[1] = -1; isColon = true; }
else { currentDigits[0] = h12 / 10; currentDigits[1] = h12 % 10; }
currentDigits[2] = mins / 10;
currentDigits[3] = mins % 10;
for (int i = 0; i < 4; i++) {
int dev = MAX_DEVICES - 1 - i;
if (i == 1 && isColon) {
if (!prevIsColon) { displayColon(dev); prevIsColon = true; }
} else if (currentDigits[i] != prevDigits[i]) {
displayDigit(dev, currentDigits[i]); prevDigits[i] = currentDigits[i];
}
}
}
void scrollText(const char *pMsg) {
uint8_t charBuf[8];
mx.clear();
while (*pMsg != '\0') {
uint8_t charLen = mx.getChar(*pMsg++, sizeof(charBuf), charBuf);
for (uint8_t i = 0; i < charLen; i++) {
mx.transform(MD_MAX72XX::TSL);
mx.setColumn(0, charBuf[i]);
delay(35); // Reverted to 35ms
server.handleClient();
}
mx.transform(MD_MAX72XX::TSL);
mx.setColumn(0, 0);
delay(35);
}
for (uint8_t i = 0; i < MAX_DEVICES * 8; i++) {
mx.transform(MD_MAX72XX::TSL);
mx.setColumn(0, 0);
delay(35);
}
}
void displayDigit(int dev, int digit) {
if (digit >= 0 && digit <= 9) {
for (int row = 0; row < 8; row++) mx.setRow(dev, row, digits[digit][row]);
} else {
for (int row = 0; row < 8; row++) mx.setRow(dev, row, 0);
}
}
void displayColon(int dev) {
for (int row = 0; row < 8; row++) mx.setRow(dev, row, colon[row]);
}