Compare commits
8 Commits
f37942d267
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 91fbcb1c8f | |||
| dfd8fbb976 | |||
| 26e166c8dc | |||
| 2c8a4664a4 | |||
| 0bf2880d36 | |||
| 745445edeb | |||
| 8d9ceded83 | |||
| 57d13f7fd0 |
@@ -1,2 +1,115 @@
|
|||||||
# CTA-Trains
|
# CTA for ESP32
|
||||||
|
## Notes
|
||||||
|
### CTA API interface
|
||||||
|
The CTA API can be fairly trivially queries to get a JSON response with the required metadata.
|
||||||
|
- Docs are available here: https://www.transitchicago.com/developers/ttdocs/
|
||||||
|
- API consists of two main queries
|
||||||
|
- 'Arrivals' - which lets you query all arrivals for a station available at https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx
|
||||||
|
- 'Locations' - Which lets you query all stations along a line, and returns the location of all trains. Available at https://lapi.transitchicago.com/api/1.0/ttpositions.aspx
|
||||||
|
- The API is fairly simple and robust, there's no need for tokens or 2FA or oauth, a simple custom string will provide the data needed:
|
||||||
|
- The API in this instance is simply a HTTPS query with some parameters, you can see this by pasting the following into a browser:
|
||||||
|
|
||||||
|
```
|
||||||
|
To query XML: https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key=<KEYID>&mapid=<mapID>
|
||||||
|
To query JSON: https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key=<KEYID>&mapid=40380&outputType=JSON
|
||||||
|
|
||||||
|
Simple HTTP queries use the '?' to define start of parameters, and each parameter is seperated by an '&'
|
||||||
|
In the above, we are querying the 'ttarrivals' endpoint, used for the Arrivals API
|
||||||
|
KEYID is your API key
|
||||||
|
MAPID is the ID of a station on the CTA, such as "40380". These station numbers are available on the ttdocs website.
|
||||||
|
```
|
||||||
|
|
||||||
|
- The below is an example returned of the JSON station for mapid 40380, station Clarke/Lake
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The Arrivals API returns all the future trains to arrive at a station.
|
||||||
|
- JSON is typically an easier format to work with than XML, both are essentially ways to structure data that are nicely readable by applications (a tree structure).
|
||||||
|
- In the above example, we have an array of next arrivals, starting at the top of the tree, we can pick each like you would in a folder. Some pseudocode (i.e. made up simplified language):
|
||||||
|
- Say return json is `output.json = https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key=<KEYID>&mapid=40380&outputType=JSON`
|
||||||
|
- `output->ctatt` gives us tmst, errCd, errNm and eta 'folders'
|
||||||
|
- `output->ctatt->eta` gives us a list of folders for all the next upcoming trains.
|
||||||
|
- `output->ctatt->eta->0` gives us the next train.
|
||||||
|
- `output->ctatt->eta->1` gives us the second to next train.
|
||||||
|
- `output->ctatt->eta->0-arrT` gives us the time the next train will arrive in an ISO date format, i.e. "2026-07-12T00:50:16"
|
||||||
|
- The problem with the Arrivals API, is you then need to query every station one by one, save the next arrival time of a train and then do something with the result (say, turn on an LED if it's less than 2 minutes away).
|
||||||
|
- That's a lot of queries to check for the entire network. Especially for a little ESP32.
|
||||||
|
- Very wasteful and abusive of the API for just your usage.
|
||||||
|
- Enter the Locations API. Rather than query a station, we can query the entire line and get a list of trains on it.
|
||||||
|
|
||||||
|
```
|
||||||
|
To query JSON: https://lapi.transitchicago.com/api/1.0/ttpositions.aspx?key=<KEYID>&rt=<route>&outputType=JSON
|
||||||
|
|
||||||
|
KEYID is your API key
|
||||||
|
RT is the identifier of the line, e.g. 'Blue'. These are in the appendix on the ttdocs website.
|
||||||
|
```
|
||||||
|
|
||||||
|
- The below is an example returned of the JSON station for Blue route
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The Locations API install returns all trains along a route.
|
||||||
|
- In the above example, we have an array of all trains, starting at the top of the tree, we can pick each like you would in a folder. Some pseudocode:
|
||||||
|
- Say return json is output.json which = https://lapi.transitchicago.com/api/1.0/ttpositions.aspx?key=<KEYID>&rt=Blue&outputType=JSON
|
||||||
|
- `output->tmst` gives us the time we made this query, this is handy to find the current time.
|
||||||
|
- `output->ctatt->route->0` gives us all the trains on the first route (which is Blue, as that's what we asked for)
|
||||||
|
- `output->ctatt->route->0->train` gives us a list of folders for all the active trains.
|
||||||
|
- `output->ctatt->route->0->train->0` gives us the first train.
|
||||||
|
- `output->ctatt->route->0->train->1` gives us the second train.
|
||||||
|
- `output->ctatt->route->0->train->0->nextStaNm` gives us the name of it's next station.
|
||||||
|
- `output->ctatt->route->0->train->0->nextStaId` gives us the ID of it's next station.
|
||||||
|
- `output->ctatt->route->0->train->0->arrT` gives us the time to the next station in ISO format. "2026-07-12T00:50:16"
|
||||||
|
- So now, in a single query, we have all the information we need to know how far away "a train" is from any station on a line.
|
||||||
|
- A single query tells us everything we need to know to light up a line.
|
||||||
|
|
||||||
|
So example pseudocode:
|
||||||
|
```
|
||||||
|
# Create a simple 2D array of all stations (imagine table with two columns) and when the next train arrives. Let's set arrival time to 999 for now (i.e. not any time soon).
|
||||||
|
# We can use the station number instead of the name to keep code simpler (don't have to deal with spaces or symbols)
|
||||||
|
stations_eta = <array>
|
||||||
|
stations_eta[40380] = 999
|
||||||
|
stations_eta[40390] = 999
|
||||||
|
.... etc ....
|
||||||
|
|
||||||
|
# Get route data in JSON format
|
||||||
|
output_json = http_query(https://lapi.transitchicago.com/api/1.0/ttpositions.aspx?key=<KEYID>&rt=Blue&outputType=JSON)
|
||||||
|
|
||||||
|
# Get that into a useful array format instead of just a blob
|
||||||
|
output = json(output_json)
|
||||||
|
|
||||||
|
# We need the time this query was generated to be used for math later. Fortunately the API response stores this
|
||||||
|
time_now = output->tmst
|
||||||
|
|
||||||
|
# Now we can loop through all the trains and match up to the stations
|
||||||
|
for train in output->ctatt->route->0->train:
|
||||||
|
# 'train' will now refer to the first train in the folder, aka 0.
|
||||||
|
# So we can save the next stop of the train
|
||||||
|
next_stop = train->nextStaId
|
||||||
|
# The time of the next stop
|
||||||
|
time_to_stop = train->arrT
|
||||||
|
# Get the time difference.
|
||||||
|
time_eta = time_to_stop - time_now
|
||||||
|
# In our made up language, this is going to be in seconds.
|
||||||
|
time_eta_mins = time_eta / 60
|
||||||
|
|
||||||
|
# Now we can update the station array from earlier
|
||||||
|
stations_eta[next_stop] = time_eta_mins
|
||||||
|
|
||||||
|
# The loop will now run again, but next time it will refer to the second train (aka 1)
|
||||||
|
|
||||||
|
# We now have an array of all stations and the next train to arrive at them. For the ones with none any time soon, they'll still be 999 minutes away.
|
||||||
|
print(stations_eta[40380])
|
||||||
|
---> 6
|
||||||
|
|
||||||
|
print(stations_eta[40390])
|
||||||
|
---> 11
|
||||||
|
```
|
||||||
|
|
||||||
|
### ESP32 Implementation
|
||||||
|
- Micropython, Arduino IDE and PlatformIO are the three best options
|
||||||
|
- Easier programming - go with MicroPython
|
||||||
|
- Simpler setup, use either the Arduino IDE (for quickly getting going) or using PlatformIO with VS Code.
|
||||||
|
- Both of these are just fancy wrappers around C++ language, so plenty of documentation to google.
|
||||||
|
- The latter is harder to initially get going (mostly cause of VS Code weirdness), but it's a much nicer environment to code in than Arduino IDE.
|
||||||
|
- PlatformIO is completely compatable with Arduino code, so can always start in Arduino IDE and move from there.
|
||||||
|
- Example Arduino IDE code for the API is available in /example/src/
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <ArduinoJson.h> // Include ArduinoJson library
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
const char* ssid = "OSULL";
|
||||||
|
const char* password = "thisispassword";
|
||||||
|
const char* APIKey = "f4b7aff25779483bba8cac91900f6efc";
|
||||||
|
// Use HTTP instead of HTTPS for a bit more simplicity
|
||||||
|
String serverName = "http://lapi.transitchicago.com/api/1.0/ttpositions.aspx";
|
||||||
|
|
||||||
|
|
||||||
|
// Last run time for the loop, don't need to query too often, stored in milliseconds
|
||||||
|
unsigned long lastTime = 0;
|
||||||
|
// How often to update table (every 60 seconds)
|
||||||
|
// unsigned long queryTime = 60*1000
|
||||||
|
unsigned long queryTime = 5*1000;
|
||||||
|
|
||||||
|
// How far away the train (in seconds) should be from the station to trigger
|
||||||
|
unsigned int time_to_led = 120;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(1000);
|
||||||
|
Serial.print("Connecting to WiFi...");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Connected to WiFi!");
|
||||||
|
Serial.print("IP Address: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Only run if we haven't run in queryTime
|
||||||
|
if ((millis() - lastTime) > queryTime) {
|
||||||
|
// Query the HTTP endpoint with a POST request and parameters, only interested in Blue route for now
|
||||||
|
const char* route = "Blue";
|
||||||
|
|
||||||
|
// http object let's us query a webserver
|
||||||
|
HTTPClient http;
|
||||||
|
|
||||||
|
String serverPath = serverName + "?key=" + APIKey + "&rt=" + route + "&outputType=JSON";
|
||||||
|
http.useHTTP10(true);
|
||||||
|
http.begin(serverPath.c_str());
|
||||||
|
int httpResponse = http.GET();
|
||||||
|
|
||||||
|
// Minimum size acquired from pasting raw JSON into here: https://arduinojson.org/v5/assistant/
|
||||||
|
DynamicJsonDocument location_json(9128);
|
||||||
|
deserializeJson(location_json, http.getStream());
|
||||||
|
|
||||||
|
// Store time we queried server
|
||||||
|
const char* time_c = location_json["ctatt"]["tmst"];
|
||||||
|
struct tm time_tm;
|
||||||
|
strptime(time_c, "%Y-%m-%dT%H:%M:%S", &time_tm);
|
||||||
|
time_t time = mktime(&time_tm);
|
||||||
|
|
||||||
|
Serial.print("Time of query: ");
|
||||||
|
Serial.println(time_tm.tm_hour);
|
||||||
|
|
||||||
|
const char* nextStaID = location_json["ctatt"]["route"][0]["train"][0]["nextStaID"];
|
||||||
|
Serial.println(nextStaID);
|
||||||
|
|
||||||
|
// Move through the array to find
|
||||||
|
for (JsonObject train : location_json["ctatt"]["route"][0]["train"].as<JsonArray>()) {
|
||||||
|
unsigned int nextSta = train["nextStaId"];
|
||||||
|
const char* arrTime = train["arrT"];
|
||||||
|
// Annoying multistep process to get ISO time into time_t format
|
||||||
|
struct tm eta_tm;
|
||||||
|
strptime(arrTime, "%Y-%m-%dT%H:%M:%S", &eta_tm);
|
||||||
|
time_t eta = mktime(&eta_tm);
|
||||||
|
|
||||||
|
// Save station name for pretty output
|
||||||
|
const char* stationName = train["nextStaNm"];
|
||||||
|
|
||||||
|
double difference = difftime(eta, time);
|
||||||
|
|
||||||
|
if(difference < time_to_led) {
|
||||||
|
String output = String("Train arriving soon at ") + stationName + " in " + difference + " seconds.";
|
||||||
|
Serial.println(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user