mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-21 04:13:32 +00:00
Compare commits
4 Commits
fix/web-ui
...
update-rgb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53e4995c4 | ||
|
|
a0f19d8972 | ||
|
|
4f126d6133 | ||
|
|
5dde1125e9 |
@@ -1,43 +1,43 @@
|
||||
{
|
||||
"web_display_autostart": true,
|
||||
"schedule": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"mode": "per-day",
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
}
|
||||
@@ -51,46 +51,46 @@
|
||||
"end_time": "07:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": "America/New_York",
|
||||
"timezone": "America/Chicago",
|
||||
"location": {
|
||||
"city": "Tampa",
|
||||
"state": "Florida",
|
||||
"city": "Dallas",
|
||||
"state": "Texas",
|
||||
"country": "US"
|
||||
},
|
||||
"display": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
requests>=2.33.0
|
||||
urllib3>=2.6.3
|
||||
urllib3>=1.26.0
|
||||
Pillow>=12.2.0
|
||||
pytz>=2022.1
|
||||
numpy>=1.24.0
|
||||
|
||||
@@ -235,6 +235,8 @@ class DisplayHelper:
|
||||
PIL Image with no data message
|
||||
"""
|
||||
img = self.create_base_image((0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
font = ImageFont.load_default()
|
||||
self._draw_centered_text(message, font, (0, 0, 0), (150, 150, 150))
|
||||
|
||||
|
||||
@@ -823,7 +823,7 @@ class DisplayController:
|
||||
scroll_h = getattr(plugin_instance, 'scroll_helper', None)
|
||||
if scroll_h is not None:
|
||||
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
|
||||
|
||||
# 3. Mirror fallback — static plugins (clock, weather) show same frame
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
if os.getenv("EMULATOR", "false") == "true":
|
||||
from RGBMatrixEmulator import RGBMatrix, RGBMatrixOptions
|
||||
else:
|
||||
@@ -60,7 +58,6 @@ class DisplayManager:
|
||||
|
||||
def _setup_matrix(self):
|
||||
"""Initialize the RGB matrix with configuration settings."""
|
||||
_init_error_str = None
|
||||
try:
|
||||
# Allow callers (e.g., web UI) to force non-hardware fallback mode
|
||||
if getattr(self, '_force_fallback', False):
|
||||
@@ -90,7 +87,7 @@ class DisplayManager:
|
||||
options.disable_hardware_pulsing = hardware_config.get('disable_hardware_pulsing', False)
|
||||
options.show_refresh_rate = hardware_config.get('show_refresh_rate', False)
|
||||
options.limit_refresh_rate_hz = hardware_config.get('limit_refresh_rate_hz', 90)
|
||||
options.gpio_slowdown = runtime_config.get('gpio_slowdown', 3)
|
||||
options.gpio_slowdown = runtime_config.get('gpio_slowdown', 2)
|
||||
|
||||
# Disable internal privilege dropping - we manage this via systemd or remain root
|
||||
# This prevents the library from dropping to 'daemon' user which breaks file permissions
|
||||
@@ -144,7 +141,6 @@ class DisplayManager:
|
||||
self._draw_test_pattern()
|
||||
|
||||
except Exception as e:
|
||||
_init_error_str = str(e)
|
||||
logger.error(f"Failed to initialize RGB Matrix: {e}", exc_info=True)
|
||||
# Create a fallback image for web preview using configured dimensions when available
|
||||
self.matrix = None
|
||||
@@ -168,38 +164,9 @@ class DisplayManager:
|
||||
except Exception: # nosec B110 - best-effort fallback visualization; drawing errors must not crash startup
|
||||
# Best-effort; ignore drawing errors in fallback
|
||||
pass
|
||||
logger.error(
|
||||
f"Matrix initialization failed — running in fallback/simulation mode "
|
||||
f"(size {fallback_width}x{fallback_height}). Error: {e}. "
|
||||
"On Raspberry Pi 5: ensure rpi-rgb-led-matrix was built from the latest "
|
||||
"submodule (re-run first_time_install.sh). gpio_slowdown of 2–3 is typical for Pi 5 PIO mode."
|
||||
)
|
||||
logger.error(f"Matrix initialization failed, using fallback mode with size {fallback_width}x{fallback_height}. Error: {e}")
|
||||
# Do not raise here; allow fallback mode so web preview and non-hardware environments work
|
||||
|
||||
# Write hardware status file so the web UI can surface init failures
|
||||
_hw_status = {"ok": self.matrix is not None, "error": _init_error_str}
|
||||
_status_path = "/tmp/led_matrix_hw_status.json" # nosec B108
|
||||
try:
|
||||
if os.path.islink(_status_path):
|
||||
logger.warning("Skipping hardware status write: %s is a symlink", _status_path)
|
||||
else:
|
||||
_fd, _tmp_path = tempfile.mkstemp(dir="/tmp", prefix=".led_hw_") # nosec B108
|
||||
try:
|
||||
with os.fdopen(_fd, "w") as _f:
|
||||
json.dump(_hw_status, _f)
|
||||
_f.flush()
|
||||
os.fsync(_f.fileno())
|
||||
os.chmod(_tmp_path, 0o600)
|
||||
os.replace(_tmp_path, _status_path)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(_tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
except Exception:
|
||||
logger.error("Failed to write hardware status file", exc_info=True)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Get the display width."""
|
||||
@@ -780,8 +747,8 @@ class DisplayManager:
|
||||
try:
|
||||
self.image = Image.new('RGB', (self.width, self.height))
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
except (OSError, RuntimeError, ValueError, MemoryError):
|
||||
logger.debug("Canvas reset during cleanup failed", exc_info=True)
|
||||
except Exception:
|
||||
pass
|
||||
# Reset the singleton state when cleaning up
|
||||
DisplayManager._instance = None
|
||||
DisplayManager._initialized = False
|
||||
|
||||
@@ -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,50 +575,41 @@ 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):
|
||||
try:
|
||||
self.logger.info(f"Fetching plugin registry from {self.REGISTRY_URL}")
|
||||
response = self._http_get_with_retries(self.REGISTRY_URL, timeout=10)
|
||||
response.raise_for_status()
|
||||
self.registry_cache = response.json()
|
||||
self.registry_cache_time = current_time
|
||||
self.logger.info(f"Fetched registry with {len(self.registry_cache.get('plugins', []))} plugins")
|
||||
return self.registry_cache
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(f"Error fetching registry: {e}")
|
||||
if raise_on_failure:
|
||||
raise
|
||||
# Prefer stale cache over an empty list so the plugin list UI
|
||||
# keeps working on a flaky connection (e.g. Pi on WiFi). Bump
|
||||
# registry_cache_time into a short backoff window so the next
|
||||
# request serves the stale payload cheaply instead of
|
||||
# re-hitting the network on every request (matches the
|
||||
# pattern used by github_cache / commit_info_cache).
|
||||
if self.registry_cache:
|
||||
self.logger.warning("Falling back to stale registry cache")
|
||||
self.registry_cache_time = (
|
||||
time.time() + self._failure_backoff_seconds - 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)
|
||||
response.raise_for_status()
|
||||
self.registry_cache = response.json()
|
||||
self.registry_cache_time = current_time
|
||||
self.logger.info(f"Fetched registry with {len(self.registry_cache.get('plugins', []))} plugins")
|
||||
return {"plugins": []}
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Error parsing registry JSON: {e}")
|
||||
if raise_on_failure:
|
||||
raise
|
||||
if self.registry_cache:
|
||||
self.registry_cache_time = (
|
||||
time.time() + self._failure_backoff_seconds - self.registry_cache_timeout
|
||||
)
|
||||
return self.registry_cache
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(f"Error fetching registry: {e}")
|
||||
if raise_on_failure:
|
||||
raise
|
||||
# Prefer stale cache over an empty list so the plugin list UI
|
||||
# keeps working on a flaky connection (e.g. Pi on WiFi). Bump
|
||||
# registry_cache_time into a short backoff window so the next
|
||||
# request serves the stale payload cheaply instead of
|
||||
# re-hitting the network on every request (matches the
|
||||
# pattern used by github_cache / commit_info_cache).
|
||||
if self.registry_cache:
|
||||
self.logger.warning("Falling back to stale registry cache")
|
||||
self.registry_cache_time = (
|
||||
time.time() + self._failure_backoff_seconds - self.registry_cache_timeout
|
||||
)
|
||||
return self.registry_cache
|
||||
return {"plugins": []}
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Error parsing registry JSON: {e}")
|
||||
if raise_on_failure:
|
||||
raise
|
||||
if self.registry_cache:
|
||||
self.registry_cache_time = (
|
||||
time.time() + self._failure_backoff_seconds - self.registry_cache_timeout
|
||||
)
|
||||
return self.registry_cache
|
||||
return {"plugins": []}
|
||||
return {"plugins": []}
|
||||
|
||||
def search_plugins(self, query: str = "", category: str = "", tags: List[str] = None, fetch_commit_info: bool = True, include_saved_repos: bool = True, saved_repositories_manager = None) -> List[Dict]:
|
||||
"""
|
||||
|
||||
@@ -716,33 +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 = "/tmp/ledmatrix_reconciliation.json"
|
||||
try:
|
||||
if not _os.path.islink(_recon_path):
|
||||
_fd, _tmp = _tempfile.mkstemp(dir="/tmp", prefix=".led_recon_")
|
||||
with _os.fdopen(_fd, "w") as _f:
|
||||
_json.dump(_recon_status, _f)
|
||||
_os.replace(_tmp, _recon_path)
|
||||
except Exception as _e:
|
||||
_logger.warning("[Reconciliation] Could not write status file: %s", _e)
|
||||
except Exception as e:
|
||||
_logger.error("[Reconciliation] Error: %s", e, exc_info=True)
|
||||
finally:
|
||||
|
||||
@@ -699,7 +699,7 @@ def save_main_config():
|
||||
|
||||
# Handle display settings
|
||||
display_fields = ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping',
|
||||
'gpio_slowdown', 'rp1_rio', 'scan_mode', 'disable_hardware_pulsing', 'inverse_colors', 'show_refresh_rate',
|
||||
'gpio_slowdown', 'scan_mode', 'disable_hardware_pulsing', 'inverse_colors', 'show_refresh_rate',
|
||||
'pwm_bits', 'pwm_dither_bits', 'pwm_lsb_nanoseconds', 'limit_refresh_rate_hz', 'use_short_date_format',
|
||||
'max_dynamic_duration_seconds', 'led_rgb_sequence', 'multiplexing', 'panel_type']
|
||||
|
||||
@@ -747,14 +747,6 @@ def save_main_config():
|
||||
# Handle runtime settings
|
||||
if 'gpio_slowdown' in data:
|
||||
current_config['display']['runtime']['gpio_slowdown'] = int(data['gpio_slowdown'])
|
||||
if 'rp1_rio' in data:
|
||||
try:
|
||||
rp1_val = int(data['rp1_rio'])
|
||||
if rp1_val not in (0, 1):
|
||||
return jsonify({'status': 'error', 'message': "rp1_rio must be 0 (PIO) or 1 (RIO)"}), 400
|
||||
current_config['display']['runtime']['rp1_rio'] = rp1_val
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'status': 'error', 'message': "rp1_rio must be 0 or 1"}), 400
|
||||
|
||||
# Handle checkboxes - coerce to bool to ensure proper JSON types
|
||||
for checkbox in ['disable_hardware_pulsing', 'inverse_colors', 'show_refresh_rate']:
|
||||
@@ -1384,52 +1376,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)
|
||||
subprocess.run(
|
||||
['git', 'fetch', 'origin', 'main', '--quiet'],
|
||||
capture_output=True, timeout=10, cwd=cwd,
|
||||
)
|
||||
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.debug("check-update: %s", e)
|
||||
return jsonify(_safe)
|
||||
|
||||
@api_v3.route('/system/action', methods=['POST'])
|
||||
def execute_system_action():
|
||||
"""Execute system actions (start/stop/reboot/etc)"""
|
||||
@@ -1581,23 +1527,6 @@ def execute_system_action():
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500
|
||||
|
||||
@api_v3.route('/hardware/status', methods=['GET'])
|
||||
def get_hardware_status():
|
||||
"""Return LED matrix hardware initialization status written by display_manager at startup."""
|
||||
status_path = "/tmp/led_matrix_hw_status.json" # nosec B108
|
||||
try:
|
||||
with open(status_path) as f:
|
||||
hw_data = json.load(f)
|
||||
return jsonify({"status": "success", "data": hw_data})
|
||||
except FileNotFoundError:
|
||||
return jsonify({"status": "success", "data": {"ok": None, "error": "Display service not yet started"}})
|
||||
except (json.JSONDecodeError, PermissionError):
|
||||
logger.error("Failed to read hardware status file", exc_info=True)
|
||||
return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500
|
||||
except Exception:
|
||||
logger.error("Unexpected error reading hardware status", exc_info=True)
|
||||
return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500
|
||||
|
||||
@api_v3.route('/display/current', methods=['GET'])
|
||||
def get_display_current():
|
||||
"""Get current display state"""
|
||||
@@ -2479,19 +2408,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 = "/tmp/ledmatrix_reconciliation.json"
|
||||
try:
|
||||
with open(_recon_path) as _f:
|
||||
data = json.load(_f)
|
||||
return jsonify({'status': 'success', 'data': data})
|
||||
except FileNotFoundError:
|
||||
return jsonify({'status': 'success', 'data': {'done': False, 'unresolved': []}})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
@api_v3.route('/plugins/config', methods=['GET'])
|
||||
def get_plugin_config():
|
||||
"""Get plugin configuration"""
|
||||
|
||||
@@ -41,7 +41,7 @@ def get_local_ips():
|
||||
ip = ip.strip()
|
||||
if ip and not ip.startswith("127.") and ip != "192.168.4.1":
|
||||
ips.append(ip)
|
||||
except Exception: # nosec B110 - hostname -I output parsing; non-critical startup info
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: try socket method
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global showNotification, updateSystemStats, htmx */
|
||||
/* global showNotification, updateSystemStats */
|
||||
// LED Matrix v3 JavaScript
|
||||
// Additional helpers for HTMX and Alpine.js integration
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1';
|
||||
removeButton.addEventListener('click', function() {
|
||||
window.removeCustomFeedRow(this);
|
||||
removeCustomFeedRow(this);
|
||||
});
|
||||
const removeIcon = document.createElement('i');
|
||||
removeIcon.className = 'fas fa-trash';
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
const parts = formatter.formatToParts(now);
|
||||
const offsetPart = parts.find(p => p.type === 'timeZoneName');
|
||||
return offsetPart ? offsetPart.value : '';
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,6 @@
|
||||
<p class="mt-1 text-sm text-gray-600">Configure LED matrix hardware settings and display options.</p>
|
||||
</div>
|
||||
|
||||
<!-- Hardware status banner: shown when display service is in fallback/simulation mode -->
|
||||
<div x-data="{ show: false, errorMsg: '' }"
|
||||
x-init="fetch('/api/v3/hardware/status').then(r => r.json()).then(d => {
|
||||
const hw = (d && d.data) || {};
|
||||
if (hw.ok === false) { show = true; errorMsg = hw.error || 'Unknown error'; }
|
||||
}).catch(() => {})"
|
||||
x-show="show"
|
||||
style="display:none"
|
||||
class="bg-yellow-50 border border-yellow-300 rounded-lg p-4 mb-6">
|
||||
<p class="font-semibold text-yellow-800"><i class="fas fa-exclamation-triangle mr-2"></i>LED matrix running in simulation mode</p>
|
||||
<p class="text-sm text-yellow-700 mt-1">Hardware initialization failed: <span x-text="errorMsg" class="font-mono text-xs break-all"></span></p>
|
||||
<p class="text-sm text-yellow-700 mt-2">
|
||||
On Raspberry Pi 5: ensure the library was rebuilt from the latest submodule
|
||||
(<code class="bg-yellow-100 px-1 rounded">first_time_install.sh</code>)
|
||||
and try adjusting <strong>GPIO Slowdown</strong> (start at 3, reduce if the display looks dim or choppy).
|
||||
Check the <a href="/v3/logs" class="underline font-medium">Logs tab</a> for the full error.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form hx-post="/api/v3/config/main"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Content-Type": "application/json"}'
|
||||
@@ -168,7 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown</label>
|
||||
<input type="number"
|
||||
@@ -176,20 +157,9 @@
|
||||
name="gpio_slowdown"
|
||||
value="{{ main_config.display.runtime.gpio_slowdown or 3 }}"
|
||||
min="0"
|
||||
max="10"
|
||||
max="5"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Pi 3: 1–2 · Pi 4: 2–4 · Pi 5 PIO: 1–3. Increase if display shows garbage; in RIO mode higher values may improve performance.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rp1_rio" class="block text-sm font-medium text-gray-700">
|
||||
RP1 Backend <span class="text-xs text-gray-400 font-normal">(Pi 5 only)</span>
|
||||
</label>
|
||||
<select id="rp1_rio" name="rp1_rio" class="form-control">
|
||||
<option value="0" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 0 %}selected{% endif %}>0 — PIO (default, low CPU)</option>
|
||||
<option value="1" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 1 %}selected{% endif %}>1 — RIO (higher throughput; slowdown inverted)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Pi 5 RP1 coprocessor mode. Ignored on Pi 3/4.</p>
|
||||
<p class="mt-1 text-sm text-gray-600">GPIO slowdown factor (0-5)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1,53 +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';
|
||||
fetch('/api/v3/plugins/reconciliation-status')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (resp) {
|
||||
var d = resp.data || {};
|
||||
if (!d.done || !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.style.setProperty('display', 'flex', 'important');
|
||||
})
|
||||
.catch(function () {});
|
||||
|
||||
window.dismissReconciliationBanner = function () {
|
||||
var banner = document.getElementById('reconciliation-banner');
|
||||
banner.style.setProperty('display', 'none', 'important');
|
||||
try {
|
||||
fetch('/api/v3/plugins/reconciliation-status')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (resp) {
|
||||
var d = resp.data || {};
|
||||
if (d.unresolved && d.unresolved.length) {
|
||||
var key = d.unresolved.map(function (i) { return i.plugin_id; }).sort().join(',');
|
||||
sessionStorage.setItem(DISMISS_KEY, key);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
};
|
||||
}());
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user