#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_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 MAIN_X MARGIN_LEFT #define MAIN_Y 9 #define MAIN_WIDTH (WIDTH - 2 * MARGIN_LEFT) #define MAIN_HEIGHT (8 * CY) #define CLOCK_X MARGIN_LEFT #define CLOCK_Y 113 #define STATUS_X 83 #define STATUS_Y CLOCK_Y #define STATUS_WIDTH (12 * CX) #define STATUS_HEIGHT CY // Function definitions void drawLayout(); uint16_t rgb(uint16_t, uint16_t, uint16_t); String fetchDepartures(); // Peripherals Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RESET); // App state interface class State { public: virtual void enter() = 0; virtual void tick() = 0; virtual ~State() = default; }; class StateConnecting : public State { public: void enter() override; void tick() override; }; class StateFetching : public State { public: void enter() override; void tick() 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; }; // 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.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.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.setCursor(STATUS_X, STATUS_Y); display.print(" fetching"); String departuresRaw = fetchDepartures(); setState(departuresRaw); }; void StateFetching::tick() { } StateShowingDepartures::StateShowingDepartures(String &departuresRaw) { deserializeJson(departures, departuresRaw); } void StateShowingDepartures::enter() { Serial.println("Entering StateShowingDepartures"); entered = millis(); display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); display.fillRect(MAIN_X, MAIN_Y, MAIN_WIDTH, MAIN_HEIGHT, COLOR_BG); int line = 0; for (JsonVariant departure : departures["departures"].as()) { display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); display.printf("%2s %-15.15s ", departure["symbol"].as(), departure["direction"].as() ); 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++; } } void StateShowingDepartures::tick() { unsigned long elapsed = millis() - entered; if (elapsed > 25000) { setState(); } } 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.setRotation(3); Serial.println("display initialized"); Serial.printf("display dimensions: %" PRId16 "x%" PRId16 "\n", display.width(), display.height()); display.setTextSize(1); display.setTextColor(COLOR_TEXT); drawLayout(); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); pinMode(BUTTON_PIN, INPUT_PULLUP); attachInterrupt(BUTTON_PIN, onButtonFalling, FALLING); state = std::make_unique(); } #define QUERY_STRING "stop_id=de:08221:1225&platform=A" 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 { 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 drawLayout() { display.fillScreen(COLOR_BG); display.drawFastHLine(5, 4, 150, COLOR_DIVIDER); display.drawFastHLine(5, 109, 150, COLOR_DIVIDER); display.drawFastHLine(5, 123, 150, COLOR_DIVIDER); display.setCursor(CLOCK_X, CLOCK_Y); display.print("13:12"); } // TODO: Error handling String fetchDepartures() { std::unique_ptr https(new BearSSL::WiFiClientSecure); https->setInsecure(); HTTPClient client; if (!client.begin(*https, "vrnp.beany.club", 443, "/departures?" QUERY_STRING)) { 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(); }