Compare commits
	
		
			3 Commits
		
	
	
		
			72734c7faa
			...
			a563cc45b5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a563cc45b5 | |||
| 1c1aad5846 | |||
| f8fe64b1b3 | 
| @ -11,3 +11,4 @@ | |||||||
| - [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 | - [ ] Use unidecode to replace non-ascii stuff in the backend | ||||||
|  | - [ ] Add query parameter for selecting EFAClient | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ | |||||||
| #include <Adafruit_ST7735.h> | #include <Adafruit_ST7735.h> | ||||||
| #include <ESP8266WiFi.h> | #include <ESP8266WiFi.h> | ||||||
| #include <ESP8266HTTPClient.h> | #include <ESP8266HTTPClient.h> | ||||||
|  | #include <memory> | ||||||
| #include <SPI.h> | #include <SPI.h> | ||||||
| #include <WiFiClientSecureBearSSL.h> | #include <WiFiClientSecureBearSSL.h> | ||||||
| 
 | 
 | ||||||
| @ -41,49 +42,153 @@ | |||||||
| #define CLOCK_X MARGIN_LEFT | #define CLOCK_X MARGIN_LEFT | ||||||
| #define CLOCK_Y 113 | #define CLOCK_Y 113 | ||||||
| 
 | 
 | ||||||
| #define STATUS_X 95 | #define STATUS_X 83 | ||||||
| #define STATUS_Y CLOCK_Y | #define STATUS_Y CLOCK_Y | ||||||
| #define STATUS_WIDTH (10 * CX) | #define STATUS_WIDTH (12 * CX) | ||||||
| #define STATUS_HEIGHT CY | #define STATUS_HEIGHT CY | ||||||
| 
 | 
 | ||||||
|  | // Function definitions
 | ||||||
|  | 
 | ||||||
|  | void drawLayout(); | ||||||
|  | uint16_t rgb(uint16_t, uint16_t, uint16_t); | ||||||
|  | String fetchDepartures(); | ||||||
|  | 
 | ||||||
| // Peripherals
 | // Peripherals
 | ||||||
| 
 | 
 | ||||||
| Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RESET); | Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RESET); | ||||||
| 
 | 
 | ||||||
| // App state
 | // App state interface
 | ||||||
| 
 | 
 | ||||||
| typedef enum { | class State { | ||||||
|   CONNECTING, | public: | ||||||
|   SHOWING_DEPARTURES |   virtual void enter() = 0; | ||||||
| } State; |   virtual void tick() = 0; | ||||||
|  |   virtual ~State() = default; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| State state = CONNECTING; | class StateConnecting : public State { | ||||||
| State previous_state = state; | public: | ||||||
| int frame = 0; |   void enter() override; | ||||||
| // Use this variable to trigger state enter effects
 |   void tick() override; | ||||||
| int state_changed = 1; | }; | ||||||
|  | 
 | ||||||
|  | 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> state = std::make_unique<StateConnecting>(); | ||||||
|  | 
 | ||||||
|  | template <typename StateType, typename... Args> | ||||||
|  | void setState(Args&&... args) { | ||||||
|  |   state = std::make_unique<StateType>(std::forward<Args>(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<StateFetching>(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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<StateConnecting>(); | ||||||
|  |     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<StateShowingDepartures>(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<JsonArray>()) { | ||||||
|  |     display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); | ||||||
|  |     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(); | ||||||
|  |       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<const char*>()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     line++; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void StateShowingDepartures::tick() { | ||||||
|  |   unsigned long elapsed = millis() - entered; | ||||||
|  |   if (elapsed > 25000) { | ||||||
|  |     setState<StateFetching>(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| int buttonPushed = false; | int buttonPushed = false; | ||||||
| JsonDocument departures; |  | ||||||
| 
 | 
 | ||||||
| void ICACHE_RAM_ATTR onButtonFalling() { | void ICACHE_RAM_ATTR onButtonFalling() { | ||||||
|   buttonPushed = true; |   buttonPushed = true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void setup() { | void setup() { | ||||||
|   Serial.begin(9600); |   Serial.begin(9600); | ||||||
| 
 | 
 | ||||||
| @ -102,10 +207,39 @@ void setup() { | |||||||
|    |    | ||||||
|   pinMode(BUTTON_PIN, INPUT_PULLUP); |   pinMode(BUTTON_PIN, INPUT_PULLUP); | ||||||
|   attachInterrupt(BUTTON_PIN, onButtonFalling, FALLING); |   attachInterrupt(BUTTON_PIN, onButtonFalling, FALLING); | ||||||
|  | 
 | ||||||
|  |   state = std::make_unique<StateConnecting>(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #define QUERY_STRING "stop_id=de:08221:1225&platform=A" | #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
 | // TODO: Error handling
 | ||||||
| String fetchDepartures() { | String fetchDepartures() { | ||||||
|   std::unique_ptr<BearSSL::WiFiClientSecure> https(new BearSSL::WiFiClientSecure); |   std::unique_ptr<BearSSL::WiFiClientSecure> https(new BearSSL::WiFiClientSecure); | ||||||
| @ -131,69 +265,3 @@ String fetchDepartures() { | |||||||
| 
 | 
 | ||||||
|   return client.getString(); |   return client.getString(); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| void loop() { |  | ||||||
|   if (state != previous_state) { |  | ||||||
|     state_changed = 1; |  | ||||||
|     previous_state = state; |  | ||||||
|   } |  | ||||||
|   logic_loop(); |  | ||||||
|   frame++; |  | ||||||
|   state_changed = 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void logic_loop() { |  | ||||||
|   switch (state) { |  | ||||||
|     case CONNECTING: { |  | ||||||
|       if (WiFi.status() != WL_CONNECTED) { |  | ||||||
|         display.fillRect(STATUS_X, STATUS_Y, STATUS_WIDTH, STATUS_HEIGHT, COLOR_BG); |  | ||||||
|         display.setCursor(STATUS_X, STATUS_Y); |  | ||||||
|         display.printf(" connect %c", "|/-\\"[frame % 4]); |  | ||||||
|         delay(FRAME_DELAY); |  | ||||||
|         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(); |  | ||||||
|       deserializeJson(departures, departuresRaw); |  | ||||||
|        |  | ||||||
|       state = SHOWING_DEPARTURES; |  | ||||||
|       frame = 0; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|        |  | ||||||
|     case SHOWING_DEPARTURES: |  | ||||||
|       if (WiFi.status() != WL_CONNECTED) { |  | ||||||
|         state = CONNECTING; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       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<JsonArray>()) { |  | ||||||
|         display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); |  | ||||||
|         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(); |  | ||||||
|           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<const char*>()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         line++; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       delay(25000); |  | ||||||
|       state = CONNECTING; |  | ||||||
|       return; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ | |||||||
| 
 | 
 | ||||||
|       vrnp-static = pkgs.buildGoModule { |       vrnp-static = pkgs.buildGoModule { | ||||||
|         pname = "vrnp"; |         pname = "vrnp"; | ||||||
|         version = "0.0.8"; |         version = "0.0.9"; | ||||||
|         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. | ||||||
|  | |||||||
							
								
								
									
										35
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								main.go
									
									
									
									
									
								
							| @ -55,6 +55,7 @@ var allEfaClients []EFAClient = []EFAClient{ | |||||||
| 	BwegtEFAClient{}, | 	BwegtEFAClient{}, | ||||||
| 	VRNEFAClient{}, | 	VRNEFAClient{}, | ||||||
| 	KVVEFAClient{}, | 	KVVEFAClient{}, | ||||||
|  | 	VAGEFAClient{}, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type VRNEFAClient struct { | type VRNEFAClient struct { | ||||||
| @ -151,6 +152,38 @@ func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) { | |||||||
| 	return req, nil | 	return req, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type VAGEFAClient struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c VAGEFAClient) GetName() string { | ||||||
|  | 	return "VAG" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c VAGEFAClient) BuildRequest(stopId string) (*http.Request, error) { | ||||||
|  | 	// Create request object | ||||||
|  | 	req, err := http.NewRequest("GET", "https://efa.vagfr.de/vagfr3/XSLT_DM_REQUEST", nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Configure our request | ||||||
|  | 	query := url.Values{} | ||||||
|  | 	query.Set("coordOutputFormat", "EPSG:4326") | ||||||
|  | 	query.Set("depType", "stopEvents") | ||||||
|  | 	query.Set("includeCompleteStopSeq", "0") | ||||||
|  | 	query.Set("limit", "10") | ||||||
|  | 	query.Set("locationServerActive", "0") | ||||||
|  | 	query.Set("mode", "direct") | ||||||
|  | 	query.Set("name_dm", stopId) | ||||||
|  | 	query.Set("outputFormat", "json") | ||||||
|  | 	query.Set("type_dm", "stop") | ||||||
|  | 	query.Set("useOnlyStops", "1") | ||||||
|  | 	query.Set("useRealtime", "1") | ||||||
|  | 	req.URL.RawQuery = query.Encode() | ||||||
|  | 
 | ||||||
|  | 	return req, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) { | func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) { | ||||||
| 	req, err := c.BuildRequest(stopId) | 	req, err := c.BuildRequest(stopId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -281,7 +314,7 @@ func main() { | |||||||
| 			platform = &query["platform"][0] | 			platform = &query["platform"][0] | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		c := efaClients[(efaClient.Add(1)-1)%uint64(len(efaClients))] | 		c := efaClients[(efaClient.Add(1) - 1) % uint64(len(efaClients))] | ||||||
| 		ds, err := FetchDepartures(c, stopId) | 		ds, err := FetchDepartures(c, stopId) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user