Skip to content

Instantly share code, notes, and snippets.

@joeycastillo
Created November 4, 2019 03:31
Show Gist options
  • Save joeycastillo/12273bd0befc297f20f61146fa9806f5 to your computer and use it in GitHub Desktop.
Save joeycastillo/12273bd0befc297f20f61146fa9806f5 to your computer and use it in GitHub Desktop.
e-ink departures board for NYC subway
#include <SPI.h>
#include <WiFiNINA.h>
#include <RTClib.h>
#include <ArduinoJson.h>
#define ENABLE_GxEPD2_GFX 0
#include <GxEPD2_BW.h>
#include "BabelSPIFlash.h"
#include "BabelTypesetterGFX.h"
#include "arduino_secrets.h"
char ssid[] = SECRET_SSID;
char pass[] = SECRET_PASS;
int keyIndex = 0;
int status = WL_IDLE_STATUS;
// Initialize the WiFi client library
#define SPIWIFI SPI // The SPI port
#define SPIWIFI_SS 13 // Chip select pin
#define ESP32_RESETN 12 // Reset pin
#define SPIWIFI_ACK 11 // a.k.a BUSY or READY pin
#define ESP32_GPIO0 -1
WiFiClient client;
RTC_Millis rtc;
unsigned long lastConnectionTime = 0;
const unsigned long postingInterval = 180L * 1000L;
const int tzOffset = -5 * 60 * 60; // 4 for EDT, 5 for EST
#define MAX_DISPAY_BUFFER_SIZE 32768ul
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPAY_BUFFER_SIZE / (EPD::WIDTH / 8))
//GxEPD2_BW<GxEPD2_420, MAX_HEIGHT(GxEPD2_420)> display(GxEPD2_420(/*CS=77*/ 44, /*DC=*/ 45, /*RST=*/ 46, /*BUSY=*/ 47));
//GxEPD2_BW<GxEPD2_583, MAX_HEIGHT(GxEPD2_583)> display(GxEPD2_583(/*CS=77*/ 44, /*DC=*/ 45, /*RST=*/ 46, /*BUSY=*/ 47));
GxEPD2_BW<GxEPD2_750, MAX_HEIGHT(GxEPD2_750)> display(GxEPD2_750(/*CS=77*/ 44, /*DC=*/ 45, /*RST=*/ 46, /*BUSY=*/ 47));
BabelTypesetterGFX typesetter(&display, 52, &SPI);
void setup() {
Serial.begin(9600);
display.init(115200);
display.setRotation(2);
typesetter.begin();
typesetter.textColor = GxEPD_BLACK;
typesetter.setLayoutArea(20, 20, 480 - 40, 800 - 40);
WiFi.setPins(SPIWIFI_SS, SPIWIFI_ACK, ESP32_RESETN, ESP32_GPIO0, &SPIWIFI);
// check for the WiFi module:
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
// don't continue
while (true);
}
String fv = WiFi.firmwareVersion();
if (fv < "1.0.0") {
Serial.println("Please upgrade the firmware");
}
// attempt to connect to Wifi network:
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to open SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
// you're connected now, so print out the status:
printWiFiStatus();
WiFi.lowPowerMode();
}
void loop() {
if ((millis() + postingInterval) - lastConnectionTime > postingInterval)
{
lastConnectionTime = millis() + postingInterval;
client.setTimeout(10000);
if (!client.connect("www.yourserver.com", 80)) {
Serial.println(F("Connection failed"));
return;
}
// Send HTTP request
client.println("GET /trains.py HTTP/1.1");
client.println("Host: www.yourserver.com");
client.println("User-Agent: ArduinoWiFi/1.1");
client.println("Connection: close");
if (client.println() == 0) {
Serial.println(F("Failed to send request"));
return;
}
// Check HTTP status
char status[32] = {0};
client.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
Serial.print(F("Unexpected response: "));
Serial.println(status);
return;
}
// Skip HTTP headers
char endOfHeaders[] = "\r\n\r\n";
if (!client.find(endOfHeaders)) {
Serial.println(F("Invalid response"));
return;
}
// Allocate the JSON document
// Use arduinojson.org/v6/assistant to compute the capacity.
const size_t capacity = 4096;
DynamicJsonDocument doc(capacity);
// Parse JSON object
DeserializationError error = deserializeJson(doc, client);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}
long servertime = doc["time"];
rtc.adjust(DateTime(servertime + tzOffset));
JsonArray M14N = doc["M14N"];
long M14N_0_time = M14N[0]["time"];
const char* M14N_0_train = M14N[0]["train"];
long M14N_1_time = M14N[1]["time"];
const char* M14N_1_train = M14N[1]["train"];
long M14N_2_time = M14N[2]["time"];
const char* M14N_2_train = M14N[2]["train"];
JsonArray M14S = doc["M14S"];
long M14S_0_time = M14S[0]["time"];
const char* M14S_0_train = M14S[0]["train"];
long M14S_1_time = M14S[1]["time"];
const char* M14S_1_train = M14S[1]["train"];
long M14S_2_time = M14S[2]["time"];
const char* M14S_2_train = M14S[2]["train"];
JsonArray M13N = doc["M13N"];
long M13N_0_time = M13N[0]["time"];
const char* M13N_0_train = M13N[0]["train"];
long M13N_1_time = M13N[1]["time"];
const char* M13N_1_train = M13N[1]["train"];
long M13N_2_time = M13N[2]["time"];
const char* M13N_2_train = M13N[2]["train"];
JsonArray M13S = doc["M13S"];
long M13S_0_time = M13S[0]["time"];
const char* M13S_0_train = M13S[0]["train"];
long M13S_1_time = M13S[1]["time"];
const char* M13S_1_train = M13S[1]["train"];
long M13S_2_time = M13S[2]["time"];
const char* M13S_2_train = M13S[2]["train"];
JsonArray G30N = doc["G30N"];
long G30N_0_time = G30N[0]["time"];
const char* G30N_0_train = G30N[0]["train"];
long G30N_1_time = G30N[1]["time"];
const char* G30N_1_train = G30N[1]["train"];
long G30N_2_time = G30N[2]["time"];
const char* G30N_2_train = G30N[2]["train"];
JsonArray G30S = doc["G30S"];
long G30S_0_time = G30S[0]["time"];
const char* G30S_0_train = G30S[0]["train"];
long G30S_1_time = G30S[1]["time"];
const char* G30S_1_train = G30S[1]["train"];
long G30S_2_time = G30S[2]["time"];
const char* G30S_2_train = G30S[2]["train"];
JsonArray L13N = doc["L13N"];
long L13N_0_time = L13N[0]["time"];
const char* L13N_0_train = L13N[0]["train"];
long L13N_1_time = L13N[1]["time"];
const char* L13N_1_train = L13N[1]["train"];
long L13N_2_time = L13N[2]["time"];
const char* L13N_2_train = L13N[2]["train"];
JsonArray L13S = doc["L13S"];
long L13S_0_time = L13S[0]["time"];
const char* L13S_0_train = L13S[0]["train"];
long L13S_1_time = L13S[1]["time"];
const char* L13S_1_train = L13S[1]["train"];
long L13S_2_time = L13S[2]["time"];
const char* L13S_2_train = L13S[2]["train"];
display.firstPage();
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, 800, 48, GxEPD_BLACK);
typesetter.setCursor(0, 0);
typesetter.textColor = GxEPD_WHITE;
char buf[128];
DateTime now = rtc.now();
char ampm[3] = "?M";
long arrValue;
const char* train;
char *spaces = " ";
DateTime arrTime;
if (now.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%d:%02d %s", (now.hour() % 12) == 0 ? 12 : (now.hour() % 12), now.minute(), ampm);
typesetter.print(buf);
// draw battery level.
// TODO: make this reflect the actual battery level
display.drawRect(620 + 0, 4, 16, 8, GxEPD_WHITE);
display.drawRect(620 + 16, 6, 1, 4, GxEPD_WHITE);
display.fillRect(620 + 2, 6, 12, 4, GxEPD_WHITE);
// draw signal strength.
// TODO: make this reflect the actual WiFi signal strength
display.drawRect(606 + 0, 11, 2, 0, GxEPD_WHITE);
display.drawRect(606 + 3, 11, 2, -2, GxEPD_WHITE);
display.drawRect(606 + 6, 11, 2, -4, GxEPD_WHITE);
display.drawRect(606 + 9, 11, 2, -6, GxEPD_WHITE);
typesetter.textSize = 2;
typesetter.setCursor(160, 8);
typesetter.print("HOUSE TRAIN SCHEDULE\n");
typesetter.textColor = GxEPD_BLACK;
typesetter.textSize = 1;
typesetter.bold = true;
typesetter.print("\nTo Church Ave To Court Square\n");
typesetter.bold = false;
typesetter.print(" Broadway\n");
for(int i = 0; i < 3; i++) {
arrValue = G30S[i]["time"];
train = G30S[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", "Ⓖ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print(spaces);
arrValue = G30N[i]["time"];
train = G30N[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", "Ⓖ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print("\n");
}
typesetter.bold = true;
typesetter.print("\n");
typesetter.print("To Manhattan To Brooklyn / Queens\n");
typesetter.bold = false;
typesetter.print(" Hewes Street\n");
for(int i = 0; i < 3; i++) {
arrValue = M14N[i]["time"];
train = M14N[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", train[0] == 'J' ? "Ⓙ" : "Ⓜ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print(spaces);
arrValue = M14S[i]["time"];
train = M14S[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", train[0] == 'J' ? "Ⓙ" : "Ⓜ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print("\n");
}
typesetter.print(" Lorimer Street\n");
for(int i = 0; i < 3; i++) {
arrValue = M13N[i]["time"];
train = M13N[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", train[0] == 'J' ? "Ⓙ" : "Ⓜ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print(spaces);
arrValue = M13S[i]["time"];
train = M13S[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", train[0] == 'J' ? "Ⓙ" : "Ⓜ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print("\n");
}
typesetter.print(" Montrose Ave\n");
for(int i = 0; i < 3; i++) {
arrValue = L13N[i]["time"];
train = L13N[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", "Ⓛ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print(spaces);
arrValue = L13S[i]["time"];
train = L13S[i]["train"];
arrTime = DateTime(arrValue + tzOffset);
if (arrTime.hour() > 11) ampm[0] = 'P'; else ampm[0] = 'A';
sprintf(buf, "%s %2d:%02d:%02d %s", "Ⓛ", (arrTime.hour() % 12) == 0 ? 12 : (arrTime.hour() % 12), arrTime.minute(), arrTime.minute(), ampm);
typesetter.print(buf);
typesetter.print("\n");
}
while (display.nextPage());
display.hibernate();
}
}
void printWiFiStatus() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print your WiFi shield's IP address:
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.print(rssi);
Serial.println(" dBm");
}
certifi==2019.9.11
chardet==3.0.4
gdbm==0.0.0
gtfs-realtime-bindings==0.0.6
idna==2.8
protobuf==3.10.0
protobuf3-to-dict==0.1.5
python-dotenv==0.10.3
requests==2.22.0
six==1.12.0
sqlite3==0.0.0
Tkinter==0.0.0
urllib3==1.25.6
#!/path/to/venv/bin/python3
from google.transit import gtfs_realtime_pb2
import requests
import time
import os
import json
from dotenv import load_dotenv, find_dotenv
from protobuf_to_dict import protobuf_to_dict
import operator
# You should have a .env file in the same directory as this script with your MTA API key set as follows:
# API_KEY=00112233445566778899AABBCCDDEEFF
# Get an API key here: https://datamine.mta.info/user/register
load_dotenv(find_dotenv())
api_key = os.environ['API_KEY']
# List of feeds: https://datamine.mta.info/list-of-feeds
# List of stops: https://gist.github.com/konecnyna/07f0436ff8b4a89d9f814938b12a3c25
things = [
{'url': 'http://datamine.mta.info/mta_esi.php?key={}&feed_id=36', 'stops': ['M14N', 'M14S', 'M13N', 'M13S'], 'train': 'J'}, # Hewes and Lorimer J
{'url': 'http://datamine.mta.info/mta_esi.php?key={}&feed_id=21', 'stops': ['M14N', 'M14S', 'M13N', 'M13S'], 'train': 'M'}, # Hewes and Lorimer M
{'url': 'http://datamine.mta.info/mta_esi.php?key={}&feed_id=31', 'stops': ['G30N', 'G30S'], 'train': 'G'}, # Broadway
{'url': 'http://datamine.mta.info/mta_esi.php?key={}&feed_id=2', 'stops': ['L13N', 'L13S'], 'train': 'L'}, # Montrose Av
]
output = {}
for thing in things:
time.sleep(1)
url = thing['url'].format(api_key)
feed = gtfs_realtime_pb2.FeedMessage()
response = requests.get(url)
feed.ParseFromString(response.content)
subway_feed = protobuf_to_dict(feed)
realtime_data = subway_feed['entity']
collected_times = []
def station_time_lookup(train_data, station):
for trains in train_data:
if trains.get('trip_update', False) != False:
unique_train_schedule = trains['trip_update']
unique_arrival_times = unique_train_schedule.get('stop_time_update', list())
for scheduled_arrivals in unique_arrival_times:
if scheduled_arrivals.get('stop_id', False) == station:
time_data = scheduled_arrivals['arrival']
unique_time = time_data['time']
if unique_time != None:
collected_times.append({'time': unique_time, 'train': thing['train']})
for stop in thing['stops']:
station_time_lookup(realtime_data, stop)
if not stop in output:
output[stop] = []
output[stop].extend(collected_times)
output[stop].sort(key=operator.itemgetter('time'))
collected_times = []
for key in output:
output[key] = output[key][:3]
output['time'] = int(time.time())
json = json.dumps(output)
print("Content-type: application/json; charset=utf-8")
print("Content-Length: {}\n".format(len(json)))
print(json, end='')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment