mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-15 10:03:31 +00:00
* feat(sync): multi-display wireless sync — extend scrolling across two LED matrices Adds a leader/follower sync system that extends Vegas scroll mode content continuously across two physically adjacent LED matrix units over WiFi. Architecture: - Leader broadcasts scroll position via UDP at ~90fps; follower renders the offset slice of the same image at 60fps using dead reckoning to absorb UDP jitter (smooth, stutter-free motion) - At each cycle transition the leader sends the composed scroll image via TCP (PNG-compressed ~15–40KB) so both displays render pixel-identical content regardless of plugin data timing differences - Auto-discovery via UDP subnet broadcast — no IP configuration required - Heartbeat watchdog (6s timeout) falls back to standalone if peer goes offline Key files: - src/common/sync_manager.py — new: UDP/TCP state machine, hello/ack handshake, scroll_x sender/receiver, TCP image transfer, pending-image flag for clean cycle transitions - src/display_controller.py — follower render loop with dead reckoning: advances local position at configured scroll speed, corrects drift toward received scroll_x (20% on >10px gap, 5% near target, snap on cycle reset); _follower_pending_new_image holds last frame during TCP image gap - src/vegas_mode/render_pipeline.py — leader sends scroll_x at ~90fps, start_new_cycle() resets position to display_width (not 0) and sends TCP image in background thread - src/vegas_mode/coordinator.py — set_sync_manager() / set_update_callback() wiring; defers hot-swap recompose while sync is active - web_interface/blueprints/api_v3.py — sync config save endpoint, GET /api/v3/sync/status for live status polling - web_interface/templates/v3/partials/display.html — Multi-Display Sync section: role selector (Standalone/Leader/Follower), position (Left/Right of leader, follower only), UDP port, live status indicator - config/config.template.json — sync block: role, port, follower_position Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): address PR review findings - sync_manager: replace Optional[callable] with proper Callable types from typing; tighten set_on_new_cycle/set_on_scroll_image/set_on_follower_connected signatures to match their actual callback signatures - sync_manager: log a one-shot warning when send_frame produces a packet exceeding the 65000-byte UDP cap instead of silently dropping it - display_controller: correct stale comment in _send_follower_frame (was "30fps / PNG encode/decode"; actual behavior is ~90fps raw RGB) - display.html: guard setInterval with window.syncStatusInterval to prevent duplicate pollers if the script runs more than once - display.html: replace innerHTML with DOM node creation + textContent for status icon/text to avoid inserting API-derived values via innerHTML Skip: time.time() → monotonic and self.config staleness are pre-existing issues not introduced by this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): address second round of PR review findings - sync_manager: guard TCP image receive against OOM — validate length against 10 MB cap before allocating; log and close on invalid length - display_controller: _follower_gated_update now allows update_display() through when the leader is offline (is_follower_active() == False) so the display recovers normally when falling back to standalone mode - coordinator: normalize a standalone SyncManager to None in set_sync_manager() so the render pipeline never treats a no-op manager as an active one - coordinator: derive _UPDATE_TICK_FRAMES from target_fps * 4 instead of the hardcoded 500 so the ~4s cadence holds at any configured FPS - render_pipeline: replace bare except/pass on blank-frame push with logger.exception() so failures are visible in logs Skip: config.template.json comments — JSON does not support inline comments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): address third round of PR review findings - sync_manager: use 'with socket.socket(...)' in send_scroll_image so the TCP socket is always closed even if connect/sendall raises - sync_manager: add _scroll_image_lock to serialize all reads/writes to _on_scroll_image and _pending_scroll_image between _image_server_loop and set_on_scroll_image, eliminating the lost-delivery race; callback is invoked outside the lock to avoid holding it during user code - sync_manager: validate scroll image dimensions (max 100000×256) and catch DecompressionBombError before img.load() in _image_server_loop - sync_manager: log socket close exceptions at debug level in stop() instead of silently passing - sync_manager: replace hardcoded /tmp/ with tempfile.gettempdir() for STATUS_FILE (atomic write was already in place) - sync_manager: check _RAW_MAGIC first in _follower_recv_loop routing so magic-tagged frames are always identified correctly regardless of size Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): address fourth round of PR review findings - sync_manager: log INCOMPATIBLE error only on state transition (guard with prev_state != LeaderState.INCOMPATIBLE) so repeated hello packets from an incompatible follower don't spam the log - sync_manager: replace O(n²) bytes concatenation in TCP image receive loop with bytearray + extend() for linear-time accumulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): suppress Codacy false positives - display_controller: rename local var 'sh' to 'scroll_h' so Codacy's pattern matcher doesn't confuse it with the 'sh' shell library - sync_manager: add '# nosec B104' to all socket.bind("") calls — binding to all interfaces is intentional (UDP broadcast reception and TCP image server must accept connections from any local interface) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sync): add nosec B104 to socket creation lines for Codacy Codacy attributes the bind-to-all-interfaces finding to the socket.socket() creation lines (140, 439) rather than the .bind() calls. Added # nosec B104 there too so the suppression is seen at the line Codacy reports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
3.8 KiB
JSON
144 lines
3.8 KiB
JSON
{
|
|
"web_display_autostart": true,
|
|
"schedule": {
|
|
"enabled": true,
|
|
"mode": "per-day",
|
|
"start_time": "07:00",
|
|
"end_time": "23:00",
|
|
"days": {
|
|
"monday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"tuesday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"wednesday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"thursday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"friday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"saturday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"sunday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
}
|
|
}
|
|
},
|
|
"dim_schedule": {
|
|
"enabled": false,
|
|
"dim_brightness": 30,
|
|
"mode": "global",
|
|
"start_time": "20:00",
|
|
"end_time": "07:00",
|
|
"days": {
|
|
"monday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"tuesday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"wednesday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"thursday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"friday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"saturday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
},
|
|
"sunday": {
|
|
"enabled": true,
|
|
"start_time": "20:00",
|
|
"end_time": "07:00"
|
|
}
|
|
}
|
|
},
|
|
"timezone": "America/Chicago",
|
|
"location": {
|
|
"city": "Dallas",
|
|
"state": "Texas",
|
|
"country": "US"
|
|
},
|
|
"display": {
|
|
"hardware": {
|
|
"rows": 32,
|
|
"cols": 64,
|
|
"chain_length": 2,
|
|
"parallel": 1,
|
|
"brightness": 90,
|
|
"hardware_mapping": "adafruit-hat",
|
|
"scan_mode": 0,
|
|
"pwm_bits": 9,
|
|
"pwm_dither_bits": 1,
|
|
"pwm_lsb_nanoseconds": 130,
|
|
"disable_hardware_pulsing": false,
|
|
"inverse_colors": false,
|
|
"show_refresh_rate": false,
|
|
"led_rgb_sequence": "RGB",
|
|
"limit_refresh_rate_hz": 100
|
|
},
|
|
"runtime": {
|
|
"gpio_slowdown": 3
|
|
},
|
|
"display_durations": {},
|
|
"use_short_date_format": true,
|
|
"vegas_scroll": {
|
|
"enabled": false,
|
|
"scroll_speed": 50,
|
|
"separator_width": 32,
|
|
"plugin_order": [],
|
|
"excluded_plugins": [],
|
|
"target_fps": 125,
|
|
"buffer_ahead": 2
|
|
}
|
|
},
|
|
"sync": {
|
|
"role": "standalone",
|
|
"port": 5765,
|
|
"follower_position": "left"
|
|
},
|
|
"plugin_system": {
|
|
"plugins_directory": "plugin-repos",
|
|
"auto_discover": true,
|
|
"auto_load_enabled": true
|
|
},
|
|
"web-ui-info": {
|
|
"enabled": true,
|
|
"display_duration": 10
|
|
}
|
|
}
|