Making something useful; an ESP8266 that tells you when the bus gets here.

I’ve been working a lot with APIs lately and figured that making something displaying information from an API with an ESP and an OLED sounded like a perfect weekend project. Preferably for something that i actually need to know, like when the bus gets here! But; GraphQL-queries….
The bus-company where i live (Oslo, Norway) is Ruter, which offers a full fledged web-page for just this usecase. But heavy webpages with lots of graphics are not great for microcontrollers like my ESPs .
Luckily, they also have an API which is free of charge and publicly available so, why not make our own?
The API
The local public transport-company, Ruter, uses the APIs of the national public transport company (?) EnTur.
The API is well documented on their website:
https://developer.entur.org/pages-journeyplanner-journeyplanner-v2
Now, what i wanted to query is the planned and actual departure-times from a local stop so that i can see when i need to leave my house to get to where i need to go on time.
The Journey-planner API lets you do just that.; The API lets you query for information based on a speciic stop, or a ‘quay’ (subsection of a stop, typically one side of the road for a buss-stop like the one i wanted)
I was really only interested in busses in one direction (going _from_ my house, obviously) and because the screen i was using was so small, i chose to only implement this and not busses both ways at my stop. But you could easily do that as well.
Finding what the stop-code for your stop actually is was not straight forward, but i used their trip-planner (https://ruter.no/reiseplanlegger) and saw that when you search for a specific stop, the stopCode is actually passed as a value in the query to their backend right in the URL.

Speaking of GraphQL….
GraphQL is a queryformat i had not previouly been aquainted with. I’ve mostly gotten by with using REST-style APIs, and been happily oblivious to the wonderous world of graphQL queries.
The format of these queries can be a bit daunting at first, but there are actually several upsides to them, especially working with embedded systems / IoT devices , where memory, bandwith, and resources in general, come at a premium.
What’s so cool about GraphQL is that what you put in is what you get out: this means that, if i was to query this API from a computer, or i would want more data, i could just add more fields in the query, and these fields ( if they exist) would be populated. Now, with this project, i just want some basic information back, and any additional data i get back from the API that im not going to use are just wasted resources in the Microcontrollers end.
With a regular REST-API, you would just have to query the API and handle what ever you got back through filtering on the receivers side; Getting a massive respons to a simple query is not ideal when using a small chip like the ESP8266, as this would impact performance.
The Query
To help you figure out the correct query EnTur hosts a great IDE with examples where you can try out different parameters and queries.
One of the standard queries for endpoints has these parameters, and returns a whole lot of interesting stuff.

I wanted to query the “quay” endpoint, and wanted some basic info both about the stop and about the departures, so i devised this query which i ended up using in the project:
{ quay( id: "NSR:Quay:7176" ) { name id estimatedCalls( numberOfDepartures: 3 ) { expectedDepartureTime destinationDisplay { frontText } serviceJourney { line { publicCode name } } expectedDeparture { time } } } }
But now for the part you are actually interested in; The code for the ESP
Writing the code for the ESP8266
The code needed to implement 3 big things to get where i wanted to go;
Code for setting up and writing to the OLED display
Making the request to the API. Actually multiple APIs, as i figured out it would be good to add a clock to know what time it is at the moment. This is somewhat incredibly one of the few things NOT available through the API.
Note to self; would make for a good feature request
Handling/Unpacking the JSON-response from the API
As previously mentioned, i wrote this code to fit on a screen that is 128 x 64, which is quite small, as this is what i had at hand. I would recommend considering a bigger one for usability and readability if you are making a permanent device. If your screen is a different resolution than the one i used, thats an easy fix in the code
I had some issues managing the array where the JSON data from the response was to be put; i like to use the ArduinoJSON – library and found that they have an “assistant” that turned out to be super helpfull in determining the correct array-size and how to define it in the code. Check it out here!
I also had som issues with the actual query and errors relating to the querytype; This was because i was sending the API content-type headers indicating that the query was in JSON-format when graphQL is something completely different. Obviously!

I have added quite a few comments throught the code underneath to help the reader (or my future self) make sense of it; There is also definitly room for improvement here, so i might get back to that when i find the time.
/* Ruter screen for busstop monitoring GraphQL - Querying for a Bus Source: Bitbrb.com */ #include <ESP8266WiFi.h> #include <ESP8266HTTPClient.h> #include <WifiClientSecure.h> #include <ArduinoJson.h> //Display libraries: #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> const char* wifiName = "YourWifiName"; const char* wifiPass = "YourWifiPassword"; //Web Server address to read/write from, Aka the API Endpoint const char* host = "https://api.entur.io/journey-planner/v2/graphql"; const char* fingerprint = "8F:38:83:6D:35:9A:28:E1:15:21:12:9F:14:4C:0E:F0:C2:C9:DF:95"; //Time-API address const char* timehost = "http://worldtimeapi.org/api/timezone/Europe/Oslo"; //Setup for display #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); void setup() { Serial.begin(115200); delay(10); Serial.println(); Serial.print("Connecting to "); Serial.println(wifiName); WiFi.begin(wifiName, wifiPass); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); //Local IP-address assigned to the ESP //init display if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64 Serial.println(F("SSD1306 allocation failed")); for(;;); } } void loop() { //Work starts here //Make request to api for bus times HTTPClient http; //Declare object of class HTTPClient Serial.print("connecting to:"); Serial.println(host); Serial.print("SHA1 fingerprint used::"); Serial.println(fingerprint); http.begin(host, fingerprint); //Specify request destination and fingerprint for cert to make it do HTTPS http.addHeader("Content-Type", "application/graphql"); //Content type here is important; Its not JSON http.addHeader("ET-Client-Name", "Esp8266-BitBrb"); Serial.print("HTTPs has begun"); int httpCode = http.POST("{quay(id:\"NSR:Quay:102546\"){name id estimatedCalls(numberOfDepartures:3){expectedDepartureTime destinationDisplay{frontText}serviceJourney{line{publicCode name}}expectedDeparture{time}}}}"); //Send the request Serial.print("POST sent"); String payload = http.getString(); //Get the response payload from server Serial.print("Response Code:"); //200 is OK Serial.println(httpCode); //Print HTTP return code Serial.print("Returned data from Server:"); Serial.println(payload); //Print request response payload if(httpCode == 200) { // Allocate JsonBuffer // Use arduinojson.org/assistant to compute the neccessary capacity. const size_t capacity = JSON_ARRAY_SIZE(3) + 11*JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(3) + 3*JSON_OBJECT_SIZE(4) + 720; DynamicJsonDocument jsonBuffer(capacity); // Parse JSON object DeserializationError error = deserializeJson(jsonBuffer, payload); if (error) { Serial.print(F("deserializeJson() failed: ")); Serial.println(error.c_str()); return; } //Unpack the response //2DO; Write a loop and function to make this look prettier... JsonObject data_quay = jsonBuffer["data"]["quay"]; const char* data_quay_name = data_quay["name"]; // name of busstop goes here JsonArray data_quay_estimatedCalls = data_quay["estimatedCalls"]; JsonObject data_quay_estimatedCalls_0 = data_quay_estimatedCalls[0]; const char* data_quay_estimatedCalls_0_expectedDepartureTime = data_quay_estimatedCalls_0["expectedDepartureTime"]; // timestamp const char* data_quay_estimatedCalls_0_destinationDisplay_frontText = data_quay_estimatedCalls_0["destinationDisplay"]["frontText"]; // Destination of bus const char* data_quay_estimatedCalls_0_serviceJourney_line_publicCode = data_quay_estimatedCalls_0["serviceJourney"]["line"]["publicCode"]; // "31" const char* data_quay_estimatedCalls_0_serviceJourney_line_name = data_quay_estimatedCalls_0["serviceJourney"]["line"]["name"]; // line-descripion String data_quay_estimatedCalls_0_expectedDeparture_time = data_quay_estimatedCalls_0["expectedDeparture"]["time"]; String data_quay_estimatedCalls_0_expectedDeparture_time_mod = data_quay_estimatedCalls_0_expectedDeparture_time.substring(0,5); JsonObject data_quay_estimatedCalls_1 = data_quay_estimatedCalls[1]; const char* data_quay_estimatedCalls_1_destinationDisplay_frontText = data_quay_estimatedCalls_1["destinationDisplay"]["frontText"]; const char* data_quay_estimatedCalls_1_serviceJourney_line_publicCode = data_quay_estimatedCalls_1["serviceJourney"]["line"]["publicCode"]; String data_quay_estimatedCalls_1_expectedDeparture_time = data_quay_estimatedCalls_1["expectedDeparture"]["time"]; String data_quay_estimatedCalls_1_expectedDeparture_time_mod = data_quay_estimatedCalls_1_expectedDeparture_time.substring(0,5); JsonObject data_quay_estimatedCalls_2 = data_quay_estimatedCalls[2]; const char* data_quay_estimatedCalls_2_destinationDisplay_frontText = data_quay_estimatedCalls_2["destinationDisplay"]["frontText"]; const char* data_quay_estimatedCalls_2_serviceJourney_line_publicCode = data_quay_estimatedCalls_2["serviceJourney"]["line"]["publicCode"]; String data_quay_estimatedCalls_2_expectedDeparture_time = data_quay_estimatedCalls_2["expectedDeparture"]["time"]; String data_quay_estimatedCalls_2_expectedDeparture_time_mod = data_quay_estimatedCalls_2_expectedDeparture_time.substring(0,5); // print extracted values Serial.println(F("Response:")); Serial.println(data_quay_name); Serial.println(data_quay_estimatedCalls_0_expectedDepartureTime); Serial.println(data_quay_estimatedCalls_0_destinationDisplay_frontText); Serial.println(data_quay_estimatedCalls_0_serviceJourney_line_publicCode); Serial.println(data_quay_estimatedCalls_0_serviceJourney_line_name); Serial.println(data_quay_estimatedCalls_0_expectedDeparture_time); http.end(); //Close connection //Update the clock String timeVariable = GetTime(); Serial.println(timeVariable); //Start wiritng to screen display.clearDisplay(); display.display(); delay(500); display.setTextColor(WHITE); display.setTextSize(1); display.setCursor(0,0); display.println(data_quay_name); //display.setCursor(95,0); display.setCursor(0,15); display.print(timeVariable); // Display static text display.setCursor(0, 35); display.print(data_quay_estimatedCalls_0_serviceJourney_line_publicCode); display.print(" "); display.print(data_quay_estimatedCalls_0_destinationDisplay_frontText); display.display(); display.setCursor(95, 35); display.println(data_quay_estimatedCalls_0_expectedDeparture_time_mod); display.display(); delay(500); display.setCursor(0, 45); display.print(data_quay_estimatedCalls_1_serviceJourney_line_publicCode); display.print(" "); display.print(data_quay_estimatedCalls_1_destinationDisplay_frontText); display.display(); display.setCursor(95, 45); display.println(data_quay_estimatedCalls_1_expectedDeparture_time_mod); display.display(); delay(500); display.setCursor(0, 55); display.print(data_quay_estimatedCalls_2_serviceJourney_line_publicCode); display.print(" "); display.print(data_quay_estimatedCalls_2_destinationDisplay_frontText); display.display(); display.setCursor(95, 55); display.println(data_quay_estimatedCalls_2_expectedDeparture_time_mod); display.display(); delay(500); } else { Serial.println("An issue occured in returning http response"); } delay(60000); //GET Data at every 5 seconds } String GetTime(){ HTTPClient httptime; //Declare object of class HTTPClient httptime.begin(timehost); //Specify request destination int httpCode = httptime.GET(); //Send the request String payload = httptime.getString(); //Get the response payload from server const size_t capacity = JSON_OBJECT_SIZE(15) + 350; DynamicJsonDocument doc(capacity); deserializeJson(doc, payload); String utc_datetime = doc["datetime"]; String datetime = utc_datetime.substring(11,16); //11,19 to get HH:mm:ss, 11, 16 to get HH:mm return datetime; }