test

Posted På februari 28, 2026
#include <WiFi.h>
#include <time.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>

// ---------------- Inställningar Hårdvara ----------------
#define CLK_PIN 5
#define DT_PIN 19
#define SW_PIN 15
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define CS_PIN 21

// ---------------- WiFi & Tid ----------------

const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.nist.gov";
const char* ntpServer3 = "time.google.com";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;

// ---------------- Globala Objekt ----------------
MD_Parola myDisplay = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
MD_MAX72XX myMatrix = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
AsyncWebServer server(80);
Preferences preferences;

// ---------------- Variabler för Logik ----------------
// Lägen för menysystemet
enum AppMode { MODE_CLOCK, MODE_BUS, MODE_TEMP };
AppMode currentMode = MODE_CLOCK;

// Buss-konfiguration (5 platser)
struct BusConfig {
String name;
String url;
};
BusConfig busConfigs[5];
int currentBusIndex = 0; // Vilken av de 5 platserna som är vald

// Variabler för Rotary Encoder
int lastStateCLK;
unsigned long lastButtonPress = 0;
int clickCount = 0;

// Variabler för display-timing
unsigned long lastDisplaySwitch = 0;
int busDisplayState = 0; // 0=Countdown, 1=NextTime, 2=NextNextTime
String cachedBusTimes[3]; // Sparar de 3 tiderna
unsigned long lastBusFetch = 0;
bool busDataLoaded = false;

// ---------------- Bitmaps & Tecken ----------------
const char awithRing = char(229);
const char awithDots = char(228);
const char owithDots = char(246);

const unsigned char sun_bitmap[] PROGMEM = { 0x12, 0xbc, 0x7e, 0x7f, 0xfe, 0x7e, 0x3d, 0x48 };
const unsigned char cloud_bitmap[] PROGMEM = { 0x00, 0x00, 0x1c, 0x7e, 0xff, 0x7e, 0x00, 0x00 };
const unsigned char rain_bitmap[] PROGMEM = { 0x1c, 0x7e, 0xff, 0x7e, 0x00, 0x2a, 0x00, 0x54 };

// ---------------- HTML för Webbgränssnittet ----------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP32 Display Config</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; text-align: center; margin:0; padding:20px; background-color:#f4f4f4; }
h2 { color: #333; }
form { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
.input-group { margin-bottom: 15px; text-align: left; }
label { font-weight: bold; display: block; margin-bottom: 5px; }
input[type=text] { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; }
input[type=submit] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; font-size: 16px; }
input[type=submit]:hover { background-color: #45a049; }
</style>
</head>
<body>
<h2>Konfigurera Busslinjer</h2>
<form action="/save" method="post">
%FORM_INPUTS%
<input type="submit" value="Spara Inställningar">
</form>
</body>
</html>
)rawliteral";

// ---------------- Funktioner ----------------

// Ersätt placeholders i HTML
String processor(const String& var){
if(var == "FORM_INPUTS"){
String html = "";
for(int i=0; i<5; i++){
html += "<div class='input-group'>";
html += "<label>Plats " + String(i+1) + " Namn (t.ex. Buss 9):</label>";
html += "<input type='text' name='name" + String(i) + "' value='" + busConfigs[i].name + "'>";
html += "<label>ResRobot API URL:</label>";
html += "<input type='text' name='url" + String(i) + "' value='" + busConfigs[i].url + "'>";
html += "</div><hr>";
}
return html;
}
return String();
}

void loadSettings() {
preferences.begin("bus-config", false); // Namespace "bus-config"
for (int i = 0; i < 5; i++) {
busConfigs[i].name = preferences.getString(("name" + String(i)).c_str(), "Linje " + String(i+1));
busConfigs[i].url = preferences.getString(("url" + String(i)).c_str(), "");
}
// Ladda default URL om tomt (för test)
if (busConfigs[0].url == "") {
busConfigs[0].name = "9 Borjegatan";
busConfigs[0].url = "https://api.resrobot.se/v2.1/departureBoard?id=740007844&format=json&accessId=1658c2d5-35b4-42a2-b6d7-a691221c07e6";
}
preferences.end();
}

// Hämtar 3 avgångar och sparar i global array
bool fetchBusData(String apiUrl) {
if (apiUrl == "" || WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
// Vi lägger till parametrar för att säkerställa att vi får framtida resor
// ResRobot returnerar normalt nästa avgångar från "nu" om inget annat anges.
http.begin(apiUrl);
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(8192); // Lite större för att vara säker
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
JsonArray departures = doc["Departure"].as<JsonArray>();
int found = 0;
for (JsonObject dep : departures) {
if (found < 3) {
// Vi sparar både datum och tid för att kunna räkna rätt vid dygnsbryt
// Formatet blir "YYYY-MM-DD HH:MM:SS"
String d = dep["date"].as<String>();
String t = dep["time"].as<String>();
cachedBusTimes[found] = d + " " + t;
found++;
}
}
for(int k=found; k<3; k++) cachedBusTimes[k] = "";
http.end();
return (found > 0);
}
}
http.end();
return false;
}

// ---------------- SETUP ----------------
void setup() {
Serial.begin(115200);

// Initiera Display
myDisplay.begin();
myMatrix.begin();
myDisplay.setIntensity(0);
myDisplay.displayClear();
myDisplay.print("Startar...");

// Ladda inställningar
loadSettings();

// WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" CONNECTED");
Serial.println(WiFi.localIP());
myDisplay.print("Wifi OK");
delay(1000);
myDisplay.print(WiFi.localIP());
delay(1000);

// Tid
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);

// Webbserver
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});

server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request){
preferences.begin("bus-config", false);
for(int i=0; i<5; i++){
if(request->hasParam("name"+String(i), true)){
String n = request->getParam("name"+String(i), true)->value();
busConfigs[i].name = n;
preferences.putString(("name"+String(i)).c_str(), n);
}
if(request->hasParam("url"+String(i), true)){
String u = request->getParam("url"+String(i), true)->value();
busConfigs[i].url = u;
preferences.putString(("url"+String(i)).c_str(), u);
}
}
preferences.end();
request->send(200, "text/plain", "Sparat! Starta om buss-laget.");
});

server.begin();

// Pins
pinMode(CLK_PIN, INPUT);
pinMode(DT_PIN, INPUT);
pinMode(SW_PIN, INPUT_PULLUP);
lastStateCLK = digitalRead(CLK_PIN);
myDisplay.displayClear();
}

// ---------------- LOOP ----------------
void loop() {
// 1. Hantera Input (Knapp & Vrid)
handleInput();

// 2. Visa baserat på läge
switch (currentMode) {
case MODE_CLOCK:
runClockMode();
break;
case MODE_BUS:
runBusMode();
break;
case MODE_TEMP:
runTempMode();
break;
}
}

// ---------------- Input Hantering ----------------
void handleInput() {
// Läs Rotary för att byta busslinje (endast i BUS läge)
int currentStateCLK = digitalRead(CLK_PIN);
if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {
if (digitalRead(DT_PIN) != currentStateCLK) {
// Clockwise
if (currentMode == MODE_BUS) {
currentBusIndex++;
if(currentBusIndex > 4) currentBusIndex = 0;
busDataLoaded = false; // Tvinga ny hämtning
myDisplay.displayClear();
myDisplay.print(busConfigs[currentBusIndex].name);
delay(1000);
}
} else {
// Counter-Clockwise
if (currentMode == MODE_BUS) {
currentBusIndex--;
if(currentBusIndex < 0) currentBusIndex = 4;
busDataLoaded = false; // Tvinga ny hämtning
myDisplay.displayClear();
myDisplay.print(busConfigs[currentBusIndex].name);
delay(1000);
}
}
}
lastStateCLK = currentStateCLK;

// Läs Knapp för att byta MODE
if (digitalRead(SW_PIN) == LOW) {
if (millis() - lastButtonPress > 200) { // Debounce
myDisplay.displayClear();
if (currentMode == MODE_CLOCK) currentMode = MODE_BUS;
else if (currentMode == MODE_BUS) currentMode = MODE_TEMP;
else currentMode = MODE_CLOCK;
// Initiera det nya läget
if(currentMode == MODE_CLOCK) myDisplay.print("Klocka");
if(currentMode == MODE_BUS) {
myDisplay.print("Buss");
busDataLoaded = false;
}
if(currentMode == MODE_TEMP) myDisplay.print("Temp");
delay(1000);
myDisplay.displayClear();
lastButtonPress = millis();
}
}
}

// ---------------- Läge 1: Klocka ----------------
void runClockMode() {
static bool colonVisible = true;
static unsigned long lastUpdate = 0;
if (myDisplay.displayAnimate()) {
struct tm timeinfo;
if(!getLocalTime(&timeinfo)) return;

if (millis() - lastUpdate >= 1000) {
colonVisible = !colonVisible;
lastUpdate = millis();
char timeString[6];
strftime(timeString, sizeof(timeString), colonVisible ? "%H:%M" : "%H %M", &timeinfo);
myDisplay.setTextAlignment(PA_CENTER);
myDisplay.print(timeString);
updateProgressBar(timeinfo.tm_sec);
}
}
}

// ---------------- Läge 2: Buss ----------------
// --- Uppdaterad runBusMode med automatisk omladdning ---
void runBusMode() {
// 1. Kontrollera omladdning (samma logik som tidigare)
bool refreshNeeded = false;
if (busDataLoaded && cachedBusTimes[0] != "") {
int secondsLeft = calculateTimeLeft(cachedBusTimes[0]);
if (secondsLeft < -30) {
refreshNeeded = true;
}
}

if (millis() - lastBusFetch > 300000) refreshNeeded = true;

if (!busDataLoaded || refreshNeeded) {
if(fetchBusData(busConfigs[currentBusIndex].url)) {
busDataLoaded = true;
lastBusFetch = millis();
busDisplayState = 0;
lastDisplaySwitch = millis();
} else {
myDisplay.print("API Fel");
return;
}
}

// 2. Växla vy var 3:e sekund
if (millis() - lastDisplaySwitch > 3000) {
busDisplayState++;
if (busDisplayState > 2) busDisplayState = 0;
lastDisplaySwitch = millis();
myDisplay.displayClear();
}

// 3. Rendera texten - Nu med "minuter kvar" på alla tre lägen
String timeStr = cachedBusTimes[busDisplayState];
if (timeStr != "") {
int seconds = calculateTimeLeft(timeStr);
// Skapa prefix för att veta vilken buss i ordningen det är (valfritt)
// T.ex. "1: 5 m", "2: 15 m" etc. Eller bara "Om 5 m"
if (seconds <= 0 && seconds > -30) {
myDisplay.print("Nu!");
} else if (seconds < 0) {
myDisplay.print("Gått");
} else {
myDisplay.print(GetTimeLeft(seconds));
}
} else {
myDisplay.print("-");
}
}

// ---------------- Läge 3: Temp ----------------
void runTempMode() {
// Uppdatera temp sällan, t.ex. en gång per inpassering eller var 5e minut
// Här kör vi en förenklad version som hämtar en gång när man går in i läget
static unsigned long lastTempFetch = 0;
static String cachedTemp = "--";
if (millis() - lastTempFetch > 60000 || cachedTemp == "--") { // Hämta varje minut
cachedTemp = getTemperature();
lastTempFetch = millis();
}
myDisplay.setTextAlignment(PA_CENTER);
myDisplay.print(cachedTemp);
}

// ---------------- Hjälpfunktioner (Från din kod) ----------------

void updateProgressBar(int seconds) {
MD_MAX72XX* mx = myDisplay.getGraphicObject();
int totalLEDs = 8 * MAX_DEVICES;
int ledToLight = totalLEDs - 1 - ((seconds * totalLEDs) / 60);
for (int i = 0; i < totalLEDs; i++) mx->setPoint(7, i, false);
if (ledToLight >= 0 && ledToLight < totalLEDs) mx->setPoint(7, ledToLight, true);
}

String getCurrentTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return "";
char timeString[10];
strftime(timeString, sizeof(timeString), "%H:%M:%S", &timeinfo);
return String(timeString);
}

int calculateTimeLeft(String nextDepartureFull) {
// nextDepartureFull förväntas vara "YYYY-MM-DD HH:MM:SS"
struct tm now;
if (!getLocalTime(&now)) return -1;

struct tm depTime = {0};
// Parsar ut år, månad, dag, tim, min, sek
int year, month, day, hour, minute, second;
sscanf(nextDepartureFull.c_str(), "%d-%d-%d %d:%d:%d",
&year, &month, &day, &hour, &minute, &second);

depTime.tm_year = year - 1900;
depTime.tm_mon = month - 1;
depTime.tm_mday = day;
depTime.tm_hour = hour;
depTime.tm_min = minute;
depTime.tm_sec = second;

// Räkna ut skillnaden i sekunder
time_t now_t = mktime(&now);
time_t dep_t = mktime(&depTime);

return (int)difftime(dep_t, now_t);
}

String GetTimeLeft(int remainingSeconds) {
int minutes = remainingSeconds / 60;
int hours = minutes / 60;
minutes %= 60;
if (hours == 0) return String(minutes) + " min";
else return String(hours) + ":" + String(minutes);
}

String getTemperature() {
const String apiKey = "1e3f5f5693f6ee2987fbaa971bf20efc";
const String city = "Uppsala,SE";
const String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&units=metric&appid=" + apiKey;

HTTPClient http;
http.begin(url);
int httpCode = http.GET();
String result = "Err";
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
float temp = doc["main"]["temp"];
int rounded = round(temp);
result = String(rounded) + char(176) + "C";
}
http.end();
return result;
}