From de00c7c80b82aba9ff874b10f3633bf3450bab19 Mon Sep 17 00:00:00 2001 From: Tyler Porter Date: Sun, 10 Mar 2024 21:19:56 -0400 Subject: [PATCH] Re-wire config/layout classes --- .gitignore | 2 +- rewrite/config.json.example | 52 ++++ rewrite/config/__init__.py | 69 +++- rewrite/config/layout.py | 55 ++++ rewrite/coordinates/README.md | 38 +++ rewrite/coordinates/w128h32.json.example | 381 +++++++++++++++++++++++ rewrite/coordinates/w128h64.json.example | 362 +++++++++++++++++++++ rewrite/coordinates/w192h64.json.example | 362 +++++++++++++++++++++ rewrite/coordinates/w32h32.json.example | 366 ++++++++++++++++++++++ rewrite/coordinates/w64h32.json.example | 353 +++++++++++++++++++++ rewrite/coordinates/w64h64.json.example | 337 ++++++++++++++++++++ rewrite/main.py | 10 +- rewrite/screens/clock.py | 2 +- rewrite/screens/game.py | 6 +- rewrite/screens/static.py | 10 +- rewrite/screens/weather.py | 2 +- rewrite/utils/__init__.py | 34 +- rewrite/utils/font.py | 14 +- 18 files changed, 2433 insertions(+), 22 deletions(-) create mode 100644 rewrite/config.json.example create mode 100644 rewrite/config/layout.py create mode 100644 rewrite/coordinates/README.md create mode 100644 rewrite/coordinates/w128h32.json.example create mode 100644 rewrite/coordinates/w128h64.json.example create mode 100644 rewrite/coordinates/w192h64.json.example create mode 100644 rewrite/coordinates/w32h32.json.example create mode 100644 rewrite/coordinates/w64h32.json.example create mode 100644 rewrite/coordinates/w64h64.json.example diff --git a/.gitignore b/.gitignore index e8b4f3dd..c86b9d82 100644 --- a/.gitignore +++ b/.gitignore @@ -104,7 +104,7 @@ venv.bak/ .mypy_cache/ # Ignore our customized config.json -/config*.json +config*.json emulator_config.json # Ignore any customized coordinate configs diff --git a/rewrite/config.json.example b/rewrite/config.json.example new file mode 100644 index 00000000..fb159e6e --- /dev/null +++ b/rewrite/config.json.example @@ -0,0 +1,52 @@ +{ + "preferred": { + "teams": ["Cubs"], + "divisions": ["NL Central", "NL Wild Card"] + }, + "news_ticker": { + "team_offday": true, + "always_display": false, + "preferred_teams": true, + "display_no_games_live": false, + "traderumors": true, + "mlb_news": true, + "countdowns": true, + "date": true, + "date_format": "%A, %B %-d" + }, + "standings": { + "team_offday": false, + "mlb_offday": true, + "always_display": false, + "display_no_games_live": true + }, + "rotation": { + "enabled": true, + "scroll_until_finished": true, + "only_preferred": false, + "only_live": true, + "rates": { + "live": 15.0, + "final": 15.0, + "pregame": 15.0 + }, + "while_preferred_team_live": { + "enabled": false, + "during_inning_breaks": false + } + }, + "weather": { + "apikey": "YOUR_API_KEY_HERE", + "location": "Chicago,il,us", + "metric_units": false + }, + "time_format": "12h", + "end_of_day": "00:00", + "full_team_names": true, + "short_team_names_for_runs_hits": true, + "preferred_game_update_delay_in_10s_of_seconds": 0, + "pregame_weather": true, + "scrolling_speed": 2, + "debug": false, + "demo_date": false +} diff --git a/rewrite/config/__init__.py b/rewrite/config/__init__.py index 50e114de..42d6fd69 100644 --- a/rewrite/config/__init__.py +++ b/rewrite/config/__init__.py @@ -1,9 +1,68 @@ -from utils.font import FontCache +import json, os + +from config.layout import Layout + +from utils import logger as ScoreboardLogger +from utils import deep_update, read_json class Config: - def __init__(self): - self.font_cache = FontCache() + def __init__(self, config_filename, width, height): + self.width = width + self.height = height + self.dimensions = (width, height) + + config = self.__fetch_config(config_filename) + self.__parse_config(config) + + self.layout = Layout(width, height) + + def __fetch_config(self, name): + """ + Loads a config (JSON-formatted) with a custom filename. Falls back to a default if not found. + """ + filename = f"{name}.json" + reference_filename = f"config.json.example" + + custom_config = read_json(filename) + reference_config = read_json(reference_filename) + + if custom_config: + # Retain only the values that are valid. + config = deep_update(reference_config, custom_config) + + return config + + return reference_config + + def __parse_config(self, config): + """ + Convert a JSON config file to callable attributes on the Config class. + + If the key is nested, the top level key serves as its attribute prefix, i.e.: + + { + "type": { + "key1": 1, + "key2": { "one": 1, "two": 2 } + } + } + + config.type_key1 + #=> 1 + config.type_key2 + #=> { "one": 1, "two": 2 } + + If not nested, returns the result with no namespace, i.e.: + + { "type": "config" } - def font(self, font_name): - return self.font_cache.fetch_font(font_name) + config.type + #=> "config" + """ + for key in config: + if isinstance(config[key], dict): + for value in config[key]: + setattr(self, f"{key}_{value}", config[key][value]) + else: + setattr(self, key, config[key]) diff --git a/rewrite/config/layout.py b/rewrite/config/layout.py new file mode 100644 index 00000000..1e1564a0 --- /dev/null +++ b/rewrite/config/layout.py @@ -0,0 +1,55 @@ +import os, sys + +from utils import logger as ScoreboardLogger +from utils.font import FontCache +from utils import deep_update, read_json + + +class Layout: + LAYOUT_DIRECTORY = os.path.abspath(os.path.join("../../coordinates")) + FONTNAME_DEFAULT = "4x6" + + def __init__(self, width, height): + self.width = width + self.height = height + + self.font_cache = FontCache(self.FONTNAME_DEFAULT) + + self._json = self.__fetch_layout() + + def font(self, font_name): + """ + Fetches a font from the font cache. + """ + return self.font_cache.fetch_font(font_name) + + def font_size(self, font_name): + return self.font_cache.font_size(font_name) + + @property + def default_font(self): + return self.font_cache.default_font + + def __fetch_layout(self): + filename = "coordinates/w{}h{}.json".format(self.width, self.height) + reference_filename = "{}.example".format(filename) + reference_layout = read_json(reference_filename) + if not reference_layout: + # Unsupported coordinates + ScoreboardLogger.error( + "Invalid matrix dimensions provided. See top of README for supported dimensions." + "\nIf you would like to see new dimensions supported, please file an issue on GitHub!" + ) + sys.exit(1) + + # Load and merge any layout customizations + custom_layout = read_json(filename) + if custom_layout: + ScoreboardLogger.info( + "Custom '%dx%d.json' found. Merging with default reference layout.", self.width, self.height + ) + layout = deep_update(reference_layout, custom_layout) + + return layout + + return reference_layout diff --git a/rewrite/coordinates/README.md b/rewrite/coordinates/README.md new file mode 100644 index 00000000..0aa630e8 --- /dev/null +++ b/rewrite/coordinates/README.md @@ -0,0 +1,38 @@ +These JSON files are named in correspondence to the dimensions of the LED board used when running the software. A file, located in the `coordinates` directory with a filename `wh.json.example` tells the scoreboard that those dimensions are officially supported. This `.example` file is required and you will need to copy one of the existing files into a file that matches your dimensions. + +# Custom Coordinates +You can edit these coordinates to display parts of the scoreboard in any way you choose. Simply copy the file corresponding to your board's dimensions to `wh.json`. This JSON file only needs to contain the parts you wish to override but it's often easier to just make a copy of the full example file and edit the values you want to change. + +## Example +If you have a 64x32 board, copy `w64h32.json.example` to a new file called `w64h32.json`, then edit the coordinates in that file as you see fit. Your customized coordinates will always take precedence. + +## Fonts +Any scoreboard element that prints text can accept a `"font_name"` attribute. Supported fonts need to be named with `x.bdf` (or `xB.bdf` for bold fonts). The font loader will search `assets/` first for the specified font and then it will fall back to searching `matrix/fonts/` if one was not found. + +## States +The layout can have a couple of different states where things are rendered differently. Adding an object named for the layout state and giving it the same properties from the parent object will change the positioning of that parent object only when that state is found. For instance, when a game enters the `Warmup` state, the text `Warmup` appears under the time and the scrolling text is moved down. +* `warmup` will only render on the `pregame` screen and appears when a game enters the `Warmup` status. This usually happens 15-20 minutes before a game begins. +* `nohit` and `perfect_game` will only render on the live game screen and appears when a game returns that it is currently a no hitter or perfect game and the `innings_until_display` of `nohitter` has passed. +* The `runs_hits_errors` section enables the addition of hits and errors to the game screen. + * `show` turns this feature on or off. + * `compress_digits` will reduce the space between digits when the number of runs or hits is > 9. + * `spacing` is the number of pixels between the runs/hits and hits/errors. + +## Pitch Data +* `enabled` (true/false) turn feature on/off +* `mph` (true/false) When rendering pitch speed add mph after (99 mph) +* `desc_length` (short/long) The short or long pitch type description, you can change both the short and long description to your liking in data/pitches as long as you do not change the index value. + +## Play Result +* `enabled` (true/false) turn feature on/off +* `desc_length` (short/long) The short or long play result description. You can change both the short and long description to your liking in data/plays. + +## Updates +The software develops and releases features with full support for the default layouts, so custom layouts may look unsatisfactory if you update to later versions of the scoreboard. If you as a user decide to create a custom layout file, you are responsible for tweaking the coordinates to your liking with each update. + +## Current Issues +A couple of things are not completely implemented or have some implementation details you should understand. + +* `bases` currently requires an even `size` value to be rendered correctly +* Not all options are enabled on all board sizes by default. For example pitch count and pitch type are not enabled by default on boards smaller than 64x64. Options are "disabled" by forcing them to render outside the board, by setting X and Y coordinates less than 0 or greater than the height or width of the board. + diff --git a/rewrite/coordinates/w128h32.json.example b/rewrite/coordinates/w128h32.json.example new file mode 100644 index 00000000..c181acdf --- /dev/null +++ b/rewrite/coordinates/w128h32.json.example @@ -0,0 +1,381 @@ +{ + "defaults": { + "font_name": "6x12" + }, + "bases": { + "1B": { + "x": 88, + "y": 9, + "size": 12 + }, + "2B": { + "x": 80, + "y": 1, + "size": 12 + }, + "3B": { + "x": 72, + "y": 9, + "size": 12 + } + }, + "final": { + "inning": { + "x": 96, + "y": 14 + }, + "scrolling_text": { + "x": 0, + "y": 30, + "width": 128 + }, + "nohit_text": { + "x": 85, + "y": 22 + } + }, + "inning": { + "break": { + "number": { + "x": 30, + "y": 29 + }, + "text": { + "x": 8, + "y": 29 + }, + "due_up": { + "due": { + "x": 77, + "y": 8 + }, + "up": { + "x": 98, + "y": 8 + }, + "divider": { + "draw": true, + "x": 64, + "y_start": 0, + "y_end": 64 + }, + "leadoff": { + "x": 67, + "y": 16 + }, + "on_deck": { + "x": 67, + "y": 24 + }, + "in_hole": { + "x": 67, + "y": 32 + } + } + }, + "number": { + "x": 128, + "y": 8 + }, + "arrow": { + "size": 3, + "up": { + "x_offset": -5, + "y_offset": -4 + }, + "down": { + "x_offset": -5, + "y_offset": -2 + } + } + }, + "outs": { + "1": { + "x": 110, + "y": 24, + "size": 4, + "nohit": { + "x": 110, + "y": 26, + "size": 4 + }, + "perfect_game": { + "x": 110, + "y": 26, + "size": 4 + } + }, + "2": { + "x": 116, + "y": 24, + "size": 4, + "nohit": { + "x": 116, + "y": 26, + "size": 4 + }, + "perfect_game": { + "x": 116, + "y": 26, + "size": 4 + } + }, + "3": { + "x": 122, + "y": 24, + "size": 4, + "nohit": { + "x": 122, + "y": 26, + "size": 4 + }, + "perfect_game": { + "x": 122, + "y": 26, + "size": 4 + } + } + }, + "batter_count": { + "x": 110, + "y": 22, + "nohit": { + "x": 110, + "y": 16 + }, + "perfect_game": { + "x": 110, + "y": 16 + } + }, + "nohitter": { + "x": 110, + "y": 24, + "innings_until_display": 5 + }, + "atbat": { + "batter": { + "font_name": "5x8", + "x": 52, + "y": 30, + "width": 40, + "offset": 56 + }, + "pitcher": { + "font_name": "5x8", + "x": 1, + "y": 30, + "width": 40 + }, + "pitch": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "mph": false, + "desc_length": "short" + }, + "pitch_count": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 64, + "strikeout": { + "x": 60, + "y": 30, + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 60, + "y": 30, + "desc_length": "short", + "enabled": true + } + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 30, + "width": 128, + "warmup": { + "x": 0, + "y": 30, + "width": 128 + } + }, + "start_time": { + "x": 95, + "y": 15 + }, + "warmup_text": { + "x": 95, + "y": 15 + } + }, + "standings": { + "__comment__": "Delete this next line and make offset 8 to make the font much larger but only display 4 teams", + "font_name": "4x6", + "offset": 6, + "height": 32, + "width": 128, + "divider": { + "x": 25 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 1 + }, + "record": { + "x": 64 + }, + "games_back": { + "x": 126 + } + }, + "postseason": { + "matchup_y_gap": 6, + "series_x_gap": 16, + "wc_x_start": 34, + "wc_y_start": 6, + "ds_a_y_start": 25, + "__comment": "all other coords are based off wild card position" + } + }, + "status": { + "text": { + "x": 96, + "y": 14, + "short_text": false + }, + "scrolling_text": { + "x": 0, + "y": 30, + "width": 128 + } + }, + "teams": { + "background": { + "away": { + "width": 64, + "height": 10, + "x": 0, + "y": 0 + }, + "home": { + "width": 64, + "height": 10, + "x": 0, + "y": 10 + } + }, + "name": { + "away": { + "x": 2, + "y": 8 + }, + "home": { + "x": 2, + "y": 18 + } + }, + "accent": { + "away": { + "width": 2, + "height": 10, + "x": 0, + "y": 0 + }, + "home": { + "width": 2, + "height": 10, + "x": 0, + "y": 10 + } + }, + "record": { + "enabled": false, + "away": { + "font_name": "4x6", + "x": 19, + "y": 7 + }, + "home": { + "font_name": "4x6", + "x": 19, + "y": 17 + } + }, + "runs": { + "runs_hits_errors": { + "show": false, + "compress_digits": false, + "spacing": 3 + }, + "away": { + "x": 63, + "y": 8 + }, + "home": { + "x": 63, + "y": 18 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 30, + "width": 128 + }, + "time": { + "x": 64, + "y": 9 + }, + "conditions": { + "x": 100, + "y": 22 + }, + "temperature": { + "x": 10, + "y": 22 + }, + "wind_speed": { + "x": 0, + "y": -2 + }, + "wind_dir": { + "x": 0, + "y": -2 + }, + "wind": { + "x": 40, + "y": 22 + }, + "weather_icon": { + "x": 2, + "y": 1, + "width": 15, + "height": 15 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 121, + "y": 25 + }, + "text": { + "x": 125, + "y": 31 + } + } +} diff --git a/rewrite/coordinates/w128h64.json.example b/rewrite/coordinates/w128h64.json.example new file mode 100644 index 00000000..184a7e0c --- /dev/null +++ b/rewrite/coordinates/w128h64.json.example @@ -0,0 +1,362 @@ +{ + "defaults": { + "font_name": "7x13" + }, + "bases": { + "1B": { + "x": 112, + "y": 42, + "size": 10 + }, + "2B": { + "x": 103, + "y": 33, + "size": 10 + }, + "3B": { + "x": 94, + "y": 42, + "size": 10 + } + }, + "final": { + "inning": { + "x": 67, + "y": 61 + }, + "scrolling_text": { + "x": 0, + "y": 44, + "width": 128 + }, + "nohit_text": { + "x": 1, + "y": 61 + } + }, + "inning": { + "break": { + "number": { + "x": 3, + "y": 58 + }, + "text": { + "x": 3, + "y": 44 + }, + "due_up": { + "due": { + "font_name": "5x7", + "x": 38, + "y": 44 + }, + "up": { + "x": 38, + "y": 56 + }, + "divider": { + "draw": false, + "x": 36, + "y_start": 32, + "y_end": 64 + }, + "leadoff": { + "font_name": "7x13", + "x": 64, + "y": 42 + }, + "on_deck": { + "x": 64, + "y": 52 + }, + "in_hole": { + "x": 64, + "y": 62 + } + } + }, + "number": { + "x": 87, + "y": 42, + "nohit": { + "x": 93, + "y": 42 + }, + "perfect_game": { + "x": 93, + "y": 42 + } + }, + "arrow": { + "size": 6, + "up": { + "x_offset": -11, + "y_offset": -8 + }, + "down": { + "x_offset": -11, + "y_offset": -4 + } + } + }, + "outs": { + "1": { + "x": 95, + "y": 56, + "size": 5 + }, + "2": { + "x": 106, + "y": 56, + "size": 5 + }, + "3": { + "x": 118, + "y": 56, + "size": 5 + } + }, + "atbat": { + "batter": { + "font_name": "5x7", + "x": 1, + "y": 60, + "width": 41 + }, + "pitcher": { + "font_name": "5x7", + "x": 1, + "y": 40, + "width": 41 + }, + "pitch": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": true, + "mph": true, + "desc_length": "long" + }, + "pitch_count": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 68, + "strikeout": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + }, + "play_result": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + } + }, + "batter_count": { + "x": 66, + "y": 62, + "nohit": { + "x": 66, + "y": 64 + }, + "perfect_game": { + "x": 66, + "y": 64 + } + }, + "nohitter": { + "x": 66, + "y": 53, + "innings_until_display": 5 + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 46, + "width": 128, + "warmup": { + "x": 0, + "y": 46, + "width": 128 + } + }, + "start_time": { + "x": 66, + "y": 60 + }, + "warmup_text": { + "x": 66, + "y": 60 + } + }, + "standings": { + "start": 1, + "offset": 12, + "height": 60, + "width": 128, + "divider": { + "x": 32 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 1 + }, + "record": { + "x": 59 + }, + "games_back": { + "x": 124 + } + }, + "postseason": { + "matchup_y_gap": 12, + "series_x_gap": 32, + "wc_x_start": 3, + "wc_y_start": 12, + "ds_a_y_start": 51, + "__comment": "all other coords are based off wild card position" + } + }, + "status": { + "text": { + "x": 56, + "y": 46, + "short_text": false + }, + "scrolling_text": { + "x": 0, + "y": 58, + "width": 128 + } + }, + "teams": { + "background": { + "away": { + "width": 128, + "height": 16, + "x": 0, + "y": 0 + }, + "home": { + "width": 128, + "height": 16, + "x": 0, + "y": 16 + } + }, + "accent": { + "away": { + "width": 2, + "height": 16, + "x": 0, + "y": 0 + }, + "home": { + "width": 2, + "height": 16, + "x": 0, + "y": 16 + } + }, + "name": { + "away": { + "font_name": "9x18B", + "x": 4, + "y": 13 + }, + "home": { + "font_name": "9x18B", + "x": 4, + "y": 29 + } + }, + "record": { + "enabled": false, + "away": { + "x": 30, + "y": 13 + }, + "home": { + "x": 30, + "y": 29 + } + }, + "runs": { + "runs_hits_errors": { + "show": true, + "compress_digits": true, + "spacing": 5 + }, + "away": { + "font_name": "9x18B", + "x": 126, + "y": 13 + }, + "home": { + "font_name": "9x18B", + "x": 126, + "y": 29 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 60, + "width": 128 + }, + "time": { + "x": 80, + "y": 15 + }, + "conditions": { + "x": 80, + "y": 30 + }, + "temperature": { + "x": 20, + "y": 44 + }, + "wind_speed": { + "x": 0, + "y": -4 + }, + "wind_dir": { + "x": 0, + "y": -4 + }, + "wind": { + "x": 80, + "y": 44 + }, + "weather_icon": { + "x": 6, + "y": 2, + "width": 30, + "height": 30, + "rescale_icon": 2 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 121, + "y": 57 + }, + "text": { + "x": 125, + "y": 63 + } + } +} diff --git a/rewrite/coordinates/w192h64.json.example b/rewrite/coordinates/w192h64.json.example new file mode 100644 index 00000000..6f010530 --- /dev/null +++ b/rewrite/coordinates/w192h64.json.example @@ -0,0 +1,362 @@ +{ + "defaults": { + "font_name": "7x13" + }, + "bases": { + "1B": { + "x": 176, + "y": 42, + "size": 10 + }, + "2B": { + "x": 167, + "y": 33, + "size": 10 + }, + "3B": { + "x": 158, + "y": 42, + "size": 10 + } + }, + "final": { + "inning": { + "x": 99, + "y": 61 + }, + "scrolling_text": { + "x": 0, + "y": 44, + "width": 192 + }, + "nohit_text": { + "x": 1, + "y": 61 + } + }, + "inning": { + "break": { + "number": { + "x": 3, + "y": 58 + }, + "text": { + "x": 3, + "y": 44 + }, + "due_up": { + "due": { + "font_name": "5x7", + "x": 38, + "y": 44 + }, + "up": { + "x": 38, + "y": 56 + }, + "divider": { + "draw": false, + "x": 36, + "y_start": 32, + "y_end": 64 + }, + "leadoff": { + "font_name": "7x13", + "x": 64, + "y": 42 + }, + "on_deck": { + "x": 64, + "y": 52 + }, + "in_hole": { + "x": 64, + "y": 62 + } + } + }, + "number": { + "x": 151, + "y": 42, + "nohit": { + "x": 93, + "y": 42 + }, + "perfect_game": { + "x": 93, + "y": 42 + } + }, + "arrow": { + "size": 6, + "up": { + "x_offset": -11, + "y_offset": -8 + }, + "down": { + "x_offset": -11, + "y_offset": -4 + } + } + }, + "outs": { + "1": { + "x": 159, + "y": 56, + "size": 5 + }, + "2": { + "x": 170, + "y": 56, + "size": 5 + }, + "3": { + "x": 181, + "y": 56, + "size": 5 + } + }, + "atbat": { + "batter": { + "font_name": "5x7", + "x": 1, + "y": 60, + "width": 87 + }, + "pitcher": { + "font_name": "5x7", + "x": 1, + "y": 40, + "width": 87 + }, + "pitch": { + "font_name": "5x7", + "x": 1, + "y": 50, + "enabled": true, + "mph": true, + "desc_length": "long" + }, + "pitch_count": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 68, + "strikeout": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + }, + "play_result": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + } + }, + "batter_count": { + "x": 130, + "y": 62, + "nohit": { + "x": 66, + "y": 64 + }, + "perfect_game": { + "x": 66, + "y": 64 + } + }, + "nohitter": { + "x": 66, + "y": 53, + "innings_until_display": 5 + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 46, + "width": 192, + "warmup": { + "x": 0, + "y": 46, + "width": 192 + } + }, + "start_time": { + "x": 99, + "y": 60 + }, + "warmup_text": { + "x": 99, + "y": 60 + } + }, + "standings": { + "start": 1, + "offset": 12, + "height": 60, + "width": 192, + "divider": { + "x": 32 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 3 + }, + "record": { + "x": 59 + }, + "games_back": { + "x": 124 + } + }, + "postseason": { + "matchup_y_gap": 12, + "series_x_gap": 32, + "wc_x_start": 3, + "wc_y_start": 12, + "ds_a_y_start": 51, + "__comment": "all other coords are based off wild card position" + } + }, + "status": { + "text": { + "x": 56, + "y": 46, + "short_text": false + }, + "scrolling_text": { + "x": 0, + "y": 58, + "width": 192 + } + }, + "teams": { + "background": { + "away": { + "width": 192, + "height": 16, + "x": 0, + "y": 0 + }, + "home": { + "width": 192, + "height": 16, + "x": 0, + "y": 16 + } + }, + "accent": { + "away": { + "width": 2, + "height": 16, + "x": 0, + "y": 0 + }, + "home": { + "width": 2, + "height": 16, + "x": 0, + "y": 16 + } + }, + "name": { + "away": { + "font_name": "9x18B", + "x": 4, + "y": 13 + }, + "home": { + "font_name": "9x18B", + "x": 4, + "y": 29 + } + }, + "record": { + "enabled": false, + "away": { + "x": 30, + "y": 13 + }, + "home": { + "x": 30, + "y": 29 + } + }, + "runs": { + "runs_hits_errors": { + "show": true, + "compress_digits": true, + "spacing": 9 + }, + "away": { + "font_name": "9x18B", + "x": 190, + "y": 13 + }, + "home": { + "font_name": "9x18B", + "x": 190, + "y": 29 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 60, + "width": 192 + }, + "time": { + "x": 80, + "y": 15 + }, + "conditions": { + "x": 80, + "y": 30 + }, + "temperature": { + "x": 20, + "y": 44 + }, + "wind_speed": { + "x": 0, + "y": -4 + }, + "wind_dir": { + "x": 0, + "y": -4 + }, + "wind": { + "x": 80, + "y": 44 + }, + "weather_icon": { + "x": 6, + "y": 2, + "width": 30, + "height": 30, + "rescale_icon": 2 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 121, + "y": 57 + }, + "text": { + "x": 125, + "y": 63 + } + } +} diff --git a/rewrite/coordinates/w32h32.json.example b/rewrite/coordinates/w32h32.json.example new file mode 100644 index 00000000..31a8dd0a --- /dev/null +++ b/rewrite/coordinates/w32h32.json.example @@ -0,0 +1,366 @@ +{ + "defaults": { + "font_name": "4x6" + }, + "bases": { + "1B": { + "x": 24, + "y": 24, + "size": 6 + }, + "2B": { + "x": 19, + "y": 19, + "size": 6 + }, + "3B": { + "x": 14, + "y": 24, + "size": 6 + } + }, + "final": { + "inning": { + "x": 16, + "y": 20 + }, + "scrolling_text": { + "x": 0, + "y": 31, + "width": 32 + }, + "nohit_text": { + "x": 10, + "y": 26 + } + }, + "inning": { + "break": { + "number": { + "x": 11, + "y": 29 + }, + "text": { + "x": 11, + "y": 22 + }, + "due_up": { + "due": { + "x": 33, + "y": 33 + }, + "up": { + "x": 33, + "y": 33 + }, + "divider": { + "draw": false, + "x": 33, + "y_start": 33, + "y_end": 33 + }, + "leadoff": { + "x": 33, + "y": 33 + }, + "on_deck": { + "x": 33, + "y": 33 + }, + "in_hole": { + "x": 33, + "y": 33 + } + } + }, + "number": { + "x": 32, + "y": 20 + }, + "arrow": { + "size": 2, + "up": { + "x_offset": -4, + "y_offset": -4 + }, + "down": { + "x_offset": -4, + "y_offset": -3 + } + } + }, + "outs": { + "1": { + "x": 1, + "y": 26, + "size": 2, + "nohit": { + "x": 1, + "y": 28, + "size": 2 + }, + "perfect_game": { + "x": 1, + "y": 28, + "size": 2 + } + }, + "2": { + "x": 5, + "y": 26, + "size": 2, + "nohit": { + "x": 5, + "y": 28, + "size": 2 + }, + "perfect_game": { + "x": 5, + "y": 28, + "size": 2 + } + }, + "3": { + "x": 9, + "y": 26, + "size": 2, + "nohit": { + "x": 9, + "y": 28, + "size": 2 + }, + "perfect_game": { + "x": 9, + "y": 28, + "size": 2 + } + } + }, + "atbat": { + "batter": { + "x": 33, + "y": 33, + "width": 10 + }, + "pitcher": { + "x": 33, + "y": 33, + "width": 12 + }, + "pitch": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "mph": false, + "desc_length": "short" + }, + "pitch_count": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 16, + "strikeout": { + "x": 33, + "y": 33, + "desc_length": "short", + "enabled": false + }, + "play_result": { + "x": 33, + "y": 33, + "desc_length": "short", + "enabled": false + } + }, + "batter_count": { + "x": 1, + "y": 23, + "nohit": { + "x": 1, + "y": 21 + }, + "perfect_game": { + "x": 1, + "y": 21 + } + }, + "nohitter": { + "x": 1, + "y": 27, + "innings_until_display": 5 + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 31, + "width": 32, + "warmup": { + "x": 0, + "y": 31, + "width": 32 + } + }, + "start_time": { + "x": 16, + "y": 20 + }, + "warmup_text": { + "x": 16, + "y": 20 + } + }, + "standings": { + "offset": 6, + "height": 30, + "width": 32, + "divider": { + "x": 13 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 1 + }, + "record": { + "x": 15 + }, + "games_back": { + "x": 64 + } + } + }, + "status": { + "text": { + "x": 16, + "y": 20, + "short_text": true + }, + "scrolling_text": { + "x": 0, + "y": 31, + "width": 32 + } + }, + "teams": { + "background": { + "away": { + "width": 32, + "height": 7, + "x": 0, + "y": 0 + }, + "home": { + "width": 32, + "height": 7, + "x": 0, + "y": 7 + } + }, + "accent": { + "away": { + "width": 1, + "height": 7, + "x": 0, + "y": 0 + }, + "home": { + "width": 1, + "height": 7, + "x": 0, + "y": 7 + } + }, + "name": { + "away": { + "x": 3, + "y": 6 + }, + "home": { + "x": 3, + "y": 13 + } + }, + "record": { + "enabled": false, + "away": { + "x": 4, + "y": 6 + }, + "home": { + "x": 4, + "y": 13 + } + }, + "runs": { + "runs_hits_errors": { + "show": false, + "compress_digits": true, + "spacing": 2 + }, + "away": { + "x": 31, + "y": 6 + }, + "home": { + "x": 31, + "y": 13 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 30, + "width": 32 + }, + "time": { + "x": 16, + "y": 23 + }, + "conditions": { + "x": 0, + "y": -2 + }, + "temperature": { + "x": 25, + "y": 12 + }, + "wind_speed": { + "x": 0, + "y": -2 + }, + "wind_dir": { + "x": 0, + "y": -2 + }, + "wind": { + "x": 0, + "y": -2 + }, + "weather_icon": { + "x": 1, + "y": 1, + "width": 15, + "height": 15 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 25, + "y": 25 + }, + "text": { + "x": 29, + "y": 31 + } + } +} diff --git a/rewrite/coordinates/w64h32.json.example b/rewrite/coordinates/w64h32.json.example new file mode 100644 index 00000000..a9ee194b --- /dev/null +++ b/rewrite/coordinates/w64h32.json.example @@ -0,0 +1,353 @@ +{ + "defaults": { + "font_name": "4x6" + }, + "bases": { + "1B": { + "x": 56, + "y": 20, + "size": 6 + }, + "2B": { + "x": 51, + "y": 15, + "size": 6 + }, + "3B": { + "x": 46, + "y": 20, + "size": 6 + } + }, + "final": { + "inning": { + "x": 32, + "y": 20 + }, + "scrolling_text": { + "x": 0, + "y": 31, + "width": 64 + }, + "nohit_text": { + "x": 1, + "y": 20 + } + }, + "inning": { + "break": { + "number": { + "x": 2, + "y": 29 + }, + "text": { + "x": 2, + "y": 22 + }, + "due_up": { + "due": { + "x": 18, + "y": 22 + }, + "up": { + "x": 18, + "y": 28 + }, + "divider": { + "draw": true, + "x": 15, + "y_start": 14, + "y_end": 32 + }, + "leadoff": { + "x": 32, + "y": 20 + }, + "on_deck": { + "x": 32, + "y": 26 + }, + "in_hole": { + "x": 32, + "y": 32 + } + } + }, + "number": { + "x": 46, + "y": 21, + "nohit": { + "x": 46, + "y": 20 + }, + "perfect_game": { + "x": 46, + "y": 20 + } + }, + "arrow": { + "size": 3, + "up": { + "x_offset": -5, + "y_offset": -4 + }, + "down": { + "x_offset": -5, + "y_offset": -2 + } + } + }, + "outs": { + "1": { + "x": 49, + "y": 28, + "size": 2 + }, + "2": { + "x": 53, + "y": 28, + "size": 2 + }, + "3": { + "x": 57, + "y": 28, + "size": 2 + } + }, + "atbat": { + "batter": { + "x": 1, + "y": 29, + "width": 20 + }, + "pitcher": { + "x": 1, + "y": 21, + "width": 24 + }, + "pitch": { + "x": 1, + "y": 50, + "enabled": false, + "mph": false, + "desc_length": "short" + }, + "pitch_count": { + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 36, + "strikeout": { + "x": 15, + "y": 29, + "font_name": "5x7", + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 15, + "y": 29, + "font_name": "5x7", + "desc_length": "short", + "enabled": true + } + + }, + "batter_count": { + "x": 34, + "y": 30, + "nohit": { + "x": 34, + "y": 32 + }, + "perfect_game": { + "x": 34, + "y": 32 + } + }, + "nohitter": { + "x": 34, + "y": 26, + "innings_until_display": 5 + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 31, + "width": 64, + "warmup": { + "x": 0, + "y": 31, + "width": 64 + } + }, + "start_time": { + "x": 32, + "y": 20 + }, + "warmup_text": { + "x": 32, + "y": 20 + } + }, + "standings": { + "offset": 6, + "height": 30, + "width": 64, + "divider": { + "x": 13 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 1 + }, + "record": { + "x": 31 + }, + "games_back": { + "x": 64 + } + }, + "postseason": { + "matchup_y_gap": 6, + "series_x_gap": 16, + "wc_x_start": 2, + "wc_y_start": 6, + "ds_a_y_start": 25, + "__comment": "all other coords are based off wild card position" + } + }, + "status": { + "text": { + "x": 32, + "y": 20, + "short_text": false + }, + "scrolling_text": { + "x": 0, + "y": 31, + "width": 64 + } + }, + "teams": { + "background": { + "away": { + "width": 64, + "height": 7, + "x": 0, + "y": 0 + }, + "home": { + "width": 64, + "height": 7, + "x": 0, + "y": 7 + } + }, + "name": { + "away": { + "x": 4, + "y": 6 + }, + "home": { + "x": 4, + "y": 13 + } + }, + "accent": { + "away": { + "width": 2, + "height": 7, + "x": 0, + "y": 0 + }, + "home": { + "width": 2, + "height": 7, + "x": 0, + "y": 7 + } + }, + "record": { + "enabled": false, + "away": { + "x": 15, + "y": 6 + }, + "home": { + "x": 15, + "y": 13 + } + }, + "runs": { + "runs_hits_errors": { + "show": true, + "compress_digits": false, + "spacing": 3 + }, + "away": { + "x": 62, + "y": 6 + }, + "home": { + "x": 62, + "y": 13 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 30, + "width": 64 + }, + "time": { + "x": 40, + "y": 8 + }, + "conditions": { + "x": 40, + "y": 16 + }, + "temperature": { + "x": 10, + "y": 23 + }, + "wind_speed": { + "x": 0, + "y": -2 + }, + "wind_dir": { + "x": 0, + "y": -2 + }, + "wind": { + "x": 40, + "y": 23 + }, + "weather_icon": { + "x": 2, + "y": 1, + "width": 15, + "height": 15 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 57, + "y": 25 + }, + "text": { + "x": 61, + "y": 31 + } + } +} diff --git a/rewrite/coordinates/w64h64.json.example b/rewrite/coordinates/w64h64.json.example new file mode 100644 index 00000000..5ec273bb --- /dev/null +++ b/rewrite/coordinates/w64h64.json.example @@ -0,0 +1,337 @@ +{ + "defaults": { + "font_name": "5x8" + }, + "bases": { + "1B": { + "x": 20, + "y": 46, + "size": 14 + }, + "2B": { + "x": 11, + "y": 37, + "size": 14 + }, + "3B": { + "x": 2, + "y": 46, + "size": 14 + } + }, + "final": { + "inning": { + "x": 32, + "y": 27 + }, + "scrolling_text": { + "x": 0, + "y": 38, + "width": 64 + }, + "nohit_text": { + "x": 24, + "y": 49 + } + }, + "inning": { + "break": { + "number": { + "x": 35, + "y": 27 + }, + "text": { + "x": 16, + "y": 27 + }, + "due_up": { + "due": { + "x": 16, + "y": 36 + }, + "up": { + "x": 35, + "y": 36 + }, + "divider": { + "draw": false + }, + "leadoff": { + "x": 2, + "y": 45 + }, + "on_deck": { + "x": 2, + "y": 53 + }, + "in_hole": { + "x": 2, + "y": 61 + } + } + }, + "number": { + "x": 61, + "y": 60 + }, + "arrow": { + "size": 3, + "up": { + "x_offset": -5, + "y_offset": -6 + }, + "down": { + "x_offset": -5, + "y_offset": -1 + } + } + }, + "outs": { + "1": { + "x": 46, + "y": 48, + "size": 3 + }, + "2": { + "x": 51, + "y": 48, + "size": 3 + }, + "3": { + "x": 56, + "y": 48, + "size": 3 + } + }, + "batter_count": { + "x": 46, + "y": 46 + }, + "nohitter": { + "x": 30, + "y": 45, + "innings_until_display": 5 + }, + "atbat": { + "batter": { + "x": 1, + "y": 36, + "width": 46 + }, + "pitcher": { + "x": 1, + "y": 28, + "width": 50 + }, + "pitch": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "mph": false, + "desc_length": "short" + }, + "pitch_count": { + "font_name": "4x6", + "x": 1, + "y": 50, + "enabled": false, + "append_pitcher_name": false + }, + "loop": 64, + "strikeout": { + "x": 31, + "y": 36, + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 31, + "y": 36, + "desc_length": "short", + "enabled": true + } + }, + "pregame": { + "scrolling_text": { + "x": 0, + "y": 45, + "width": 64, + "warmup": { + "x": 0, + "y": 51, + "width": 64 + } + }, + "start_time": { + "x": 32, + "y": 27 + }, + "warmup_text": { + "x": 32, + "y": 27 + } + }, + "standings": { + "font_name": "4x6", + "start": 16, + "offset": 6, + "height": 30, + "width": 64, + "divider": { + "x": 13 + }, + "stat_title": { + "x": 28 + }, + "team": { + "name": { + "x": 1 + }, + "record": { + "x": 31 + }, + "games_back": { + "x": 64 + } + }, + "postseason": { + "matchup_y_gap": 6, + "series_x_gap": 16, + "wc_x_start": 2, + "wc_y_start": 20, + "ds_a_y_start": 39, + "__comment": "all other coords are based off wild card position" + } + }, + "status": { + "text": { + "x": 32, + "y": 27, + "short_text": false + }, + "scrolling_text": { + "x": 0, + "y": 38, + "width": 64 + } + }, + "teams": { + "background": { + "away": { + "width": 64, + "height": 10, + "x": 0, + "y": 0 + }, + "home": { + "width": 64, + "height": 10, + "x": 0, + "y": 10 + } + }, + "name": { + "away": { + "x": 4, + "y": 8 + }, + "home": { + "x": 4, + "y": 18 + } + }, + "accent": { + "away": { + "width": 2, + "height": 10, + "x": 0, + "y": 0 + }, + "home": { + "width": 2, + "height": 10, + "x": 0, + "y": 10 + } + }, + "record": { + "enabled": false, + "away": { + "font_name": "4x6", + "x": 18, + "y": 7 + }, + "home": { + "font_name": "4x6", + "x": 18, + "y": 17 + } + }, + "runs": { + "runs_hits_errors": { + "show": false, + "compress_digits": false, + "spacing": 3 + }, + "away": { + "x": 63, + "y": 8 + }, + "home": { + "x": 63, + "y": 18 + } + } + }, + "offday": { + "scrolling_text": { + "x": 0, + "y": 40, + "width": 64 + }, + "time": { + "x": 40, + "y": 7 + }, + "conditions": { + "x": 39, + "y": 16 + }, + "temperature": { + "x": 10, + "y": 23 + }, + "wind_speed": { + "x": 0, + "y": -2 + }, + "wind_dir": { + "x": 0, + "y": -2 + }, + "wind": { + "x": 39, + "y": 23 + }, + "weather_icon": { + "x": 2, + "y": 1, + "width": 15, + "height": 15 + } + }, + "network": { + "font_name": "4x6", + "background": { + "width": 7, + "height": 7, + "x": 57, + "y": 57 + }, + "text": { + "x": 61, + "y": 63 + } + } +} diff --git a/rewrite/main.py b/rewrite/main.py index 7155dbda..3429d35a 100644 --- a/rewrite/main.py +++ b/rewrite/main.py @@ -25,14 +25,14 @@ ScoreboardLogger.warning("We recommend MLB-StatsAPI 1.6.1 or higher. You may want to re-run install.sh") -def main(matrix): - # TODO: Configure matrix dimensions - ScoreboardLogger.info(f"{SCRIPT_NAME} - v#{SCRIPT_VERSION} (32x32)") +def main(matrix, config_path): + config = Config(config_path, matrix.width, matrix.height) + + ScoreboardLogger.info(f"{SCRIPT_NAME} - v#{SCRIPT_VERSION} ({matrix.width}x{matrix.height})") canvas = matrix.CreateFrameCanvas() screen_queue = PriorityQueue(10) - config = Config() screen_manager = ScreenManager(matrix, canvas, config, screen_queue) render_thread = threading.Thread(target=screen_manager.start, name="render_thread", daemon=True) @@ -53,7 +53,7 @@ def main(matrix): matrix = RGBMatrix(options=matrix_options) try: - main(matrix) + main(matrix, command_line_args.config) except: ScoreboardLogger.exception("Untrapped error in main!") sys.exit(1) diff --git a/rewrite/screens/clock.py b/rewrite/screens/clock.py index 1ea08d62..59412568 100644 --- a/rewrite/screens/clock.py +++ b/rewrite/screens/clock.py @@ -13,7 +13,7 @@ def render(self): time_format_str = "%H:%M%p" time_text = time.strftime(time_format_str) - font = self.config.font("4x6") + font = self.config.layout.font("4x6") graphics.DrawText(self.canvas, font, 5, 5, (255, 255, 255), time_text) diff --git a/rewrite/screens/game.py b/rewrite/screens/game.py index 5984c450..1a995361 100644 --- a/rewrite/screens/game.py +++ b/rewrite/screens/game.py @@ -10,8 +10,8 @@ class GameScreen(ScreenBase): MAX_DURATION_SECONDS = 3 def render(self): - weather_text = "It's a game!" + game_text = "It's a game!" - font = self.config.font("4x6") + font = self.config.layout.font("4x6") - graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), weather_text) + graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), game_text) diff --git a/rewrite/screens/static.py b/rewrite/screens/static.py index c2502c1e..a6dc2717 100644 --- a/rewrite/screens/static.py +++ b/rewrite/screens/static.py @@ -13,13 +13,15 @@ class StaticScreen(ScreenBase): MIN_DURATION_SECONDS = 3 - # TODO: Pull this from config. - LOGO_PATH = os.path.abspath(os.path.join(__file__, "./../../assets/logo/mlb-w32h32.png")) - def __init__(self, *args): super(self.__class__, self).__init__(*args) - with Image.open(self.LOGO_PATH) as logo: + dimensions = self.manager.config.dimensions + logo_path = os.path.abspath( + os.path.join(__file__, f"./../../assets/logo/mlb-w{dimensions[0]}h{dimensions[1]}.png") + ) + + with Image.open(logo_path) as logo: self.manager.matrix.SetImage(logo.convert("RGB")) def render(self): diff --git a/rewrite/screens/weather.py b/rewrite/screens/weather.py index 52f93ec3..4cc3309a 100644 --- a/rewrite/screens/weather.py +++ b/rewrite/screens/weather.py @@ -15,7 +15,7 @@ def __init__(self, *args): def render(self): weather_text = "It's weathery" - font = self.config.font("4x6") + font = self.config.layout.font("4x6") graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), weather_text) diff --git a/rewrite/utils/__init__.py b/rewrite/utils/__init__.py index 0096d35b..153327aa 100644 --- a/rewrite/utils/__init__.py +++ b/rewrite/utils/__init__.py @@ -1,7 +1,9 @@ -import argparse +import argparse, os, json from utils import logger as ScoreboardLogger +from collections.abc import Mapping + def args(): parser = argparse.ArgumentParser() @@ -169,3 +171,33 @@ def led_matrix_options(args): options.disable_hardware_pulsing = True return options + + +def read_json(path): + """ + Read a file expected to contain valid JSON to dict. + + If the file is not present returns an empty dict. + """ + j = {} + if os.path.isfile(path): + j = json.load(open(path)) + else: + ScoreboardLogger.info(f"Could not find json file {path}. Skipping.") + return j + + +def deep_update(reference, overrides): + """ + Performs a deep merge of two dicts. + + If the key is not present in the reference, the override is ignored. + """ + for key, value in list(overrides.items()): + if isinstance(value, Mapping) and value: + returned = deep_update(reference.get(key, {}), value) + reference[key] = returned + else: + reference[key] = overrides[key] + + return reference diff --git a/rewrite/utils/font.py b/rewrite/utils/font.py index 1ed1ac5a..49057926 100644 --- a/rewrite/utils/font.py +++ b/rewrite/utils/font.py @@ -1,13 +1,19 @@ import os +from dataclasses import dataclass + from driver import graphics class FontCache: FONT_PATHS = ["../assets/fonts/patched", "../submodules/matrix/fonts"] - def __init__(self): + def __init__(self, default_fontname): self.font_cache = {} + self.default_fontname = default_fontname + + # Preload the default font + self.default_font = self.fetch_font(self.default_fontname) def fetch_font(self, font_name): if font_name in self.font_cache: @@ -22,3 +28,9 @@ def fetch_font(self, font_name): self.font_cache[font_name] = font return font + + def font_size(self, font_name): + if font_name[-1] == "B": + font_name = font_name[:-1] + + return tuple(int(part) for part in font_name.split("x"))