Kod

Posted På januari 10, 2026

#include <WiFi.h>

#include <time.h>

#include <MD_Parola.h>

#include <MD_MAX72xx.h>

#include <SPI.h>

#include <HTTPClient.h>

#include <ArduinoJson.h>

//gpio5 = clk

//gpio15 = sw

//#define BUTTON_PIN 15  // Define the pin number for the button

//#include <fonts/font5x7.h>  // Include the 5x7 font header

//#include <MD_MAX72XX_Fonts.h>

#define CLK_PIN 5  // Pin connected to CLK on KY-040

#define DT_PIN 19  // Pin connected to DT on KY-040

#define SW_PIN 15  // Pin connected to SW on KY-040

 

//https://www.teachmemicro.com/esp32-pinout-diagram-wroom-32/

 

int lastStateCLK;

int currentStateCLK;

int currentDirection;

int clickCount = 0;

unsigned long lastButtonPress = 0;



// WiFi credentials

const char* ssid = "Downingstreet10";

const char* password = "grus3000";

 

// NTP Server details

const char* ntpServer3 = "pool.ntp.org";

const char* ntpServer2 = "time.nist.gov";

const char* ntpServer1 = "time.google.com";

const long gmtOffset_sec = 3600;      // CET (UTC+1)

const int daylightOffset_sec = 3600;  // 1 hour for daylight saving time

//const long gmtOffset_sec = 0;      // CET (UTC+1)

//const int daylightOffset_sec = 0;  // 1 hour for daylight saving time



//UL api Börjegatan

String apiBorjeURL = "https://api.resrobot.se/v2.1/departureBoard?id=740007844&format=json&accessId=1658c2d5-35b4-42a2-b6d7-a691221c07e6";     // Replace 'xxx' with your actual accessId

String apiDalgatanURL = "https://api.resrobot.se/v2.1/departureBoard?id=740007658&format=json&accessId=1658c2d5-35b4-42a2-b6d7-a691221c07e6";  // Replace 'xxx' with your actual accessId

 

//Ekonomikum

//https://api.resrobot.se/v2.1/departureBoard?id=740007480&format=json&accessId=1658c2d5-35b4-42a2-b6d7-a691221c07e6

 

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW

#define MAX_DEVICES 4  // 4 blocks

#define CS_PIN 21



const char awithRing = char(229);

const char awithDots = char(228);

const char owithDots = char(246);

 

const char bigAwithRing = char(197);

const char bigAwithDots = char(196);

const char bigOwithDots = char(214);

 

//https://mischianti.org/images-to-byte-array-online-converter-cpp-arduino/

// Bitmap for a simple sun symbol (8x8)

 const unsigned char sun_bitmap [8] PROGMEM = {

 0x12, 0xbc, 0x7e, 0x7f, 0xfe, 0x7e, 0x3d, 0x48

};

 

const unsigned char cloud_bitmap [8] PROGMEM = {

 0x00, 0x00, 0x1c, 0x7e, 0xff, 0x7e, 0x00, 0x00

};

 

// 'mist', 8x8px

const unsigned char mist_bitmap [8] PROGMEM = {

 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x54, 0x28, 0x40

};

// 'rain', 8x8px

const unsigned char rain_bitmap [8] PROGMEM = {

 0x1c, 0x7e, 0xff, 0x7e, 0x00, 0x2a, 0x00, 0x54

};

// 'thunder', 8x8px

const unsigned char thunder_bitmap [8] PROGMEM = {

 0x04, 0x08, 0x10, 0x3c, 0x04, 0x08, 0x10, 0x20

};

// 'snow', 8x8px

const unsigned char snow_bitmap [8] PROGMEM = {

 0x91, 0x52, 0x3c, 0x3f, 0xfc, 0x3c, 0x4a, 0x89

};



// create an instance of the MD_Parola class

 

MD_Parola myDisplay = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

MD_MAX72XX myMatrix = MD_MAX72XX(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

 

void setup() {




 myDisplay.begin();

 myMatrix.begin();

 myDisplay.setIntensity(0);

 //  myDisplay.setFont(font5x7);  // Set the condensed 5x7 font

 

 myDisplay.displayClear();

 

 Serial.printf("Connecting to %s ", ssid);

 myDisplay.setTextAlignment(PA_LEFT);

 

 myDisplay.displayClear();

 //myDisplay.displayScroll(ssid, PA_LEFT, PA_SCROLL_LEFT, 100);

 myDisplay.displayAnimate();  // Start the animation

 while (!myDisplay.displayAnimate()) {

   // You can put some code here if needed or keep it empty

   //  Serial.begin(115200);

 }

 

 WiFi.begin(ssid, password);

 while (WiFi.status() != WL_CONNECTED) {

   //myDisplay.displayClear();

   delay(500);

   Serial.print(".");

   //myDisplay.print(".");

 }

 

 Serial.println(" CONNECTED");

 myDisplay.print("Wifi OK");

 delay(1000);

 

 //String ip = WiFi.localIP().toString();

 //char ip_char[ip.length() + 1];  // Correctly size the char array

 //ip.toCharArray(ip_char, ip.length() + 1);

 

 //myDisplay.displayClear();

 //myDisplay.displayScroll(ip_char, PA_LEFT, PA_SCROLL_LEFT, 100);

 

 // Keep animating the scroll until it's finished

 //while (!myDisplay.displayAnimate()) {

 // You can put some code here if needed or keep it empty

 //}

 

 myDisplay.displayClear();  // Clear the display after the scroll is finished

 

 // Init and get the time

 //configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

 myDisplay.print("Time");

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

 delay(500);

 myDisplay.displayClear();

 

 // Disconnect WiFi as it's no longer needed

 //WiFi.disconnect(true);

 //WiFi.mode(WIFI_OFF);

 //myDisplay.print("Disconnected");

 

 pinMode(CLK_PIN, INPUT);

 pinMode(DT_PIN, INPUT);

 pinMode(SW_PIN, INPUT_PULLUP);

 // Read the initial state of the CLK pin

 lastStateCLK = digitalRead(CLK_PIN);

}

 

void loop() {

 

 // Read the current state of the CLK pin

 currentStateCLK = digitalRead(CLK_PIN);

 

 // If the previous and the current state of CLK are different, then a pulse occurred

 if (currentStateCLK != lastStateCLK) {

   // If the DT state is different from the CLK state, then the direction is CW

   if (digitalRead(DT_PIN) != currentStateCLK) {

     currentDirection = currentDirection + 1;  // Clockwise

     Serial.println("Clockwise");

     //myDisplay.print(currentDirection);

   } else {

     currentDirection = currentDirection - 1;  // Counterclockwise

     Serial.println("Counterclockwise");

     //myDisplay.print(currentDirection);

   }

 }

 

 // Remember the last CLK state

 lastStateCLK = currentStateCLK;

 

 // Check if the button is pressed

 if (digitalRead(SW_PIN) == LOW) {

   // Debounce

   if (millis() - lastButtonPress > 50) {

     Serial.println("Button pressed");

     clickCount = clickCount + 1;

     if (clickCount > 4) {

       clickCount = 1;

     }

     setVerticalColumn(clickCount);

 

     // Reset the timer for multi-click detection

     lastButtonPress = millis();

   }

 }

 

 // Check for option selection after a delay

 if (millis() - lastButtonPress > 1000 && clickCount > 0) {

   switch (clickCount) {

     case 1:

       ShowBusTime(apiBorjeURL, "2", String("9 fr") + awithRing + String("n B") + bigOwithDots + String("rjegatan"));

       break;

     case 2:

       ShowBusTime(apiDalgatanURL, "1", String("1 fr") + awithRing + String("n Dalgatan"));

       break;

     case 3:

       DisplayWeather();

       break;

     case 4:

       DisplayIP();

       break;

   }

   clickCount = 0;

 }




 static bool colonVisible = true;            // Track whether the colon is currently visible

 static unsigned long lastUpdateTime = 0;    // Track the last time the colon visibility and progress bar were updated

 const unsigned long updateInterval = 1000;  // Interval to update colon visibility and progress bar (1 second)

 

 // Handle the scrolling animation

 if (myDisplay.displayAnimate()) {

   struct tm timeinfo;

   int retryCount = 0;

   const int maxRetries = 10;

   while (!getLocalTime(&timeinfo) && retryCount < maxRetries) {

     Serial.println("Retrying to obtain time...");

     retryCount++;

     myDisplay.displayClear();

     myDisplay.print(retryCount);

     delay(2000);  // Wait a bit before retrying

   }

   if (retryCount >= maxRetries) {

     Serial.println("Failed to obtain time after multiple attempts");

     myDisplay.print("Error!");

     return;

   }

 

   // Toggle colon visibility and update the progress bar based on time elapsed

   unsigned long currentTime = millis();

   if (currentTime - lastUpdateTime >= updateInterval) {

     colonVisible = !colonVisible;  // Toggle the colon visibility

     lastUpdateTime = currentTime;  // Update the last toggle time

 

     // Format the time string with or without the colon

     char timeString[9];

     if (colonVisible) {

       strftime(timeString, sizeof(timeString), "%H.%M", &timeinfo);

     } else {

       strftime(timeString, sizeof(timeString), "%H %M", &timeinfo);  // Replace colon with space

     }

 

     // Display the updated time

     myDisplay.setTextAlignment(PA_CENTER);

     myDisplay.print(timeString);

 

     // Update the progress bar on the last row

     int seconds = timeinfo.tm_sec;

     updateProgressBar(seconds);

   }

 }



 delay(100);  // Adjust this delay to control how often the loop runs

}

 

void DisplayIP() {

 //myDisplay.setFont(font5x7);

 String ip = WiFi.localIP().toString();

 char ip_char[ip.length() + 1];  // Correctly size the char array

 ip.toCharArray(ip_char, ip.length() + 1);

 myDisplay.displayClear();

 myDisplay.displayScroll(ip_char, PA_LEFT, PA_SCROLL_LEFT, 50);

 while (!myDisplay.displayAnimate()) {

   // You can put some code here if needed or keep it empty

 }

 //myDisplay.setFont(nullptr);  // Revert to the default font

}

 

void DisplayWeather() {

 myDisplay.displayClear();

 String weatherIcon = getWeatherIcon();



 myDisplay.displayClear();

 

 myMatrix.clear();  // Clear the display before drawing

 

 const uint8_t* selectedBitmap = nullptr;  // Pointer to selected bitmap



 if (weatherIcon == "01d") {

   selectedBitmap = sun_bitmap;

 } else if (weatherIcon == "02d" || weatherIcon == "03d" || weatherIcon == "04d") {

   selectedBitmap = cloud_bitmap;

 } else if (weatherIcon == "09d" || weatherIcon == "10d") {

   selectedBitmap = rain_bitmap;

 } else if (weatherIcon == "11d") {

   selectedBitmap = thunder_bitmap;

 } else if (weatherIcon == "13d") {

   selectedBitmap = snow_bitmap;

 } else if (weatherIcon == "50d") {

   selectedBitmap = mist_bitmap;

 } else {

   selectedBitmap = sun_bitmap;  // Default case

 }

 

 myDisplay.print(getTemperature());

 

 for (int y = 0; y < 8; y++) {

   myMatrix.setRow(3, y, selectedBitmap[y]);  // Set each row of the bitmap

 }

 // Update the display

 //delay(2000);

 

 //myDisplay.displayClear();

 

 delay(2000);

 myDisplay.displayClear();

}

 

void ShowBusTime(String url, String direction, String message) {

 String nextDepartureTime = getNextDepartureTime(url, direction);

 //myDisplay.displayClear();

 //myDisplay.print(nextDepartureTime);

 //delay(2000);

 String currentTime = getCurrentTime();

 int remainingSeconds = calculateTimeLeft(nextDepartureTime);

 String TimeLeft = GetTimeLeft(remainingSeconds);

 String shortDepartureTime = nextDepartureTime;

 shortDepartureTime.remove(shortDepartureTime.length() - 3);

 

 myDisplay.displayClear();

 myDisplay.print(String(shortDepartureTime));

 delay(1500);

 myDisplay.displayClear();

 myDisplay.print("om " + String(TimeLeft));

 delay(1500);

 myDisplay.displayClear();

 char message_char[message.length() + 1];  // Correctly size the char array

 message.toCharArray(message_char, message.length() + 1);

 myDisplay.displayScroll(message_char, PA_LEFT, PA_SCROLL_LEFT, 60);

 while (!myDisplay.displayAnimate()) {

   // You can put some code here if needed or keep it empty

 }

}

 

void setVerticalColumn(int input) {

 // Clear the display before lighting up new LEDs

 myDisplay.displayClear();

 

 // Validate the input range (1 to 4)

 if (input < 1 || input > 4) {

   return;  // Invalid input, do nothing

 }

 MD_MAX72XX* mx = myDisplay.getGraphicObject();

 // Mapping input to corresponding rows of the first vertical column (column 0)

 // The input controls which 2 LEDs to turn on in the first column (0)

 int row1 = (input - 1) * 2;      // First LED row based on input

 int row2 = (input - 1) * 2 + 1;  // Second LED row based on input

 

 // Light up the two LEDs in the first column (column 0)

 mx->setPoint(row1, 0, true);  // Turn on the pixel at (row1, column 0)

 mx->setPoint(row2, 0, true);  // Turn on the pixel at (row2, column 0)

 

 // Refresh the display after setting the points

 myDisplay.displayAnimate();

}

 

void updateProgressBar(int seconds) {

 MD_MAX72XX* mx = myDisplay.getGraphicObject();

 

 int totalLEDs = 8 * MAX_DEVICES;

 int ledToLight = totalLEDs - 1 - ((seconds * totalLEDs) / 60);

 

 // Clear the last row across all devices

 for (int i = 0; i < totalLEDs; i++) {

   mx->setPoint(7, i, false);  // Turn off all LEDs in the last row

 }

 

 // Light up only the calculated LED

 if (ledToLight >= 0 && ledToLight < totalLEDs) {

   mx->setPoint(7, ledToLight, true);

 }

}

 

String getWeatherIcon() {

 // Replace with your OpenWeatherMap API key and city ID for Uppsala

 const String apiKey = "1e3f5f5693f6ee2987fbaa971bf20efc";

 const String city = "Uppsala,SE";  // Uppsala, Sweden

 const String units = "metric";     // Celsius

 const String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&units=" + units + "&appid=" + apiKey;

 

 HTTPClient http;

 http.begin(url);

 int httpResponseCode = http.GET();

 

 if (httpResponseCode > 0) {

   String payload = http.getString();

 

   const char* jsonString = payload.c_str();

 

   // Create a JSON document

   StaticJsonDocument<2048> doc;

 

   // Deserialize the JSON string

   DeserializationError error = deserializeJson(doc, jsonString);

   if (error) {

     Serial.print("Failed to parse JSON: ");

     Serial.println(error.f_str());

     return "";

   }

 

   // Access the "icon" value

   const char* icon = doc["weather"][0]["icon"];



   http.end();

   return icon;

 } else {

   http.end();

   return "";

 }

}

 

String getTemperature() {

 // Replace with your OpenWeatherMap API key and city ID for Uppsala

 const String apiKey = "1e3f5f5693f6ee2987fbaa971bf20efc";

 const String city = "Uppsala,SE";  // Uppsala, Sweden

 const String units = "metric";     // Celsius

 const String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&units=" + units + "&appid=" + apiKey;

 

 HTTPClient http;

 http.begin(url);

 int httpResponseCode = http.GET();

 

 if (httpResponseCode > 0) {

   String payload = http.getString();

   int tempIndex = payload.indexOf("\"temp\":") + 7;                             // Find where the temperature starts in the JSON

   String temp = payload.substring(tempIndex, payload.indexOf(",", tempIndex));  // Extract the temperature value

   http.end();

 

   float number = temp.toFloat();

 

   // Round the number to the nearest integer

   int roundedNumber = round(number);  // Round to nearest integer

 

   // Optional: Convert back to string if needed

   String roundedTemp = String(roundedNumber);

 

   char degreeSymbol = char(176);

   return String(roundedTemp) + degreeSymbol;  // Return the temperature with the 'C' symbol

   //return String(roundedTemp);

 } else {

   http.end();

   return "N/A";  // Return "N/A" if the request failed

 }

}

 

String getNextDepartureTime(String apiURL, String direction) {

 if (WiFi.status() == WL_CONNECTED) {

   HTTPClient http;

   String originalApiURL = apiURL;  // Save the original API URL (removed const)

   bool retryWithNextDay = false;

 

   do {

     http.begin(apiURL);         // Specify the API URL (modified in case of retry)

     int httpCode = http.GET();  // Make the GET request

 

     if (httpCode > 0) {                   // Check for a successful connection

       String payload = http.getString();  // Get the response payload

 

       // Parse the JSON response

       StaticJsonDocument<2048> doc;

       DeserializationError error = deserializeJson(doc, payload);

 

       if (!error) {

         String currentTime = getCurrentTime();  // Get current time in HH:MM:SS format

         //Serial.println("Current Time: " + currentTime);

 

         JsonArray departures = doc["Departure"].as<JsonArray>();

         for (JsonObject departure : departures) {

           if (departure["directionFlag"] == "2") {

             String departureTime = departure["time"].as<String>();  // Get departure time

 

             // Compare departure time with current time

             if (departureTime > currentTime) {

               http.end();            // Close connection before returning

               return departureTime;  // Return the next valid departure time

             }

           }

         }

       } else {

         //Serial.println("Failed to parse JSON");

         myDisplay.displayClear();

         myDisplay.print("E parse JSON");

         delay(2000);

       }

     } else {

       //Serial.println("Error on HTTP request");

       myDisplay.displayClear();

       myDisplay.print("E http");

       delay(2000);

     }

 

     http.end();  // Close connection

 

     // If no valid departure was found, update the API URL with the next day

     if (!retryWithNextDay) {

       String nextDate = getNextDate();  // Function to get the next day's date in YYYY-MM-DD format

       apiURL = originalApiURL + "&date=" + nextDate;

       retryWithNextDay = true;  // Ensure we retry only once

     } else {

       break;  // If we already retried with the next day, exit the loop

     }

 

   } while (retryWithNextDay);

 } else {

   // Serial.println("Not connected to WiFi");

   myDisplay.displayClear();

   myDisplay.print("E wifi");

   delay(2000);

 }

 

 return "";  // Return an empty string if no valid departure time was found

}

 

// Function to get the next day's date in YYYY-MM-DD format

String getNextDate() {

 time_t now = time(nullptr);

 now += 24 * 60 * 60;  // Add one day (24 hours * 60 minutes * 60 seconds)

 struct tm* timeinfo = localtime(&now);

 char buffer[11];  // YYYY-MM-DD format

 strftime(buffer, sizeof(buffer), "%Y-%m-%d", timeinfo);

 return String(buffer);

}



// Function to get current time as a formatted string (HH:MM:SS)

String getCurrentTime() {

 struct tm timeinfo;

 if (!getLocalTime(&timeinfo)) {

   Serial.println("Failed to obtain time");

   return "";

 }

 char timeString[10];  // Buffer for the formatted time (HH:MM:SS)

 strftime(timeString, sizeof(timeString), "%H:%M:%S", &timeinfo);

 return String(timeString);

}

 

// Function to calculate how many seconds are left until the next departure

int calculateTimeLeft(String nextDeparture) {

 struct tm timeinfo;

 if (!getLocalTime(&timeinfo)) {

   Serial.println("Failed to obtain current time");

   return -1;

 }

 

 // Get the current time in seconds since midnight

 int currentSeconds = timeinfo.tm_hour * 3600 + timeinfo.tm_min * 60 + timeinfo.tm_sec;

 

 // Parse the next departure time (HH:MM:SS) and convert it to seconds since midnight

 int depHour = nextDeparture.substring(0, 2).toInt();

 int depMinute = nextDeparture.substring(3, 5).toInt();

 int depSecond = nextDeparture.substring(6, 8).toInt();

 int departureSeconds = depHour * 3600 + depMinute * 60 + depSecond;

 

 // Calculate the difference in seconds

 int remainingSeconds = departureSeconds - currentSeconds;

 

 // If the departure is on the next day (in case of times past midnight)

 if (remainingSeconds < 0) {

   remainingSeconds += 86400;  // 86400 seconds in a day

 }

 

 return remainingSeconds;

}

 

// Function to display how much time is left until the next departure

String GetTimeLeft(int remainingSeconds) {

 if (remainingSeconds < 0) {

   Serial.println("No valid departure time found.");

   return "Ingen avgång hittad";

 }

 

 int hours = remainingSeconds / 3600;

 remainingSeconds %= 3600;

 int minutes = remainingSeconds / 60;

 int seconds = remainingSeconds % 60;

 

 // Display the time left

 if (hours == 0) {

   return String(minutes) + " min";

 } else {

   return String(hours) + ":" + String(minutes);

 }

}