#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);
}
}