Compare commits

...

24 Commits
gtfs-r ... main

Author SHA1 Message Date
Nahuel Lofeudo 014e9dd0d3 Fixed rounded corners in the backing 2025-12-26 17:45:13 +00:00
Nahuel Lofeudo d3d55eb85e changes to the enclosure when migrating to FreeCad 1.0 2025-12-26 14:18:08 +00:00
Nahuel Lofeudo 03d22f47d7 Cleaned up a bunch of typos, removed unused code and made it tidier. 2025-01-12 14:35:04 +00:00
Nahuel Lofeudo d9811d3f91 Updated requirements.txt 2025-01-12 14:32:38 +00:00
Nahuel Lofeudo cc2b63e816 Use Urllib3 to stream the initial feed. 2023-11-18 16:13:01 +00:00
Nahuel Lofeudo b5d9fa1dd8 Add timeouts to the calls to the GTFS-R API 2023-11-11 15:46:51 +00:00
Nahuel Lofeudo 3c3199e3eb Catch exceptions in refresh() 2023-11-10 09:57:10 +00:00
Nahuel Lofeudo 9f87527d9a Add some last-resort exception handling 2023-10-08 15:27:35 +01:00
Nahuel Lofeudo 35d0261682 Make the GTFS-R client resilient to network errors. 2023-09-23 21:21:34 +01:00
Nahuel Lofeudo 36749669b4 Replace ad-hoc scheduler with package schedule 2023-09-17 17:47:08 +01:00
Nahuel Lofeudo d5c57b35eb
Merge pull request #6 from Gultak/Gultak-patch-4
Make the font file configurable.
2023-09-17 16:43:38 +01:00
Nahuel Lofeudo 3c2aa25cd8
Merge pull request #7 from Gultak/Gultak-patch-1
Update README.md
2023-09-08 14:12:43 +01:00
Gultak 076066ce0b Make the font file configurable. 2023-08-27 16:46:16 +01:00
Gultak e59201bfa3
Update README.md
Update README.md to include installation of `python3-pip`.
2023-08-22 22:50:15 +01:00
Nahuel Lofeudo 8bf7503a39 Do not initialize Pygame modules that we don't use (avoid sound underruns) 2023-07-09 22:01:49 +01:00
Nahuel Lofeudo c244a4bc21 Update gitignore 2023-06-24 22:45:30 +01:00
Nahuel Lofeudo e39e18d243 Fix lookup of walk time to the stop 2023-06-24 22:44:03 +01:00
Nahuel Lofeudo ccf7c62727 Remove VS Code directories from repo 2023-06-24 06:21:00 +01:00
Nahuel Lofeudo b16830020d Add more debug info 2023-06-24 06:06:17 +01:00
Nahuel Lofeudo 12cb88ed20 Move the feed .zip file to /tmp. Add debug info on refresh. 2023-06-24 05:55:23 +01:00
Nahuel Lofeudo f8857b3015 Add debug info for when a bus's headsign isnt found. 2023-06-24 05:26:52 +01:00
Nahuel Lofeudo 053e1191e0 Update enclosure model. Ignore exported STLs 2023-06-24 05:22:51 +01:00
Nahuel Lofeudo 4517bf0dbb Add code to debug an issue with destinations 2023-06-20 16:28:40 +01:00
Nahuel Lofeudo a89d28129f Update GTFS-R URL for nationaltransport.ie 2023-06-20 06:26:29 +01:00
12 changed files with 256 additions and 332 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
__pycache__/*
.vscode/*
Enclosure/*.stl
*test*
*.svg
*.FCStd1

View File

@ -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
}

16
.vscode/launch.json vendored
View File

@ -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.

View File

@ -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
```

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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

View File

@ -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