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:
*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>
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):
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.
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.
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.
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.
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).
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:
Graphics for board logo inspo:
In Fusion:
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.
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):
Schematic for the PCB itself, sans LEDs
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:
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:
Cardboard test
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.
Lasercutter working its magic. The rastering looked really good; I was quite happy with my choice to go with acrylic versus wood.
Board complete!
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):
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:
The updated single layer PCB design. I kept the GND layer but got rid of a bunch of necessary vias.
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.
Example of how each of the boards connected:
The biggest HTMAA PCB? The whole thing was (almost exactly) 24" x 18" big.
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)
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.
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).
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:
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:
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...).
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:
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.
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.
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.
#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:
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