HTMAA 2024
Sungmoon Lim

final project


final project

Update 1 (25 October)

I'll be providing final project updates on this page as I go through the maker process. This is an attempt to keep myself accountable and ensure that I'm making weekly progress.

I had a moment where I came up with several other random ideas that seemed interesting (this week, we're playing around with input devices, which made me think of creating a self-watering plant vase that relies on measuring soil moistness; I also went back to the wearable idea for a second) but I think I'll stick with the LED map idea as I still find it interesting, though I'm a bit concerned about how long soldering took me last week, as well as the complexity of the PCB layout. I met with Anthony to discuss project timeline and different steps involved in executing this, and came up with the below project plan (pending change).

First, things I will need:

boardupdate3

*In terms of the LEDs, if I wanted to plot every single station in the MTA system, I would need 472. Anthony said that I should start with a line or two initially then decide, given that 500 LEDs would make for a very ambitious project for the scope of this class, and because there are cost constraints per person. I'm also hoping to work with a 4 layer PCB. Initially wanted to make the map on a sheet of wood (simply for aesthetic purposes) and lasercut the subway lines and design the board as such, but I was advised against this as it would make the project much more complex.

Given the scope of this project, I will start off with a small prototype as an initial step. I determined that the first thing that I should do is figure how to program this. I'm going to start off with writing code for just one subway line to practice controlling the various LEDs using a single microcontroller and better understand power distribution for the PCB.

I decided to test this with one of the subway lines in New York just to make sure that data fetching worked properly, and that I could properly assign an LED to a station. The code can be found towards the end of this page./p>

Update 2 (3 November)
vinyl

Since the code for the isolated train line worked, I decided to focus more on the actual construction of the project. After giving some more thought, I scrapped some of my initial brainstorming and came up with the above design. The final deliverable will be made in 3 layers: (1) a wooden board on top with the subway line traces and holes for the LEDs (I decided I wanted to do this because I think it'd look nice, and so I can incorporate more CAD into my project), (2) standoffs to separate the wooden layer from the PCB and all of the wiring in the back, (3) the PCB itself that houses all of the connections.

Given the final 17 December deadline for this class, I've drafted a rough timeline of action items so I stay on track (particularly as I'll be out of town for 2 weeks in late November and early December):

boardupdate3

Because I have a general idea of what the code will look like, my first task for myself for now is to design the wooden board for the actual map.

The 2024 version of the NYC MTA map is quite ugly and hard to design around as the subway lines are squiggly. I found a different version that I found more accurate, better looking, and easier to design for; I got permission from the map creator to use it for my project (he's actually also based in New York, perhaps not surprisingly given his project) and started to vectorize it in Illustrator. Since I'll be using WS2818 LEDs, I designed the map so that each subway station is replaced by a 5mm circle to later fit the LED in.

boardupdate3
Update 3 (18 November)

I've started the delightful task of designing the schematic for this project. I'm hoping to get the schematic and PCB design finalized with a good amount of time, so it's my top priotity for the next week or so. Quentin suggested that I look more into Charlieplexing when designing my schematic, so that I could control all of my hundreds of LEDs just with one XIAO ESP32C3. I am budgeting 2 weeks to design this thing given its complexity.

Update 4 (25 November)

I finished the 2D design of the map! Now it's (in theory) ready to be laser cut. It was honestly more difficult than I had expected, since the vectorization didn't work perfectly, and while I loved the original map design, some elements of it wouldn't work super well for my project (for example, the map represented some subway lines with two separate lines to show the local versus the express trains. While true that they may technically use different tracks, I will be combining them into one line for the sake of simplicity and less visual chaos). I used Illustrator for this:

The lasercutter design aligns with the map like this (I had to be strategic in placing some of the stations since they would be too close to each other for soldering (since the LEDs are 5mm big, I had to leave some room). This is generally what the process looked like, though I'll probably do another review to make sure that the stations are more or less placed where they should be. I'm less concerned about the exact spacing because that doesn't really matter (and subway maps aren't 100% to scale in terms of subway station spacing anyways), I mostly care about the order of the stations and that each LED is programmable that it represents a specific station, which I'll do via software.

boardupdate3 boardupdate3

And here is the full map. I've added the silhouettes of the boroughs, but we'll see whether I actually want to etch this out. I decided to take out the subway line labels (the letters and numbers) because the map looked way too chaotic as is. The circles will be cut all the way through, so there are holes for the LEDs to shine through (I'm planning to use black acrylic for this now instead of wood -- I think it'll look quite nice). I'll etch out the subway lines so people kind of know what it's supposed to be, even if they aren't familiar with New York.

boardupdate3

The map isn't 100% aligned with the original map, because I omitted some parts for this project (sorry to those who take the S line, to those who live on Staten Island, etc).

Update 5 (28 November)

I am (mostly) finished with the schematic and am starting to think about how to design the PCB, all on KiCad again. In tandem, I decided that I would make a wooden easel for this project instead of 3d printing something given the size; I actually don't think I'll use the easel after presentation week (I plan to hang this up somewhere instead) but for the purposes of this class, I started to CAD an easel design of sorts in Fusion. I wanted to keep it fairly simple but have something in Helvetica font, as it would be emblematic of the subway logo design. Instead of a specific station entrance design like I'd originally thought, I kept it generic. I found these graphics online and copied them over to my Fusion file.

Sketch of board idea:

boardupdate3

Graphics for board logo inspo:

boardupdate3 boardupdate3

In Fusion:

boardupdate3 boardupdate3 boardupdate3 boardupdate3

Truthfully I'm not sure if I'll actually end up using this easel as I'd like to hang up my map on a wall (thought it may be hard since it'll be fairly heavy). I'm glad I designed it for the purposes of the class showcase, but I think the map would look better on its own? I think I'll finish making the map and see how I like it and whether it feels like it's missing something, then I'll decide whether or not I actually want to cut and build this, or if I just want to design and print some poster corner esque casings. Either way, the CAD is done and ready to go.

Update 5 (29 November)

The schematic for the PCB is completed, and I've moved onto the actual PCB design portion of this project. I actually separated each of the subway lines to make the overall schematic look less chaotic (and the schematic design more managable for me). In turn, I ended up with a bunch of schematics, which I've pasted below. It actually wasn't too difficult despite the sheer size of the project because once I figured out the logic for one subway line and figured out how to chain the LEDs together, I could do the same thing for the other subway lines. I will use a ground plane for this project because honestly, connecting everything to GND sounds like a nightmare in terms of routing.

Schematics for each of the lines (9 lines total, organized by color for simplicity):

boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3

Schematic for the PCB itself, sans LEDs

boardupdate3

I didn't do a great job of screenshotting the PCB design steps, but for the actual PCB design, I first exported my map design from Illustrator as an svg file then imported said svg file into KiCad. I put a WS2818B in each of the circles first before attempting any connections.

My map in KiCad view:

boardupdate3
Update 6 (9 December)

Today was a big day -- I had to play catch up a little bit as I was out last week for a conference. I couldn't do much while traveling, but I did finish the PCB design since I could do everything from a computer. Before getting into PCB creation, I was getting a bit antsy about having finished all of my designs and CAD but not having something tangible to show for it, so I decided to start off by lasercutting the vectorized map!

Because of the size of my board, I had to use the larger laser cutter in the EECS lab. I exported my Illustrator file as an svg and sent it over to CorelDRAW to clean things up. I also decided to get rid of the borough outlines, as it made it kind of look messy -- I'll be sticking to just the stations and actual lines.

I was a bit nervous since it was my first time cutting something big on acrylic, so I wanted to first test it out on cardboard. The design first looked like this:

boardupdate3

Cardboard test

boardupdate3

The cardboard ended up (mostly) looking really good, but there were 2 stations that didn't cut all the way through (they weren't converted to hairline, which was an oversight on my part) and one station that was only halfway cut. I adjusted these, then loaded my sheet of acrylic, and started the job.

boardupdate3

Lasercutter working its magic. The rastering looked really good; I was quite happy with my choice to go with acrylic versus wood.

Board complete!

boardupdate3 boardupdate3

Since the board design was successful, I decided to next shift focus and decide what to do about my PCB. Since my PCB is the size of my map, which is 24" x 18", Anthony said that I would have to vinyl cut this PCB as no mill on campus could handle a PCB of this size (also, who makes giant PCBs anyways). I knew that this would be a headache to solder, but I was okay with that. However, I didn't really consider how this would work for a two-layer PCB (which is how I designed mine... but by choice but perhaps by skill, since I couldn't figure out how to get everything on one layer and had to resort to putting things on another layer).

My PCB design (kept the KiCad box thing for scale):

boardupdate3

Quentin helped me a ton here and basically helped me reroute the second layer (had to use some wires and get creative) so that everything could fit into one layer, which solved the issue of having to mill a gigantic double sided board. Instead of using a vinyl cutter, we also decided to cut this PCB up into 9 smaller (still large) PCBs and connect them afterwards.

Quentin helping me with redesigning parts of the PCB:

boardupdate3

The updated single layer PCB design. I kept the GND layer but got rid of a bunch of necessary vias.

boardupdate3

I had to do all the milling for this board (in the end, 9 6x8" boards) in CBA because I had to use the Carvera. Quentin helped me change my PCB into an image (check out the pixels of this thing); after he got me set up on the mill, I started to mill each of my boards one by one. It was a frustrating process and I ended up having to re-mill several of them.

boardupdate3 boardupdate3 boardupdate3

Example of how each of the boards connected:

boardupdate3

The biggest HTMAA PCB? The whole thing was (almost exactly) 24" x 18" big.

boardupdate3

I soldered in between the PCBs to ensure connections (I generally soldered directly, but also used zero ohm resistors and wires to make connections, especially in small areas like pads or narrow connections)

boardupdate3

This section of the board kept shorting... Shoutout to Anthony who was a massive help in figuring it out (another section shorted, and I ended up just cutting out the whole thing because it wasn't an essential part of the board, lol). It was kind of fun debugging this, except for the fact that it was December 10th and this was due on December 17th.

boardupdate3
Update 7 (11 December)

I continued to work on my mega PCB and the software in conjunction, but I realized that I wanted a plan B just in case the board didn't work for whatever reason (Neil talked about Murphy's Law in class today, so...)

Given this, I decided to also look into using LED strips as a backup in case my board failed. I was fairly confident about my board, but I want another option because in the end, I wanted my project to work and to have a functional map. LED strips have their own annoyances, especially when trying to design it as a map, but at least I could check that the LEDs were wired correctly and working before loading software onto them. I ran this idea past Quentin and Anthony and they both agreed that it could be a good option.

I quickly redesigned a new PCB on KiCad that could handle several LED strips at once by using pin headers (I soldered on female pin headers, as Alec told me that this would be more secure and offer a better connection than soldering on male pin headers). I connected the GND and power pins to headers so that the board could handle a total of 9 different LED strips (this is essentially the 'motherboard' of the larger board and keeps all the essential connections of that PCB, just without the fancy NYC design).

boardupdate3

In my head, using the LED strips would be a much quicker alternative (read: it was not...) so that I could potentially make two boards. I decided to shift priorities and focus on finishing the project with the LED strips first so I'd have a 100% working project in the end (well perhaps not a foolproof plan, but at least I'd be able to iteratively test my software throughout the process), while working on the XL PCB in tandem.

As for the LED strips themselves, I had to make sure the LEDs aligned perfectly (well, near-perfectly) with the LED holes that I lasercut before. This proved to be difficult, but it still worked better than I'd anticipated:

boardupdate3

I repeated this process for each of the lines and made note of station jumps (sections of the LED strip that didn't fit into the LED holes and thus had to be "skipped" when station naming later).

Aligning the LEDs look SUCH a long time. Looking back, I totally underestimated this step and overestimated the PCB soldering. If I had just committed to the soldering, I think I could've been done with it in a day. (A very full day, but still in one day).

Since I have an understanding of all of the bits and pieces of my project now (for both the XL PCB version and the LED strips version, I've listed out everything I have used/will use below:

boardupdate3
Update 8 (12 December)

Using the code format from networking and communications week, I started to test out controlling multiple subway lines at once, ensuring that the live-time fetching worked properly despite having multiple lines.

The MTA APIs that I will be using are here: 123, ACE, NQRW, L, 456, 7, G, BDFM, JZ.

First, before aligning all of the LEDs as it was quite a tedious process, I first just tried my code with one line, the 123 (red). I first mapped it to just 50 LEDs to test the data fetching, and I tried my best to cross-check it with the train arrival data on Google Maps to ensure that the delays weren't too severe:

#include <Adafruit_NeoPixel.h>
        #include <WiFi.h>
        #include <HTTPClient.h>
        #include <WiFiClientSecure.h>
        #include <ArduinoJson.h>
        
        #define PIN_STRIP3 4  // 123 strip
        #define NUM_LEDS 50

        Adafruit_NeoPixel strip3 = Adafruit_NeoPixel(NUM_LEDS, PIN_STRIP3, NEO_GRB + NEO_KHZ800); // 123
        
        // Colors
        const uint32_t RED = strip3.Color(255, 0, 0);     // 123 lines
        const uint32_t OFF = strip3.Color(0, 0, 0);       // LED off
        
        // WiFi credentials
        const char* ssid = "USED MY HOTSPOT";
        const char* password = "PASSWORD";
        
        // MTA Feed URL
        const char* line123URL = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs";
        
        // Station mapping struct
        struct Station {
            const char* id;
            const char* name;
            int ledIndex;
        };
        
        // 123 subway line mapping (50 LEDs)
        Station line123Stations[] = {
            // 1 Line (Inbound)
            {"101", "Van Cortlandt Park-242 St", 0},
            {"103", "238 St", 1},
            {"104", "231 St", 2},
            {"106", "225 St", 3},
            {"107", "215 St", 4},
            {"108", "207 St", 5},
            {"109", "Dyckman St", 6},
            {"110", "191 St", 7},
            {"111", "181 St", 8},
            {"112", "168 St-Washington Hts", 9},
            {"113", "157 St", 10},
            {"114", "145 St", 11},
            {"115", "137 St-City College", 12},
            {"116", "125 St", 13},
            {"117", "116 St-Columbia University", 14},
            {"118", "Cathedral Pkwy (110 St)", 15},
            {"119", "103 St", 16},
            {"120", "96 St", 17},
            {"121", "86 St", 18},
            {"122", "79 St", 19},
            {"123", "72 St", 20},
            {"124", "66 St-Lincoln Center", 21},
            {"125", "59 St-Columbus Circle", 22},
            {"126", "50 St", 23},
            {"127", "Times Sq-42 St", 24},
            {"128", "34 St-Penn Station", 25},
            {"129", "28 St", 26},
            {"130", "23 St", 27},
            {"131", "18 St", 28},
            {"132", "14 St", 29},
            {"133", "Christopher St-Sheridan Sq", 30},
            {"134", "Houston St", 31},
            {"135", "Canal St", 32},
            {"136", "Franklin St", 33},
            {"137", "Chambers St", 34},
            {"138", "WTC Cortlandt", 35},
            {"139", "Rector St", 36},
            {"140", "South Ferry", 37},
            // 2/3 Line specific stations (Brooklyn)
            {"201", "Clark St", 38},
            {"202", "Borough Hall", 39},
            {"203", "Hoyt St", 40},
            {"204", "Nevins St", 41},
            {"205", "Atlantic Av-Barclays Ctr", 42},
            {"206", "Bergen St", 43},
            {"207", "Grand Army Plaza", 44},
            {"208", "Eastern Pkwy-Brooklyn Museum", 45},
            {"209", "Franklin Av", 46},
            {"210", "President St", 47},
            {"211", "Sterling St", 48},
            {"212", "Winthrop St", 49}
        };
        
        void setup() {
            Serial.begin(115200);
            delay(1000);
        
            // Initialize LED strip
            strip3.begin();
            strip3.setBrightness(50);
            strip3.show();
        
            // Connect to WiFi
            Serial.println("Connecting to WiFi...");
            WiFi.begin(ssid, password);
            while (WiFi.status() != WL_CONNECTED) {
                delay(500);
                Serial.print(".");
            }
            Serial.println("\nConnected!");
        }
        
        void loop() {
            if(WiFi.status() == WL_CONNECTED) {
                get123Data();
                delay(20000); // Update every 20 seconds
            }
        }
        
        int findLEDIndex(const char* stationId, Station* stations, int numStations) {
            for(int i = 0; i < numStations; i++) {
                if(strcmp(stations[i].id, stationId) == 0) {
                    return stations[i].ledIndex;
                }
            }
            return -1;  // Station not found
        }
        
        void get123Data() {
            WiFiClientSecure client;
            client.setInsecure();
            HTTPClient https;
            Serial.println("Getting 123 data...");
            
            if(https.begin(client, line123URL)) {
                int httpCode = https.GET();
                if(httpCode > 0) {
                    String payload = https.getString();
                    update123Strip();
                }
                https.end();
            }
        }
        
        void update123Strip() {
            // Clear all LEDs first
            for(int i = 0; i < NUM_LEDS; i++) {
                strip3.setPixelColor(i, OFF);
            }
            
            // GTFS data parsing
            // for specific trains found in GTFS data
            // int ledIndex = findLEDIndex(stationId, line123Stations, sizeof(line123Stations)/sizeof(Station));
            // if(ledIndex >= 0) {
            //     strip3.setPixelColor(ledIndex, RED);
            // }
            
            strip3.show();
        

It worked well! You'll see that I'm still missing some LEDs as I haven't finished aligning all of them to my board yet, but the ones that I programmed (123) are working as intended. (You'll also see me trying to avoid getting in the picture by way of reflection in the acrylic board...).

vinyl vinyl

Here's a video that shows the refresh rate. You'll see that there are jumps in stations, but that's because I haven't properly mapped each station number yet, which I have to do to ensure that it aligns with the LED positioning/alignment with my map (also because of handling both local and express lines in the same code; I'm not sure if I'll be able to parse out the individual lines, as I grouped the LEDs by color):

As shown in my code, I also wanted to map out stations that were not only at the station (LED on), but also trains that were quickly approaching the stations (LED blinking). No train at the station and no train appraoching meant that the LED would be off entirely.

In order to do this, I defined the time threshold:

#define APPROACHING_TIME 120 // 2 minutes in seconds for approaching trains
      

In the parseTrainData function, it checks this condition:


        if (timeToArrival <= 0) {
        states[ledIndex].hasTrainAtStation = true;
     } else if (timeToArrival <= APPROACHING_TIME) {
        states[ledIndex].hasTrainApproaching = true;
     }
      

        if (states[i].hasTrainAtStation) {
            strip.setPixelColor(i, color);
     }
        else if (states[i].hasTrainApproaching) {
            strip.setPixelColor(i, blinkState ? color : OFF);  // Makes LED blink
     }
      

I managed the blink effect with this:

const unsigned long BLINK_INTERVAL = 500; // 500ms blink rate

After confirming that that worked, I mapped each station array, this time for two different lines assigning an individual neopixel to each station. While I won't be assigning consecutive LEDs to each station due to the jumps in the LED strip positioning, I tested to make sure that multiple lines could go at once:

#include <Adafruit_NeoPixel.h>
        #include <WiFi.h>
        #include <HTTPClient.h>
        #include <WiFiClientSecure.h>
        #include <ArduinoJson.h>
        
        #define PIN_STRIP1 2  // ACE strip
        #define PIN_STRIP2 3  // NQRW strip
        #define NUM_LEDS 50
        
        Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(NUM_LEDS, PIN_STRIP1, NEO_GRB + NEO_KHZ800); // ACE
        Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(NUM_LEDS, PIN_STRIP2, NEO_GRB + NEO_KHZ800); // NQRW
        
        // Colors
        const uint32_t BLUE = strip1.Color(0, 0, 255);    // ACE lines
        const uint32_t YELLOW = strip2.Color(255, 255, 0); // NQRW lines
        const uint32_t OFF = strip1.Color(0, 0, 0);       // LED off
        
        // WiFi credentials
        const char* ssid = "HOTSPOT";
        const char* password = "PASSWORD";
        
        // MTA Feed URLs
        const char* aceURL = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-ace";
        const char* nqrwURL = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-nqrw";
        
        // Station mapping structs
        struct Station {
            const char* id;
            const char* name;
            int ledIndex;
        };
        
        // ACE Station Mapping (50 LEDs)
        Station aceStations[] = {
            // A Line (Inbound)
            {"A02", "Inwood-207 St", 0},
            {"A03", "Dyckman St", 1},
            // ... [Previous station entries remain the same]
        };
        
        // NQRW Station Mapping (50 LEDs)
        Station nqrwStations[] = {
            // N Line (Inbound)
            {"N02", "Astoria-Ditmars Blvd", 0},
            {"N03", "Astoria Blvd", 1},
            // ... [Previous station entries remain the same]
        };
        
        void setup() {
            Serial.begin(115200);
            delay(1000);
        
            // Initialize LED strips
            strip1.begin();
            strip2.begin();
            strip1.setBrightness(50);
            strip2.setBrightness(50);
            strip1.show();
            strip2.show();
        
            // Connect to WiFi
            Serial.println("Connecting to WiFi...");
            WiFi.begin(ssid, password);
            while (WiFi.status() != WL_CONNECTED) {
                delay(500);
                Serial.print(".");
            }
            Serial.println("\nConnected!");
        }
        
        void loop() {
            if(WiFi.status() == WL_CONNECTED) {
                getACEData();
                getNQRWData();
                delay(30000); // Update every 30 seconds
            }
        }
        
        int findLEDIndex(const char* stationId, Station* stations, int numStations) {
            for(int i = 0; i < numStations; i++) {
                if(strcmp(stations[i].id, stationId) == 0) {
                    return stations[i].ledIndex;
                }
            }
            return -1;  // Station not found
        }
        
        void getACEData() {
            WiFiClientSecure client;
            client.setInsecure();
            HTTPClient https;
            Serial.println("Getting ACE data...");
            
            if(https.begin(client, aceURL)) {
                int httpCode = https.GET();
                if(httpCode > 0) {
                    String payload = https.getString();
                    updateACEStrip();
                }
                https.end();
            }
        }
        
        void getNQRWData() {
            WiFiClientSecure client;
            client.setInsecure();
            HTTPClient https;
            Serial.println("Getting NQRW data...");
            
            if(https.begin(client, nqrwURL)) {
                int httpCode = https.GET();
                if(httpCode > 0) {
                    String payload = https.getString();
                    updateNQRWStrip();
                }
                https.end();
            }
        }
        
        void updateACEStrip() {
            // Clear all LEDs first
            for(int i = 0; i < NUM_LEDS; i++) {
                strip1.setPixelColor(i, OFF);
            }
            
            //parse GTFS data and light up LEDs
            strip1.show();
        }
        
        void updateNQRWStrip() {
            // Clear all LEDs first
            for(int i = 0; i < NUM_LEDS; i++) {
                strip2.setPixelColor(i, OFF);
            }
            
            // parse GTFS data and light up LEDs
            strip2.show();
        

That also worked. I repeated the process for one more line, but this was all powered by my computer; if I were to add more lines, I was advised by Anthony that I would need additional power. I wired my PCB to a barrel jack and connected the barrel jack to a 5v8a adapter.

I then adjusted the code to map out specific stations, and tried this on the 123, ACE, and 456 lines using the same code structure. Again, it worked:

Update 9 (13 December)

Because I opted to use the LED strips, the more stations I added, the messier the electronics got. Instead of using my initial idea of lasercutting and assembling an easel, I CADed (with the help of Ray from the Arch section... architects are so good at CAD. And fast!) poster corners and measures the length and width of the map, so that I would have a "poster box" that keeps all of the electronics enclosed and away from the public eye. I also cut 2 panels on a second acrylic board which serve as a little door for all of the wiring to go through, and where I'll connect the electronics to the power cable). I decided to opt out of making the subway sign because it looked kind of corny and distracting (the map was busy enough). For the PCB version of this map, I think I'll just directly raster/etch 'New York City Subway' on a corner of the map.

boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3 boardupdate3

I was a little frustrated that I'm not able to check 100% whether or not my map is working accurately. I always conceptualized this project as more of an art piece as opposed to a practical way of planning your commute, but for the purposes of debugging this and making sure the refresh was working correctly, I wanted to find a way to make sure that my LEDs were aligned with the actual MTA feed without any distortion.

To address this, I found a website called The Weekendest that uses the same API to map trains to stations. This website maps all trains (while I'm only concerned with trains that are currently at a station, or almost at the station (less than 2 minutes away). I used this to generally map out which stations should be lit up, and the two were almost perfectly aligned.

boardupdate3

Another thing I realized while writing the code for this is that I would only be able to map trains in one direction (though the direction is really just a matter of how I choose to assign the stations to the LEDs). I chose to program all southbound trains for this project, as mapping both would be very chaotic (I tried this with two lines). I decided to keep either southbound or northbound, testing both and back and forth, instead of programming both on the same LED strips (mostly for aesthetic purposes).


        #include <Adafruit_NeoPixel.h>
        #include <WiFi.h>
        #include <HTTPClient.h>
        #include <WiFiClientSecure.h>
        #include <ArduinoJson.h>
        
        // Pin definitions for each line
        #define PIN_123   2   // 123 lines (red)
        #define PIN_ACE   3   // ACE lines (blue)
        #define PIN_NQRW  4   // NQRW lines (yellow)
        #define PIN_L     5   // L line (gray)
        #define PIN_456   6   // 456 lines (green)
        #define PIN_7     7   // 7 line (purple)
        #define PIN_BDFM  21  // BDFM lines (orange)
        #define PIN_G     10  // G line (light green)
        #define PIN_JZ    9   // JZ lines (brown)
        
        #define NUM_LEDS 110  // LEDs per strip
        #define APPROACHING_TIME 120 // 2 minutes in seconds for approaching trains
        
        // Initialize LED strips
        Adafruit_NeoPixel strip123(NUM_LEDS, PIN_123, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripACE(NUM_LEDS, PIN_ACE, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripNQRW(NUM_LEDS, PIN_NQRW, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripL(NUM_LEDS, PIN_L, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel strip456(NUM_LEDS, PIN_456, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel strip7(NUM_LEDS, PIN_7, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripBDFM(NUM_LEDS, PIN_BDFM, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripG(NUM_LEDS, PIN_G, NEO_GRB + NEO_KHZ800);
        Adafruit_NeoPixel stripJZ(NUM_LEDS, PIN_JZ, NEO_GRB + NEO_KHZ800);
        
        // Define colors for each line
        const uint32_t RED = strip123.Color(255, 0, 0);        // 123
        const uint32_t BLUE = stripACE.Color(0, 0, 255);       // ACE
        const uint32_t YELLOW = stripNQRW.Color(255, 255, 0);  // NQRW
        const uint32_t GRAY = stripL.Color(128, 128, 128);     // L
        const uint32_t GREEN = strip456.Color(0, 255, 0);      // 456
        const uint32_t PURPLE = strip7.Color(128, 0, 128);     // 7
        const uint32_t ORANGE = stripBDFM.Color(255, 165, 0);  // BDFM
        const uint32_t LIGHTGREEN = stripG.Color(144, 238, 144); // G
        const uint32_t BROWN = stripJZ.Color(165, 42, 42);     // JZ
        const uint32_t OFF = strip123.Color(0, 0, 0);
        
        // WiFi credentials
        const char* ssid = "HOTSPOT";
        const char* password = "PASSWORD";
        
        // MTA Feed URLs
        const char* url123 = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-123";
        const char* urlACE = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-ace";
        const char* urlNQRW = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-nqrw";
        const char* urlL = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-l";
        const char* url456 = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-456";
        const char* url7 = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-7";
        const char* urlBDFM = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-bdfm";
        const char* urlG = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-g";
        const char* urlJZ = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-jz";
        
        // Global timing variables
        bool blinkState = false;
        unsigned long lastBlink = 0;
        unsigned long lastUpdate = 0;
        const unsigned long BLINK_INTERVAL = 500;  // 500ms blink rate
        const unsigned long UPDATE_INTERVAL = 30000; // 30 second update interval
        
        // Station state tracking
        struct StationState {
            bool hasTrainAtStation;
            bool hasTrainApproaching;
        };
        
        // Arrays to track station states
        StationState line123States[NUM_LEDS];
        StationState lineACEStates[NUM_LEDS];
        StationState lineNQRWStates[NUM_LEDS];
        StationState lineLStates[NUM_LEDS];
        StationState line456States[NUM_LEDS];
        StationState line7States[NUM_LEDS];
        StationState lineBDFMStates[NUM_LEDS];
        StationState lineGStates[NUM_LEDS];
        StationState lineJZStates[NUM_LEDS];
        
        // Function declarations
        void setup();
        void loop();
        void initializeStrips();
        void connectToWiFi();
        void fetchMTAData();
        void updateAllStrips();
        void updateStrip(Adafruit_NeoPixel &strip, StationState* states, uint32_t color);
        void clearAllStrips();
        int getLedIndexForStation(const char* stationId);
        
        void setup() {
            Serial.begin(115200);
            delay(1000);
        
            initializeStrips();
            connectToWiFi();
        
            // Clear all strips initially
            clearAllStrips();
        }
        
        void loop() {
            unsigned long currentTime = millis();
        
            // Handle LED blinking
            if (currentTime - lastBlink >= BLINK_INTERVAL) {
                blinkState = !blinkState;
                lastBlink = currentTime;
                updateAllStrips();
            }
        
            // Update train data periodically
            if (currentTime - lastUpdate >= UPDATE_INTERVAL) {
                if (WiFi.status() == WL_CONNECTED) {
                    fetchMTAData();
                    lastUpdate = currentTime;
                } else {
                    // Try to reconnect to WiFi if disconnected
                    connectToWiFi();
                }
            }
        }
        
        void initializeStrips() {
            // Initialize all LED strips
            strip123.begin();
            stripACE.begin();
            stripNQRW.begin();
            stripL.begin();
            strip456.begin();
            strip7.begin();
            stripBDFM.begin();
            stripG.begin();
            stripJZ.begin();
        
            // Set brightness for all strips
            uint8_t brightness = 50;  // 50% brightness
            strip123.setBrightness(brightness);
            stripACE.setBrightness(brightness);
            stripNQRW.setBrightness(brightness);
            stripL.setBrightness(brightness);
            strip456.setBrightness(brightness);
            strip7.setBrightness(brightness);
            stripBDFM.setBrightness(brightness);
            stripG.setBrightness(brightness);
            stripJZ.setBrightness(brightness);
        }
        
        void connectToWiFi() {
            Serial.print("Connecting to WiFi");
            WiFi.begin(ssid, password);
        
            int attempts = 0;
            while (WiFi.status() != WL_CONNECTED && attempts < 20) {
                delay(500);
                Serial.print(".");
                attempts++;
            }
        
            if (WiFi.status() == WL_CONNECTED) {
                Serial.println("\nConnected to WiFi");
                Serial.print("IP: ");
                Serial.println(WiFi.localIP());
            } else {
                Serial.println("\nFailed to connect to WiFi");
            }
        }
        
        void clearAllStrips() {
            for(int i = 0; i < NUM_LEDS; i++) {
                strip123.setPixelColor(i, OFF);
                stripACE.setPixelColor(i, OFF);
                stripNQRW.setPixelColor(i, OFF);
                stripL.setPixelColor(i, OFF);
                strip456.setPixelColor(i, OFF);
                strip7.setPixelColor(i, OFF);
                stripBDFM.setPixelColor(i, OFF);
                stripG.setPixelColor(i, OFF);
                stripJZ.setPixelColor(i, OFF);
            }
            
            strip123.show();
            stripACE.show();
            stripNQRW.show();
            stripL.show();
            strip456.show();
            strip7.show();
            stripBDFM.show();
            stripG.show();
            stripJZ.show();
        }
        
        void fetchMTAData() {
            WiFiClientSecure client;
            client.setInsecure();  // Required for HTTPS connections
            HTTPClient http;
        
            // Fetch data for each line
            fetchLineData(url123, line123States);
            fetchLineData(urlACE, lineACEStates);
            fetchLineData(urlNQRW, lineNQRWStates);
            fetchLineData(urlL, lineLStates);
            fetchLineData(url456, line456States);
            fetchLineData(url7, line7States);
            fetchLineData(urlBDFM, lineBDFMStates);
            fetchLineData(urlG, lineGStates);
            fetchLineData(urlJZ, lineJZStates);
        }
        
        void fetchLineData(const char* url, StationState* states) {
            WiFiClientSecure client;
            client.setInsecure();
            HTTPClient http;
        
            if (http.begin(client, url)) {
                int httpCode = http.GET();
                
                if (httpCode > 0) {
                    if (httpCode == HTTP_CODE_OK) {
                        String payload = http.getString();
                        parseTrainData(payload, states);
                    }
                }
                http.end();
            }
        }
        
        void parseTrainData(String& payload, StationState* states) {
            DynamicJsonDocument doc(32768);  // Adjust size if needed
            DeserializationError error = deserializeJson(doc, payload);
        
            if (error) {
                Serial.print("JSON parsing failed: ");
                Serial.println(error.c_str());
                return;
            }
        
            // Reset all states
            for (int i = 0; i < NUM_LEDS; i++) {
                states[i].hasTrainAtStation = false;
                states[i].hasTrainApproaching = false;
            }
        
            // Parse train data
            JsonArray trains = doc["trains"].as();
            for (JsonObject train : trains) {
                const char* stopId = train["stopId"];
                int arrivalTime = train["arrivalTime"];
                int currentTime = train["currentTime"];
                
                int ledIndex = getLedIndexForStation(stopId);
                if (ledIndex >= 0 && ledIndex < NUM_LEDS) {
                    int timeToArrival = arrivalTime - currentTime;
                    
                    if (timeToArrival <= 0) {
                        states[ledIndex].hasTrainAtStation = true;
                    } else if (timeToArrival <= APPROACHING_TIME) {
                        states[ledIndex].hasTrainApproaching = true;
                    }
                }
            }
        }
        
        void updateAllStrips() {
            updateStrip(strip123, line123States, RED);
            updateStrip(stripACE, lineACEStates, BLUE);
            updateStrip(stripNQRW, lineNQRWStates, YELLOW);
            updateStrip(stripL, lineLStates, GRAY);
            updateStrip(strip456, line456States, GREEN);
            updateStrip(strip7, line7States, PURPLE);
            updateStrip(stripBDFM, lineBDFMStates, ORANGE);
            updateStrip(stripG, lineGStates, LIGHTGREEN);
            updateStrip(stripJZ, lineJZStates, BROWN);
        }
        
        void updateStrip(Adafruit_NeoPixel &strip, StationState* states, uint32_t color) {
            for (int i = 0; i < NUM_LEDS; i++) {
                if (states[i].hasTrainAtStation) {
                    strip.setPixelColor(i, color);
                }
                else if (states[i].hasTrainApproaching) {
                    strip.setPixelColor(i, blinkState ? color : OFF);
                }
                else {
                    strip.setPixelColor(i, OFF);
                }
            }
            strip.show();
        }
        
        // Station ID to LED mapping
        int getLedIndexForStation(const char* stationId) {
            // 123 Line Mapping (Red Line)
            if (strcmp(stationId, "101") == 0) return 1;  // Van Cortlandt Park-242 St
            if (strcmp(stationId, "103") == 0) return 3;  // 238 St
            if (strcmp(stationId, "104") == 0) return 4;  // 231 St
            if (strcmp(stationId, "106") == 0) return 7;  // 225 St
            if (strcmp(stationId, "107") == 0) return 8;  // 215 St
            if (strcmp(stationId, "108") == 0) return 10; // 207 St
            if (strcmp(stationId, "109") == 0) return 12; // Dyckman St
            if (strcmp(stationId, "110") == 0) return 13; // 191 St
            if (strcmp(stationId, "111") == 0) return 15; // 181 St
            if (strcmp(stationId, "112") == 0) return 17; // 168 St-Washington Hts
            if (strcmp(stationId, "113") == 0) return 19; // 157 St
            if (strcmp(stationId, "114") == 0) return 20; // 145 St
            if (strcmp(stationId, "115") == 0) return 22; // 137 St-City College
            if (strcmp(stationId, "116") == 0) return 24; // 125 St
            if (strcmp(stationId, "117") == 0) return 26; // 116 St-Columbia University
            if (strcmp(stationId, "118") == 0) return 28; // Cathedral Pkwy
            if (strcmp(stationId, "119") == 0) return 30; // 103 St
            if (strcmp(stationId, "120") == 0) return 31; // 96 St
            if (strcmp(stationId, "121") == 0) return 33; // 86 St
            if (strcmp(stationId, "122") == 0) return 35; // 79 St
            if (strcmp(stationId, "123") == 0) return 37; // 72 St
            if (strcmp(stationId, "124") == 0) return 39; // 66 St-Lincoln Center
            if (strcmp(stationId, "125") == 0) return 41; // 59 St-Columbus Circle
            if (strcmp(stationId, "126") == 0) return 43; // 50 St
            if (strcmp(stationId, "127") == 0) return 45; // Times Sq-42 St
            if (strcmp(stationId, "128") == 0) return 47; // 34 St-Penn Station
            if (strcmp(stationId, "129") == 0) return 49; // 28 St
            if (strcmp(stationId, "130") == 0) return 51; // 23 St
            if (strcmp(stationId, "131") == 0) return 53; // 18 St
            if (strcmp(stationId, "132") == 0) return 55; // 14 St
            if (strcmp(stationId, "133") == 0) return 57; // Christopher St
            if (strcmp(stationId, "134") == 0) return 59; // Houston St
            if (strcmp(stationId, "135") == 0) return 61; // Canal St
            if (strcmp(stationId, "136") == 0) return 63; // Franklin St
            if (strcmp(stationId, "137") == 0) return 65; // Chambers St
            if (strcmp(stationId, "138") == 0) return 67; // WTC Cortlandt
            if (strcmp(stationId, "139") == 0) return 69; // Rector St
            if (strcmp(stationId, "140") == 0) return 71; // South Ferry

            // ACE Line Mapping (Blue Line)
            if (strcmp(stationId, "A02") == 0) return 2;  // Inwood-207 St
            if (strcmp(stationId, "A03") == 0) return 5;  // Dyckman St
            if (strcmp(stationId, "A05") == 0) return 9;  // 181 St
            if (strcmp(stationId, "A06") == 0) return 11; // 175 St
            if (strcmp(stationId, "A07") == 0) return 14; // 168 St
            if (strcmp(stationId, "A09") == 0) return 16; // 145 St
            if (strcmp(stationId, "A10") == 0) return 18; // 135 St
            if (strcmp(stationId, "A11") == 0) return 21; // 125 St
            if (strcmp(stationId, "A12") == 0) return 23; // 116 St
            if (strcmp(stationId, "A14") == 0) return 25; // Cathedral Pkwy
            if (strcmp(stationId, "A15") == 0) return 27; // 103 St
            if (strcmp(stationId, "A16") == 0) return 29; // 96 St
            if (strcmp(stationId, "A17") == 0) return 32; // 86 St
            if (strcmp(stationId, "A18") == 0) return 34; // 81 St-Museum of Natural History
            if (strcmp(stationId, "A19") == 0) return 36; // 72 St
            if (strcmp(stationId, "A20") == 0) return 38; // 59 St-Columbus Circle
            if (strcmp(stationId, "A21") == 0) return 40; // 50 St
            if (strcmp(stationId, "A22") == 0) return 42; // 42 St Port Authority
            if (strcmp(stationId, "A23") == 0) return 44; // 34 St-Penn Station
            if (strcmp(stationId, "A24") == 0) return 46; // 23 St
            if (strcmp(stationId, "A25") == 0) return 48; // 14 St
            if (strcmp(stationId, "A27") == 0) return 50; // W 4 St-Washington Sq
            if (strcmp(stationId, "A28") == 0) return 52; // Canal St
            if (strcmp(stationId, "A31") == 0) return 54; // Chambers St
            if (strcmp(stationId, "A32") == 0) return 56; // Fulton St
            if (strcmp(stationId, "A33") == 0) return 58; // High St
            if (strcmp(stationId, "A34") == 0) return 60; // Jay St-MetroTech

            // NQRW Line Mapping (Yellow Line)
            if (strcmp(stationId, "N02") == 0) return 2;  // Astoria-Ditmars Blvd
            if (strcmp(stationId, "N03") == 0) return 6;  // Astoria Blvd
            if (strcmp(stationId, "N04") == 0) return 9;  // 30 Av
            if (strcmp(stationId, "N05") == 0) return 12; // Broadway
            if (strcmp(stationId, "N06") == 0) return 15; // 36 Av
            if (strcmp(stationId, "N07") == 0) return 18; // 39 Av-Dutch Kills
            if (strcmp(stationId, "N08") == 0) return 21; // Queensboro Plaza
            if (strcmp(stationId, "N09") == 0) return 24; // Lexington Av/59 St
            if (strcmp(stationId, "N10") == 0) return 27; // 5 Av/59 St
            if (strcmp(stationId, "Q01") == 0) return 30; // 57 St-7 Av
            if (strcmp(stationId, "R14") == 0) return 33; // 49 St
            if (strcmp(stationId, "R15") == 0) return 36; // Times Sq-42 St
            if (strcmp(stationId, "R16") == 0) return 39; // 34 St-Herald Sq
            if (strcmp(stationId, "R17") == 0) return 42; // 28 St
            if (strcmp(stationId, "R18") == 0) return 45; // 23 St
            if (strcmp(stationId, "R19") == 0) return 48; // 14 St-Union Sq
            if (strcmp(stationId, "R20") == 0) return 51; // 8 St-NYU
            if (strcmp(stationId, "R21") == 0) return 54; // Prince St
            if (strcmp(stationId, "R22") == 0) return 57; // Canal St
            if (strcmp(stationId, "R23") == 0) return 60; // City Hall
            if (strcmp(stationId, "R24") == 0) return 63; // Cortlandt St
            if (strcmp(stationId, "R25") == 0) return 66; // Rector St
            if (strcmp(stationId, "R26") == 0) return 69; // Whitehall St

            // L Line Mapping (Gray Line)
            if (strcmp(stationId, "L01") == 0) return 2;  // 8 Av
            if (strcmp(stationId, "L02") == 0) return 5;  // 6 Av
            if (strcmp(stationId, "L03") == 0) return 8;  // 14 St-Union Sq
            if (strcmp(stationId, "L05") == 0) return 11; // 3 Av
            if (strcmp(stationId, "L06") == 0) return 14; // 1 Av
            if (strcmp(stationId, "L08") == 0) return 17; // Bedford Av
            if (strcmp(stationId, "L10") == 0) return 20; // Graham Av
            if (strcmp(stationId, "L11") == 0) return 23; // Grand St
            if (strcmp(stationId, "L12") == 0) return 24; // Montrose Av
            if (strcmp(stationId, "L13") == 0) return 29; // Morgan Av
            if (strcmp(stationId, "L14") == 0) return 32; // Jefferson St
            if (strcmp(stationId, "L15") == 0) return 35; // DeKalb Av
            if (strcmp(stationId, "L16") == 0) return 38; // Myrtle-Wyckoff Avs
            if (strcmp(stationId, "L17") == 0) return 41; // Halsey St
            if (strcmp(stationId, "L19") == 0) return 44; // Wilson Av
            if (strcmp(stationId, "L20") == 0) return 47; // Bushwick Av-Aberdeen St
            if (strcmp(stationId, "L21") == 0) return 50; // Broadway Junction
            if (strcmp(stationId, "L22") == 0) return 54; // Atlantic Av
            if (strcmp(stationId, "L24") == 0) return 55; // Livonia Av
            if (strcmp(stationId, "L25") == 0) return 59; // New Lots Av
            if (strcmp(stationId, "L26") == 0) return 67; // East 105 St
            if (strcmp(stationId, "L27") == 0) return 69; // Canarsie-Rockaway Pkwy

            // 456 Line Mapping (Green Line)
            if (strcmp(stationId, "401") == 0) return 2;  // Woodlawn
            if (strcmp(stationId, "402") == 0) return 5;  // Mosholu Pkwy
            if (strcmp(stationId, "405") == 0) return 8;  // Bedford Park Blvd-Lehman College
            if (strcmp(stationId, "406") == 0) return 11; // Kingsbridge Rd
            if (strcmp(stationId, "407") == 0) return 14; // Fordham Rd
            if (strcmp(stationId, "408") == 0) return 15; // 183 St
            if (strcmp(stationId, "409") == 0) return 20; // Burnside Av
            if (strcmp(stationId, "410") == 0) return 23; // 176 St
            if (strcmp(stationId, "411") == 0) return 26; // Mt Eden Av
            if (strcmp(stationId, "412") == 0) return 29; // 170 St
            if (strcmp(stationId, "413") == 0) return 32; // 167 St
            if (strcmp(stationId, "414") == 0) return 35; // 161 St-Yankee Stadium
            if (strcmp(stationId, "415") == 0) return 38; // 149 St-Grand Concourse
            if (strcmp(stationId, "416") == 0) return 41; // 138 St-Grand Concourse
            if (strcmp(stationId, "418") == 0) return 44; // 125 St
            if (strcmp(stationId, "419") == 0) return 47; // 116 St
            if (strcmp(stationId, "420") == 0) return 50; // 110 St
            if (strcmp(stationId, "423") == 0) return 53; // 86 St
            if (strcmp(stationId, "424") == 0) return 56; // 77 St
            if (strcmp(stationId, "425") == 0) return 59; // 68 St-Hunter College
            if (strcmp(stationId, "426") == 0) return 62; // 59 St
            if (strcmp(stationId, "427") == 0) return 65; // 51 St
            if (strcmp(stationId, "428") == 0) return 68; // Grand Central-42 St
            if (strcmp(stationId, "429") == 0) return 71; // 33 St
            if (strcmp(stationId, "430") == 0) return 74; // 28 St
            if (strcmp(stationId, "431") == 0) return 77; // 23 St
            if (strcmp(stationId, "432") == 0) return 80; // 14 St-Union Sq
            if (strcmp(stationId, "433") == 0) return 83; // Astor Pl
            if (strcmp(stationId, "434") == 0) return 86; // Bleecker St
            if (strcmp(stationId, "435") == 0) return 89; // Spring St
            if (strcmp(stationId, "436") == 0) return 92; // Canal St
        
            // continued mappings for the remaining subway lines (total 9 lines mapped out, this part of code omitted on website)
        
            return -1; // Station not found
        }  

A couple of the subway lines branch out (like the 456 line in the screenshot below). Because I was limited by the LED strips, I milled a second PCB that would control these additional stations with different strips (3 strips, one strip for each "branch"), and ran this code in conjunction with my main PCB. It would have been ideal to control each line with a singular pin in terms of simplicity and efficiency, but you know, we adapt and move forward.

boardupdate3

            #include <Adafruit_NeoPixel.h>
            #include <WiFi.h>
            #include <HTTPClient.h>
            #include <WiFiClientSecure.h>
            #include <ArduinoJson.h>
                
                // Pin definitions for 456 branches
                #define PIN_4_BRANCH  4  // 4 train branch
                #define PIN_5_BRANCH  5  // 5 train branch
                #define PIN_6_BRANCH  6  // 6 train branch
                
                #define NUM_LEDS 110  // LEDs per strip
                #define APPROACHING_TIME 120 // 2 minutes in seconds
                
                // Initialize LED strips
                Adafruit_NeoPixel strip456Main(NUM_LEDS, PIN_456_MAIN, NEO_GRB + NEO_KHZ800);
                Adafruit_NeoPixel strip4Branch(NUM_LEDS, PIN_4_BRANCH, NEO_GRB + NEO_KHZ800);
                Adafruit_NeoPixel strip5Branch(NUM_LEDS, PIN_5_BRANCH, NEO_GRB + NEO_KHZ800);
                Adafruit_NeoPixel strip6Branch(NUM_LEDS, PIN_6_BRANCH, NEO_GRB + NEO_KHZ800);
                
                // Colors
                const uint32_t GREEN = strip456Main.Color(0, 255, 0);
                const uint32_t OFF = strip456Main.Color(0, 0, 0);
                
                // WiFi credentials
                const char* ssid = "YOUR_SSID";
                const char* password = "YOUR_PASSWORD";
                
                // MTA Feed URL
                const char* url456 = "https://api.mta.info/GTFS/feeds/nyct%2Fgtfs-456";
                
                // Global timing variables
                bool blinkState = false;
                unsigned long lastBlink = 0;
                const unsigned long BLINK_INTERVAL = 500;
                
                // Station state tracking
                struct StationState {
                    bool hasTrainAtStation;
                    bool hasTrainApproaching;
                };
                
                // Arrays to track station states for each section
                StationState mainLineStates[NUM_LEDS];
                StationState line4States[NUM_LEDS];
                StationState line5States[NUM_LEDS];
                StationState line6States[NUM_LEDS];
                
                // Station mapping struct
                struct StationMap {
                    const char* id;
                    const char* name;
                    uint8_t ledIndex;
                    uint8_t lineType;  // 0 = main, 4 = line 4, 5 = line 5, 6 = line 6
                };
                
                // Station mapping array
                const StationMap stationMaps[] = {
                    // Main trunk (shared by all lines)
                    {"621", "125 St", 1, 0},
                    {"622", "116 St", 3, 0},
                    {"623", "110 St", 5, 0},
                    {"624", "103 St", 7, 0},
                    {"625", "96 St", 9, 0},
                    {"626", "86 St", 11, 0},
                    {"627", "77 St", 13, 0},
                    {"628", "68 St-Hunter College", 15, 0},
                    {"629", "59 St", 17, 0},
                    {"630", "51 St", 19, 0},
                    {"631", "Grand Central-42 St", 21, 0},
                    {"632", "33 St", 23, 0},
                    {"633", "28 St", 25, 0},
                    {"634", "23 St", 27, 0},
                    {"635", "14 St-Union Sq", 29, 0},
                    {"636", "Astor Pl", 31, 0},
                    {"637", "Bleecker St", 33, 0},
                    {"638", "Spring St", 35, 0},
                    {"639", "Canal St", 37, 0},
                    {"640", "Brooklyn Bridge", 39, 0},
                
                    // Line 4 branch
                    {"401", "Woodlawn", 1, 4},
                    {"402", "Mosholu Pkwy", 3, 4},
                    {"403", "Bedford Park Blvd-Lehman College", 5, 4},
                    {"404", "Kingsbridge Rd", 7, 4},
                    {"405", "Fordham Rd", 9, 4},
                    {"406", "183 St", 11, 4},
                    {"407", "Burnside Av", 13, 4},
                    {"408", "176 St", 15, 4},
                    {"409", "Mt Eden Av", 17, 4},
                    {"410", "170 St", 19, 4},
                    {"411", "167 St", 21, 4},
                    {"412", "161 St-Yankee Stadium", 23, 4},
                    {"413", "149 St-Grand Concourse", 25, 4},
                    {"414", "138 St-Grand Concourse", 27, 4},
                
                    // Line 5 branch (Dyre Av branch)
                    {"501", "Eastchester-Dyre Av", 1, 5},
                    {"502", "Baychester Av", 3, 5},
                    {"503", "Gun Hill Rd", 5, 5},
                    {"504", "Pelham Pkwy", 7, 5},
                    {"505", "Morris Park", 9, 5},
                
                    // Line 6 branch (Pelham Bay Park branch)
                    {"601", "Pelham Bay Park", 1, 6},
                    {"602", "Buhre Av", 3, 6},
                    {"603", "Middletown Rd", 5, 6},
                    {"604", "Westchester Sq-E Tremont Av", 7, 6},
                    {"605", "Zerega Av", 9, 6},
                    {"606", "Castle Hill Av", 11, 6},
                    {"607", "Parkchester", 13, 6},
                    {"608", "St Lawrence Av", 15, 6},
                    {"609", "Morrison Av-Soundview", 17, 6},
                    {"610", "Elder Av", 19, 6},
                    {"611", "Whitlock Av", 21, 6},
                    {"612", "Hunts Point Av", 23, 6},
                    {"613", "Longwood Av", 25, 6},
                    {"614", "E 149 St", 27, 6},
                    {"615", "E 143 St-St Mary's St", 29, 6}
                };
                
                void setup() {
                    Serial.begin(115200);
                    
                    // Initialize LED strips
                    strip456Main.begin();
                    strip4Branch.begin();
                    strip5Branch.begin();
                    strip6Branch.begin();
                    
                    // Set brightness
                    strip456Main.setBrightness(50);
                    strip4Branch.setBrightness(50);
                    strip5Branch.setBrightness(50);
                    strip6Branch.setBrightness(50);
                    
                    // Clear all strips
                    clearAllStrips();
                    
                    // Connect to WiFi
                    WiFi.begin(ssid, password);
                    while (WiFi.status() != WL_CONNECTED) {
                        delay(500);
                        Serial.print(".");
                    }
                    Serial.println("\nConnected to WiFi");
                }
                
                void loop() {
                    unsigned long currentTime = millis();
                    
                    // Update blink state
                    if (currentTime - lastBlink >= BLINK_INTERVAL) {
                        blinkState = !blinkState;
                        lastBlink = currentTime;
                        updateAllStrips();
                    }
                    
                    // Fetch new data every 30 seconds
                    static unsigned long lastUpdate = 0;
                    if (currentTime - lastUpdate >= 30000) {
                        fetchMTAData();
                        lastUpdate = currentTime;
                    }
                }
                
                void clearAllStrips() {
                    for(int i = 0; i < NUM_LEDS; i++) {
                        strip456Main.setPixelColor(i, OFF);
                        strip4Branch.setPixelColor(i, OFF);
                        strip5Branch.setPixelColor(i, OFF);
                        strip6Branch.setPixelColor(i, OFF);
                    }
                    strip456Main.show();
                    strip4Branch.show();
                    strip5Branch.show();
                    strip6Branch.show();
                }
                
                void updateAllStrips() {
                    updateStrip(strip456Main, mainLineStates, GREEN);
                    updateStrip(strip4Branch, line4States, GREEN);
                    updateStrip(strip5Branch, line5States, GREEN);
                    updateStrip(strip6Branch, line6States, GREEN);
                }
                
                void updateStrip(Adafruit_NeoPixel &strip, StationState* states, uint32_t color) {
                    for(int i = 0; i < NUM_LEDS; i++) {
                        if(states[i].hasTrainAtStation) {
                            strip.setPixelColor(i, color);
                        }
                        else if(states[i].hasTrainApproaching) {
                            strip.setPixelColor(i, blinkState ? color : OFF);
                        }
                        else {
                            strip.setPixelColor(i, OFF);
                        }
                    }
                    strip.show();
                }
                
                void fetchMTAData() {
                    WiFiClientSecure client;
                    client.setInsecure();
                    HTTPClient http;
                    
                    if(http.begin(client, url456)) {
                        int httpCode = http.GET();
                        if(httpCode > 0) {
                            String payload = http.getString();
                            parseTrainData(payload);
                        }
                        http.end();
                    }
                }
                
                void parseTrainData(String& payload) {
                    // Reset all states
                    memset(mainLineStates, 0, sizeof(mainLineStates));
                    memset(line4States, 0, sizeof(line4States));
                    memset(line5States, 0, sizeof(line5States));
                    memset(line6States, 0, sizeof(line6States));
                    
                    DynamicJsonDocument doc(32768);
                    DeserializationError error = deserializeJson(doc, payload);
                    
                    if(error) {
                        Serial.println("JSON parsing failed");
                        return;
                    }
                    
                    JsonArray trains = doc["trains"].as();
                    for(JsonObject train : trains) {
                        const char* stopId = train["stopId"];
                        int arrivalTime = train["arrivalTime"];
                        int currentTime = train["currentTime"];
                        const char* routeId = train["routeId"];
                        
                        // Find the station in our mapping
                        for(const StationMap& station : stationMaps) {
                            if(strcmp(station.id, stopId) == 0) {
                                int timeToArrival = arrivalTime - currentTime;
                                
                                // Update appropriate state array based on lineType
                                StationState* states;
                                switch(station.lineType) {
                                    case 0: states = mainLineStates; break;
                                    case 4: states = line4States; break;
                                    case 5: states = line5States; break;
                                    case 6: states = line6States; break;
                                    default: continue;
                                }
                                
                                if(timeToArrival <= 0) {
                                    states[station.ledIndex].hasTrainAtStation = true;
                                } else if(timeToArrival <= APPROACHING_TIME) {
                                    states[station.ledIndex].hasTrainApproaching = true;
                                }
                                break;
                            }
                        }
                    }
                }
        

Another item that came up was the issue of multiple train lines at a station. This isn't a super common issue as it only affects some of the main transfer stations, but for major transfer stations like Times Square, there are multiple trains present at any given time. I added in further code which tracks which trains are present at or approaching each station.

For stations that have multiple trains (for example, Times Square) I have the LED "station" cycle through the colors of whatever train is present at that station.

This was a bit cumbersome as it required re-organizing the LED strips by physical location rather than by train line; for the purposes of the demo, I kept it as each strip representing a different line, but this code provides the function of representing each LED as an individual station, which is how I would have programmed it if I had used the XL PCB approach.


            // Define train line colors
            const uint32_t COLOR_1_2_3 = strip.Color(255, 0, 0);      // Red
            const uint32_t COLOR_A_C_E = strip.Color(0, 0, 255);      // Blue
            const uint32_t COLOR_N_Q_R_W = strip.Color(255, 255, 0);  // Yellow
            const uint32_t COLOR_7 = strip.Color(128, 0, 128);        // Purple
            const uint32_t COLOR_4_5_6 = strip.Color(0, 255, 0);      // Green
            const uint32_t OFF = strip.Color(0, 0, 0);

            // Structure to track trains at a station
            struct TrainPresence {
                bool hasLine1 = false;
                bool hasLine2 = false;
                bool hasLine3 = false;
                bool hasLineA = false;
                bool hasLineC = false;
                bool hasLineE = false;
                bool hasLineN = false;
                bool hasLineQ = false;
                bool hasLineR = false;
                bool hasLineW = false;
                bool hasLine7 = false;
                bool hasLine4 = false;
                bool hasLine5 = false;
                bool hasLine6 = false;
            };

            // Station state tracking
            struct StationState {
                TrainPresence trainsPresent;
                TrainPresence trainsApproaching;
                uint8_t colorIndex;  // Current position in color cycle
            };

            // Function to update LED color for a station with multiple trains
            void updateStationLED(int ledIndex, StationState& state) {
                vector colors;  // Vector to hold colors of present trains
                
                // Add colors for present trains
                if(state.trainsPresent.hasLine1 || 
                state.trainsPresent.hasLine2 || 
                state.trainsPresent.hasLine3) {
                    colors.push_back(COLOR_1_2_3);
                }
                
                if(state.trainsPresent.hasLineA || 
                state.trainsPresent.hasLineC || 
                state.trainsPresent.hasLineE) {
                    colors.push_back(COLOR_A_C_E);
                }
                
                if(state.trainsPresent.hasLineN || 
                state.trainsPresent.hasLineQ || 
                state.trainsPresent.hasLineR || 
                state.trainsPresent.hasLineW) {
                    colors.push_back(COLOR_N_Q_R_W);
                }
                
                if(state.trainsPresent.hasLine7) {
                    colors.push_back(COLOR_7);
                }

                if(state.trainsPresent.hasLine4 || 
                state.trainsPresent.hasLine5 || 
                state.trainsPresent.hasLine6) {
                    colors.push_back(COLOR_4_5_6);
                }

                // Handle different scenarios
                if(colors.empty()) {
                    // No trains present, check for approaching trains
                    handleApproachingTrains(ledIndex, state);
                }
                else if(colors.size() == 1) {
                    // One line present, solid color
                    strip.setPixelColor(ledIndex, colors[0]);
                }
                else {
                    // Multiple lines present, cycle through colors
                    unsigned long currentTime = millis();
                    int colorIndex = (currentTime / 1000) % colors.size(); // Change color every second
                    strip.setPixelColor(ledIndex, colors[colorIndex]);
                }
            }

            // Handle approaching trains
            void handleApproachingTrains(int ledIndex, StationState& state) {
                vector approachingColors;
                
                // Add colors for approaching trains
                if(state.trainsApproaching.hasLine1 || 
                state.trainsApproaching.hasLine2 || 
                state.trainsApproaching.hasLine3) {
                    approachingColors.push_back(COLOR_1_2_3);
                }
                // 

                if(!approachingColors.empty()) {
                    // Blink the approaching train colors
                    if(blinkState) {
                        int colorIndex = (millis() / 1000) % approachingColors.size();
                        strip.setPixelColor(ledIndex, approachingColors[colorIndex]);
                    } else {
                        strip.setPixelColor(ledIndex, OFF);
                    }
                } else {
                    strip.setPixelColor(ledIndex, OFF);
                }
            }

            // Main update function
            void updateStrip() {
                static unsigned long lastUpdate = 0;
                unsigned long currentTime = millis();

                // Update blink state every 500ms
                if(currentTime - lastUpdate >= 500) {
                    blinkState = !blinkState;
                    lastUpdate = currentTime;
                }

                // Update each station
                for(int i = 0; i < NUM_STATIONS; i++) {
                    updateStationLED(i, stationStates[i]);
                }
                
                strip.show();
            }

            // Process incoming train data
            void processTrainUpdate(const char* trainLine, const char* stationId, bool isApproaching) {
                int stationIndex = getStationIndex(stationId);
                if(stationIndex < 0) return;

                TrainPresence& trains = isApproaching ? 
                    stationStates[stationIndex].trainsApproaching :
                    stationStates[stationIndex].trainsPresent;

                // Update train presence
                if(strcmp(trainLine, "1") == 0) trains.hasLine1 = true;
                else if(strcmp(trainLine, "2") == 0) trains.hasLine2 = true;
                else if(strcmp(trainLine, "3") == 0) trains.hasLine3 = true;
                else if(strcmp(trainLine, "A") == 0) trains.hasLineA = true;
                //
            }
        

Here's a sped up version of the final program, which covers all of the 9 main subway lines. Accounting for the branches and having to add some additional strips to accomodate weird angles, I used a total of 20 LED strips across 2 PCBs. I toasted 5 of the LED strips unfortunately which affected a few of the subway lines, so you'll see that 5 of the branched off lines remain dark :( this remains a hardware issue, not a software issue, so I'll have to replace these LEDs. But by running the sped up simulation, we can see the "trains" move throughout the city, which looks very cool.

Some final thoughts:

boardupdate3

I want to extend a huge thanks to all members of the teaching staff, but especially to EECS' very own Anthony, whose expertise and positivity and general patience and relentless willingness to help was such an integral part of this class experience and of the success of my final project. I feel incredibly fortunate to have had the opportunity to make this class a part of my time at MIT, and the class itself was a good reminder of how special it is to be surrounded by curious and hardworking people, as well as how special it is to be able to make something just because you can and want to. It was loads of fun to go all in on a passion project; I felt that my project was especially pertinent to both my personal and professional interests, which made it all the more special.

small PCB design file big PCB design file easel CAD file lasercut map CDR file

(っ◕‿◕)っ

sungmoon@mit.edu