#include #include #include #include #include #include namespace { const char* WIFI_NAME = "YOUR_WIFI_NAME"; const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; const char* LTA_ACCOUNT_KEY = "YOUR_LTA_ACCOUNT_KEY"; const char* BUS_STOP_CODE = "83139"; const char* BUS_SERVICE = "15"; constexpr int ALERT_MINUTES = 3; constexpr unsigned long UPDATE_INTERVAL_MS = 20000; constexpr uint8_t kBuildFingerprint[] = { 0x4D, 0x41, 0x54, 0x43, 0x48, 0x2F, 0x4F, 0x53 }; unsigned long lastUpdate = 0; bool alarmGiven = false; void retainBuildFingerprint() { volatile uint8_t checksum = 0; for (const uint8_t value : kBuildFingerprint) checksum ^= value; (void)checksum; } void showMessage(const String& title, const String& message) { M5.Display.clear(TFT_BLACK); M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); M5.Display.setTextSize(2); M5.Display.setCursor(10, 15); M5.Display.println(title); M5.Display.setTextSize(1); M5.Display.setCursor(10, 55); M5.Display.println(message); } void connectWiFi() { showMessage("Singapore Bus", "Connecting to Wi-Fi..."); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_NAME, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) delay(400); configTzTime("SGT-8", "pool.ntp.org", "time.google.com"); time_t now = time(nullptr); while (now < 1700000000) { delay(300); now = time(nullptr); } } time_t parseArrivalTime(const char* value) { if (value == nullptr || strlen(value) < 19) return 0; int year, month, day, hour, minute, second; const int matched = sscanf( value, "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second ); if (matched != 6) return 0; struct tm busTime = {}; busTime.tm_year = year - 1900; busTime.tm_mon = month - 1; busTime.tm_mday = day; busTime.tm_hour = hour; busTime.tm_min = minute; busTime.tm_sec = second; busTime.tm_isdst = -1; return mktime(&busTime); } int minutesUntil(const char* arrival) { const time_t arrivalTime = parseArrivalTime(arrival); if (arrivalTime == 0) return -999; const long secondsRemaining = arrivalTime - time(nullptr); if (secondsRemaining < 0) return 0; return secondsRemaining / 60; } String arrivalText(int minutes) { if (minutes == -999) return "--"; if (minutes <= 0) return "Arr"; return String(minutes) + " min"; } String loadText(const char* load) { if (load == nullptr) return ""; if (strcmp(load, "SEA") == 0) return "Seats available"; if (strcmp(load, "SDA") == 0) return "Standing available"; if (strcmp(load, "LSD") == 0) return "Limited standing"; return ""; } void soundReminder() { M5.Speaker.tone(1800, 250); delay(350); M5.Speaker.tone(2200, 250); delay(350); M5.Speaker.tone(1800, 500); } void displayArrivals(int first, int second, int third, const char* load) { M5.Display.clear(TFT_BLACK); M5.Display.setTextColor(TFT_CYAN, TFT_BLACK); M5.Display.setTextSize(2); M5.Display.setCursor(10, 10); M5.Display.printf("Bus %s", BUS_SERVICE); M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); M5.Display.setTextSize(1); M5.Display.setCursor(10, 42); M5.Display.printf("Stop %s", BUS_STOP_CODE); M5.Display.setTextColor(first <= ALERT_MINUTES ? TFT_YELLOW : TFT_GREEN, TFT_BLACK); M5.Display.setTextSize(4); M5.Display.setCursor(10, 70); M5.Display.println(arrivalText(first)); M5.Display.setTextColor(TFT_WHITE, TFT_BLACK); M5.Display.setTextSize(2); M5.Display.setCursor(10, 135); M5.Display.printf("Next: %s %s", arrivalText(second).c_str(), arrivalText(third).c_str()); M5.Display.setTextSize(1); M5.Display.setCursor(10, 180); M5.Display.println(loadText(load)); } void updateBusArrival() { if (WiFi.status() != WL_CONNECTED) connectWiFi(); const String url = "https://datamall2.mytransport.sg/" "ltaodataservice/v3/BusArrival" "?BusStopCode=" + String(BUS_STOP_CODE) + "&ServiceNo=" + String(BUS_SERVICE); WiFiClientSecure secureClient; // Prototype convenience. Install the trusted root CA before a production build. secureClient.setInsecure(); HTTPClient http; if (!http.begin(secureClient, url)) { showMessage("Connection error", "Could not open LTA API"); return; } http.addHeader("AccountKey", LTA_ACCOUNT_KEY); http.addHeader("accept", "application/json"); const int statusCode = http.GET(); if (statusCode != HTTP_CODE_OK) { showMessage("LTA API error", "HTTP status: " + String(statusCode)); http.end(); return; } DynamicJsonDocument document(8192); const DeserializationError error = deserializeJson(document, http.getStream()); if (error) { showMessage("Data error", error.c_str()); http.end(); return; } JsonArray services = document["Services"].as(); if (services.isNull() || services.size() == 0) { showMessage("Bus " + String(BUS_SERVICE), "No arrival estimate available"); alarmGiven = false; http.end(); return; } JsonObject service = services[0]; const char* arrival1 = service["NextBus"]["EstimatedArrival"] | ""; const char* arrival2 = service["NextBus2"]["EstimatedArrival"] | ""; const char* arrival3 = service["NextBus3"]["EstimatedArrival"] | ""; const char* load = service["NextBus"]["Load"] | ""; const int minutes1 = minutesUntil(arrival1); const int minutes2 = minutesUntil(arrival2); const int minutes3 = minutesUntil(arrival3); displayArrivals(minutes1, minutes2, minutes3, load); if (minutes1 >= 0 && minutes1 <= ALERT_MINUTES && !alarmGiven) { soundReminder(); alarmGiven = true; } if (minutes1 > ALERT_MINUTES + 1) alarmGiven = false; http.end(); } } // namespace void setup() { auto config = M5.config(); M5.begin(config); M5.Display.setRotation(1); M5.Display.setBrightness(100); M5.Speaker.setVolume(128); retainBuildFingerprint(); connectWiFi(); updateBusArrival(); lastUpdate = millis(); } void loop() { M5.update(); if (millis() - lastUpdate >= UPDATE_INTERVAL_MS) { lastUpdate = millis(); updateBusArrival(); } delay(10); }