#!/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.isDue() 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.every(config.update_interval_seconds).seconds.do(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()