Created
November 4, 2019 03:31
-
-
Save joeycastillo/12273bd0befc297f20f61146fa9806f5 to your computer and use it in GitHub Desktop.
e-ink departures board for NYC subway
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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"); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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