GraphQL-Querying a Bus

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

Example-Query for the code

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.

GraphQL; Value passed as url parameter

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.

Query for reults on the EnTur endpoint

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;


  1. Code for setting up and writing to the OLED display

  2. 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

  3. 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!

BusSign, Esp8266
Running the code on a NodeMCU

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