Skip to content

PirosB3/mta-convex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mta-convex

Real-time NYC subway arrival display. A Convex backend stores users, their tracked stops (with direction labels), and station names — fetches live MTA GTFS-RT data on demand, and serves enriched arrivals to a Raspberry Pi e-ink display.

The display refreshes every 15 seconds using partial refresh (flicker-free), with a full panel refresh every 5 minutes to clear ghosting.

┌─────────────────────────────────────────────────────────────────────────┐
│  Hey Daniel          WED, MAR 18, 2026                       8:42 AM   │
├═════════════════════════════════════════════════════════════════════════╡
│  Eastern Pkwy-Brooklyn Museum                                           │
│  [2]  Manhattan     3min  ·  11min                                      │
│  [3]  Manhattan     9min  ·  17min                                      │
│ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -   │
│  Franklin Av-Medgar Evers College                                       │
│  [4]  Manhattan     6min  ·  19min                                      │
│  [5]  Manhattan     8min  ·  21min                                      │
│                                               UPDATED 8:42:01 AM       │
├═════════════════════════════════════════════════════════════════════════╡
█  11°C  PARTLY CLOUDY                  H 13° / L 7°  Wind 13 km/h NW  █
└─────────────────────────────────────────────────────────────────────────┘

The Pi is a pure renderer — all display metadata (station names, direction labels) lives in the Convex backend.

What you need

Hardware

Accounts

  • Convex — free tier is enough

On the Pi

  • Python 3.11+
  • Pillow (pip install Pillow)
  • Waveshare e-Paper Python library (see Pi setup)

Repository structure

convex/
  schema.ts            users, stops (with directions), stations tables
  stops.ts             add/remove/list stops
  stations.ts          upsert + seed from bundled JSON
  arrivals.ts          GTFS-RT fetch + enriched response
  http.ts              GET /arrivals endpoint
  crons.ts             (empty — no scheduled jobs)
  data/
    stations.json      992 MTA subway stops, bundled at build time
display/
  display.py           Raspberry Pi display script

Part 1: Convex backend

Deploy

npm install
npm run dev    # first run: prompts you to create a Convex project

This generates your deployment URL (e.g. https://happy-animal-123.convex.site) and saves it to .env.local.

To deploy to production:

npm run deploy

Seed station names

Station names come from the 992-entry convex/data/stations.json bundled in the repo (sourced from MTA GTFS static data). Seed them once after deploy:

npx convex run stations:syncStations

Create your user

npx convex run users:create '{"userId":"your-uuid","alias":"Your Name"}'

Save the returned _id — you'll need it as CONVEX_USER_ID.

Add your stops

Find your directional stop IDs using the MTA stop ID reference, then add each one with its direction labels:

npx convex run stops:add '{
  "userId": "<_id>",
  "stopId": "238N",
  "ordering": 0,
  "directions": [
    {"line": "2", "label": "Manhattan"},
    {"line": "3", "label": "Manhattan"}
  ]
}'

directions controls which lines and labels are shown per stop on the display. ordering (integer, ascending) controls the order stops appear on screen.

Manage stops

To reorder stops:

npx convex run stops:patchOrdering '{"stopId": "238N", "ordering": 0}'

To update directions:

npx convex run stops:patchDirections '{"stopId": "238N", "directions": [{"line": "2", "label": "Manhattan"}]}'

To disable a stop (removes it from the display without deleting it), set disabled: true on the stop document in the Convex dashboard.

Public endpoint

GET https://<deployment>.convex.site/arrivals?userId=<convex_user_id>

Returns the user's alias and up to 6 arrivals per enabled stop, sorted by ordering, enriched with station name and direction labels:

{
  "alias": "Daniel",
  "stops": [
    {
      "stopId": "238N",
      "name": "Eastern Pkwy-Brooklyn Museum",
      "directions": [
        {"line": "2", "label": "Manhattan"},
        {"line": "3", "label": "Manhattan"}
      ],
      "arrivals": [
        {"route": "2", "direction": "N", "arrivalTime": 1234567890, "minutesAway": 3},
        {"route": "3", "direction": "N", "arrivalTime": 1234567950, "minutesAway": 9}
      ]
    }
  ]
}

All other functions are internal and cannot be called directly.


Part 2: Pi setup

Install the Waveshare library

pip install epaper
# or clone and install manually:
# https://github.com/waveshareteam/e-Paper

The display script expects the library at:

/usr/local/lib/python3.13/dist-packages/epaper/e-Paper/RaspberryPi_JetsonNano/python/lib

If yours is in a different location, update the sys.path.insert line at the top of display/display.py.

Install Pillow

pip install Pillow

Clone the repo

git clone https://github.com/PirosB3/mta-convex.git ~/mta-convex

Set up the systemd service

Create /etc/systemd/system/mta-display.service:

[Unit]
Description=MTA Transit E-Ink Display
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/mta-convex
ExecStart=/usr/bin/python3 -u /root/mta-convex/display/display.py
Restart=on-failure
RestartSec=10
StandardOutput=append:/root/mta-display.log
StandardError=append:/root/mta-display.log
Environment=PYTHONUNBUFFERED=1
Environment=CONVEX_ARRIVALS_URL=https://<your-deployment>.convex.site/arrivals
Environment=CONVEX_USER_ID=<your-convex-user-id>
# Optional: set lat/lon for weather (defaults to Brooklyn)
# Environment=WEATHER_LAT=40.68
# Environment=WEATHER_LON=-73.94

[Install]
WantedBy=multi-user.target

Enable and start:

systemctl daemon-reload
systemctl enable mta-display
systemctl start mta-display

Check it's running:

systemctl status mta-display
tail -f /root/mta-display.log

Auto-deploy on push

A local post-push git hook ships in .git/hooks/post-push. When you push to main, it SSHes to the Pi, runs git pull, and restarts the service automatically.

The hook uses root@mta-display.local — adjust PI, REPO_DIR, and SERVICE at the top of the file if your setup differs.


MTA stop IDs

Stop IDs follow the MTA GTFS convention: {station_id}{direction} where direction is N (uptown/Manhattan-bound) or S (downtown/outer-borough-bound). Examples:

Stop ID Station Lines
238N Eastern Pkwy-Brooklyn Museum 2/3 → Manhattan
239N Franklin Av-Medgar Evers College 4/5 → Manhattan
D25N 7 Av (Park Slope) B/Q → Manhattan
F20N Carroll St F/G → Manhattan
A27N Hoyt-Schermerhorn A/C/G → Manhattan

All 992 directional stops are in convex/data/stations.json.


How it works

convex/data/stations.json (bundled, 992 stops)
         │
         ▼ npx convex run stations:syncStations (once)
         │
Convex DB (users + stops w/ directions + stations w/ names)
         │
         ▼
Convex HTTP Action (GET /arrivals)
         │
         ├─ looks up stop directions + station names from DB
         ├─ fetches MTA GTFS-RT protobuf feeds in parallel (free, no API key)
         ├─ decodes with gtfs-realtime-bindings
         └─ returns {alias, stops: [{stopId, name, directions, arrivals}]}
                   │
                   ▼
         Raspberry Pi (polls every 15s)
                   │
                   ├─ fetches live weather from Open-Meteo (free, no key, every 5 min)
                   ├─ renders 800×480 grayscale image with Pillow
                   ├─ partial refresh (no flicker) every 15s
                   └─ full panel refresh every 5 min to clear ghosting

The Pi script has no local configuration — all station metadata is owned by the backend. Weather is fetched live in °C; set WEATHER_LAT/WEATHER_LON for your location.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors