#include #include #include #include #include #include #include #include #include "bitmaps.h" #include "wifi_credentials.h" #define TFT_CS D8 #define TFT_DC D4 #define TFT_RESET D3 // Button stuff #define BUTTON_PIN D6 // Display stuff #define FRAME_DELAY 80 #define COLOR_BG (rgb(0, 0, 0)) #define COLOR_DIVIDER (rgb(0xff, 0x60, 0x0d)) #define COLOR_TOP (rgb(0xa0, 0xa0, 0xa0)) #define COLOR_TEXT (rgb(0xff, 0x60, 0x0d)) // Layout #define WIDTH 160 #define HEIGHT 128 #define CX 6 #define CY 8 #define MARGIN_LEFT 5 #define MARGIN_RIGHT 5 #define TOP_X MARGIN_LEFT #define TOP_Y 4 #define TOP_WIDTH (WIDTH - 2 * MARGIN_LEFT) #define TOP_HEIGHT CY #define MAIN_X MARGIN_LEFT #define MAIN_Y 19 #define MAIN_WIDTH (WIDTH - 2 * MARGIN_LEFT) #define MAIN_HEIGHT (8 * CY) #define CLOCK_X MARGIN_LEFT #define CLOCK_Y 113 #define CLOCK_WIDTH (5 * CX) #define CLOCK_HEIGHT CY #define STATUS_X 83 #define STATUS_Y CLOCK_Y #define STATUS_WIDTH (12 * CX) #define STATUS_HEIGHT CY struct Timetable { const char *label; const char *path; }; // Selectable timetables Timetable timetables[] = { {"Johanneskirche Bstg. 1", "/departures?stop_id=de:08311:30104&platform=1"}, {"Johanneskirche Bstg. 2", "/departures?stop_id=de:08311:30104&platform=2"}, {"Blumenthalstr Bstg. A", "/departures?stop_id=de:08221:1225&platform=A"}, {"Blumenthalstr Bstg. B", "/departures?stop_id=de:08221:1225&platform=B"} }; size_t selectedTimetable = 0; // Function definitions void drawLayout(); void clearTop(); void clearStatus(); void clearMain(); void clearClock(); uint16_t rgb(uint16_t, uint16_t, uint16_t); String fetchDepartures(String); // Peripherals Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RESET); // App state interface // init // │ // │ // │ // │ ┌───┐ // ┌──▼──────▼┐ │ // │Connecting│ │!connected // └──┬───▲──┬┘ │ // │ │ └───┘ // │ │ // connected│ │!connected // │ │ // │ │ // │ │ ┌───┐ // ┌──▼───┴─┐ timeout (3s) ┌────────▼┐ │ // │Fetching◄──────────────────┤Selecting│ │ // └──┬───▲─┘ └────▲───┬┘ │button pressed // │ │ │ └───┘ // got response│ │timeout (25s) │ // │ │ │ // │ │ │ // │ │ │button pressed // ┌────▼───┴─────────┐ │ // │Showing Departures│───────────────┘ // └──────────────────┘ class State { public: // When the state is entered virtual void enter(); // Called in the business loop virtual void tick(); // Called when the button is pushed virtual void button(); virtual ~State() = default; }; class StateConnecting : public State { public: void enter() override; void tick() override; }; class StateFetching : public State { public: void enter() override; }; class StateShowingDepartures : public State { private: // Todo: discard JsonDocument asap, parse into better format JsonDocument departures; unsigned long entered; public: StateShowingDepartures(String&); void enter() override; void tick() override; void button() override; }; class StateSelecting : public State { private: unsigned long entered; public: void enter() override; void tick() override; void button() override; }; // Empty default implementations void State::enter() {} void State::tick() {} void State::button() {} // App state implementation uint64_t currentTick = 0; // Initially true so that enter() of the initial state is called. bool stateChanged = true; std::unique_ptr state = std::make_unique(); template void setState(Args&&... args) { state = std::make_unique(std::forward(args)...); stateChanged = true; } void StateConnecting::enter() { Serial.println("Entering StateConnecting"); display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); display.setCursor(STATUS_X, STATUS_Y); display.setTextColor(COLOR_TEXT); display.printf("connecting"); } void StateConnecting::tick() { if (WiFi.status() != WL_CONNECTED) { display.fillRect(STATUS_X + 11 * CX, STATUS_Y, CX, CY, COLOR_BG); display.setCursor(STATUS_X + 11 * CX, STATUS_Y); display.setTextColor(COLOR_TEXT); display.print("|/-\\"[currentTick % 4]); delay(125); return; } setState(); } // Fetching is a bit of an ugly duckling; it performs all its logic in enter(). // This is because the request it does is synchronous. // Sadly this means no animation is possible right now. // At least the fetch call still does yield() under the hood :) void StateFetching::enter() { Serial.println("Entering StateFetching"); if (WiFi.status() != WL_CONNECTED) { setState(); return; } display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); display.setTextColor(COLOR_TEXT); display.setCursor(STATUS_X, STATUS_Y); display.print(" fetching"); String departuresRaw = fetchDepartures(timetables[selectedTimetable].path); setState(departuresRaw); }; StateShowingDepartures::StateShowingDepartures(String &departuresRaw) { deserializeJson(departures, departuresRaw); } void StateShowingDepartures::enter() { Serial.println("Entering StateShowingDepartures"); entered = millis(); clearStatus(); clearMain(); // draw timetable display.setTextColor(COLOR_TEXT); int line = 0; for (JsonVariant departure : departures["departures"].as()) { if (line > 8) { break; } char symbol[3] = {0}; char direction[16] = {0}; utf8ToCP437German(departure["symbol"].as(), symbol, std::size(symbol)); utf8ToCP437German(departure["direction"].as(), direction, std::size(direction)); display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); display.printf("%2s %-15.15s ", symbol, direction); if (departure["leaving"].as().equals("sofort")) { int16_t x = display.getCursorX(); int16_t y = display.getCursorY(); display.drawBitmap(x + 6 * CX - tiny_train_dims[0], y, tiny_train[0], tiny_train_dims[0], tiny_train_dims[1], COLOR_TEXT); } else { display.printf("%6s", departure["leaving"].as()); } line++; } clearClock(); display.setCursor(CLOCK_X, CLOCK_Y); display.printf("%5s", departures["serverTime"].as()); } void StateShowingDepartures::tick() { unsigned long elapsed = millis() - entered; if (elapsed > 25000) { setState(); } } void StateShowingDepartures::button() { setState(); } void StateSelecting::enter() { Serial.println("Entering StateSelecting"); entered = millis(); clearMain(); clearStatus(); } void StateSelecting::tick() { unsigned long elapsed = millis() - entered; if (elapsed > 3000) { setState(); } } void StateSelecting::button() { entered = millis(); selectedTimetable = (selectedTimetable + 1) % (sizeof timetables / sizeof timetables[0]); clearTop(); display.setCursor(TOP_X, TOP_Y); display.setTextColor(COLOR_TOP); display.print(timetables[selectedTimetable].label); } // button ISR setup int buttonPushed = false; void ICACHE_RAM_ATTR onButtonFalling() { buttonPushed = true; } void setup() { Serial.begin(9600); Serial.println("serial init"); display.initR(); display.setSPISpeed(40000000); display.cp437(true); display.setRotation(3); Serial.println("display initialized"); Serial.printf("display dimensions: %" PRId16 "x%" PRId16 "\n", display.width(), display.height()); drawLayout(); display.setCursor(TOP_X, TOP_Y); display.setTextColor(COLOR_TOP); display.print(timetables[selectedTimetable].label); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); pinMode(BUTTON_PIN, INPUT_PULLUP); attachInterrupt(BUTTON_PIN, onButtonFalling, FALLING); state = std::make_unique(); } void loop() { if (stateChanged) { stateChanged = false; // Note: enter() may call setState(). In that case we want to end up right back here. state->enter(); } else if (buttonPushed) { // "Handle" jitter delay(100); buttonPushed = false; state->button(); } else { state->tick(); currentTick++; } // Let background system do its thing yield(); } uint16_t rgb(uint16_t r, uint16_t g, uint16_t b) { // Color layout: RRRRRGGGGGGBBBBB (5, 6, 5) return ((b >> 3) & 0b11111) << 11 | ((g >> 2) & 0b111111) << 5 | ((r >> 3) & 0b11111); } void clearTop() { display.fillRect(TOP_X, TOP_Y, TOP_WIDTH, TOP_HEIGHT, COLOR_BG); } void clearMain() { display.fillRect(MAIN_X, MAIN_Y, MAIN_WIDTH, MAIN_HEIGHT, COLOR_BG); } void clearStatus() { display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); } void clearClock() { display.fillRect(CLOCK_X, CLOCK_Y, CLOCK_WIDTH, CLOCK_HEIGHT, COLOR_BG); } void drawLayout() { display.fillScreen(COLOR_BG); display.drawFastHLine(5, 15, 150, COLOR_DIVIDER); display.drawFastHLine(5, 109, 150, COLOR_DIVIDER); display.drawFastHLine(5, 123, 150, COLOR_DIVIDER); } // TODO: Error handling String fetchDepartures(String path) { std::unique_ptr https(new BearSSL::WiFiClientSecure); https->setInsecure(); HTTPClient client; if (!client.begin(*https, "vrnp.beany.club", 443, path)) { display.fillScreen(COLOR_BG); display.setCursor(0, 0); display.print("begin failed"); for (;;); } client.addHeader("Authorization", AUTH_TOKEN); client.addHeader("Accept", "application/json"); int statusCode = client.GET(); if (statusCode != 200) { display.fillScreen(COLOR_BG); display.setCursor(0, 0); display.printf("http non-ok: %d\n", statusCode); for (;;); } return client.getString(); } char mapUtf8CodepointToCP437German(uint32_t codepoint) { switch (codepoint) { case 0x00e4: return 0x84; // ä case 0x00c4: return 0x8e; // Ä case 0x00f6: return 0x94; // ö case 0x00d6: return 0x99; // Ö case 0x00fc: return 0x81; // ü case 0x00dc: return 0x9a; // Ü case 0x00df: return 0xe1; // ß default: return '?'; } } /* Maps german umlauts and sharp s from utf8 to cp437 encoding. Other utf8 characters and malformed utf8 are replaced by '?'. The resulting string is truncated to cp437StrSize. */ void utf8ToCP437German(const char *utf8Str, char *cp437Str, size_t cp437StrSize) { size_t utf8StrLength = strlen(utf8Str); size_t utf8StrIndex = 0; size_t cp437StrIndex = 0; char c; // One char at the end of the cp437Str is reserved to the terminating null while (utf8StrIndex < utf8StrLength && cp437StrIndex + 1 < cp437StrSize) { uint8_t cu0 = utf8Str[utf8StrIndex]; if (cu0 < 0x80) { // ASCII maps to ASCII c = cu0; utf8StrIndex++; } else if ((cu0 & 0b11100000) == 0b11000000) { // codepoints encoded as two code units may contain german special characters if (utf8StrIndex + 1 >= utf8StrLength) { // if there's no 10xxxxxxx byte after this one, we've reached the end (malformed) c = '?'; break; } else { // otherwise decode the codepoint and map it to cp437 uint8_t cu1 = utf8Str[utf8StrIndex + 1]; uint32_t cp = (cu0 & 0x1f << 6) | cu1 & 0x3f; c = mapUtf8CodepointToCP437German(cp); utf8StrIndex += 2; } // for 3 and 4-code unit codepoints just put a ? and skip all their code units // here we dont care whether the string is malformed } else if ((cu0 & 0b11110000) == 0b11100000) { c = '?'; utf8StrIndex += 3; } else if ((cu0 & 0b11111000) == 0b11110000) { c = '?'; utf8StrIndex += 4; } else { // catch all for malformed utf8 c = '?'; utf8StrIndex++; } cp437Str[cp437StrIndex] = c; cp437StrIndex++; } cp437Str[cp437StrIndex] = '\0'; }