From 8edf2888c9b11c53f5f4165f792c0bf243f30041 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:08:13 -0400 Subject: [PATCH] fix(ledmatrix-stocks): make the bitmap fonts work, and document every setting A .bdf is a bitmap face that exists at exactly one pixel size. Choosing 5x7.bdf or 4x6.bdf at any other size raised inside FreeType, and the renderer caught it and substituted PressStart2P with a log warning -- so the font menu appeared to work while quietly ignoring the choice at twelve of the thirteen offered sizes. Bitmap faces now load at their declared PIXEL_SIZE instead. cozette.bdf is removed from all six font menus: the core ships no file of that name, so it could never load at any size (#387). README: real rendered screenshots throughout -- a gain, a loss, a crypto entry, both display modes, the inline chart on and off, and four panel sizes -- all from a recorded quote seeded into the cache the plugin already reads. The eight per-element colour keys were previously one sentence ("green for positive deltas and red for negative"); they are now a table, including the detail that price_delta has no text_color because its colour comes from the sign of the change. That takes the plugin to 41 of 41 settings documented. The scroll shots use the frames option from #369: the ticker starts off-panel, so frame 0 of a scrolling plugin is blank. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- docs/assets/ledmatrix-stocks/display-mode.png | Bin 0 -> 14462 bytes docs/assets/ledmatrix-stocks/gain-loss.png | Bin 0 -> 19203 bytes docs/assets/ledmatrix-stocks/hero.png | Bin 0 -> 1961 bytes docs/assets/ledmatrix-stocks/mock.json | 686 ++++++++++++++++++ docs/assets/ledmatrix-stocks/panel-sizes.png | Bin 0 -> 17017 bytes docs/assets/ledmatrix-stocks/shots.json | 314 ++++++++ docs/assets/ledmatrix-stocks/toggle-chart.png | Bin 0 -> 8289 bytes plugins.json | 6 +- plugins/ledmatrix-stocks/README.md | 61 +- plugins/ledmatrix-stocks/config_schema.json | 18 +- plugins/ledmatrix-stocks/display_renderer.py | 32 +- plugins/ledmatrix-stocks/manifest.json | 12 +- 13 files changed, 1105 insertions(+), 26 deletions(-) create mode 100644 docs/assets/ledmatrix-stocks/display-mode.png create mode 100644 docs/assets/ledmatrix-stocks/gain-loss.png create mode 100644 docs/assets/ledmatrix-stocks/hero.png create mode 100644 docs/assets/ledmatrix-stocks/mock.json create mode 100644 docs/assets/ledmatrix-stocks/panel-sizes.png create mode 100644 docs/assets/ledmatrix-stocks/shots.json create mode 100644 docs/assets/ledmatrix-stocks/toggle-chart.png diff --git a/README.md b/README.md index 3905ae7d..88a1e7f2 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \ | Plugin | Description | Preview | |--------|-------------|---------| -| [Stocks Ticker](./plugins/ledmatrix-stocks/) | Real-time stock & crypto prices with charts | | +| [Stocks Ticker](./plugins/ledmatrix-stocks/) | Real-time stock & crypto prices with charts | ledmatrix-stocks on an LED panel | | [Stock News](./plugins/stock-news/) | Financial headlines from RSS feeds | | ### Time & Calendar (4) diff --git a/docs/assets/ledmatrix-stocks/display-mode.png b/docs/assets/ledmatrix-stocks/display-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..3e800fc25522b0e92fda1a7383ce07e76c6f528a GIT binary patch literal 14462 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⁣}#Rdij#>S4kT(yL3 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 MLQXvG=7T@~AN~ECAOHXW literal 0 HcmV?d00001 diff --git a/docs/assets/ledmatrix-stocks/gain-loss.png b/docs/assets/ledmatrix-stocks/gain-loss.png new file mode 100644 index 0000000000000000000000000000000000000000..0b995880290c5e400b2e4bdb8b5a208ecf51a4e9 GIT binary patch literal 19203 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 a&UaHh$ak}|;n literal 0 HcmV?d00001 diff --git a/docs/assets/ledmatrix-stocks/shots.json b/docs/assets/ledmatrix-stocks/shots.json new file mode 100644 index 00000000..ab8d6068 --- /dev/null +++ b/docs/assets/ledmatrix-stocks/shots.json @@ -0,0 +1,314 @@ +{ + "plugin": "ledmatrix-stocks", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-09-02T15:30:00", + "mock_data": "mock.json", + "config": { + "enabled": true, + "stocks": { + "enabled": true, + "symbols": [ + "AAPL", + "MSFT", + "NVDA" + ] + }, + "crypto": { + "enabled": false + } + }, + "frames": 60 + }, + "shots": [ + { + "name": "hero", + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "up", + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "down", + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "MSFT" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "crypto", + "standalone": false, + "config": { + "stocks": { + "enabled": false, + "symbols": [] + }, + "crypto": { + "enabled": true, + "symbols": [ + "BTC-USD" + ] + } + }, + "frames": 60, + "width": 256 + }, + { + "name": "chart-on", + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "NVDA" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + }, + "display": { + "toggle_chart": true + } + } + }, + { + "name": "chart-off", + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "NVDA" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + }, + "display": { + "toggle_chart": false + } + } + }, + { + "name": "mode-scroll", + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + }, + "display": { + "display_mode": "scroll" + } + } + }, + { + "name": "mode-switch", + "standalone": false, + "frames": 5, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + }, + "display": { + "display_mode": "switch" + } + } + }, + { + "name": "size-64x32", + "width": 64, + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "size-128x32", + "width": 128, + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + }, + { + "name": "size-256x32", + "width": 256, + "standalone": false, + "config": { + "stocks": { + "enabled": true, + "symbols": [ + "AAPL" + ] + }, + "crypto": { + "enabled": false, + "symbols": [] + } + } + } + ], + "composites": [ + { + "name": "gain-loss", + "columns": 1, + "cells": [ + { + "shot": "up", + "label": "A gain", + "sublabel": "positive_color, green by default" + }, + { + "shot": "down", + "label": "A loss", + "sublabel": "negative_color, red by default" + }, + { + "shot": "crypto", + "label": "Crypto", + "sublabel": "its own colour block, gold by default (256 wide, where the whole line fits)" + } + ] + }, + { + "name": "toggle-chart", + "columns": 1, + "cells": [ + { + "shot": "chart-on", + "label": "toggle_chart: true" + }, + { + "shot": "chart-off", + "label": "toggle_chart: false" + } + ] + }, + { + "name": "display-mode", + "columns": 1, + "cells": [ + { + "shot": "mode-scroll", + "label": "display_mode: scroll", + "sublabel": "one continuous ribbon of every symbol" + }, + { + "shot": "mode-switch", + "label": "display_mode: switch", + "sublabel": "one symbol at a time, held for switch_duration" + } + ] + }, + { + "name": "panel-sizes", + "columns": 2, + "cells": [ + { + "shot": "size-64x32", + "label": "64 x 32" + }, + { + "shot": "size-128x32", + "label": "128 x 32" + }, + { + "shot": "size-128x64", + "label": "128 x 64" + }, + { + "shot": "size-256x32", + "label": "256 x 32" + } + ] + } + ], + "_comment": "Scroll shots use frames: the ticker starts off-panel, so frame 0 is blank. Company logos are downloaded by the plugin and cached on disk, so the very first render on a fresh machine needs network access; later runs are offline and byte-identical." +} diff --git a/docs/assets/ledmatrix-stocks/toggle-chart.png b/docs/assets/ledmatrix-stocks/toggle-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..71c159d7c07e4e9739a181ac2ea1f861263f4e13 GIT binary patch literal 8289 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 R{MP}<`Ez<_Q#Efq_%A)Es%HQI literal 0 HcmV?d00001 diff --git a/plugins.json b/plugins.json index faa6c100..bc360374 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-09-02", + "last_updated": "2026-09-03", "plugins": [ { "id": "cricket-scoreboard", @@ -829,10 +829,10 @@ "plugin_path": "plugins/ledmatrix-stocks", "stars": 0, "downloads": 0, - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "2.8.0", + "latest_version": "2.9.0", "icon": "fas fa-chart-line" }, { diff --git a/plugins/ledmatrix-stocks/README.md b/plugins/ledmatrix-stocks/README.md index 3b99de03..a41046d7 100644 --- a/plugins/ledmatrix-stocks/README.md +++ b/plugins/ledmatrix-stocks/README.md @@ -4,6 +4,12 @@ A scrolling ticker for the LEDMatrix display showing live stock and cryptocurrency prices, percent changes, and optional inline price charts. Data comes from Yahoo Finance — no API key required. +![AAPL scrolling across a 128x32 panel: the Apple logo, the symbol, the price, +and a green gain](../../docs/assets/ledmatrix-stocks/hero.png) + +*Every image in this README is real plugin output, rendered at the true panel +size from a recorded quote so it reproduces exactly.* + ## Features - Live stock and crypto prices via Yahoo Finance (no API key) @@ -70,10 +76,38 @@ generated from it. The most-used keys, with their actual nesting: ### `customization.*` -Per-element font, size, and color overrides for stocks and crypto. Each -of `symbol`, `price`, and `price_delta` has its own `font`, `font_size`, -and color settings. Defaults use `PressStart2P-Regular.ttf` at size 8, -with green for positive deltas and red for negative. +Per-element font, size, and colour overrides, kept separately for stocks and +crypto so the two are distinguishable at a glance. Each of `symbol`, `price` +and `price_delta` has its own `font` and `font_size`; the colour keys differ +by element: + +| Key | Default | Notes | +|---|---|---| +| `customization.stocks.symbol.text_color` | `[255, 255, 255]` | Ticker symbol, stocks | +| `customization.stocks.price.text_color` | `[255, 255, 255]` | Price line, stocks | +| `customization.stocks.price_delta.positive_color` | `[0, 255, 0]` | Change when up, stocks | +| `customization.stocks.price_delta.negative_color` | `[255, 0, 0]` | Change when down, stocks | +| `customization.crypto.symbol.text_color` | `[255, 215, 0]` | Ticker symbol, crypto | +| `customization.crypto.price.text_color` | `[255, 215, 0]` | Price line, crypto | +| `customization.crypto.price_delta.positive_color` | `[0, 255, 0]` | Change when up, crypto | +| `customization.crypto.price_delta.negative_color` | `[255, 0, 0]` | Change when down, crypto | + +`price_delta` has no `text_color` — its colour is chosen by the sign of the +change, from the two keys above. + +![A gain, a loss, and a crypto entry](../../docs/assets/ledmatrix-stocks/gain-loss.png) + +Five fonts are offered per element. Three are TrueType and work at every size +in the 4–16 range; two are `.bdf` bitmap faces, which exist at exactly one +size each and are loaded at that size whatever `font_size` says: + +| Font | Kind | Sizes | +|---|---|---| +| `PressStart2P-Regular.ttf` | TrueType | any | +| `4x6-font.ttf` | TrueType | any | +| `5by7.regular.ttf` | TrueType | any | +| `5x7.bdf` | bitmap | 7 only | +| `4x6.bdf` | bitmap | 6 only | ## Symbol format @@ -119,6 +153,25 @@ Symbols are upper-case only (`aapl` is rejected — use `AAPL`), and cannot contain spaces. If the field refuses what you typed, the web UI names the value it rejected. +### The two display modes + +![scroll versus switch](../../docs/assets/ledmatrix-stocks/display-mode.png) + +`scroll` renders every symbol into one continuous ribbon and slides it past; +`switch` shows a single symbol at a time and cuts to the next after +`switch_duration` seconds. + +### The inline chart + +`display.toggle_chart` draws the day's price history behind the text, sized by +`chart_width_px` and `chart_height_px`: + +![toggle_chart on and off](../../docs/assets/ledmatrix-stocks/toggle-chart.png) + +### Panel sizes + +![The same symbol on four panel sizes](../../docs/assets/ledmatrix-stocks/panel-sizes.png) + ### A note on display width Longer symbols take more room on the panel. `^GSPC` and `GC=F` are five and diff --git a/plugins/ledmatrix-stocks/config_schema.json b/plugins/ledmatrix-stocks/config_schema.json index 03562804..1263fc9c 100644 --- a/plugins/ledmatrix-stocks/config_schema.json +++ b/plugins/ledmatrix-stocks/config_schema.json @@ -214,8 +214,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -257,8 +256,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -300,8 +298,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -366,8 +363,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -409,8 +405,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -452,8 +447,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, diff --git a/plugins/ledmatrix-stocks/display_renderer.py b/plugins/ledmatrix-stocks/display_renderer.py index 56708af1..943e3f38 100644 --- a/plugins/ledmatrix-stocks/display_renderer.py +++ b/plugins/ledmatrix-stocks/display_renderer.py @@ -138,6 +138,20 @@ def __init__(self, config: Dict[str, Any], display_width: int, display_height: i self.price_font = self._load_custom_font_from_element_config(stocks_custom.get('price', {})) self.price_delta_font = self._load_custom_font_from_element_config(stocks_custom.get('price_delta', {})) + @staticmethod + def _bdf_pixel_size(path): + """The pixel size a .bdf font declares, or None if it does not.""" + try: + with open(path, "r", encoding="latin-1") as handle: + for line in handle: + if line.startswith("PIXEL_SIZE"): + return int(line.split()[1]) + if line.startswith("CHARS"): + break # past the header; no point reading the glyphs + except (OSError, ValueError, IndexError): + return None + return None + def _load_custom_font_from_element_config(self, element_config: Dict[str, Any]) -> ImageFont.FreeTypeFont: """ Load a custom font from an element configuration dictionary. @@ -165,13 +179,25 @@ def _load_custom_font_from_element_config(self, element_config: Dict[str, Any]) self.logger.debug(f"Loaded font: {font_name} at size {font_size}") return font elif font_path.lower().endswith('.bdf'): - # PIL's ImageFont.truetype() can sometimes handle BDF files - # If it fails, we'll fall through to the default font + # A .bdf is a bitmap face that exists at exactly one pixel + # size; FreeType refuses every other. Retry at the size the + # file declares rather than dropping to the default font -- + # otherwise picking 5x7.bdf works at size 7 and nowhere else. try: font = ImageFont.truetype(font_path, font_size) self.logger.debug(f"Loaded BDF font: {font_name} at size {font_size}") return font - except Exception: + except OSError: + native = self._bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + font = ImageFont.truetype(font_path, native) + self.logger.debug( + "Loaded bitmap font %s at its native size %d " + "(requested %d)", font_name, native, font_size) + return font + except OSError: + pass self.logger.warning(f"Could not load BDF font {font_name} with PIL, using default") # Fall through to default else: diff --git a/plugins/ledmatrix-stocks/manifest.json b/plugins/ledmatrix-stocks/manifest.json index 8f8621d3..ad2cb2c2 100644 --- a/plugins/ledmatrix-stocks/manifest.json +++ b/plugins/ledmatrix-stocks/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-stocks", "name": "Stock & Crypto Ticker", - "version": "2.8.0", + "version": "2.9.0", "description": "Displays stock tickers with prices, changes, and optional charts for stocks and cryptocurrencies. Supports scroll and switch display modes.", "author": "LEDMatrix Team", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-stocks", @@ -39,6 +39,12 @@ } ], "versions": [ + { + "version": "2.9.0", + "released": "2026-09-03", + "notes": "Bitmap fonts and documentation. A .bdf face exists at exactly one pixel size, so choosing 5x7.bdf or 4x6.bdf at any other size raised inside FreeType and the renderer silently substituted PressStart2P; they now load at their own declared size. cozette.bdf is removed from all six font menus because the core ships no such file, so it could never load (#387). The README gains real rendered screenshots -- a gain, a loss, a crypto entry, both display modes, the inline chart on and off, and four panel sizes -- and documents the eight per-element colour keys that were previously summarised in a sentence.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "2.8.0", "released": "2026-08-19", @@ -71,7 +77,7 @@ { "released": "2026-07-10", "version": "2.3.2", - "notes": "Bump logo_x (2px→4px) and logo_gap (4px→8px) so the ~26px-tall logo isn't cramped against the display edge and the symbol/price text on the 32px-tall display.", + "notes": "Bump logo_x (2px\u21924px) and logo_gap (4px\u21928px) so the ~26px-tall logo isn't cramped against the display edge and the symbol/price text on the 32px-tall display.", "ledmatrix_min_version": "2.0.0" }, { @@ -116,7 +122,7 @@ "ledmatrix_min_version": "2.0.0" } ], - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "stars": 0, "downloads": 0, "verified": true,