1 Commits

Author SHA1 Message Date
Chuck
45bf5db2b1 chore: remove march-madness from bundled plugin-repos
March Madness is now available in the ledmatrix-plugins monorepo store
(ChuckBuilds/ledmatrix-plugins/plugins/march-madness) and should be
installed via the Plugin Store like any other plugin.

Removing the bundled copy so new installs don't automatically include it.
Existing users keep their installed version until they choose to uninstall.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:58:10 -04:00
9 changed files with 42 additions and 290 deletions

View File

@@ -1,10 +1,5 @@
# LEDMatrix
[![License](https://img.shields.io/badge/license-GPL--3.0-green)](LICENSE)
[![Discord](https://img.shields.io/badge/Discord-community-5865F2?logo=discord&logoColor=white)](https://discord.gg/RdrC37rEag)
[![GitHub Stars](https://img.shields.io/github/stars/ChuckBuilds/ledmatrix?style=flat&color=yellow)](https://github.com/ChuckBuilds/ledmatrix)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/77fc9b446a5948e5b0aed7a7aaeb1bab)](https://app.codacy.com/gh/ChuckBuilds/LEDMatrix/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
## Welcome to LEDMatrix!
Welcome to the LEDMatrix Project! This open-source project enables you to run an information-rich display on a Raspberry Pi connected to an LED RGB Matrix panel. Whether you want to see your calendar, weather forecasts, sports scores, stock prices, or any other information at a glance, LEDMatrix brings it all together.
@@ -132,15 +127,10 @@ The system supports live, recent, and upcoming game information for multiple spo
| This project can be finnicky! RGB LED Matrix displays are not built the same or to a high-quality standard. We have seen many displays arrive dead or partially working in our discord. Please purchase from a reputable vendor. |
### Raspberry Pi
- Raspberry Pi Zero's don't have enough processing power for this project.
- **Raspberry Pi 3B, 4, or 5**
- Raspberry Pi Zero's don't have enough processing power for this project and the Pi 5 is unsupported due to new GPIO output.
- **Raspberry Pi 3B or 4 (NOT RPi 5!)**
[Amazon Affiliate Link Raspberry Pi 4 4GB RAM](https://amzn.to/4dJixuX)
[Amazon Affiliate Link Raspberry Pi 4 8GB RAM](https://amzn.to/4qbqY7F)
- **Pi 5 users**: the installer automatically detects Pi 5 and builds the `rpi-rgb-led-matrix` library with RP1 support. If you previously installed on a Pi 4 and migrated the SD card, or if you see `mmap` errors in the logs, force a fresh library build:
```bash
sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh
```
- Pi 5 config: leave `rp1_rio` at `0` (PIO mode, default) and set `gpio_slowdown` to `1` or `2`.
### RGB Matrix Bonnet / HAT
@@ -592,7 +582,7 @@ These settings control runtime behavior and GPIO timing:
- **Critical setting**: Must match your Raspberry Pi model for stability
- **Raspberry Pi 3**: Use 3
- **Raspberry Pi 4**: Use 4
- **Raspberry Pi 5**: Use 12 in PIO mode (`rp1_rio: 0`, the default); start with `1` and increase if you see flickering
- **Raspberry Pi 5**: Use 5 (or higher if needed)
- **Raspberry Pi Zero/1**: Use 1-2
- Incorrect values can cause display corruption, flickering, or system instability
- If you experience issues, try adjusting this value up or down by 1

View File

@@ -36,17 +36,9 @@ if [ -r /proc/device-tree/model ]; then
DEVICE_MODEL=$(tr -d '\0' </proc/device-tree/model)
echo "Detected device: $DEVICE_MODEL"
else
DEVICE_MODEL=""
echo "⚠ Could not detect Raspberry Pi model (continuing anyway)"
fi
# Detect Pi 5 for hardware-specific install decisions (RP1 library verification)
IS_PI5=0
if echo "${DEVICE_MODEL:-}" | grep -qi "Raspberry Pi 5"; then
IS_PI5=1
echo "Raspberry Pi 5 detected — will verify RP1 library support."
fi
# Check OS version - must be Raspberry Pi OS Lite (Trixie)
echo ""
echo "Checking operating system requirements..."
@@ -791,28 +783,9 @@ CURRENT_STEP="Build and install rpi-rgb-led-matrix"
echo "Step 6: Building and installing rpi-rgb-led-matrix..."
echo "-----------------------------------------------------"
# On Pi 5, also check that the installed library has rp1_rio support.
# A library built before Pi 5 support was added imports fine but maps to the
# Pi 3 peripheral bus address (0x3f000000) instead of the RP1 chip at runtime.
_HAS_RP1=0
if python3 -c 'from rgbmatrix import RGBMatrixOptions; assert hasattr(RGBMatrixOptions(), "rp1_rio")' >/dev/null 2>&1; then
_HAS_RP1=1
fi
_SKIP_BUILD=0
# If already installed and not forcing rebuild, skip expensive build
if python3 -c 'from rgbmatrix import RGBMatrix, RGBMatrixOptions' >/dev/null 2>&1 && [ "${RPI_RGB_FORCE_REBUILD:-0}" != "1" ]; then
if [ "$IS_PI5" = "1" ] && [ "$_HAS_RP1" = "0" ]; then
echo "⚠ Pi 5 detected: installed rgbmatrix lacks rp1_rio support (older build)."
echo " Forcing rebuild to get Pi 5 RP1 support..."
else
_SKIP_BUILD=1
fi
fi
if [ "$_SKIP_BUILD" = "1" ]; then
_skip_suffix=""
if [ "$IS_PI5" = "1" ]; then _skip_suffix=" with Pi 5 RP1 support"; fi
echo "rgbmatrix already installed${_skip_suffix}; skipping build (set RPI_RGB_FORCE_REBUILD=1 to force rebuild)."
echo "rgbmatrix Python package already available; skipping build (set RPI_RGB_FORCE_REBUILD=1 to force rebuild)."
else
# Ensure rpi-rgb-led-matrix submodule is initialized
if [ ! -d "$PROJECT_ROOT_DIR/rpi-rgb-led-matrix-master" ]; then
@@ -879,17 +852,6 @@ except Exception as e:
PY
then
echo "✓ rpi-rgb-led-matrix installed and verified"
# Pi 5: confirm the freshly-built library has rp1_rio support
if [ "$IS_PI5" = "1" ]; then
if python3 -c 'from rgbmatrix import RGBMatrixOptions; assert hasattr(RGBMatrixOptions(), "rp1_rio")' >/dev/null 2>&1; then
echo "✓ Pi 5 RP1 (rp1_rio) support confirmed"
else
echo "⚠ rp1_rio not found after rebuild — the submodule may be an older version."
echo " Try updating the submodule and rebuilding:"
echo " git submodule update --remote rpi-rgb-led-matrix-master"
echo " sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh"
fi
fi
else
echo "✗ rpi-rgb-led-matrix import test failed"
exit 1

View File

@@ -110,10 +110,9 @@ class DisplayManager:
options.rp1_rio = runtime_config.get('rp1_rio')
else:
logger.warning(
"rp1_rio is set in config but the installed rgbmatrix library does "
"not support it — the library was likely built without Pi 5 RP1 "
"support (mmap to 0x3f000000 instead of RP1 chip). "
"Fix: sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh"
"rp1_rio is set in config but the current RGBMatrixOptions "
"implementation does not support it (RGBMatrixEmulator or older "
"library version) — value will be ignored"
)
logger.info(f"Initializing RGB Matrix with settings: rows={options.rows}, cols={options.cols}, chain_length={options.chain_length}, parallel={options.parallel}, hardware_mapping={options.hardware_mapping}")

View File

@@ -10,7 +10,6 @@ import json
import stat
import subprocess
import shutil
import threading
import zipfile
import tempfile
import requests
@@ -101,10 +100,6 @@ class PluginStoreManager:
# handlers. Bumping the cached-entry timestamp on failure serves
# the stale payload cheaply until the backoff expires.
self._failure_backoff_seconds = 60
# Prevents concurrent callers from each firing a network request when
# the registry cache expires. Only one thread fetches; others wait and
# then get the result from the warm cache (double-checked locking).
self._registry_fetch_lock = threading.Lock()
# Ensure plugins directory exists
self.plugins_dir.mkdir(exist_ok=True)
@@ -580,15 +575,6 @@ class PluginStoreManager:
(current_time - self.registry_cache_time) < self.registry_cache_timeout):
return self.registry_cache
with self._registry_fetch_lock:
# Re-check inside the lock — a concurrent caller that was waiting
# may have already populated the cache while we blocked.
current_time = time.time()
if (self.registry_cache and self.registry_cache_time and
not force_refresh and
(current_time - self.registry_cache_time) < self.registry_cache_timeout):
return self.registry_cache
try:
self.logger.info(f"Fetching plugin registry from {self.REGISTRY_URL}")
response = self._http_get_with_retries(self.REGISTRY_URL, timeout=10)

View File

@@ -716,41 +716,6 @@ def _run_startup_reconciliation() -> None:
"manual 'Reconcile' action to resolve.",
len(result.inconsistencies_manual),
)
# Write status file so the web UI can surface unresolved issues as a
# banner without the user having to read journalctl. Mirrors the
# hw_status pattern (/tmp/led_matrix_hw_status.json).
import json as _json, tempfile as _tempfile, os as _os
_recon_status = {
"done": True,
"successful": result.reconciliation_successful,
"fixed_count": len(result.inconsistencies_fixed),
"unresolved": [
{
"plugin_id": inc.plugin_id,
"type": inc.inconsistency_type.value,
"description": inc.description,
}
for inc in result.inconsistencies_manual
],
}
_recon_path = _os.path.join(_tempfile.gettempdir(), "ledmatrix_reconciliation.json")
_tmp = None
try:
if not _os.path.islink(_recon_path):
_fd, _tmp = _tempfile.mkstemp(dir=_tempfile.gettempdir(), prefix=".led_recon_")
with _os.fdopen(_fd, "w") as _f:
_json.dump(_recon_status, _f)
_os.replace(_tmp, _recon_path)
_tmp = None # Rename succeeded; nothing to clean up
except (OSError, ValueError, TypeError) as _e:
_logger.warning("[Reconciliation] Could not write status file: %s", _e)
finally:
if _tmp is not None and _os.path.exists(_tmp):
try:
_os.unlink(_tmp)
except OSError:
pass
except Exception as e:
_logger.error("[Reconciliation] Error: %s", e, exc_info=True)
finally:

View File

@@ -2,17 +2,14 @@ from flask import Blueprint, request, jsonify, Response
import json
import os
import re
import stat
import sys
import subprocess
import tempfile
import time
import hashlib
import uuid
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, Any
logger = logging.getLogger(__name__)
@@ -1387,59 +1384,6 @@ def get_system_version():
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
_update_check_cache: Dict[str, Any] = {'result': None, 'ts': 0.0}
_UPDATE_CHECK_TTL = 300 # 5 minutes — avoids a git fetch on every page load
@api_v3.route('/system/check-update', methods=['GET'])
def check_for_update():
"""Check whether a newer LEDMatrix commit is available on origin/main."""
now = time.time()
if _update_check_cache['result'] and now - _update_check_cache['ts'] < _UPDATE_CHECK_TTL:
return jsonify(_update_check_cache['result'])
_safe: Dict[str, Any] = {'update_available': False, 'remote_sha': 'unknown', 'commits_behind': 0}
try:
cwd = str(PROJECT_ROOT)
fetch_result = subprocess.run(
['git', 'fetch', 'origin', 'main', '--quiet'],
capture_output=True, timeout=10, cwd=cwd,
)
if fetch_result.returncode != 0:
logger.warning("check-update: git fetch failed (rc=%d): %s",
fetch_result.returncode,
fetch_result.stderr.decode(errors='replace').strip())
_update_check_cache['result'] = _safe
_update_check_cache['ts'] = now
return jsonify(_safe)
local = subprocess.run(
['git', 'rev-parse', 'HEAD'],
capture_output=True, text=True, timeout=5, cwd=cwd,
).stdout.strip()
remote = subprocess.run(
['git', 'rev-parse', 'origin/main'],
capture_output=True, text=True, timeout=5, cwd=cwd,
).stdout.strip()
if not local or not remote:
return jsonify(_safe)
if local == remote:
result: Dict[str, Any] = {'update_available': False, 'remote_sha': remote, 'commits_behind': 0}
else:
count_str = subprocess.run(
['git', 'rev-list', 'HEAD..origin/main', '--count'],
capture_output=True, text=True, timeout=5, cwd=cwd,
).stdout.strip()
count = int(count_str) if count_str.isdigit() else 0
result = {'update_available': count > 0, 'remote_sha': remote, 'commits_behind': count}
_update_check_cache['result'] = result
_update_check_cache['ts'] = now
return jsonify(result)
except Exception as e:
logger.warning("check-update failed: %s", e)
return jsonify(_safe)
@api_v3.route('/system/action', methods=['POST'])
def execute_system_action():
"""Execute system actions (start/stop/reboot/etc)"""
@@ -2489,28 +2433,6 @@ def reconcile_plugin_state():
status_code=500
)
@api_v3.route('/plugins/reconciliation-status', methods=['GET'])
def get_reconciliation_status():
"""Return the result of the last startup reconciliation from /tmp status file."""
_recon_path = os.path.join(tempfile.gettempdir(), "ledmatrix_reconciliation.json")
try:
st = os.lstat(_recon_path)
except FileNotFoundError:
return jsonify({'status': 'success', 'data': {'done': False, 'unresolved': []}})
if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode):
logger.warning("[Reconciliation] Status file is not a regular file: %s", _recon_path)
return jsonify({'status': 'success', 'data': {'done': False, 'unresolved': []}})
try:
with open(_recon_path) as _f:
data = json.load(_f)
return jsonify({'status': 'success', 'data': data})
except json.JSONDecodeError:
logger.exception("[Reconciliation] Failed to parse status file: %s", _recon_path)
return jsonify({'status': 'success', 'data': {'done': False, 'unresolved': []}})
except PermissionError:
logger.exception("[Reconciliation] Permission denied reading status file: %s", _recon_path)
return jsonify({'status': 'success', 'data': {'done': False, 'unresolved': []}})
@api_v3.route('/plugins/config', methods=['GET'])
def get_plugin_config():
"""Get plugin configuration"""

View File

@@ -843,14 +843,6 @@ async function updateFontPreview() {
return;
}
// BDF bitmap fonts cannot be rendered server-side — skip the API call
if (family.toLowerCase().endsWith('.bdf')) {
previewImage.style.display = 'none';
loadingText.style.display = 'block';
loadingText.textContent = 'Preview not available for BDF bitmap fonts';
return;
}
// Show loading state
loadingText.textContent = 'Loading preview...';
loadingText.style.display = 'block';

View File

@@ -1,66 +1,3 @@
<!-- Reconciliation warning banner: shown when startup reconciliation found stale plugin config entries -->
<div id="reconciliation-banner" class="bg-yellow-50 border border-yellow-300 rounded-lg p-4 mb-4 flex items-start" style="display:none !important" role="alert">
<div class="flex-shrink-0 mr-3 mt-0.5">
<i class="fas fa-exclamation-triangle text-yellow-500"></i>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-yellow-800">Plugin Config Warning</p>
<p class="text-sm text-yellow-700 mt-1" id="reconciliation-banner-text"></p>
</div>
<button type="button" onclick="window.dismissReconciliationBanner()" class="ml-4 flex-shrink-0 text-yellow-500 hover:text-yellow-700" aria-label="Dismiss">
<i class="fas fa-times"></i>
</button>
</div>
<script>
(function () {
var DISMISS_KEY = 'ledmatrix-recon-dismissed';
var _recon_timer = null;
function checkReconciliation() {
fetch('/api/v3/plugins/reconciliation-status')
.then(function (r) { return r.json(); })
.then(function (resp) {
var d = resp.data || {};
if (!d.done) {
// Reconciliation still running — poll again shortly
_recon_timer = setTimeout(checkReconciliation, 2000);
return;
}
_recon_timer = null;
if (!d.unresolved || d.unresolved.length === 0) return;
var key = d.unresolved.map(function (i) { return i.plugin_id; }).sort().join(',');
if (sessionStorage.getItem(DISMISS_KEY) === key) return;
var ids = d.unresolved.map(function (i) { return i.plugin_id; }).join(', ');
document.getElementById('reconciliation-banner-text').textContent =
'Stale plugin config entries found: ' + ids +
'. Remove them from config.json or reinstall via the Plugin Store.';
var banner = document.getElementById('reconciliation-banner');
banner.dataset.dismissKey = key;
banner.style.setProperty('display', 'flex', 'important');
})
.catch(function () {});
}
checkReconciliation();
window.dismissReconciliationBanner = function () {
var banner = document.getElementById('reconciliation-banner');
banner.style.setProperty('display', 'none', 'important');
if (_recon_timer !== null) {
clearTimeout(_recon_timer);
_recon_timer = null;
}
// Persist dismissal immediately so the banner won't reappear on reload
// even if the background sync fetch below fails.
var key = banner.dataset.dismissKey;
if (key) {
try { sessionStorage.setItem(DISMISS_KEY, key); } catch (e) {}
}
// Background sync only — do not rely on this for DISMISS_KEY or hiding.
fetch('/api/v3/plugins/reconciliation-status').catch(function () {});
};
}());
</script>
<div class="bg-white rounded-lg shadow p-6">
<div class="border-b border-gray-200 pb-4 mb-6">
<h2 class="text-lg font-semibold text-gray-900">System Overview</h2>

View File

@@ -9,8 +9,7 @@
{% set field_id = (plugin_id ~ '-' ~ full_key)|replace('.', '-')|replace('_', '-') %}
{% set label = prop.title if prop.title else key|replace('_', ' ')|title %}
{% set description = prop.description if prop.description else '' %}
{% set _pt = prop.get('type') %}
{% set field_type = _pt if (_pt is string) else ((_pt | first) if (_pt and _pt is iterable and _pt is not string) else 'string') %}
{% set field_type = prop.type if prop.type is string else (prop.type[0] if prop.type is iterable else 'string') %}
{# Handle nested objects - check for widget first #}
{% if field_type == 'object' %}