Now that we have our API returning data, we need to store and manage it properly so we can use it when needed. The needs break down as follows:
I need to store information about a train’s arrival time and destination at my assigned station so that I know how long the train will take to get here and if it’s going where I’m going.
I need to get that data onto the display.
I need to periodically refresh that information so that when an arrival time changes in the real world, it changes on the screen.
This is a deceptively simple breakdown.
Handling the not-so-perfect Path
One of the primary struggles I’ve run into during this project boils down to a key consideration for any project: fragility. I mean this in a few ways:
While Python is a very forgiving language, CircuitPython adopts significant limitations to run on microprocessors, so many common helper libraries are unavailable. This means you’re often rolling your own code, which if you have my level of experience is probably not the optimal way of doing things. This becomes problematic for the second point, which is:
Microprocessors don’t have a lot of memory. Shocking, I know! My prior experience with smaller processors has been almost exclusively on a raspberry pi, which has evolved through the years to become a quite capable machine in the current v4 iteration. Coding with an eye for memory allocation applies an additional constraint to problem-solving.
Microprocessors can also struggle to stay connected to wifi. I don’t know if this is due to my particular wifi setup, the wifimanager library we chose, or just the reality of this microprocessor, but I often run into connection issues that mess up my API calls, which plays into the next point:
Some APIs are more fragile than others; the WMATA API in particular likes to give back random None objects or empty trains every now and then, which can mess with your code if you’re not expecting it.
So what’s the takeaway? Know specifically what you want from an API response, and build out error handling and redundancy to cover all those non-optimal scenarios. I’ll also note that this takes a lot of finessing, and there are near-endless edge cases you’ll catch several hours into the whole thing working perfectly until it doesn’t. Don’t let that get you down! I’m sharing what took me hours and hours of reconfiguring, rewriting, shower-induced brain blasts, and a whole lot of banging my head against the wall. You just need to get over this hurdle so you move on to a different hurdle.
Defining a Train
The approach I took was to create a new Train class so that I could maintain a list of two Train objects that would always represent the East- and West-bound trains at my station. I took the minimum response fields we have to have for the display to work properly:
destination: this is the abbreviated station name actually shown on the signs.
destination_name: this is the full station name.
destination_code: this is the station code used in the API call
minutes: this is how many minutes away the train is from the input station. This doesn’t only show integers, it can also display “ARR” and “BRD” for arrival and boarding, respectively.
Building our Trains
Now that we have our class defined, the code is pretty simple. One really nice thing the WMATA API does is return the trains in descending arrival order; that means once we find a train going the right direction, we can be confident it’s the closest train and thus the train we want. Another nice thing is that (at least at my station), the Westbound train and Eastbound train have different letters in their station codes, so we can simply compare the first character of the station code to determine which direction the train is going.
Let’s take a look at the code:
# set up two train directions (A station code and B station code)
A_train=None
B_train=None
# check trains in json response for correct destination code prefixes
try:
for item in json_data['Trains']:
if item['Line'] is not "RD":
pass
# if no train and destination code prefix matches, add
if item['DestinationCode'][0] is "A" and A_train is None:
A_train = Train(item['Destination'], item['DestinationName'], item['DestinationCode'], item['Min'])
elif item['DestinationCode'][0] is "B" and B_train is None:
B_train = Train(item['Destination'], item['DestinationName'], item['DestinationCode'], item['Min'])
# if both trains have a train object, pass
else:
pass
except Exception as e:
print ("Error accessing the WMATA API: ", e)
pass
# merge train objects into trains array
# NOTE: None objects accepted, handled by update_trains function in display_manager.py
trains=[A_train,B_train]
# if train objects exist in trains array, add them to historical trains
if A_train is not None:
historical_trains[0] = A_train
if B_train is not None:
historical_trains[1] = B_train
# print train data
try:
for item in trains:
print("{} {}: {}".format(item.destination_code, item.destination_name, item.minutes))
except:
pass
return trains
First, we set up local variables and assign them a null value. This allows us to return those variables each time we run the function and handle any None values in our display function. I chose to do it this way because I wanted the sign to visually communicate to me that it was displaying non-standard data, and I found it easier to contain all of the different visual changes in the display manager instead of trying to send that data back as part of the train function.
NOTE: We wrap everything in a lot of try statements to handle wifi connection issues. This can often result in the whole function not returning much-that’s okay! We’ll handle it in the next post, just bear with me!
Next, we sort through the API response looking for our trains. Back to my earlier point about knowing what we’re looking for, this acknowledges there might be wacky data in the response. I’m on the Red line, so you can generally expect only Red line trains to pass through, but that 0.1% chance will be enough to crash the whole thing if you don’t account for it, so double-check that all your trains are actually running on the right line.
Finally, we check the letter character of the DestinationCode field against our known endpoints, “A” and “B”. If we find one and we don’t already have a Train object in that variable, then we can create a new Train object and populate it with our three mandatory fields.
Keeping our Trains on the Tracks
Let’s assume this all went smoothly the first time: great! You’re done, shoot that data over the display manager and grab a beer, your work here is done.
Now let’s assume this did not go smoothly, and you couldn’t get data for one of your Trains. How do you handle it? A few options:
Implode: we expected data and we got nothing. We’re not cut out for this, we should just crash the whole program and give up.
Display exactly what you got: nothing. Do you want to know how close the train is? Forget it!
Display a best guess at how far away the train is based on what you got last time.
Histrionics aside, the third option is clearly the best here because it fits the real-world scenario the best. We need to know how close the train is, and seeing that it’s 4 minutes away when it’s actually 3 minutes away is clearly more useful than seeing nothing! Now we could get fancy here and measure how much time it’s been since we got data from the API so we could adjust the arrival time, but I think that’s overengineering without knowing how often this happens. It turns out there’s an easier way to solve this problem, which we’ll touch on in the next post.
The way we’ll solve it today is by keeping a historical train set; every time we successfully build Train objects off of API data, we save them into a historical_trains set, overwriting as we go. This way we always have the most recent accurate train data available to us, and we can swap them out if the need arises. Note that we don’t do that swap in this function; the goal here is to just return the objects we got from the API, so if we get None, we return None. The historical_trains list is built here but used in our display_manager; we’re trying to set ourselves up for success over there instead of handling things here.
Finally, I print out a bit of train data as a sanity check, then we return a bundle of train objects.
Conclusion
Everything is really coming together! We have all our main components:
A way to display text on our panels
A way to connect to the internet to get train data
A way to store the train data we want to show on the panels
Now we’re at a point where we can start building out our main operating loop and connecting all these pieces together; this is the exciting part! Join me for the next one and consider subscribing if you want to be notified when the next post drops!
Code Example
# queries WMATA API to return an array of two Train objects | |
# input is StationCode from secrets.py, and a historical_trains array | |
def get_trains(StationCode, historical_trains): | |
try: | |
# query WMATA API with input StationCode | |
URL = 'https://api.wmata.com/StationPrediction.svc/json/GetPrediction/' | |
payload = {'api_key': secrets['wmata api key']} | |
response = wifi.get(URL + StationCode, headers=payload) | |
json_data = response.json() | |
except Exception as e: | |
print("Failed to get data, retrying\n", e) | |
wifi.reset() | |
# set up two train directions (A station code and B station code) | |
A_train=None | |
B_train=None | |
# check trains in json response for correct destination code prefixes | |
try: | |
for item in json_data['Trains']: | |
if item['Line'] is not "RD": | |
pass | |
# if no train and destination code prefix matches, add | |
if item['DestinationCode'][0] is "A" and A_train is None: | |
A_train = Train(item['Destination'], item['DestinationName'], item['DestinationCode'], item['Min']) | |
elif item['DestinationCode'][0] is "B" and B_train is None: | |
B_train = Train(item['Destination'], item['DestinationName'], item['DestinationCode'], item['Min']) | |
# if both trains have a train object, pass | |
else: | |
pass | |
except Exception as e: | |
print ("Error accessing the WMATA API: ", e) | |
pass | |
# merge train objects into trains array | |
# NOTE: None objects accepted, handled by update_trains function in display_manager.py | |
trains=[A_train,B_train] | |
# if train objects exist in trains array, add them to historical trains | |
if A_train is not None: | |
historical_trains[0] = A_train | |
if B_train is not None: | |
historical_trains[1] = B_train | |
# print train data | |
try: | |
for item in trains: | |
print("{} {}: {}".format(item.destination_code, item.destination_name, item.minutes)) | |
except: | |
pass | |
return trains |