Compare commits
	
		
			No commits in common. "6e53521b8406a67c1acaa93980814fa6b6b93674" and "a563cc45b5069a93ba88e85b6d68cd227911a695" have entirely different histories.
		
	
	
		
			6e53521b84
			...
			a563cc45b5
		
	
		
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @ -2,21 +2,13 @@ | ||||
| 
 | ||||
| ## TODO | ||||
| 
 | ||||
| ### Server | ||||
| 
 | ||||
| - [x] Use timeout for fetching departures | ||||
| - [x] Add basic auth | ||||
| - [x] Create Nix package | ||||
| - [x] Create container | ||||
| - [x] Write ESP8266 client | ||||
| - [ ] Make port configurable | ||||
| - [x] Transfer using JSON | ||||
| - [x] Correctly implement basic auth | ||||
| - [ ] Make port configurable | ||||
| - [ ] Use unidecode to replace non-ascii stuff | ||||
| - [ ] Use unidecode to replace non-ascii stuff in the backend | ||||
| - [ ] Add query parameter for selecting EFAClient | ||||
| - [ ] Return current time for displaying clock | ||||
| 
 | ||||
| ### Client | ||||
| 
 | ||||
| - [x] Write ESP8266 client | ||||
| - [x] Add code for handling button presses | ||||
| - [ ] Add clock | ||||
|  | ||||
| @ -21,10 +21,10 @@ | ||||
| #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 | ||||
| 
 | ||||
| @ -34,48 +34,24 @@ | ||||
| #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_Y 9 | ||||
| #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); | ||||
| String fetchDepartures(); | ||||
| 
 | ||||
| // Peripherals
 | ||||
| 
 | ||||
| @ -83,40 +59,10 @@ 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 void enter() = 0; | ||||
|   virtual void tick() = 0; | ||||
|   virtual ~State() = default; | ||||
| }; | ||||
| 
 | ||||
| @ -129,6 +75,7 @@ public: | ||||
| class StateFetching : public State { | ||||
| public: | ||||
|   void enter() override; | ||||
|   void tick() override; | ||||
| }; | ||||
| 
 | ||||
| class StateShowingDepartures : public State { | ||||
| @ -140,24 +87,8 @@ 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; | ||||
| @ -176,7 +107,6 @@ void StateConnecting::enter() { | ||||
| 
 | ||||
|   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"); | ||||
| } | ||||
| 
 | ||||
| @ -184,7 +114,6 @@ 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; | ||||
| @ -204,16 +133,19 @@ void StateFetching::enter() { | ||||
|     setState<StateConnecting>(); | ||||
|     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); | ||||
|   String departuresRaw = fetchDepartures(); | ||||
|    | ||||
|   setState<StateShowingDepartures>(departuresRaw); | ||||
| }; | ||||
| 
 | ||||
| void StateFetching::tick() { | ||||
| } | ||||
| 
 | ||||
| StateShowingDepartures::StateShowingDepartures(String &departuresRaw) { | ||||
|   deserializeJson(departures, departuresRaw); | ||||
| } | ||||
| @ -222,26 +154,16 @@ void StateShowingDepartures::enter() { | ||||
|   Serial.println("Entering StateShowingDepartures"); | ||||
|   entered = millis(); | ||||
| 
 | ||||
|   clearStatus(); | ||||
|   clearMain(); | ||||
|   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); | ||||
|   display.fillRect(MAIN_X, MAIN_Y, MAIN_WIDTH, MAIN_HEIGHT, COLOR_BG); | ||||
| 
 | ||||
|   // draw timetable
 | ||||
|   display.setTextColor(COLOR_TEXT); | ||||
|   int line = 0; | ||||
|   for (JsonVariant departure : departures["departures"].as<JsonArray>()) { | ||||
|     if (line > 8) { | ||||
|       break; | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     char symbol[3] = {0}; | ||||
|     char direction[16] = {0}; | ||||
| 
 | ||||
|     utf8ToCP437German(departure["symbol"].as<const char *>(), symbol, std::size(symbol)); | ||||
|     utf8ToCP437German(departure["direction"].as<const char *>(), direction, std::size(direction)); | ||||
| 
 | ||||
|     display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); | ||||
|     display.printf("%2s %-15.15s ", symbol, direction); | ||||
|     display.printf("%2s %-15.15s ", | ||||
|       departure["symbol"].as<const char*>(), | ||||
|       departure["direction"].as<const char *>() | ||||
|     ); | ||||
|     if (departure["leaving"].as<String>().equals("sofort")) { | ||||
|       int16_t x = display.getCursorX(); | ||||
|       int16_t y = display.getCursorY(); | ||||
| @ -261,36 +183,6 @@ void StateShowingDepartures::tick() { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void StateShowingDepartures::button() { | ||||
|   setState<StateSelecting>(); | ||||
| } | ||||
| 
 | ||||
| void StateSelecting::enter() { | ||||
|   Serial.println("Entering StateSelecting"); | ||||
|   entered = millis(); | ||||
|    | ||||
|   clearMain(); | ||||
|   clearStatus(); | ||||
| } | ||||
| 
 | ||||
| void StateSelecting::tick() { | ||||
|   unsigned long elapsed = millis() - entered; | ||||
|   if (elapsed > 3000) { | ||||
|     setState<StateFetching>(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 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() { | ||||
| @ -303,15 +195,13 @@ void setup() { | ||||
|   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()); | ||||
| 
 | ||||
|   display.setTextSize(1); | ||||
|   display.setTextColor(COLOR_TEXT); | ||||
|   drawLayout(); | ||||
|   display.setCursor(TOP_X, TOP_Y); | ||||
|   display.setTextColor(COLOR_TOP); | ||||
|   display.print(timetables[selectedTimetable].label); | ||||
| 
 | ||||
|   WiFi.begin(WIFI_SSID, WIFI_PASSWORD); | ||||
|    | ||||
| @ -321,16 +211,13 @@ void setup() { | ||||
|   state = std::make_unique<StateConnecting>(); | ||||
| } | ||||
| 
 | ||||
| #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 if (buttonPushed) { | ||||
|     // "Handle" jitter
 | ||||
|     delay(100); | ||||
|     buttonPushed = false; | ||||
|     state->button(); | ||||
|   } else { | ||||
|     state->tick(); | ||||
|     currentTick++; | ||||
| @ -344,36 +231,22 @@ uint16_t rgb(uint16_t r, uint16_t g, uint16_t b) { | ||||
|   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, 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(String path) { | ||||
| String fetchDepartures() { | ||||
|   std::unique_ptr<BearSSL::WiFiClientSecure> https(new BearSSL::WiFiClientSecure); | ||||
|   https->setInsecure(); | ||||
|    | ||||
|   HTTPClient client; | ||||
|   if (!client.begin(*https, "vrnp.beany.club", 443, path)) { | ||||
|   if (!client.begin(*https, "vrnp.beany.club", 443, "/departures?" QUERY_STRING)) { | ||||
|     display.fillScreen(COLOR_BG); | ||||
|     display.setCursor(0, 0); | ||||
|     display.print("begin failed"); | ||||
| @ -392,70 +265,3 @@ String fetchDepartures(String path) { | ||||
| 
 | ||||
|   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'; | ||||
| } | ||||
| @ -16,7 +16,7 @@ | ||||
| 
 | ||||
|       vrnp-static = pkgs.buildGoModule { | ||||
|         pname = "vrnp"; | ||||
|         version = "0.0.10"; | ||||
|         version = "0.0.9"; | ||||
|         vendorHash = null; | ||||
| 
 | ||||
|         # For building the package, we use only the files not ignored by Git as inputs. | ||||
|  | ||||
							
								
								
									
										9
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								main.go
									
									
									
									
									
								
							| @ -17,7 +17,6 @@ import "time" | ||||
| // JSON unmarshaling types for departure monitor API | ||||
| 
 | ||||
| type DMResponse struct { | ||||
| 	DateTime DMDateTime `json:"dateTime"` | ||||
| 	DepartureList []DMDeparture `json:"departureList"` | ||||
| } | ||||
| 
 | ||||
| @ -214,7 +213,6 @@ func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) { | ||||
| 
 | ||||
| type Departures struct { | ||||
| 	Departures []Departure `json:"departures"` | ||||
| 	ServerTime string      `json:"serverTime"` | ||||
| } | ||||
| 
 | ||||
| type Departure struct { | ||||
| @ -275,12 +273,7 @@ func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, | ||||
| 		return cmp.Compare(a.Countdown, b.Countdown) | ||||
| 	}) | ||||
| 
 | ||||
| 	dt := response.DateTime | ||||
| 
 | ||||
| 	return Departures{ | ||||
| 		ds, | ||||
| 		fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute), | ||||
| 	}, nil | ||||
| 	return Departures{ds}, nil | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user