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;
}