Upload files to "/"
This commit is contained in:
@@ -0,0 +1,293 @@
|
|||||||
|
#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 = "Ocelot";
|
||||||
|
const char* password = "YOUR_PASSWORD";
|
||||||
|
const char* apiKey = "d0128438a6bf2336df0e3e1faefcb556";
|
||||||
|
const char* city = "Leduc,CA";
|
||||||
|
|
||||||
|
// --- 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]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user