-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
executable file
·187 lines (153 loc) · 6.16 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/env python3
from config import Config
from curses import COLOR_GREEN, COLOR_RED
from datetime import datetime
import os
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
from gtfs_client import GTFSClient
# Constants
# The font is JD LCD Rounded by Jecko Development
# https://fontstruct.com/fontstructions/show/459792/jd_lcd_rounded
TEXT_FONT = 'jd_lcd_rounded.ttf'
LINE_COUNT = 6
COLOR_LCD_AMBER : pygame.Color = pygame.Color(0xf4, 0xcb, 0x60)
COLOR_LCD_GREEN: pygame.Color = pygame.Color(0xb3, 0xff, 0x00)
COLOR_LCD_RED: pygame.Color = pygame.Color(0xff, 0x3a, 0x4a)
COLOR_BACKGROUND = pygame.Color(0, 0, 0)
UPDATE_INTERVAL_SECONDS = 62
TEXT_SIZE = 160 # Size of the font in pixels
# Offsets of each part within a line
XOFFSET_ROUTE = 24
XOFFSET_DESTINATION = 300
XOFFSEET_TIME_LEFT = 1606
INTER_LINE_SPACE = -15
# Some global variables
window : pygame.Surface = None
font: pygame.font.Font = None
update_queue = queue.Queue(maxsize=10)
def get_line_offset(line: int) -> int:
""" Calculate the Y offset within the display for a given text line """
global font
return line * (font.get_height() + INTER_LINE_SPACE)
def write_entry(line: int,
route: str = '', destination: str = '', time_left: str = '',
time_color: Color = COLOR_LCD_AMBER, text_color: Color = COLOR_LCD_AMBER):
""" Draws on the screen buffer an entry corresponding to an arrival time. """
# Step 1: Render the fragments
route_img = font.render(route[0:4], True, text_color)
destination_img = font.render(destination[0:21], True, text_color)
time_left_img = font.render(time_left[0:5], True, time_color)
# Compose the line
vertical_offset = get_line_offset(line)
window.blit(route_img, dest=(XOFFSET_ROUTE, vertical_offset))
window.blit(destination_img, dest=(XOFFSET_DESTINATION, vertical_offset))
window.blit(time_left_img, dest=(XOFFSEET_TIME_LEFT, vertical_offset))
def write_line(line: int, text: str, text_color: Color = COLOR_LCD_AMBER):
""" Draws on the screen buffer an arbitrary text. """
# Step 1: Render the fragments
text_img = font.render(text, True, text_color)
# Compose the line
vertical_offset = get_line_offset(line)
window.blit(text_img, dest=(XOFFSET_ROUTE, vertical_offset))
def update_screen(config: Config, updates: list[ArrivalTime]) -> None:
""" Repaint the screen with the new arrival times """
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.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)
except Exception as e:
print("Error updating screen: ", str(e))
def clear_screen() -> None:
""" Clear screen """
pygame.draw.rect(surface=window, color=COLOR_BACKGROUND, width=0, rect=(0, 0, window.get_width(), window.get_height()))
def init_screen() -> pygame.Surface:
""" Create a Surface to draw on, with the given size, using either X11/Wayland (desktop) or directfb (no desktop) """
pygame.display.init()
window = pygame.display.set_mode((0, 0))
pygame.mouse.set_visible(False)
return window
def main():
""" Main function """
global font
global window
global update_queue
config = Config()
# Initialise graphics context
pygame.display.init()
pygame.font.init()
window = init_screen()
pygame.font.init()
font = pygame.font.Font(config.font_file or TEXT_FONT, TEXT_SIZE)
# Init screen
clear_screen()
write_line(0, "Dublin Bus display")
write_line(1, "Loading feeds...")
pygame.display.flip()
# Create scheduler; load time tables
scheduler = GTFSClient(feed_url=config.gtfs_feed_url,
gtfs_r_url=config.gtfs_api_url,
gtfs_r_api_key=config.gtfs_api_key,
stop_codes=config.stop_codes,
routes_for_stops=config.routes_for_stops(),
update_queue=update_queue,
update_interval_seconds=config.update_interval_seconds)
# 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:
try:
# 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:
running = False
pygame.display.flip()
# Pygame event handling ends
# 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
sleep(0.2)
except Exception as e:
print("Exception in main loop: ", str(e))
pygame.quit()
exit(0)
if __name__ == "__main__":
main()