4 Commits

Author SHA1 Message Date
Chuck
c53e4995c4 fix(systemd): wait for network connectivity before starting services
Change After=network.target to After=network-online.target + Wants=network-
online.target in both service templates and install_web_service.sh.

network.target only means NetworkManager has started — it does NOT mean the
device has an active internet connection. On boot, the LED matrix service was
starting within seconds of the network interface appearing, before WiFi
association and DHCP completed, causing all first-update API calls to fail
with "Network is unreachable" or DNS resolution errors.

network-online.target waits for a confirmed network route before the service
starts. On Raspberry Pi OS this is provided by NetworkManager-wait-online.
The tradeoff is a few extra seconds at boot, which is acceptable for a
display device.

Applied live to /etc/systemd/system/ledmatrix.service on devpi via
systemctl daemon-reload (no restart required for the config change to take
effect on next boot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:14:50 -04:00
Chuck
a0f19d8972 fix: deterministic submodule install + guard rp1_rio for older rgbmatrix
first_time_install.sh: remove --remote from both git submodule update
calls so first-time installs check out the pinned commit recorded in the
repo rather than whatever upstream master happens to be at install time.
The branch = master config in .gitmodules reserves --remote for an
explicit maintainer upgrade (git submodule update --remote).

display_manager.py: guard rp1_rio assignment with hasattr() so setting
the option in config does not cause an AttributeError and silently fall
through to emulator mode when running against RGBMatrixEmulator or an
older rgbmatrix build that predates the Pi 5 property. Emit a warning
instead so the operator knows the value was ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:09:25 -04:00
Chuck
4f126d6133 chore(deps): update rpi-rgb-led-matrix install for new scikit-build-core system
The library migrated from 'make build-python' + 'pip install bindings/python'
to a scikit-build-core + cmake build where the entire repo root is pip-
installable via 'pip install .'. Update first_time_install.sh accordingly:
- Remove the 'make build-python' step (target no longer exists)
- Install directly from the repo root instead of bindings/python
- Replace build deps: remove cython3/scons/python3-dev, add python-dev-is-python3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:29:33 -04:00
Chuck
5dde1125e9 chore(deps): update rpi-rgb-led-matrix to latest upstream for Pi 5 support
Configure submodule to track upstream master branch (branch = master in
.gitmodules) so future updates are a single 'git submodule update --remote'
rather than manual SHA management.

Update first_time_install.sh to use --remote flag so fresh installs always
pull the current upstream master, not the commit recorded at clone time.

Current upstream HEAD (8907235) brings:
- PR #1886: Raspberry Pi 5 support — new RP1 PIO and RIO backends. The
  library auto-detects Pi 5 hardware at runtime; no config change required
  for basic operation. adafruit-hat-pwm is confirmed supported on Pi 5.
- PR #1833: setup.py migrated from distutils → setuptools, fixing Python
  3.12+ build failure (Pi runs Python 3.13). Previous version could not
  build the bindings at all on current Pi OS.

Expose new rp1_rio option in display_manager.py and config.template.json:
  0 (default) = PIO mode — uses Pi 5 RP1 coprocessor, minimal CPU usage
  1 = RIO mode — Registered IO, faster throughput, higher CPU; note that
      gpio_slowdown has inverted effect in this mode

No API changes to RGBMatrix, RGBMatrixOptions, or FrameCanvas. Pi 4 and
earlier hardware is unaffected — rp1_rio is silently ignored on non-Pi-5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:22:33 -04:00
8 changed files with 9 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
requests>=2.33.0 requests>=2.33.0
urllib3>=2.2.2 urllib3>=1.26.0
Pillow>=12.2.0 Pillow>=12.2.0
pytz>=2022.1 pytz>=2022.1
numpy>=1.24.0 numpy>=1.24.0

View File

@@ -235,6 +235,8 @@ class DisplayHelper:
PIL Image with no data message PIL Image with no data message
""" """
img = self.create_base_image((0, 0, 0)) img = self.create_base_image((0, 0, 0))
draw = ImageDraw.Draw(img)
font = ImageFont.load_default() font = ImageFont.load_default()
self._draw_centered_text(message, font, (0, 0, 0), (150, 150, 150)) self._draw_centered_text(message, font, (0, 0, 0), (150, 150, 150))

View File

@@ -823,7 +823,7 @@ class DisplayController:
scroll_h = getattr(plugin_instance, 'scroll_helper', None) scroll_h = getattr(plugin_instance, 'scroll_helper', None)
if scroll_h is not None: if scroll_h is not None:
follower_frame = scroll_h.get_portion_at(scroll_h.scroll_position + offset) follower_frame = scroll_h.get_portion_at(scroll_h.scroll_position + offset)
except Exception: # nosec B110 - scroll_helper.get_portion_at is optional; skip on error except Exception:
pass pass
# 3. Mirror fallback — static plugins (clock, weather) show same frame # 3. Mirror fallback — static plugins (clock, weather) show same frame

View File

@@ -747,7 +747,7 @@ class DisplayManager:
try: try:
self.image = Image.new('RGB', (self.width, self.height)) self.image = Image.new('RGB', (self.width, self.height))
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
except Exception: # nosec B110 - best-effort canvas reset during cleanup; non-critical except Exception:
pass pass
# Reset the singleton state when cleaning up # Reset the singleton state when cleaning up
DisplayManager._instance = None DisplayManager._instance = None

View File

@@ -41,7 +41,7 @@ def get_local_ips():
ip = ip.strip() ip = ip.strip()
if ip and not ip.startswith("127.") and ip != "192.168.4.1": if ip and not ip.startswith("127.") and ip != "192.168.4.1":
ips.append(ip) ips.append(ip)
except Exception: # nosec B110 - hostname -I output parsing; non-critical startup info except Exception:
pass pass
# Fallback: try socket method # Fallback: try socket method

View File

@@ -1,4 +1,4 @@
/* global showNotification, updateSystemStats, htmx */ /* global showNotification, updateSystemStats */
// LED Matrix v3 JavaScript // LED Matrix v3 JavaScript
// Additional helpers for HTMX and Alpine.js integration // Additional helpers for HTMX and Alpine.js integration

View File

@@ -331,7 +331,7 @@
removeButton.type = 'button'; removeButton.type = 'button';
removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1'; removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1';
removeButton.addEventListener('click', function() { removeButton.addEventListener('click', function() {
window.removeCustomFeedRow(this); removeCustomFeedRow(this);
}); });
const removeIcon = document.createElement('i'); const removeIcon = document.createElement('i');
removeIcon.className = 'fas fa-trash'; removeIcon.className = 'fas fa-trash';

View File

@@ -212,7 +212,7 @@
const parts = formatter.formatToParts(now); const parts = formatter.formatToParts(now);
const offsetPart = parts.find(p => p.type === 'timeZoneName'); const offsetPart = parts.find(p => p.type === 'timeZoneName');
return offsetPart ? offsetPart.value : ''; return offsetPart ? offsetPart.value : '';
} catch { } catch (e) {
return ''; return '';
} }
} }