Skip to content

T2C-Screen : serveur Node/Express qui ingère GTFS statique + GTFS-RT, expose une API sécurisée par clé/scopes et sert une mini SPA d’affichage. Le repo inclut aussi un projet ESP32 pour matrice HUB75 consommant l’API, plus des scripts Python d’envoi d’images. Dockerfile/docker-compose pour déploiement.

Notifications You must be signed in to change notification settings

MaximilienHe/t2c-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

T2C-Screen Hero

T2C-Screen

Affichage « temps réel » des prochains départs T2C (Clermont-Ferrand) basé sur GTFS statique + GTFS-RT Trip Updates .

Le repo contient :

  • un serveur Node/Express qui télécharge et indexe le GTFS statique, puis expose une API.
  • un frontend statique (SPA simple) pour configurer la ligne et l’arrêt, et un mode “panneau” .
  • un projet ESP32 + matrice HUB75 qui consomme l’API et peut aussi afficher des messages custom.
  • des scripts Python pour envoyer des bitmaps vers l’ESP32.

Fonctionnalités

  • Téléchargement et parsing du GTFS statique (routes/stops/trips/stop_times).
  • Lecture GTFS-RT (TripUpdates) avec cache court côté serveur.
  • API :
    • liste des lignes
    • suggestions d’arrêts par ligne
    • prochains départs par sens (grouping direction/headsign)
  • Frontend :
    • mode config (inputs)
    • mode display (plein écran) via query string
  • Auth API par clé API + scopes (ex: departures:read).

Structure du repo

  • src/ : serveur Node “moderne” (celui utilisé par le Dockerfile)
    • src/index.js : bootstrap (config, services, serveur)
    • src/config.js : lecture des variables d’environnement
    • src/app.js : init Express (helmet, compression, logs, static)
    • src/routes/api.js : routes /api/* + middleware auth scopes
    • src/controllers/* : contrôleurs routes/stops/departures/health
    • src/services/* :
      • gtfsStatic.service.js : download ZIP, parse CSV, build indexes
      • gtfsRt.service.js : fetch GTFS-RT, cache, decode protobuf
      • departures.service.js : logique de calcul des départs
      • keysStore.service.js : chargement keys.json + lookup token (SHA-256)
    • src/middleware/* : request id, auth API key, 404, error handler
    • src/utils/* : normalisation, fetch avec timeout
  • public/ : frontend statique
    • index.html, style.css, app.js
    • appelle /api/routes, /api/stops, /api/departures
  • docker-compose.yaml : déploiement container (NAS/serveur)
  • Dockerfile : build Node 22 alpine
  • screen-esp/ : projet PlatformIO ESP32
    • src/main.cpp : affichage matrice + fetch API + serveur HTTP local
    • include/config.h : pins HUB75 / résolution
    • python-send-image/ : scripts PIL -> RGB565 -> POST vers ESP
  • server.js : ancien serveur monolithique (présent pour référence). Le serveur actif est celui dans src/.

Sources de données

  • GTFS statique (ZIP) : variable GTFS_ZIP_URL
  • GTFS-RT TripUpdates : variable GTFS_RT_URL

Le serveur télécharge le ZIP, construit des index :

  • routeByShortName : ex "A" -> {route_id, color, longName}
  • stopsByName : nom normalisé -> liste de stop_id
  • routeStopsByRouteId : set des arrêts desservis par une route
  • tripInfoById : trip_id -> {headsign, directionId, routeId}
  • canonicalHeadsignByRouteDir : headsign « majoritaire » par direction
  • stopDirsByRouteStop : inférence direction possible par stopId

Ensuite, pour /api/departures, le serveur lit le GTFS-RT, filtre par route, garde les stopTimeUpdate correspondant à l’arrêt, et regroupe par direction.

API HTTP

Toutes les routes API sont sous /api.

Health

  • GET /api/_health/live : OK si process up
  • GET /api/_health/ready : 200 si GTFS statique chargé, sinon 503

Données

  • GET /api/routes (scope routes:read)
  • GET /api/stops?route=A[&q=jaude] (scope stops:read)
  • GET /api/departures?route=A&stopName=Jaude&max=2 (scope departures:read)
    • Variante plus robuste : stopId au lieu de stopName :
      • GET /api/departures?route=A&stopId=STOP_ID&max=2

Réponse typique /api/departures :

{
  "route": "A",
  "routeColor": "#ffcc33",
  "updatedAt": "2026-01-08T...Z",
  "maxDepartures": 2,
  "stopName": "Jaude",
  "stopId": "...",
  "directions": [
    {
      "dirKey": "dir_0",
      "headsign": "...",
      "directionId": 0,
      "departures": [
        {"timeIso":"...","inMinutes":3},
        {"timeIso":"...","inMinutes":12}
      ]
    }
  ]
}

Auth par clés API (keys.json)

Le serveur protège routes/stops/departures via src/middleware/apiKeyAuth.js.

  • Le client envoie la clé soit :
    • Authorization: Bearer <TOKEN>
    • ou X-API-Key: <TOKEN>
  • Le serveur compare sha256(token) avec tokenSha256 stocké dans keys.json (comparaison timing-safe).
  • Chaque clé a des scopes. Exemple :
    • app mobile : routes:read, stops:read, departures:read
    • esp32 : departures:read

Format de keys.json

keys.json est monté en volume Docker (read-only) et doit rester hors git .

{
  "keys": [
    { "id": "mobile", "tokenSha256": "<hex_sha256>", "scopes": ["routes:read","stops:read","departures:read"] },
    { "id": "esp32",  "tokenSha256": "<hex_sha256>", "scopes": ["departures:read"] }
  ]
}

Génération rapide des clés

Principe :

  1. tu génères un token secret (random).
  2. tu calcules son SHA-256 en hex .
  3. tu mets le SHA dans keys.json, et tu donnes le token en clair au client (mobile/ESP32).

Rapide :

openssl rand -base64 32 | tr -d '\n' | openssl dgst -sha256

1) Générer un token

Option OpenSSL :

openssl rand -base64 32

Option Node :

node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

2) Calculer le SHA-256 (hex)

Option OpenSSL :

echo -n "TOKEN_ICI" | openssl dgst -sha256

Tu récupères l’empreinte hex (sans espaces) et tu la mets dans tokenSha256.

Option Node :

node -e "const c=require('crypto');const t=process.argv[1];console.log(c.createHash('sha256').update(t,'utf8').digest('hex'))" "TOKEN_ICI"

3) Rotation

  • Pour invalider un client : remplacer son entrée dans keys.json par un nouveau SHA.
  • Redémarrer le container pour recharger le fichier (ou ajouter un reload si tu veux).

Frontend (public/)

Le frontend est servi par Express (static), et appelle l’API :

  • suggestions lignes : /api/routes
  • suggestions arrêts : /api/stops?route=A&q=...
  • départs : /api/departures?route=A&stopName=...&max=... (ou stopId=...)

Mode display

Ajouter des query params :

  • ?mode=display&route=A&stop=Jaude&max=2
  • recommandé : &stopId=STOP_ID

Notes :

  • en mode display, les inputs sont désactivés et la mise en page passe en plein écran.

Important : auth côté frontend

Si tu protèges /api/* et que le frontend est servi par le même serveur, mettre une clé dans public/app.js l’expose . Options possibles :

  • laisser le frontend non-auth (API publique) et réserver l’auth aux clients “privés”
  • ou ajouter une route “backend” non protégée (proxy) côté serveur uniquement pour le frontend
  • ou faire une auth navigateur (cookie/session) si usage interne

(Actuellement, le frontend appelle /api/* sans headers : si l’auth est active, il faut adapter fetch() pour envoyer la clé, ou désactiver l’auth sur les endpoints utilisés par le navigateur.)

ESP32 (screen-esp/)

Le code screen-esp/src/main.cpp :

  • se connecte au WiFi
  • interroge périodiquement l’API :
    • GET /api/departures?route=A&stopName=Jaude&max=2
  • affiche 2 lignes (2 sens) sur une matrice HUB75 128x32 (2 panneaux 64x32 chainés)
  • fait défiler le terminus si trop long

Il embarque aussi un petit serveur HTTP local (port 80) pour :

  • POST /config : changer route / stop
  • POST /message/text : push message texte (défilement si long)
  • POST /message/bitmap : push bitmap RGB565
  • POST /message/clear : purge les messages
  • POST /display/brightness : luminosité (0-255)

Déploiement / exécution

Local (Node)

  1. installer :
npm install
  1. lancer :
npm run start

Le serveur écoute sur APP_PORT (défaut 8080).

Docker

Le Dockerfile exécute node src/index.js et sert aussi public/.

Avec docker-compose.yaml :

  • expose 8086:8080 (port externe 8086)
  • monte ./keys.json dans /app/keys.json (read-only)
  • configure via variables d’environnement
docker compose up -d --build

Variables d’environnement

  • APP_PORT (défaut 8080)
  • GTFS_ZIP_URL
  • GTFS_RT_URL
  • GTFS_REFRESH_HOURS (défaut 24)
  • GTFS_RT_CACHE_SECONDS (défaut 5)
  • FETCH_TIMEOUT_MS (défaut 8000)
  • LOG_LEVEL (défaut info)
  • TRUST_PROXY (true/false)
  • KEYS_FILE (défaut keys.json)

Sécurité / bonnes pratiques

  • keys.json dans .gitignore (OK) et monté en read-only.
  • Log : headers authorization et x-api-key sont redacted (src/logger.js).
  • Si Cloudflare Tunnel / reverse proxy : garder TRUST_PROXY=true.
  • En prod, préférer des tokens longs (>= 256 bits) et rotation régulière.

Notes

  • server.js est une version monolithique historique. La version maintenue est src/*.
  • Le frontend actuel ne gère pas l’auth. Si tu actives l’auth sur /api/routes|stops|departures, adapte public/app.js ou expose des endpoints dédiés.

About

T2C-Screen : serveur Node/Express qui ingère GTFS statique + GTFS-RT, expose une API sécurisée par clé/scopes et sert une mini SPA d’affichage. Le repo inclut aussi un projet ESP32 pour matrice HUB75 consommant l’API, plus des scripts Python d’envoi d’images. Dockerfile/docker-compose pour déploiement.

Topics

Resources

Stars

Watchers

Forks