diff --git a/plugins.json b/plugins.json index fe71fed6..cdf6b706 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-08-20", + "last_updated": "2026-08-19", "plugins": [ { "id": "cricket-scoreboard", @@ -439,7 +439,7 @@ "last_updated": "2026-08-03", "verified": true, "screenshot": "", - "latest_version": "1.12.13" + "latest_version": "1.12.14" }, { "id": "march-madness", diff --git a/plugins/ledmatrix-flights/manager.py b/plugins/ledmatrix-flights/manager.py index 06385747..094c07c3 100644 --- a/plugins/ledmatrix-flights/manager.py +++ b/plugins/ledmatrix-flights/manager.py @@ -591,6 +591,29 @@ def _resolve_metar_airports(self, codes) -> list: @property def display_width(self) -> int: + """Width to render at: the panel, or the slice Vegas asked for. + + Vegas requests a narrower render so a layout built for the full panel + does not read as sparse in the ticker. It normally delivers that by + narrowing the shared canvas for the duration of the call, which this + property picks up for free through matrix.width -- but it cannot narrow + the canvas in offscreen mode, where it only sets the hint. Reading the + hint covers both paths. + + Without this the map was composed at the full panel width and then + cropped by the adapter: on one rig, of twelve narrow requests only + three were honoured and nine rendered full width. The composite cache + below is already keyed by size and expects both widths, so honouring + the request adds an entry rather than thrashing. + """ + hint = getattr(self, "get_vegas_render_width", None) + if callable(hint): + try: + requested = int(hint()) + if requested > 0: + return requested + except (TypeError, ValueError): + pass return self._display_manager_ref.matrix.width @property diff --git a/plugins/ledmatrix-flights/manifest.json b/plugins/ledmatrix-flights/manifest.json index a293588b..01e4c977 100644 --- a/plugins/ledmatrix-flights/manifest.json +++ b/plugins/ledmatrix-flights/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-flights", "name": "Flight Tracker", - "version": "1.12.13", + "version": "1.12.14", "description": "Real-time aircraft tracking with ADS-B/FlightRadar24/OpenSky/adsb.fi/adsb.lol data, map backgrounds, area mode, flight tracking, anchor airport, flight records, and optional airport weather (METAR/TAF/PIREP/SIGMET via the free NOAA Aviation Weather Center API)", "author": "ChuckBuilds", "entry_point": "manager.py", @@ -37,6 +37,12 @@ "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0", "versions": [ + { + "version": "1.12.14", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Compose the map at the width the ticker asked for. Vegas requests a narrower render so a layout built for the full panel does not read as sparse in the scroll, and normally delivers it by narrowing the shared canvas -- which this plugin picked up for free through matrix.width. It cannot narrow the canvas in offscreen mode, though, where it only sets the hint, so the same map came back at two different widths depending on which path the adapter took: on one rig only three of twelve narrow requests were honoured and the other nine rendered the full panel and were cropped afterwards. The width property now reads the hint, which covers both paths and falls back to the panel width outside a Vegas request. The composite cache is already keyed by size and expects both widths, so this adds an entry rather than invalidating one." + }, { "version": "1.12.13", "released": "2026-08-19", diff --git a/plugins/ledmatrix-flights/test_vegas_render_width.py b/plugins/ledmatrix-flights/test_vegas_render_width.py new file mode 100644 index 00000000..78f22c93 --- /dev/null +++ b/plugins/ledmatrix-flights/test_vegas_render_width.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""The map must be composed at the width the ticker asked for. + +Vegas asks a plugin for a narrower render so a layout built for the full panel +does not read as sparse in the ticker. It normally delivers that by narrowing +the shared canvas for the duration of the call, which a plugin sizing itself +from ``matrix.width`` picks up for free. But it cannot narrow the canvas in +offscreen mode -- ``_render_at`` swaps the shared canvas, which is unsafe +there -- so it only sets ``_vegas_render_width`` and notes that a plugin +reading ``get_vegas_render_width()`` still gets the narrow size while one that +only reads ``matrix.width`` "renders full width and is trimmed instead". + +This plugin relied on the canvas being narrowed. Its own docstring said the +projection scales "without any extra plumbing". On a live rig that assumption +held three times out of twelve: + + [ledmatrix-flights] Native: requesting 256px instead of 512px + [ledmatrix-flights] Native: SUCCESS - 1 images, 512px total width + [ledmatrix-flights] Native: SUCCESS - 1 images, 256px total width + +so the same map came back at two different widths depending on which path the +adapter took, and the full-width renders were cropped afterwards. + +No plugin in the repo read get_vegas_render_width() before this. + +Run: /bin/python plugins/ledmatrix-flights/test_vegas_render_width.py +""" + +import sys +from pathlib import Path + +plugin_dir = Path(__file__).resolve().parent +sys.path.insert(0, str(plugin_dir)) +for candidate in (Path("/home/rackpi/projects/LEDMatrix"), + plugin_dir.parents[2] / "LEDMatrix"): + if (candidate / "src" / "plugin_system" / "base_plugin.py").exists(): + sys.path.insert(0, str(candidate)) + break + +failures = [] + + +def check(label, ok): + print((" PASS " if ok else " FAIL ") + label) + if not ok: + failures.append(label) + + +class _Matrix: + width, height = 512, 64 + + +class _DisplayManager: + matrix = _Matrix() + + +def _plugin(): + """A FlightTracker carrying what the width property reads. + + Both names are set on purpose. This plugin keeps its own + ``_display_manager_ref``, while BasePlugin.__init__ stores the same object + as ``display_manager`` -- and get_vegas_render_width()'s fallback reads the + latter. A stub with only the private name makes the fallback miss the + matrix and return its hard-coded 128, which looks exactly like the property + being broken. + """ + from manager import FlightTrackerPlugin as _P + obj = object.__new__(_P) + dm = _DisplayManager() + obj._display_manager_ref = dm + obj.display_manager = dm + return obj + + +def main(): + print("the render width follows the ticker's request") + from src.plugin_system.base_plugin import BasePlugin + + p = _plugin() + check("the plugin inherits get_vegas_render_width from BasePlugin", + isinstance(p, BasePlugin) or hasattr(p, "get_vegas_render_width")) + + check("outside a Vegas request it is the panel width (%d)" % p.display_width, + p.display_width == 512) + + # Exactly what PluginAdapter does before calling get_vegas_content(), and + # the only thing it can do in offscreen mode. + p._vegas_render_width = 256 + check("during a narrow request it is the requested width (%d)" % p.display_width, + p.display_width == 256) + + # And exactly what the adapter's finally clause does afterwards. + p._vegas_render_width = None + check("afterwards it is the panel width again (%d)" % p.display_width, + p.display_width == 512) + + print("\nnonsense hints fall back rather than propagating") + for bad in (0, -1, "wide", None): + p._vegas_render_width = bad + ok = p.display_width == 512 + check(f"a hint of {bad!r} falls back to the panel width", ok) + + print("\nthe composite cache keys on the size, so both widths coexist") + source = (plugin_dir / "manager.py").read_text(encoding="utf-8") + check("cached_map_bgs is keyed by (width, height)", + "current_size = (self.display_width, self.display_height)" in source) + check("and it is only cleared when the view itself moves", + "if self.last_map_center != current_center or self.last_map_zoom != zoom:" + in source) + + print("\n%s" % ("FAILED: %d" % len(failures) if failures + else "All checks passed")) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())