Compare commits
	
		
			3 Commits
		
	
	
		
			a563cc45b5
			...
			6e53521b84
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e53521b84 | |||
| 4964c62212 | |||
| b753d9f8ea | 
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @ -2,13 +2,21 @@ | |||||||
| 
 | 
 | ||||||
| ## TODO | ## TODO | ||||||
| 
 | 
 | ||||||
|  | ### Server | ||||||
|  | 
 | ||||||
| - [x] Use timeout for fetching departures | - [x] Use timeout for fetching departures | ||||||
| - [x] Add basic auth | - [x] Add basic auth | ||||||
| - [x] Create Nix package | - [x] Create Nix package | ||||||
| - [x] Create container | - [x] Create container | ||||||
| - [x] Write ESP8266 client |  | ||||||
| - [ ] Make port configurable |  | ||||||
| - [x] Transfer using JSON | - [x] Transfer using JSON | ||||||
| - [x] Correctly implement basic auth | - [x] Correctly implement basic auth | ||||||
| - [ ] Use unidecode to replace non-ascii stuff in the backend | - [ ] Make port configurable | ||||||
|  | - [ ] Use unidecode to replace non-ascii stuff | ||||||
| - [ ] Add query parameter for selecting EFAClient | - [ ] 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 FRAME_DELAY 80 | ||||||
| #define COLOR_BG (rgb(0, 0, 0)) | #define COLOR_BG (rgb(0, 0, 0)) | ||||||
| #define COLOR_DIVIDER (rgb(0xff, 0x60, 0x0d)) | #define COLOR_DIVIDER (rgb(0xff, 0x60, 0x0d)) | ||||||
|  | #define COLOR_TOP (rgb(0xa0, 0xa0, 0xa0)) | ||||||
| #define COLOR_TEXT (rgb(0xff, 0x60, 0x0d)) | #define COLOR_TEXT (rgb(0xff, 0x60, 0x0d)) | ||||||
| 
 | 
 | ||||||
| // Layout
 | // Layout
 | ||||||
| 
 |  | ||||||
| #define WIDTH 160 | #define WIDTH 160 | ||||||
| #define HEIGHT 128 | #define HEIGHT 128 | ||||||
| 
 | 
 | ||||||
| @ -34,24 +34,48 @@ | |||||||
| #define MARGIN_LEFT 5 | #define MARGIN_LEFT 5 | ||||||
| #define MARGIN_RIGHT 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_X MARGIN_LEFT | ||||||
| #define MAIN_Y 9 | #define MAIN_Y 19 | ||||||
| #define MAIN_WIDTH (WIDTH - 2 * MARGIN_LEFT) | #define MAIN_WIDTH (WIDTH - 2 * MARGIN_LEFT) | ||||||
| #define MAIN_HEIGHT (8 * CY) | #define MAIN_HEIGHT (8 * CY) | ||||||
| 
 | 
 | ||||||
| #define CLOCK_X MARGIN_LEFT | #define CLOCK_X MARGIN_LEFT | ||||||
| #define CLOCK_Y 113 | #define CLOCK_Y 113 | ||||||
|  | #define CLOCK_WIDTH (5 * CX) | ||||||
|  | #define CLOCK_HEIGHT CY | ||||||
| 
 | 
 | ||||||
| #define STATUS_X 83 | #define STATUS_X 83 | ||||||
| #define STATUS_Y CLOCK_Y | #define STATUS_Y CLOCK_Y | ||||||
| #define STATUS_WIDTH (12 * CX) | #define STATUS_WIDTH (12 * CX) | ||||||
| #define STATUS_HEIGHT CY | #define STATUS_HEIGHT CY | ||||||
| 
 | 
 | ||||||
| // Function definitions
 | 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 drawLayout(); | ||||||
|  | void clearTop(); | ||||||
|  | void clearStatus(); | ||||||
|  | void clearMain(); | ||||||
|  | void clearClock(); | ||||||
| uint16_t rgb(uint16_t, uint16_t, uint16_t); | uint16_t rgb(uint16_t, uint16_t, uint16_t); | ||||||
| String fetchDepartures(); | String fetchDepartures(String); | ||||||
| 
 | 
 | ||||||
| // Peripherals
 | // Peripherals
 | ||||||
| 
 | 
 | ||||||
| @ -59,10 +83,40 @@ Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RESET); | |||||||
| 
 | 
 | ||||||
| // App state interface
 | // App state interface
 | ||||||
| 
 | 
 | ||||||
|  | //           init                                                   
 | ||||||
|  | //             │                                                    
 | ||||||
|  | //             │                                                   
 | ||||||
|  | //             │                                                    
 | ||||||
|  | //             │      ┌───┐                                         
 | ||||||
|  | //          ┌──▼──────▼┐  │                                         
 | ||||||
|  | //          │Connecting│  │!connected                               
 | ||||||
|  | //          └──┬───▲──┬┘  │                                         
 | ||||||
|  | //             │   │  └───┘                                         
 | ||||||
|  | //             │   │                                                
 | ||||||
|  | //    connected│   │!connected                                      
 | ||||||
|  | //             │   │                                                
 | ||||||
|  | //             │   │                                                
 | ||||||
|  | //             │   │                             ┌───┐              
 | ||||||
|  | //          ┌──▼───┴─┐   timeout (3s)   ┌────────▼┐  │              
 | ||||||
|  | //          │Fetching◄──────────────────┤Selecting│  │              
 | ||||||
|  | //          └──┬───▲─┘                  └────▲───┬┘  │button pressed
 | ||||||
|  | //             │   │                         │   └───┘              
 | ||||||
|  | // got response│   │timeout (25s)            │                      
 | ||||||
|  | //             │   │                         │                      
 | ||||||
|  | //             │   │                         │                      
 | ||||||
|  | //             │   │                         │button pressed        
 | ||||||
|  | //        ┌────▼───┴─────────┐               │                      
 | ||||||
|  | //        │Showing Departures│───────────────┘                      
 | ||||||
|  | //        └──────────────────┘                                      
 | ||||||
|  | 
 | ||||||
| class State { | class State { | ||||||
| public: | public: | ||||||
|   virtual void enter() = 0; |   // When the state is entered
 | ||||||
|   virtual void tick() = 0; |   virtual void enter(); | ||||||
|  |   // Called in the business loop
 | ||||||
|  |   virtual void tick(); | ||||||
|  |   // Called when the button is pushed
 | ||||||
|  |   virtual void button(); | ||||||
|   virtual ~State() = default; |   virtual ~State() = default; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -75,7 +129,6 @@ public: | |||||||
| class StateFetching : public State { | class StateFetching : public State { | ||||||
| public: | public: | ||||||
|   void enter() override; |   void enter() override; | ||||||
|   void tick() override; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class StateShowingDepartures : public State { | class StateShowingDepartures : public State { | ||||||
| @ -87,8 +140,24 @@ public: | |||||||
|   StateShowingDepartures(String&); |   StateShowingDepartures(String&); | ||||||
|   void enter() override; |   void enter() override; | ||||||
|   void tick() 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
 | // App state implementation
 | ||||||
| 
 | 
 | ||||||
| uint64_t currentTick = 0; | uint64_t currentTick = 0; | ||||||
| @ -107,6 +176,7 @@ void StateConnecting::enter() { | |||||||
| 
 | 
 | ||||||
|   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); |   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); | ||||||
|   display.setCursor(STATUS_X, STATUS_Y); |   display.setCursor(STATUS_X, STATUS_Y); | ||||||
|  |   display.setTextColor(COLOR_TEXT); | ||||||
|   display.printf("connecting"); |   display.printf("connecting"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -114,6 +184,7 @@ void StateConnecting::tick() { | |||||||
|   if (WiFi.status() != WL_CONNECTED) { |   if (WiFi.status() != WL_CONNECTED) { | ||||||
|     display.fillRect(STATUS_X + 11 * CX, STATUS_Y, CX, CY, COLOR_BG); |     display.fillRect(STATUS_X + 11 * CX, STATUS_Y, CX, CY, COLOR_BG); | ||||||
|     display.setCursor(STATUS_X + 11 * CX, STATUS_Y); |     display.setCursor(STATUS_X + 11 * CX, STATUS_Y); | ||||||
|  |     display.setTextColor(COLOR_TEXT); | ||||||
|     display.print("|/-\\"[currentTick % 4]); |     display.print("|/-\\"[currentTick % 4]); | ||||||
|     delay(125); |     delay(125); | ||||||
|     return; |     return; | ||||||
| @ -133,19 +204,16 @@ void StateFetching::enter() { | |||||||
|     setState<StateConnecting>(); |     setState<StateConnecting>(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); |   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); | ||||||
|  |   display.setTextColor(COLOR_TEXT); | ||||||
|   display.setCursor(STATUS_X, STATUS_Y); |   display.setCursor(STATUS_X, STATUS_Y); | ||||||
|   display.print("    fetching"); |   display.print("    fetching"); | ||||||
|    |    | ||||||
|   String departuresRaw = fetchDepartures(); |   String departuresRaw = fetchDepartures(timetables[selectedTimetable].path); | ||||||
|    |    | ||||||
|   setState<StateShowingDepartures>(departuresRaw); |   setState<StateShowingDepartures>(departuresRaw); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| void StateFetching::tick() { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| StateShowingDepartures::StateShowingDepartures(String &departuresRaw) { | StateShowingDepartures::StateShowingDepartures(String &departuresRaw) { | ||||||
|   deserializeJson(departures, departuresRaw); |   deserializeJson(departures, departuresRaw); | ||||||
| } | } | ||||||
| @ -154,16 +222,26 @@ void StateShowingDepartures::enter() { | |||||||
|   Serial.println("Entering StateShowingDepartures"); |   Serial.println("Entering StateShowingDepartures"); | ||||||
|   entered = millis(); |   entered = millis(); | ||||||
| 
 | 
 | ||||||
|   display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); |   clearStatus(); | ||||||
|   display.fillRect(MAIN_X, MAIN_Y, MAIN_WIDTH, MAIN_HEIGHT, COLOR_BG); |   clearMain(); | ||||||
| 
 | 
 | ||||||
|  |   // draw timetable
 | ||||||
|  |   display.setTextColor(COLOR_TEXT); | ||||||
|   int line = 0; |   int line = 0; | ||||||
|   for (JsonVariant departure : departures["departures"].as<JsonArray>()) { |   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.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); | ||||||
|     display.printf("%2s %-15.15s ", |     display.printf("%2s %-15.15s ", symbol, direction); | ||||||
|       departure["symbol"].as<const char*>(), |  | ||||||
|       departure["direction"].as<const char *>() |  | ||||||
|     ); |  | ||||||
|     if (departure["leaving"].as<String>().equals("sofort")) { |     if (departure["leaving"].as<String>().equals("sofort")) { | ||||||
|       int16_t x = display.getCursorX(); |       int16_t x = display.getCursorX(); | ||||||
|       int16_t y = display.getCursorY(); |       int16_t y = display.getCursorY(); | ||||||
| @ -183,6 +261,36 @@ 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; | int buttonPushed = false; | ||||||
| 
 | 
 | ||||||
| void ICACHE_RAM_ATTR onButtonFalling() { | void ICACHE_RAM_ATTR onButtonFalling() { | ||||||
| @ -195,13 +303,15 @@ void setup() { | |||||||
|   Serial.println("serial init"); |   Serial.println("serial init"); | ||||||
|   display.initR(); |   display.initR(); | ||||||
|   display.setSPISpeed(40000000); |   display.setSPISpeed(40000000); | ||||||
|  |   display.cp437(true); | ||||||
|   display.setRotation(3); |   display.setRotation(3); | ||||||
|   Serial.println("display initialized"); |   Serial.println("display initialized"); | ||||||
|   Serial.printf("display dimensions: %" PRId16 "x%" PRId16 "\n", display.width(), display.height()); |   Serial.printf("display dimensions: %" PRId16 "x%" PRId16 "\n", display.width(), display.height()); | ||||||
| 
 | 
 | ||||||
|   display.setTextSize(1); |  | ||||||
|   display.setTextColor(COLOR_TEXT); |  | ||||||
|   drawLayout(); |   drawLayout(); | ||||||
|  |   display.setCursor(TOP_X, TOP_Y); | ||||||
|  |   display.setTextColor(COLOR_TOP); | ||||||
|  |   display.print(timetables[selectedTimetable].label); | ||||||
| 
 | 
 | ||||||
|   WiFi.begin(WIFI_SSID, WIFI_PASSWORD); |   WiFi.begin(WIFI_SSID, WIFI_PASSWORD); | ||||||
|    |    | ||||||
| @ -211,13 +321,16 @@ void setup() { | |||||||
|   state = std::make_unique<StateConnecting>(); |   state = std::make_unique<StateConnecting>(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #define QUERY_STRING "stop_id=de:08221:1225&platform=A" |  | ||||||
| 
 |  | ||||||
| void loop() { | void loop() { | ||||||
|   if (stateChanged) { |   if (stateChanged) { | ||||||
|     stateChanged = false; |     stateChanged = false; | ||||||
|     // Note: enter() may call setState(). In that case we want to end up right back here.
 |     // Note: enter() may call setState(). In that case we want to end up right back here.
 | ||||||
|     state->enter(); |     state->enter(); | ||||||
|  |   } else if (buttonPushed) { | ||||||
|  |     // "Handle" jitter
 | ||||||
|  |     delay(100); | ||||||
|  |     buttonPushed = false; | ||||||
|  |     state->button(); | ||||||
|   } else { |   } else { | ||||||
|     state->tick(); |     state->tick(); | ||||||
|     currentTick++; |     currentTick++; | ||||||
| @ -231,22 +344,36 @@ uint16_t rgb(uint16_t r, uint16_t g, uint16_t b) { | |||||||
|   return ((b >> 3) & 0b11111) << 11 | ((g >> 2) & 0b111111) << 5 | ((r >> 3) & 0b11111); |   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() { | void drawLayout() { | ||||||
|   display.fillScreen(COLOR_BG); |   display.fillScreen(COLOR_BG); | ||||||
|   display.drawFastHLine(5, 4, 150, COLOR_DIVIDER); |   display.drawFastHLine(5, 15, 150, COLOR_DIVIDER); | ||||||
|   display.drawFastHLine(5, 109, 150, COLOR_DIVIDER); |   display.drawFastHLine(5, 109, 150, COLOR_DIVIDER); | ||||||
|   display.drawFastHLine(5, 123, 150, COLOR_DIVIDER); |   display.drawFastHLine(5, 123, 150, COLOR_DIVIDER); | ||||||
|   display.setCursor(CLOCK_X, CLOCK_Y); |  | ||||||
|   display.print("13:12"); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: Error handling
 | // TODO: Error handling
 | ||||||
| String fetchDepartures() { | String fetchDepartures(String path) { | ||||||
|   std::unique_ptr<BearSSL::WiFiClientSecure> https(new BearSSL::WiFiClientSecure); |   std::unique_ptr<BearSSL::WiFiClientSecure> https(new BearSSL::WiFiClientSecure); | ||||||
|   https->setInsecure(); |   https->setInsecure(); | ||||||
|    |    | ||||||
|   HTTPClient client; |   HTTPClient client; | ||||||
|   if (!client.begin(*https, "vrnp.beany.club", 443, "/departures?" QUERY_STRING)) { |   if (!client.begin(*https, "vrnp.beany.club", 443, path)) { | ||||||
|     display.fillScreen(COLOR_BG); |     display.fillScreen(COLOR_BG); | ||||||
|     display.setCursor(0, 0); |     display.setCursor(0, 0); | ||||||
|     display.print("begin failed"); |     display.print("begin failed"); | ||||||
| @ -265,3 +392,70 @@ String fetchDepartures() { | |||||||
| 
 | 
 | ||||||
|   return client.getString(); |   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 { |       vrnp-static = pkgs.buildGoModule { | ||||||
|         pname = "vrnp"; |         pname = "vrnp"; | ||||||
|         version = "0.0.9"; |         version = "0.0.10"; | ||||||
|         vendorHash = null; |         vendorHash = null; | ||||||
| 
 | 
 | ||||||
|         # For building the package, we use only the files not ignored by Git as inputs. |         # For building the package, we use only the files not ignored by Git as inputs. | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								main.go
									
									
									
									
									
								
							| @ -17,6 +17,7 @@ import "time" | |||||||
| // JSON unmarshaling types for departure monitor API | // JSON unmarshaling types for departure monitor API | ||||||
| 
 | 
 | ||||||
| type DMResponse struct { | type DMResponse struct { | ||||||
|  | 	DateTime DMDateTime `json:"dateTime"` | ||||||
| 	DepartureList []DMDeparture `json:"departureList"` | 	DepartureList []DMDeparture `json:"departureList"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -213,6 +214,7 @@ func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) { | |||||||
| 
 | 
 | ||||||
| type Departures struct { | type Departures struct { | ||||||
| 	Departures []Departure `json:"departures"` | 	Departures []Departure `json:"departures"` | ||||||
|  | 	ServerTime string      `json:"serverTime"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Departure struct { | type Departure struct { | ||||||
| @ -273,7 +275,12 @@ func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, | |||||||
| 		return cmp.Compare(a.Countdown, b.Countdown) | 		return cmp.Compare(a.Countdown, b.Countdown) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	return Departures{ds}, nil | 	dt := response.DateTime | ||||||
|  | 
 | ||||||
|  | 	return Departures{ | ||||||
|  | 		ds, | ||||||
|  | 		fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute), | ||||||
|  | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user