Compare commits
24 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
014e9dd0d3 | |
|
|
d3d55eb85e | |
|
|
03d22f47d7 | |
|
|
d9811d3f91 | |
|
|
cc2b63e816 | |
|
|
b5d9fa1dd8 | |
|
|
3c3199e3eb | |
|
|
9f87527d9a | |
|
|
35d0261682 | |
|
|
36749669b4 | |
|
|
d5c57b35eb | |
|
|
3c2aa25cd8 | |
|
|
076066ce0b | |
|
|
e59201bfa3 | |
|
|
8bf7503a39 | |
|
|
c244a4bc21 | |
|
|
e39e18d243 | |
|
|
ccf7c62727 | |
|
|
b16830020d | |
|
|
12cb88ed20 | |
|
|
f8857b3015 | |
|
|
053e1191e0 | |
|
|
4517bf0dbb | |
|
|
a89d28129f |
|
|
@ -1,4 +1,6 @@
|
|||
__pycache__/*
|
||||
.vscode/*
|
||||
Enclosure/*.stl
|
||||
*test*
|
||||
*.svg
|
||||
*.FCStd1
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "1f552469",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import gtfs_kit as gk\n",
|
||||
"import pandas as pd\n",
|
||||
"import datetime\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "292cc196",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"feed=gk.read_feed('google_transit_combined.zip', dist_units='km')\n",
|
||||
"feed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: GTFS Client",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "gtfs_client.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -47,15 +47,16 @@ If your display's resolution is not 1920x720, you will also need to change the c
|
|||
* libspatialindex-c6
|
||||
* yaml
|
||||
|
||||
```
|
||||
$ sudo apt install python3-iso8601 python3-zeep libsdl2-ttf-2.0-0 python3-numpy python3-pandas python3-fiona python3-pyproj libspatialindex-c6 python3-yaml
|
||||
```shell
|
||||
$ sudo apt install python3-iso8601 python3-zeep libsdl2-ttf-2.0-0 python3-numpy python3-pandas python3-fiona python3-pyproj libspatialindex-c6 python3-yaml python3-urllib3
|
||||
```
|
||||
|
||||
* pygame 2
|
||||
* GTFS-Kit
|
||||
|
||||
```
|
||||
$ sudo pip3 install pygame gtfs_kit
|
||||
```shell
|
||||
$ sudo apt install python3-pip
|
||||
$ sudo pip3 install pygame gtfs_kit schedule --break-system-packages
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import datetime
|
||||
|
||||
class ArrivalTime():
|
||||
class ArrivalTime:
|
||||
""" Represents the arrival times of buses at one of the configured stops """
|
||||
|
||||
def __init__(self, stop_id: str, route_id: str, destination: str, due_in_seconds: int, is_added: bool = False) -> None:
|
||||
|
|
@ -14,7 +14,7 @@ class ArrivalTime():
|
|||
def due_in_minutes(self) -> int:
|
||||
return int(self.due_in_seconds / 60)
|
||||
|
||||
def isDue(self) -> bool:
|
||||
def is_due(self) -> bool:
|
||||
return self.due_in_minutes < 1
|
||||
|
||||
def due_in_str(self) -> str:
|
||||
|
|
|
|||
29
config.py
29
config.py
|
|
@ -4,40 +4,45 @@ class Config:
|
|||
def __init__(self):
|
||||
# Load the config file
|
||||
with open("config.yaml") as f:
|
||||
self.__config = yaml.safe_load(f.read())
|
||||
self.config = yaml.safe_load(f.read())
|
||||
|
||||
# Pre-load some dictionaries to simplify lookups
|
||||
self.__walk_time_by_stop = {}
|
||||
for s in self.__config.get("stops", []):
|
||||
self.__walk_time_by_stop[s["stop_id"]] = s["walk_time"]
|
||||
# Preload some dictionaries to simplify lookups
|
||||
self.walk_time_by_stop = {}
|
||||
for s in self.config.get("stops", []):
|
||||
self.walk_time_by_stop[str(s["stop_id"])] = s["walk_time"]
|
||||
|
||||
@property
|
||||
def gtfs_feed_url(self) -> str:
|
||||
return self.__config.get("gtfs-feed-url")
|
||||
return self.config.get("gtfs-feed-url")
|
||||
|
||||
@property
|
||||
def gtfs_api_url(self) -> str:
|
||||
return self.__config.get("gtfs-r-api-url")
|
||||
return self.config.get("gtfs-r-api-url")
|
||||
|
||||
@property
|
||||
def gtfs_api_key(self) -> str:
|
||||
return self.__config.get("gtfs-r-api_key")
|
||||
return self.config.get("gtfs-r-api_key")
|
||||
|
||||
@property
|
||||
def update_interval_seconds(self) -> int:
|
||||
return self.__config.get("update-interval-seconds")
|
||||
return self.config.get("update-interval-seconds")
|
||||
|
||||
@property
|
||||
def font_file(self) -> str:
|
||||
return self.config.get("font-file")
|
||||
|
||||
@property
|
||||
def stop_codes(self) -> list[str]:
|
||||
return [str(s["stop_id"]) for s in self.__config.get("stops")]
|
||||
return [str(s["stop_id"]) for s in self.config.get("stops")]
|
||||
|
||||
def minutes_to_stop(self, stop_id) -> int:
|
||||
return self.__walk_time_by_stop.get(stop_id, 0)
|
||||
minutes = self.walk_time_by_stop.get(stop_id, 0)
|
||||
return minutes
|
||||
|
||||
def routes_for_stops(self) -> map:
|
||||
result = {}
|
||||
|
||||
for s in self.__config.get("stops"):
|
||||
for s in self.config.get("stops"):
|
||||
for r in s.get("routes", []):
|
||||
routes = (result.get(s.get("stop_id")) or [])
|
||||
routes.append(r)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# URLs and API keys for the different parts of the GTFS-R feed
|
||||
# You should not change these unless a new version of the API is released
|
||||
gtfs-feed-url: "https://www.transportforireland.ie/transitData/Data/GTFS_Realtime.zip"
|
||||
gtfs-r-api-url: "https://api.nationaltransport.ie/gtfsr/v2/gtfsr?format=json"
|
||||
gtfs-r-api-url: "https://api.nationaltransport.ie/gtfsr/v2/TripUpdates?format=json"
|
||||
|
||||
# You should change this one. Use the key you get from TFI when you register for GTFS-R access
|
||||
gtfs-r-api_key: "API KEY GOES HERE"
|
||||
|
|
@ -12,6 +12,9 @@ gtfs-r-api_key: "API KEY GOES HERE"
|
|||
# It must be strictly larger than 60 because the GTFS-R API will throttle us otherwise
|
||||
update-interval-seconds: 62
|
||||
|
||||
# The font to use for the display.
|
||||
font-file: "jd_lcd_rounded.ttf"
|
||||
|
||||
stops: [
|
||||
{
|
||||
# Route 15A
|
||||
|
|
|
|||
287
gtfs_client.py
287
gtfs_client.py
|
|
@ -8,12 +8,11 @@ import pandas as pd
|
|||
import queue
|
||||
import refresh_feed
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
import zipfile
|
||||
|
||||
class GTFSClient():
|
||||
class GTFSClient:
|
||||
def __init__(self, feed_url: str, gtfs_r_url: str, gtfs_r_api_key: str,
|
||||
stop_codes: list[str], routes_for_stops: dict[str, str],
|
||||
update_queue: queue.Queue, update_interval_seconds: int = 60):
|
||||
|
|
@ -21,20 +20,20 @@ class GTFSClient():
|
|||
self.stop_codes = stop_codes
|
||||
self.routes_for_stops = routes_for_stops
|
||||
|
||||
feed_name = feed_url.split('/')[-1]
|
||||
feed_name = '/tmp/' + feed_url.split('/')[-1]
|
||||
self.gtfs_r_url = gtfs_r_url
|
||||
self.gtfs_r_api_key = gtfs_r_api_key
|
||||
|
||||
# Make sure that the feed file is up to date
|
||||
try:
|
||||
last_mtime = os.stat(feed_name).st_mtime
|
||||
last_mtime = int(os.stat(feed_name).st_mtime)
|
||||
except:
|
||||
last_mtime = 0
|
||||
|
||||
refreshed, new_mtime = refresh_feed.update_local_file_from_url_v1(last_mtime, feed_name, feed_url)
|
||||
_, new_mtime = refresh_feed.update_local_file_from_url_v1(last_mtime, feed_name, feed_url)
|
||||
|
||||
# Load the feed
|
||||
self.feed = self._read_feed(feed_name, dist_units='km', stop_codes = stop_codes)
|
||||
self.feed = self._read_feed(feed_name, dist_units='km')
|
||||
gc.collect()
|
||||
self.stop_ids = self.__wanted_stop_ids()
|
||||
self.deltas = {}
|
||||
|
|
@ -45,9 +44,8 @@ class GTFSClient():
|
|||
self._update_queue = update_queue
|
||||
if update_interval_seconds and update_queue:
|
||||
self._update_interval_seconds = update_interval_seconds
|
||||
self._refresh_thread = threading.Thread(target=lambda: every(update_interval_seconds, self.refresh))
|
||||
|
||||
def _read_feed(self, path: gk.Path, dist_units: str, stop_codes: list[str]) -> gk.Feed:
|
||||
def _read_feed(self, path: str, dist_units: str) -> gk.Feed:
|
||||
"""
|
||||
NOTE: This helper method was extracted from gtfs_kit.feed to modify it
|
||||
to only load the stop_times for the stops we are interested in,
|
||||
|
|
@ -56,7 +54,7 @@ class GTFSClient():
|
|||
This version also reads CSV data straight from the zip file to avoid
|
||||
wearing out the Pi's SD card.
|
||||
"""
|
||||
FILES_TO_LOAD = [
|
||||
files_to_load = [
|
||||
# List of feed files to load. stop_times.txt is loaded separately.
|
||||
'trips.txt',
|
||||
'routes.txt',
|
||||
|
|
@ -66,15 +64,15 @@ class GTFSClient():
|
|||
'agency.txt'
|
||||
]
|
||||
|
||||
path = gk.Path(path)
|
||||
if not path.exists():
|
||||
raise ValueError(f"Path {path} does not exist")
|
||||
if not os.path.exists(path):
|
||||
raise ValueError("Path {} does not exist".format(path))
|
||||
|
||||
print("Loading GTFS feed {}".format(path), file=sys.stderr)
|
||||
gc.collect()
|
||||
|
||||
feed_dict = {table: None for table in gk.cs.GTFS_REF["table"]}
|
||||
with zipfile.ZipFile(path) as z:
|
||||
for filename in FILES_TO_LOAD:
|
||||
for filename in files_to_load:
|
||||
table = filename.split(".")[0]
|
||||
# read the file
|
||||
with z.open(filename) as f:
|
||||
|
|
@ -216,20 +214,22 @@ class GTFSClient():
|
|||
next_buses.drop(index=ids_to_delete, inplace=True)
|
||||
return next_buses
|
||||
|
||||
def __time_to_seconds(self, s: str) -> int:
|
||||
@staticmethod
|
||||
def __time_to_seconds(s: str) -> int:
|
||||
sx = s.split(":")
|
||||
if len(sx) != 3:
|
||||
print("Malformed timestamp:", s)
|
||||
return 0
|
||||
return int(sx[0]) * 3600 + int(sx[1]) * 60 + int (sx[2])
|
||||
|
||||
def __due_in_seconds(self, time_str: str) -> int:
|
||||
@staticmethod
|
||||
def __due_in_seconds(time_str: str) -> int:
|
||||
"""
|
||||
Returns the number of seconds in the future that the time_str (format hh:mm:ss) is
|
||||
"""
|
||||
now = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
tnow = self.__time_to_seconds(now)
|
||||
tstop = self.__time_to_seconds(time_str)
|
||||
tnow = GTFSClient.__time_to_seconds(now)
|
||||
tstop = GTFSClient.__time_to_seconds(time_str)
|
||||
if tstop > tnow:
|
||||
return tstop - tnow
|
||||
else:
|
||||
|
|
@ -242,88 +242,96 @@ class GTFSClient():
|
|||
Look up a destination string in Trips from the route and direction
|
||||
"""
|
||||
trips = self.feed.trips
|
||||
return trips[(trips["route_id"] == route_id) & (trips["direction_id"] == direction_id)].head(1)["trip_headsign"].item()
|
||||
destination = trips[(trips["route_id"] == route_id) & (trips["direction_id"] == direction_id)].head(1)["trip_headsign"].item()
|
||||
# For some reason destination sometimes isn't a string. Try to find out why
|
||||
if not destination.__class__ == str:
|
||||
sys.stderr.write("Destination not found for route " + str(route_id) + ", direction " + str(direction_id) + "\n")
|
||||
destination = "---- ?????? ----"
|
||||
|
||||
return destination
|
||||
|
||||
|
||||
def __poll_gtfsr_deltas(self) -> list[map, set]:
|
||||
def __poll_gtfsr_deltas(self) -> tuple[dict, list, list]:
|
||||
try:
|
||||
# Poll GTFS-R API
|
||||
if self.gtfs_r_api_key != "":
|
||||
headers = {"x-api-key": self.gtfs_r_api_key}
|
||||
response = requests.get(url = self.gtfs_r_url, headers = headers, timeout=(2, 10))
|
||||
if response.status_code != 200:
|
||||
print("GTFS-R sent non-OK response: {}\n{}".format(response.status_code, response.text))
|
||||
return {}, [], []
|
||||
|
||||
# Poll GTFS-R API
|
||||
if self.gtfs_r_api_key != "":
|
||||
headers = {"x-api-key": self.gtfs_r_api_key}
|
||||
response = requests.get(url = self.gtfs_r_url, headers = headers)
|
||||
if response.status_code != 200:
|
||||
print("GTFS-R sent non-OK response: {}\n{}".format(response.status_code, response.text))
|
||||
return ({}, [], [])
|
||||
deltas_json = json.loads(response.text)
|
||||
else:
|
||||
deltas_json = json.load(open("example.json"))
|
||||
|
||||
deltas_json = json.loads(response.text)
|
||||
else:
|
||||
deltas_json = json.load(open("example.json"))
|
||||
deltas = {}
|
||||
canceled_trips = set()
|
||||
added_stops = []
|
||||
|
||||
deltas = {}
|
||||
canceled_trips = set()
|
||||
added_stops = []
|
||||
# Pre-compute some data to use for added trips:
|
||||
relevant_service_ids = self.__current_service_ids()
|
||||
relevant_trips = self.feed.trips[self.feed.trips["service_id"].isin(relevant_service_ids)]
|
||||
relevant_route_ids = set(relevant_trips["route_id"])
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
|
||||
# Pre-compute some data to use for added trips:
|
||||
relevant_service_ids = self.__current_service_ids()
|
||||
relevant_trips = self.feed.trips[self.feed.trips["service_id"].isin(relevant_service_ids)]
|
||||
relevant_route_ids = set(relevant_trips["route_id"])
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
for e in deltas_json.get("entity", []):
|
||||
try:
|
||||
trip_update = e.get("trip_update")
|
||||
trip = trip_update.get("trip")
|
||||
trip_id = trip.get("trip_id")
|
||||
trip_action = trip.get("schedule_relationship")
|
||||
if trip_action == "SCHEDULED":
|
||||
for u in e.get("trip_update", {}).get("stop_time_update", []):
|
||||
delay = u.get("arrival", u.get("departure", {})).get("delay", 0)
|
||||
deltas_for_trip = (deltas.get(trip_id) or {})
|
||||
deltas_for_trip[u.get("stop_id")] = delay
|
||||
deltas[trip_id] = deltas_for_trip
|
||||
|
||||
for e in deltas_json.get("entity", []):
|
||||
is_deleted = e.get("is_deleted") or False
|
||||
try:
|
||||
trip_update = e.get("trip_update")
|
||||
trip = trip_update.get("trip")
|
||||
trip_id = trip.get("trip_id")
|
||||
trip_action = trip.get("schedule_relationship")
|
||||
if trip_action == "SCHEDULED":
|
||||
for u in e.get("trip_update", {}).get("stop_time_update", []):
|
||||
delay = u.get("arrival", u.get("departure", {})).get("delay", 0)
|
||||
deltas_for_trip = (deltas.get(trip_id) or {})
|
||||
deltas_for_trip[u.get("stop_id")] = delay
|
||||
deltas[trip_id] = deltas_for_trip
|
||||
elif trip_action == "ADDED":
|
||||
start_date = trip.get("start_date")
|
||||
start_time = trip.get("start_time")
|
||||
route_id = trip.get("route_id")
|
||||
direction_id = trip.get("direction_id")
|
||||
|
||||
elif trip_action == "ADDED":
|
||||
start_date = trip.get("start_date")
|
||||
start_time = trip.get("start_time")
|
||||
route_id = trip.get("route_id")
|
||||
direction_id = trip.get("direction_id")
|
||||
|
||||
# Check if the route is part of the routes we care about
|
||||
if not route_id in relevant_route_ids:
|
||||
continue
|
||||
# Check if the route is part of the routes we care about
|
||||
if not route_id in relevant_route_ids:
|
||||
continue
|
||||
|
||||
# And that it's for today
|
||||
current_time = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
if start_date > today or start_time > current_time:
|
||||
continue
|
||||
# And that it's for today
|
||||
current_time = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
if start_date > today or start_time > current_time:
|
||||
continue
|
||||
|
||||
# Look for the entry for any of the stops we want
|
||||
wanted_stop_ids = self.__wanted_stop_ids()
|
||||
for stop_time_update in e.get("trip_update").get("stop_time_update", []):
|
||||
if stop_time_update.get("stop_id", "") in wanted_stop_ids:
|
||||
arrival_time = int((stop_time_update.get("arrival", stop_time_update.get("departure", {})).get("time", 0)))
|
||||
if arrival_time < int(time.time()):
|
||||
continue
|
||||
new_arrival = ArrivalTime(
|
||||
stop_id = stop_time_update.get("stop_id"),
|
||||
route_id = self.feed.routes[self.feed.routes["route_id"] == route_id]["route_short_name"].item(),
|
||||
destination = self.__lookup_headsign_by_route(route_id, direction_id),
|
||||
due_in_seconds = arrival_time - int(time.time()),
|
||||
is_added = True
|
||||
)
|
||||
print("Added route:", new_arrival)
|
||||
added_stops.append(new_arrival)
|
||||
# Look for the entry for any of the stops we want
|
||||
wanted_stop_ids = self.__wanted_stop_ids()
|
||||
for stop_time_update in e.get("trip_update").get("stop_time_update", []):
|
||||
if stop_time_update.get("stop_id", "") in wanted_stop_ids:
|
||||
arrival_time = int((stop_time_update.get("arrival", stop_time_update.get("departure", {})).get("time", 0)))
|
||||
if arrival_time < int(time.time()):
|
||||
continue
|
||||
new_arrival = ArrivalTime(
|
||||
stop_id = stop_time_update.get("stop_code"),
|
||||
route_id = self.feed.routes[self.feed.routes["route_id"] == route_id]["route_short_name"].item(),
|
||||
destination = self.__lookup_headsign_by_route(route_id, direction_id),
|
||||
due_in_seconds = arrival_time - int(time.time()),
|
||||
is_added = True
|
||||
)
|
||||
print("Added route:", new_arrival)
|
||||
added_stops.append(new_arrival)
|
||||
|
||||
elif trip_action == "CANCELED":
|
||||
canceled_trips.add(trip_id)
|
||||
else:
|
||||
print("Unsupported action:", trip_action)
|
||||
except Exception as x:
|
||||
print("Error parsing GTFS-R entry:", str(e))
|
||||
raise(x)
|
||||
|
||||
return deltas, canceled_trips, added_stops
|
||||
elif trip_action == "CANCELED":
|
||||
canceled_trips.add(trip_id)
|
||||
else:
|
||||
print("Unsupported action:", trip_action)
|
||||
except Exception as x:
|
||||
print("Error parsing GTFS-R entry:", str(e))
|
||||
raise x
|
||||
|
||||
return deltas, canceled_trips, added_stops
|
||||
except Exception as e:
|
||||
print("Polling for GTFS-R failed:", str(e))
|
||||
return {}, [], []
|
||||
|
||||
|
||||
def get_next_n_buses(self, num_entries: int) -> pd.core.frame.DataFrame:
|
||||
|
|
@ -338,69 +346,48 @@ class GTFSClient():
|
|||
return joined_data
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
""" Start the refresh thread """
|
||||
self._refresh_thread.start()
|
||||
self.refresh()
|
||||
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
Create and enqueue the refreshed stop data
|
||||
"""
|
||||
# Retrieve the GTFS-R deltas
|
||||
deltas, canceled_trips, added_stops = self.__poll_gtfsr_deltas()
|
||||
if len(deltas) > 0 or len(canceled_trips) > 0 or len(added_stops) > 0:
|
||||
# Only update deltas and canceled trips if the API returns data
|
||||
self.deltas = deltas
|
||||
self.canceled_trips = canceled_trips
|
||||
self.added_stops = added_stops
|
||||
|
||||
arrivals = []
|
||||
# take more entries than we need in case there are cancelations
|
||||
buses = self.get_next_n_buses(15)
|
||||
|
||||
for index, bus in buses.iterrows():
|
||||
if not bus["trip_id"] in self.canceled_trips:
|
||||
delta = self.deltas.get(bus["trip_id"], {}).get(bus["stop_id"], 0)
|
||||
if delta != 0:
|
||||
print("Delta for route {} stop {} is {}".format(bus["route_short_name"], bus["stop_id"], delta))
|
||||
|
||||
arrival = ArrivalTime(stop_id = bus["stop_id"],
|
||||
route_id = bus["route_short_name"],
|
||||
destination = bus["trip_headsign"],
|
||||
due_in_seconds = self.__due_in_seconds(bus["arrival_time"]) + delta,
|
||||
is_added = False
|
||||
)
|
||||
arrivals.append(arrival)
|
||||
|
||||
if len(self.added_stops) > 0:
|
||||
# Append the added stops from GTFS-R and re-sort
|
||||
arrivals.extend(self.added_stops)
|
||||
arrivals.sort()
|
||||
|
||||
# Select the first 5 of what remains
|
||||
arrivals = arrivals[0:5]
|
||||
|
||||
if self._update_queue:
|
||||
self._update_queue.put(arrivals)
|
||||
|
||||
gc.collect()
|
||||
return arrivals
|
||||
|
||||
|
||||
def every(delay, task) -> None:
|
||||
""" Auxilliary function to schedule updates.
|
||||
Taken from https://stackoverflow.com/questions/474528/what-is-the-best-way-to-repeatedly-execute-a-function-every-x-seconds
|
||||
"""
|
||||
next_time = time.time() + delay
|
||||
while True:
|
||||
time.sleep(max(0, next_time - time.time()))
|
||||
try:
|
||||
task()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
# in production code you might want to have this instead of course:
|
||||
# logger.exception("Problem while executing repetitive task.")
|
||||
# skip tasks if we are behind schedule:
|
||||
next_time += (time.time() - next_time) // delay * delay + delay
|
||||
# Retrieve the GTFS-R deltas
|
||||
deltas, canceled_trips, added_stops = self.__poll_gtfsr_deltas()
|
||||
if len(deltas) > 0 or len(canceled_trips) > 0 or len(added_stops) > 0:
|
||||
# Only update deltas and canceled trips if the API returns data
|
||||
self.deltas = deltas
|
||||
self.canceled_trips = canceled_trips
|
||||
self.added_stops = added_stops
|
||||
|
||||
arrivals = []
|
||||
# take more entries than we need in case there are cancellations
|
||||
buses = self.get_next_n_buses(15)
|
||||
|
||||
for index, bus in buses.iterrows():
|
||||
if not bus["trip_id"] in self.canceled_trips:
|
||||
delta = self.deltas.get(bus["trip_id"], {}).get(bus["stop_id"], 0)
|
||||
if delta != 0:
|
||||
print("Delta for route {} stop {} is {}".format(bus["route_short_name"], bus["stop_id"], delta))
|
||||
|
||||
arrival = ArrivalTime(stop_id = bus["stop_code"],
|
||||
route_id = bus["route_short_name"],
|
||||
destination = bus["trip_headsign"],
|
||||
due_in_seconds = GTFSClient.__due_in_seconds(bus["arrival_time"]) + delta,
|
||||
is_added = False
|
||||
)
|
||||
arrivals.append(arrival)
|
||||
|
||||
if len(self.added_stops) > 0:
|
||||
# Append the added stops from GTFS-R and re-sort
|
||||
arrivals.extend(self.added_stops)
|
||||
arrivals.sort()
|
||||
|
||||
# Select the first 5 of what remains
|
||||
arrivals = arrivals[0:5]
|
||||
|
||||
if self._update_queue:
|
||||
self._update_queue.put(arrivals)
|
||||
|
||||
gc.collect()
|
||||
except Exception as e:
|
||||
print("Exception in refresh: {}".format(str(e)))
|
||||
|
|
|
|||
100
main.py
100
main.py
|
|
@ -7,6 +7,7 @@ import gc
|
|||
from glob import glob
|
||||
import pygame
|
||||
from pygame.locals import *
|
||||
import schedule
|
||||
from time import sleep
|
||||
import queue
|
||||
from arrival_times import ArrivalTime
|
||||
|
|
@ -71,31 +72,34 @@ def write_line(line: int, text: str, text_color: Color = COLOR_LCD_AMBER):
|
|||
|
||||
def update_screen(config: Config, updates: list[ArrivalTime]) -> None:
|
||||
""" Repaint the screen with the new arrival times """
|
||||
updates = updates[0:LINE_COUNT] # take the first X lines
|
||||
for line_num, update in enumerate(updates):
|
||||
# Find what color we need to use for the ETA
|
||||
time_to_walk = update.due_in_minutes - (config.minutes_to_stop(update.stop_id) or 0)
|
||||
lcd_color = None
|
||||
if time_to_walk > 5:
|
||||
lcd_color = COLOR_LCD_GREEN
|
||||
elif time_to_walk > 1:
|
||||
lcd_color = COLOR_LCD_AMBER
|
||||
else:
|
||||
lcd_color = COLOR_LCD_RED
|
||||
try:
|
||||
updates = updates[0:LINE_COUNT] # take the first X lines
|
||||
for line_num, update in enumerate(updates):
|
||||
# Find what color we need to use for the ETA
|
||||
time_to_walk = update.due_in_minutes - (config.minutes_to_stop(update.stop_id) or 0)
|
||||
lcd_color = None
|
||||
if time_to_walk > 5:
|
||||
lcd_color = COLOR_LCD_GREEN
|
||||
elif time_to_walk > 1:
|
||||
lcd_color = COLOR_LCD_AMBER
|
||||
else:
|
||||
lcd_color = COLOR_LCD_RED
|
||||
|
||||
# Draw the line
|
||||
write_entry(
|
||||
line = line_num,
|
||||
route = update.route_id,
|
||||
destination = update.destination,
|
||||
time_left = 'Due' if update.isDue() else update.due_in_str(),
|
||||
time_color = lcd_color,
|
||||
text_color = COLOR_LCD_GREEN if update.is_added else COLOR_LCD_AMBER
|
||||
)
|
||||
# Draw the line
|
||||
write_entry(
|
||||
line = line_num,
|
||||
route = update.route_id,
|
||||
destination = update.destination,
|
||||
time_left = 'Due' if update.is_due() else update.due_in_str(),
|
||||
time_color = lcd_color,
|
||||
text_color = COLOR_LCD_GREEN if update.is_added else COLOR_LCD_AMBER
|
||||
)
|
||||
|
||||
# Add the current time to the bottom line
|
||||
datetime_text = "Current time: " + datetime.today().strftime("%d/%m/%Y %H:%M")
|
||||
write_line(5, datetime_text)
|
||||
# Add the current time to the bottom line
|
||||
datetime_text = "Current time: " + datetime.today().strftime("%d/%m/%Y %H:%M")
|
||||
write_line(5, datetime_text)
|
||||
except Exception as e:
|
||||
print("Error updating screen: ", str(e))
|
||||
|
||||
def clear_screen() -> None:
|
||||
""" Clear screen """
|
||||
|
|
@ -120,10 +124,12 @@ def main():
|
|||
config = Config()
|
||||
|
||||
# Initialise graphics context
|
||||
pygame.init()
|
||||
pygame.display.init()
|
||||
pygame.font.init()
|
||||
|
||||
window = init_screen()
|
||||
pygame.font.init()
|
||||
font = pygame.font.Font(TEXT_FONT, TEXT_SIZE)
|
||||
font = pygame.font.Font(config.font_file or TEXT_FONT, TEXT_SIZE)
|
||||
|
||||
# Init screen
|
||||
clear_screen()
|
||||
|
|
@ -140,33 +146,39 @@ def main():
|
|||
update_queue=update_queue,
|
||||
update_interval_seconds=config.update_interval_seconds)
|
||||
|
||||
scheduler.start()
|
||||
# Schedule feed refresh, and force the first one
|
||||
schedule.every(config.update_interval_seconds).seconds.do(scheduler.refresh)
|
||||
scheduler.refresh()
|
||||
|
||||
# Main event loop
|
||||
running = True
|
||||
while running:
|
||||
# Pygame event handling begins
|
||||
if pygame.event.peek():
|
||||
for e in pygame.event.get():
|
||||
if e.type == pygame.QUIT:
|
||||
running = False
|
||||
elif e.type == pygame.KEYDOWN:
|
||||
if e.key == pygame.K_ESCAPE:
|
||||
try:
|
||||
# Pygame event handling begins
|
||||
if pygame.event.peek():
|
||||
for e in pygame.event.get():
|
||||
if e.type == pygame.QUIT:
|
||||
running = False
|
||||
pygame.display.flip()
|
||||
# Pygame event handling ends
|
||||
elif e.type == pygame.KEYDOWN:
|
||||
if e.key == pygame.K_ESCAPE:
|
||||
running = False
|
||||
pygame.display.flip()
|
||||
# Pygame event handling ends
|
||||
|
||||
# Display update begins
|
||||
if update_queue.qsize() > 0:
|
||||
clear_screen()
|
||||
updates = update_queue.get()
|
||||
update_screen(config, updates)
|
||||
# Display update begins
|
||||
schedule.run_pending()
|
||||
if update_queue.qsize() > 0:
|
||||
clear_screen()
|
||||
updates = update_queue.get()
|
||||
update_screen(config, updates)
|
||||
|
||||
pygame.display.flip()
|
||||
gc.collect()
|
||||
# Display update ends
|
||||
pygame.display.flip()
|
||||
gc.collect()
|
||||
# Display update ends
|
||||
|
||||
sleep(0.2)
|
||||
sleep(0.2)
|
||||
except Exception as e:
|
||||
print("Exception in main loop: ", str(e))
|
||||
pygame.quit()
|
||||
exit(0)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import os
|
|||
import sys
|
||||
import time
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
# First we construct a handful of functions - testing happens down at the end
|
||||
def httpdate_to_ts(dt):
|
||||
|
|
@ -17,16 +18,6 @@ def httpdate_to_ts(dt):
|
|||
def ts_to_httpdate(ts):
|
||||
return email.utils.formatdate(timeval=ts, localtime=False, usegmt=True)
|
||||
|
||||
|
||||
def write_file_with_time(filename, content, timestamp):
|
||||
# put the content into the file
|
||||
with open(filename, 'wb') as fp:
|
||||
fp.write(content)
|
||||
|
||||
# Then set the file's timestamps as requested
|
||||
os.utime(filename, times=(time.time(), timestamp))
|
||||
|
||||
|
||||
# v1: download remote file if HTTP's Last-Modified header indicates that
|
||||
# the file has been updated. This requires the remote server to support
|
||||
# sending the Last-Modified header.
|
||||
|
|
@ -51,54 +42,34 @@ def update_local_file_from_url_v1(last_mtime, local_file, url):
|
|||
|
||||
# If file is newer than last one we saw, get it
|
||||
updated = False
|
||||
if mtime > int(last_mtime):
|
||||
print('Comparing feed mtimes: feed: {} vs remote {}'.format(str(last_mtime), str(mtime)), file=sys.stderr)
|
||||
if not last_mtime or mtime > int(last_mtime):
|
||||
print('Refreshing feed..', file=sys.stderr)
|
||||
updated = True
|
||||
r2 = requests.get(url) # download the new file content
|
||||
if r2.status_code != requests.codes.ok:
|
||||
# download the new file content
|
||||
conn = urllib3.connection_from_url(url)
|
||||
r2 = conn.request(method="GET", url=url, preload_content=False)
|
||||
if r2.status != 200:
|
||||
# http request failed
|
||||
print('HEY! get for {} returned {}'.format(url, r2.status_code),
|
||||
file=sys.stderr)
|
||||
file=sys.stderr)
|
||||
try:
|
||||
r2.release_conn()
|
||||
except Exception as e:
|
||||
print('Could not release connection to {}: {}'.format(url, str(e)))
|
||||
return False, last_mtime
|
||||
|
||||
# write new content to local file
|
||||
write_file_with_time(local_file, r2.content, mtime)
|
||||
with open(local_file,'bw') as f:
|
||||
for chunk in r2.stream(amt=65536, decode_content=True):
|
||||
f.write(chunk)
|
||||
|
||||
return updated, mtime
|
||||
|
||||
|
||||
# v2: download remote file conditionally, with HTTP's If-Modified-Since header.
|
||||
# This requires the remote server to support both sending the Last-Modified
|
||||
# header and receiving the If-Modified-Since header.
|
||||
#
|
||||
def update_local_file_from_url_v2(last_mtime, local_file, url):
|
||||
|
||||
# Get the remote file, but only if it has changed
|
||||
r = requests.get(url, headers={
|
||||
'If-Modified-Since': ts_to_httpdate(last_mtime)
|
||||
})
|
||||
|
||||
updated, mtime = False, last_mtime
|
||||
|
||||
if r.status_code == requests.codes.ok:
|
||||
# File is updated and we just downloaded the content
|
||||
updated = True
|
||||
r2.release_conn()
|
||||
# Change the mtime of the file
|
||||
os.utime(local_file, (mtime, mtime))
|
||||
|
||||
# write new content to local file
|
||||
write_file_with_time(local_file, r.content, mtime)
|
||||
print('Downloaded {}.'.format(local_file), file=sys.stderr)
|
||||
else:
|
||||
print('No need to refresh feed.', file=sys.stderr)
|
||||
|
||||
# Update our notion of the file's last modification time
|
||||
if 'Last-Modified' in r.headers:
|
||||
mtime = httpdate_to_ts(r.headers['Last-Modified'])
|
||||
else:
|
||||
print('HEY! no Last-Modified header for {}'.format(url),
|
||||
file=sys.stderr)
|
||||
|
||||
elif r.status_code == requests.codes.not_modified:
|
||||
# Successful call, but no updates to file
|
||||
print('As of {}, server says {} is the same'.format(time.ctime(), url))
|
||||
else:
|
||||
# http request failed
|
||||
print('HEY! get for {} returned {}'.format(url, r.status_code),
|
||||
file=sys.stderr)
|
||||
|
||||
return updated, mtime
|
||||
return updated, mtime
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
iso8601==1.0.2
|
||||
pygame==2.1.2
|
||||
zeep==4.1.0
|
||||
gtfs_kit
|
||||
iso8601
|
||||
pandas
|
||||
pygame
|
||||
pyyaml
|
||||
requests
|
||||
urllib3
|
||||
schedule
|
||||
zeep
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue