LED Transit Map (Part 2 / 3)

Hello again folks, it's time for my update on my LED transit map! It's been a couple of weeks, I made a lot of progress on my LED map of Sound Transit's Link Light Rail, both the 1 and 2 lines are now operational on my map!

If you didn't read the first portion, most of it has changed but you can still read it here. The goal was simple, I wanted to show a live-service map of Sound Transit's Light Rail on LED strips in a way that I could hang on a wall. How did that go?

TL-DR...

Image of the finished transit map Please ignore the very messy wire-filled room, I still need to mount it

It's working!

OneBusAway

So some things changed since my last update. I was really happy to show the GTFS file feed in my last update – only to find out soon after that Sound Transit is one of the few providers that doesn't offer GTFS – at least not directly. They use OneBusAway, another relatively open standard for transit data.

OneBusAway does offer a full RESTful API, and in my case they offered a simple python package. Their docs are nicely formatted here, which I referenced quite a bit in my code. My overall strategy had to change, and I had to double check that it was a good approach with their contact to make sure I wasn't calling their API too much. Essentially:

On Startup: – Call their Routes API, which gives me all of the routes – Check against my config to see which routes I care about – For each route I care about, grab all of the trips for those routes.

Then I start a loop, where right now every 8 seconds I call their status API.

    vehicles_by_route = get_latest_feed()
    vehicles_set_this_iteration = {}
    for route_short_name in led_config:
        vehicles = vehicles_by_route.get(route_short_name)

        for vehicle_item in vehicles:
            vehicle = vehicle_item.get('vehicle')
            route: Route = vehicle_item.get('route')
            trip = vehicle_item.get('trip')
            next_stop_id = vehicle.next_stop

            stop = get_route_stop_config(route_short_name, trip.direction, next_stop_id)
            # Error handling is important folks, and makes it easier to debug
            if stop is None:
                print('WARN Stop {} was not found in config, direction {}'.format(next_stop_id, trip.direction))
                continue

Less naive way to update lights

So in my first version, I had a very naive approach to settings lights. On every iteration: – Clear the strip – Set the stations – Set the vehicles (overriding stations if there is a vehicle stopped there)

This caused a lot of slowdown, and actually the LED strips got overwhelmed at all of the changes, signals getting confused and weird lighting happening. This caused me to rethink my approach, and if you read the code you're probably wondering why I needed to save the “vehicles set this iteration”.

Rather than setting the light right there in that iteration, I now (each iteration) save which vehicles are supposed to be changed this iteration. Now I have a discreet list of only items that need to change.

I then also added to my set_single_led method a tracker to keep the last known LED color for each led. It's a simple dictionary of the LED code and the last color, and since I force that all LED changes must go through that method (encapsulation, remember, is a very good thing even in small side projects like this) I can know what the colors are right now on the strip.

So my loop now compares the last color that was set to what the next color should be. It then updates the LED only if it actually needs to. So instead of (length of strip) x 2 updates for each iteration, it's only (number of vehicles this time) - (number of vehicles who changed) + (any stations that are now empty). Much much better, and the colors are now correct.

Speaking of correct colors

LEDs are very fickle when it comes to colors. I had to decide what brightness I set my strip to on instantiation. By default it's 1, full brightness, which honestly makes it hard to look at. I kept tuning it down and frankly landed at 0.1, 10% brightness. It's still very bright even in a bright room. Here's the rub though, by being that “dark”, standard RGB color codes were very wrong. I noticed Green would come out much stronger than any other color. My station “Yellow” was more of a lime green. I had to play with the colors quite a bit, but to make something look right, that station yellow you see on the strip is actually #7F1200, closer to a scarlet, more like maroon.

So if you're working on light strips in the future, know that just because you have a hex color you like doesn't mean it's going to look right on the strip. Plan to play with your colors.

More accurate vehicle locations

In my first version I went for an algorithm in finding the percentage of the distance traveled by a train between stations, then finding the appropriate light to turn on. This was a good first approach, but ended up making it feel like trains rushed through portions, or through curves in the line wasn't very accurate.

After some trial and error, I decided on a different approach, to instead use GPS. The OneBusAway API does return lat/lon coordinates for each vehicle. What was best was to do this all manually. Which I dreaded, I very much wanted to make an algorithm to do the locations for me, but I realized that this portion of the project was less programming – but more artistic. I realized to have it look the way I wanted to, I would need to map out every single LED and have it map to exactly where I wanted trains to be.

Distances like Tukwila to Rainier Beach are about 6 miles and 10 minutes, while distances between Symphony and Westlake are only a couple of blocks, but still 2 minutes. I had to manually go station by station and gather how many lights I wanted each one to be.

My approach was pretty simple. Find the timetables gather how many minutes between each station. I still had a lot so just for buffering I added 1 to each then. With that I had about 130 of my total 160 lights used with stations and just minutes between stops. I then went through again and added where I felt necessary. Stadium and International District gets an additional one because it's going to split off there to Bellevue. This section has a slowdown, so add an additional light there. This section is at-grade, so it goes slower than other sections. Overall, that I came out with the full light strip being used.

I used this simple GetJson then to map out each LED bounding box between each station. I simply drew boxes around the line, this one has 7 between the stations so I draw seven boxes. I then copied the json into my config. Here's a sample:

            {
                "code": "40_532",
                "name": "Pioneer Square",
                "lat": 47.603199,
                "lon": -122.331581,
                "led": "1:229",
                "intermediaries": {
                  "type": "FeatureCollection",
                  "features": [
                    {
                      "type": "Feature",
                      "led": "1:228",
                      "geometry": {
                        "coordinates": [
                          [
                            -122.33415368888537,
                            47.60406523564518
                          ],
                          [
                            -122.33252440110098,
                            47.60230488328597
                          ],
                          [
                            -122.33028658414389,
                            47.603264481202274
                          ],
                          [
                            -122.33193550190164,
                            47.60505788942638
                          ],
                          [
                            -122.33414387389868,
                            47.604058617890246
                          ]
                        ],
                        "type": "LineString"
                      }
                    },
                    {
                      "type": "Feature",
                      "led": "1:227",
                      "geometry": {
                        "coordinates": [
                          [
                            -122.33581241836166,
                            47.60585199878702
                          ],
                          [
                            -122.334153685617,
                            47.604058617788695
                          ],
                          [
                            -122.33193549863327,
                            47.60504465406461
                          ],
                          [
                            -122.33355497143121,
                            47.60675859235138
                          ],
                          [
                            -122.33584186332143,
                            47.60585199878702
                          ]
                        ],
                        "type": "LineString"
                      }
                    },
                    {
                      "type": "Feature",
                      "led": "1:226",
                      "geometry": {
                        "coordinates": [
                          [
                            -122.3370883666264,
                            47.607327686853296
                          ],
                          [
                            -122.33490943958938,
                            47.60823425484057
                          ],
                          [
                            -122.33355497143121,
                            47.60676520976472
                          ],
                          [
                            -122.33582223334808,
                            47.60585861631506
                          ],
                          [
                            -122.33707855163973,
                            47.60728798278629
                          ]
                        ],
                        "type": "LineString"
                      }
                    }
                  ]
                }
            },

Yes, it was a lot. I did not enjoy doing it manually, but it gave the best results on the strip. Overall the file is currently at 10,934 lines.

Each stop has it's stop code, which is what the API says “this vehicle is en route” to. I then determine “is the vehicle at the stop”, and if not “Given the location, which bounding box is it located in”. I grab my LED code from the config, and move on.

But wait, there's more!

You may have asked, but Rob, what's with the red stations? There are in fact a few red stations on there. I didn't want to build this for today, I wanted to build this for the next several years. Those red stations are our future transit lines due to open over the next few years, and I wanted to capture them in my map.

To the bottom of the 1 line is the Federal Way Extension due open in 2026.

At the top right of the 2 line is the Downtown Redmond Extension due open in 2025.

The bridge is not lit up (working on very tiny soldering skills right now) and will connect the system probably (just me guessing) end of next year, maybe very early 2026.

Finally, the little dot on the one line is the future 130th Street Station, due open in 2026.

For a transit nerd like me it's a very fun time!

That's my project, I'll probably post here one more time when I get it mounted on the wall. I have a couple of minor bugs I want to resolve too before I get a full timelapse of it. It's been a very fun project out of my normal project scope, and I've had both a very fun and very frustrating time too building it! Developers, don't be afraid to branch out into electrical. Just be safe about it, and remember: Never skip the fuses!

I'll have one more small update showing off the mounted version, and I want to make a timelapse for you all to see it, but it's definitely a fun piece!