// Route 5 bus monitor — parses GTFS-RT feed for real-time vehicle positions // Usage: node monitor-route5.js const https = require('https'); const fs = require('fs'); const protobuf = require('/tmp/gtfs-rt/node_modules/protobufjs'); const FEED_URL = 'https://data.texas.gov/download/i5qp-g5fd/application/octet-stream'; const ROUTE_ID = '5'; const FIRST_STOP = '5854'; // Anderson/Northcross const USER_STOP = '964'; // Woodrow/Choquette const TRAVEL_SECS = 446; // 7 min 26 sec from first stop to user stop // GTFS-RT proto definition (minimal, inline) const PROTO = ` syntax = "proto2"; package transit_realtime; message FeedMessage { required FeedHeader header = 1; repeated FeedEntity entity = 2; } message FeedHeader { required string gtfs_realtime_version = 1; optional uint64 timestamp = 2; } message FeedEntity { required string id = 1; optional TripUpdate trip_update = 3; optional VehiclePosition vehicle = 4; optional Alert alert = 5; } message TripUpdate { optional TripDescriptor trip = 1; optional VehicleDescriptor vehicle = 3; repeated StopTimeUpdate stop_time_update = 2; optional uint64 timestamp = 4; message StopTimeUpdate { optional uint32 stop_sequence = 1; optional string stop_id = 4; optional StopTimeEvent arrival = 2; optional StopTimeEvent departure = 3; enum ScheduleRelationship { SCHEDULED = 0; SKIPPED = 1; NO_DATA = 2; } optional ScheduleRelationship schedule_relationship = 5; } } message StopTimeEvent { optional int32 delay = 1; optional int64 time = 2; optional int32 uncertainty = 3; } message VehiclePosition { optional TripDescriptor trip = 1; optional VehicleDescriptor vehicle = 8; optional Position position = 2; optional uint32 current_stop_sequence = 3; optional string stop_id = 7; enum VehicleStopStatus { INCOMING_AT = 0; STOPPED_AT = 1; IN_TRANSIT_TO = 2; } optional VehicleStopStatus current_status = 4; optional uint64 timestamp = 5; enum CongestionLevel { UNKNOWN = 0; RUNNING_SMOOTHLY = 1; STOP_AND_GO = 2; CONGESTION = 3; SEVERE_CONGESTION = 4; } optional CongestionLevel congestion_level = 6; enum OccupancyStatus { EMPTY = 0; MANY_SEATS = 1; FEW_SEATS = 2; STANDING_ROOM = 3; CRUSHED = 4; FULL = 5; NOT_ACCEPTING = 6; } optional OccupancyStatus occupancy_status = 9; } message TripDescriptor { optional string trip_id = 1; optional string route_id = 5; optional uint32 direction_id = 6; optional string start_time = 2; optional string start_date = 3; enum ScheduleRelationship { SCHEDULED = 0; ADDED = 1; UNSCHEDULED = 2; CANCELED = 3; } optional ScheduleRelationship schedule_relationship = 4; } message VehicleDescriptor { optional string id = 1; optional string label = 2; optional string license_plate = 3; } message Position { required float latitude = 1; required float longitude = 2; optional float bearing = 3; optional double odometer = 4; optional float speed = 5; } message Alert { repeated TimeRange active_period = 1; repeated EntitySelector informed_entity = 5; optional TranslatedString header_text = 10; optional TranslatedString description_text = 11; } message TimeRange { optional uint64 start = 1; optional uint64 end = 2; } message EntitySelector { optional string agency_id = 1; optional string route_id = 3; optional TripDescriptor trip = 4; optional string stop_id = 6; } message TranslatedString { repeated Translation translation = 1; message Translation { required string text = 1; optional string language = 2; } } `; function fetch(url) { return new Promise((resolve, reject) => { https.get(url, (res) => { const chunks = []; res.on('data', c => chunks.push(c)); res.on('end', () => resolve(Buffer.concat(chunks))); res.on('error', reject); }).on('error', reject); }); } async function main() { const root = protobuf.parse(PROTO, { keepCase: true }).root; const FeedMessage = root.lookupType('transit_realtime.FeedMessage'); const buf = await fetch(FEED_URL); const feed = FeedMessage.decode(buf); const now = Math.floor(Date.now() / 1000); const cst = new Date((now - 6*3600) * 1000); // CST offset const timeStr = cst.toISOString().replace('T', ' ').substring(0, 19) + ' CST'; // Filter Route 5 vehicles const vehicles = feed.entity .filter(e => e.vehicle && e.vehicle.trip && e.vehicle.trip.route_id === ROUTE_ID) .map(e => { const v = e.vehicle; const age = now - (v.timestamp?.low || v.timestamp || 0); const status = ['INCOMING_AT', 'STOPPED_AT', 'IN_TRANSIT_TO'][v.current_status] || 'UNKNOWN'; return { vehicleId: v.vehicle?.label || v.vehicle?.id || 'unknown', tripId: v.trip?.trip_id, directionId: v.trip?.direction_id, stopId: v.stop_id, stopSequence: v.current_stop_sequence, status, lat: v.position?.latitude, lon: v.position?.longitude, speed: v.position?.speed, bearing: v.position?.bearing, ageSec: age, timestamp: v.timestamp }; }); // Filter Route 5 trip updates const tripUpdates = feed.entity .filter(e => e.trip_update && e.trip_update.trip && e.trip_update.trip.route_id === ROUTE_ID) .map(e => { const tu = e.trip_update; const userStopUpdate = tu.stop_time_update?.find(s => s.stop_id === USER_STOP); const firstStopUpdate = tu.stop_time_update?.find(s => s.stop_id === FIRST_STOP); return { tripId: tu.trip?.trip_id, directionId: tu.trip?.direction_id, vehicleId: tu.vehicle?.label || tu.vehicle?.id, userStopDelay: userStopUpdate?.departure?.delay || userStopUpdate?.arrival?.delay || null, userStopTime: userStopUpdate?.arrival?.time || userStopUpdate?.departure?.time || null, firstStopDelay: firstStopUpdate?.departure?.delay || null, firstStopTime: firstStopUpdate?.departure?.time || null, totalStops: tu.stop_time_update?.length || 0 }; }); // Eastbound (direction 0) only const ebVehicles = vehicles.filter(v => v.directionId === 0); const ebUpdates = tripUpdates.filter(t => t.directionId === 0); const result = { timestamp: timeStr, feedTimestamp: feed.header?.timestamp?.toString(), route5_eastbound: { activeVehicles: ebVehicles.length, vehicles: ebVehicles, tripUpdates: ebUpdates.filter(t => t.userStopDelay !== null || t.userStopTime !== null) }, route5_all: { totalVehicles: vehicles.length, totalTripUpdates: tripUpdates.length } }; console.log(JSON.stringify(result, null, 2)); } main().catch(e => console.error(JSON.stringify({ error: e.message })));