feat: server-side PPTX conversion + screenshot dashboard system

PPTX Conversion:
- Remove LibreOffice from client dependencies (server uses Gotenberg)
- Update docs to reflect clients only display pre-rendered PDFs
- Clarify server-side conversion workflow in README and copilot-instructions

Screenshot System:
- Add background screenshot capture in display_manager.py
- Implement Wayland/X11 session detection with tool fallback
- Add client-side image processing (downscale + JPEG compression)
- Create timestamped files + latest.jpg symlink for easy access
- Implement file rotation (max 20 screenshots by default)
- Enhance simclient.py to transmit via MQTT dashboard topic
- Add structured JSON payload with screenshot, timestamp, system info
- New env vars: SCREENSHOT_CAPTURE_INTERVAL, SCREENSHOT_MAX_WIDTH,
  SCREENSHOT_JPEG_QUALITY, SCREENSHOT_MAX_FILES, SCREENSHOT_ALWAYS

Architecture: Two-process design with shared screenshots/ volume
This commit is contained in:
RobbStarkAustria
2025-11-30 13:49:27 +01:00
parent 65d7b99198
commit d021e21544
4 changed files with 623 additions and 100 deletions

View File

@@ -1,10 +1,40 @@
# Copilot Instructions - Infoscreen Client # Copilot Instructions - Infoscreen Client
## Quick Start for AI Assistants
### Critical Rules
-**ALWAYS use Impressive** for PDF presentations (has native auto-advance/loop)
-**NEVER suggest xdotool** approaches (failed on Raspberry Pi due to focus issues)
-**NEVER suggest video conversion** (adds complexity, had black screen issues)
-**Virtual environment MUST have** pygame + pillow (required for Impressive)
-**Client-side resize/compress** screenshots before MQTT transmission
-**Server renders PPTX → PDF via Gotenberg** (client only displays PDFs, no LibreOffice needed)
### Key Files & Locations
- **Display logic**: `src/display_manager.py` (controls presentations/video/web)
- **MQTT client**: `src/simclient.py` (event management, heartbeat, discovery)
- **Runtime state**: `src/current_event.json` (current active event)
- **Config**: `src/config/client_uuid.txt`, `src/config/last_group_id.txt`, `.env`
- **Logs**: `logs/display_manager.log`, `logs/simclient.log`
- **Screenshots**: `src/screenshots/` (shared volume between processes)
### Common Tasks Quick Reference
| Task | File | Key Method/Section |
|------|------|-------------------|
| Add event type | `display_manager.py` | `start_display_for_event()` |
| Modify presentation | `display_manager.py` | `start_presentation()` |
| Change MQTT topics | `simclient.py` | Topic constants/handlers |
| Update screenshot | `display_manager.py` | `_capture_screenshot()` |
| File downloads | `simclient.py` | `resolve_file_url()` |
---
## Project Overview ## Project Overview
This is an **Infoscreen Client** system for Raspberry Pi that creates digital signage displays. The client communicates with a server via MQTT to display presentations, videos, and web content in kiosk mode. It's designed for educational/research environments where multiple displays need to be centrally managed. **Infoscreen Client** - Digital signage system for Raspberry Pi. Displays presentations, videos, and web content in kiosk mode. Server-managed via MQTT for educational/research environments with multiple displays.
### Server-Side PPTX Rendering (Update Nov 2025) **Architecture**: Two-process design
The server now performs all PPTX → PDF conversion. The client only ever downloads and displays PDF files with Impressive. Any references below to local LibreOffice conversion are legacy and can be ignored for new deployments. Keep LibreOffice only if you must support older workflows that still send raw PPTX files. - `simclient.py` - MQTT communication (container/native)
- `display_manager.py` - Display control (host OS with X11/Wayland access)
## Architecture & Technology Stack ## Architecture & Technology Stack
@@ -12,7 +42,6 @@ The server now performs all PPTX → PDF conversion. The client only ever downlo
- **Python 3.x** - Main application language - **Python 3.x** - Main application language
- **MQTT (paho-mqtt)** - Real-time messaging with server - **MQTT (paho-mqtt)** - Real-time messaging with server
- **Impressive** - PDF presenter with native auto-advance and loop support - **Impressive** - PDF presenter with native auto-advance and loop support
- **LibreOffice** - PPTX to PDF conversion (headless mode)
- **Environment Variables** - Configuration management via `.env` files - **Environment Variables** - Configuration management via `.env` files
- **JSON** - Data exchange format for events and configuration - **JSON** - Data exchange format for events and configuration
- **Base64** - Screenshot transmission encoding - **Base64** - Screenshot transmission encoding
@@ -24,7 +53,7 @@ The server now performs all PPTX → PDF conversion. The client only ever downlo
- **Discovery System** - Automatic client registration with server - **Discovery System** - Automatic client registration with server
- **Heartbeat Monitoring** - Regular status updates and keepalive - **Heartbeat Monitoring** - Regular status updates and keepalive
- **Event Processing** - Handles presentation/content switching commands - **Event Processing** - Handles presentation/content switching commands
- **Screenshot Service** - Dashboard monitoring via image capture - **Screenshot Service** - Dashboard monitoring via image capture (captured by display_manager.py, transmitted by simclient.py)
- **File Management** - Downloads and manages presentation files - **File Management** - Downloads and manages presentation files
- **Group Management** - Supports organizing clients into groups - **Group Management** - Supports organizing clients into groups
@@ -38,7 +67,7 @@ The server now performs all PPTX → PDF conversion. The client only ever downlo
- `on_connect` re-subscribes to all topics (`discovery_ack`, `config`, `group_id`, current group events) and re-sends discovery on reconnect to re-register with the server. - `on_connect` re-subscribes to all topics (`discovery_ack`, `config`, `group_id`, current group events) and re-sends discovery on reconnect to re-register with the server.
- Heartbeats are gated by `client.is_connected()` and retry once on `NO_CONN` (rc=4). Occasional rc=4 warnings are normal right after broker restarts or brief network stalls and typically followed by a successful heartbeat. - Heartbeats are gated by `client.is_connected()` and retry once on `NO_CONN` (rc=4). Occasional rc=4 warnings are normal right after broker restarts or brief network stalls and typically followed by a successful heartbeat.
- Do not treat single rc=4 heartbeat warnings as failures. Investigate only if multiple consecutive heartbeats fail without recovery. - Do not treat single rc=4 heartbeat warnings as failures. Investigate only if multiple consecutive heartbeats fail without recovery.
- **Dashboard**: Screenshot transmission via `infoscreen/{client_id}/dashboard` - **Dashboard**: Screenshot transmission via `infoscreen/{client_id}/dashboard` (includes base64-encoded screenshot, timestamp, client status, system info)
- **Group Assignment**: Server sends group via `infoscreen/{client_id}/group_id` - **Group Assignment**: Server sends group via `infoscreen/{client_id}/group_id`
- **Events**: Content commands via `infoscreen/events/{group_id}` - **Events**: Content commands via `infoscreen/events/{group_id}`
@@ -64,8 +93,9 @@ The server now performs all PPTX → PDF conversion. The client only ever downlo
``` ```
### Presentation System (Impressive-Based) ### Presentation System (Impressive-Based)
- **PDF files** are displayed natively with Impressive PDF presenter (no conversion needed) - **Server-side conversion**: PPTX files are converted to PDF by the server using Gotenberg
- **PPTX files** are automatically converted to PDF using LibreOffice headless - **Client receives PDFs**: All presentations arrive as pre-rendered PDF files
- **Direct display**: PDF files are displayed natively with Impressive (no client-side conversion)
- **Auto-advance**: Native Impressive `--auto` parameter (no xdotool needed) - **Auto-advance**: Native Impressive `--auto` parameter (no xdotool needed)
- **Loop mode**: Impressive `--wrap` parameter for infinite looping - **Loop mode**: Impressive `--wrap` parameter for infinite looping
- **Auto-quit**: Impressive `--autoquit` parameter to exit after last slide - **Auto-quit**: Impressive `--autoquit` parameter to exit after last slide
@@ -133,7 +163,14 @@ MQTT_BROKER_FALLBACKS=host1,host2 # Fallback brokers
# Timing (seconds) # Timing (seconds)
HEARTBEAT_INTERVAL=10 # Status update frequency HEARTBEAT_INTERVAL=10 # Status update frequency
SCREENSHOT_INTERVAL=30 # Dashboard screenshot frequency SCREENSHOT_INTERVAL=30 # Dashboard screenshot transmission frequency (simclient.py)
SCREENSHOT_CAPTURE_INTERVAL=30 # Screenshot capture frequency (display_manager.py)
# Screenshot Configuration
SCREENSHOT_MAX_WIDTH=800 # Downscale width (preserves aspect ratio)
SCREENSHOT_JPEG_QUALITY=70 # JPEG compression quality (1-95)
SCREENSHOT_MAX_FILES=20 # Number of screenshots to keep (rotation)
SCREENSHOT_ALWAYS=0 # Force capture even when no display active (testing)
# File/API Server (used to download presentation files) # File/API Server (used to download presentation files)
# Defaults to the same host as MQTT_BROKER, port 8000, scheme http. # Defaults to the same host as MQTT_BROKER, port 8000, scheme http.
@@ -200,24 +237,25 @@ FILE_SERVER_SCHEME=http # http or https
- Systemd service integration for auto-start - Systemd service integration for auto-start
- Resource limits and health checks configured - Resource limits and health checks configured
- Persistent volume mounts for data - Persistent volume mounts for data
### System Dependencies
- Python 3.x runtime + virtual environment
- MQTT broker connectivity
- Display server: X11 or Wayland (for screenshots)
- **Impressive** - PDF presenter (primary tool, requires pygame + pillow in venv)
- **Chromium/Chrome** - Web kiosk mode
- **VLC** - Video playback (python-vlc preferred, vlc binary fallback)
- **Screenshot tools**:
- X11: `scrot` or `import` (ImageMagick) or `xwd`+`convert`
- Wayland: `grim` or `gnome-screenshot` or `spectacle`
## Code Style & Conventions **Note:** LibreOffice is NOT required on the client. PPTX→PDF conversion is handled server-side by Gotenberg.
### Python Code Standards ### Video Playback (python-vlc)
- Environment variable parsing with fallback defaults - **Preferred**: python-vlc (programmatic control: autoplay, loop, volume)
- Comprehensive docstrings for complex functions - **Fallback**: External vlc binary
- Logging at appropriate levels (DEBUG/INFO/WARNING/ERROR) - **Fields**: `url`, `autoplay` (bool), `loop` (bool), `volume` (0.0-1.0 → 0-100)
- Type hints where beneficial for clarity - **URL rewriting**: `server` host → configured file server
- Exception handling with specific error types - **HW decode errors**: `h264_v4l2m2m` failures are normal if V4L2 M2M unavailable; use software decode
### Configuration Management
- Environment-first configuration (12-factor app principles)
- Support for inline comments in environment values
- Graceful handling of missing/invalid configuration
- Multiple environment file locations for flexibility
### MQTT Message Handling
- JSON message format validation
- Robust payload parsing with fallbacks - Robust payload parsing with fallbacks
- Topic-specific message handlers - Topic-specific message handlers
- Retained message support where appropriate - Retained message support where appropriate
@@ -233,13 +271,14 @@ FILE_SERVER_SCHEME=http # http or https
### System Dependencies ### System Dependencies
- Python 3.x runtime - Python 3.x runtime
- Network connectivity for MQTT - Network connectivity for MQTT
- Display server (X11) for screenshot capture - Display server (X11 or Wayland) for screenshot capture
- **Impressive** - PDF presenter with auto-advance (primary presentation tool) - **Impressive** - PDF presenter with auto-advance (primary presentation tool)
- **pygame** - Required for Impressive (installed in venv) - **pygame** - Required for Impressive (installed in venv)
- **Pillow/PIL** - Required for Impressive PDF rendering (installed in venv) - **Pillow/PIL** - Required for Impressive PDF rendering (installed in venv)
- **LibreOffice** - PPTX to PDF conversion (headless mode)
- Chromium/Chrome - Web content display (kiosk mode) - Chromium/Chrome - Web content display (kiosk mode)
- VLC or MPV - Video playbook - VLC or MPV - Video playback
**Note:** LibreOffice is NOT needed on the client. The server converts PPTX to PDF using Gotenberg.
### Video playback details (python-vlc) ### Video playback details (python-vlc)
@@ -266,10 +305,10 @@ FILE_SERVER_SCHEME=http # http or https
## Presentation System Architecture ## Presentation System Architecture
### How It Works ### How It Works
1. **PDF File Received** → Event contains PDF file reference → Use directly (no conversion) 1. **Server-side Conversion** → Server converts PPTX to PDF using Gotenberg
2. **PPTX File Received**Event contains PPTX file reference 2. **Event Received**Client receives event with pre-rendered PDF file reference
3. **Convert to PDF**LibreOffice headless: `libreoffice --headless --convert-to pdf` 3. **Download PDF**Client downloads PDF from file server
4. **Cache PDF**Converted PDF stored in `presentation/` directory 4. **Cache PDF**Downloaded PDF stored in `presentation/` directory
5. **Display with Impressive** → Launch with venv environment and parameters: 5. **Display with Impressive** → Launch with venv environment and parameters:
- `--fullscreen` - Full screen mode - `--fullscreen` - Full screen mode
- `--nooverview` - No slide overview - `--nooverview` - No slide overview
@@ -295,8 +334,8 @@ FILE_SERVER_SCHEME=http # http or https
- **File**: `src/display_manager.py` - **File**: `src/display_manager.py`
- **Method**: `start_presentation()` - **Method**: `start_presentation()`
- **Key Logic**: - **Key Logic**:
1. Check if PDF file (use directly) or PPTX (needs conversion) 1. Receive event with PDF file reference (server already converted PPTX)
2. Convert PPTX to PDF if needed (cached for reuse) 2. Download PDF file if not cached
3. Set up virtual environment for Impressive (pygame + pillow) 3. Set up virtual environment for Impressive (pygame + pillow)
4. Build Impressive command with appropriate parameters 4. Build Impressive command with appropriate parameters
5. Launch process and monitor 5. Launch process and monitor
@@ -362,8 +401,8 @@ When working on this codebase:
- **ALWAYS use Impressive** for PDF presentations (primary solution) - **ALWAYS use Impressive** for PDF presentations (primary solution)
- **DO NOT suggest xdotool approaches** - they failed on Raspberry Pi due to window focus issues - **DO NOT suggest xdotool approaches** - they failed on Raspberry Pi due to window focus issues
- **DO NOT suggest video conversion** - adds complexity, had black screen issues - **DO NOT suggest video conversion** - adds complexity, had black screen issues
- **PDF files are supported natively** - no conversion needed - **All presentations are PDFs** - server converts PPTX to PDF using Gotenberg
- **PPTX conversion is automatic** - LibreOffice headless handles PPTX→PDF - **No client-side conversion** - client only displays pre-rendered PDFs
- **Virtual environment is required** - pygame + pillow must be available for Impressive - **Virtual environment is required** - pygame + pillow must be available for Impressive
- **Loop mode uses `--wrap`** - not custom scripts or workarounds - **Loop mode uses `--wrap`** - not custom scripts or workarounds
- **Auto-quit uses `--autoquit`** - native Impressive parameter - **Auto-quit uses `--autoquit`** - native Impressive parameter
@@ -391,20 +430,103 @@ CEC testing notes:
This system is designed for reliability and ease of maintenance in educational environments with multiple deployed clients. The Impressive-based presentation solution provides native auto-advance and loop support without complex window management hacks. This system is designed for reliability and ease of maintenance in educational environments with multiple deployed clients. The Impressive-based presentation solution provides native auto-advance and loop support without complex window management hacks.
## Recent changes (Oct 2025) ## Screenshot System (Nov 2025)
Summary of additions since the last Copilot instructions update: The screenshot capture and transmission system has been implemented with separation of concerns:
- Added `event_type` handling for scheduler events: `presentation`, `webuntis`, `webpage`, `website`. The display manager now uses `event_type` when available to pick the correct display path. ### Architecture
- Implemented auto-scroll for long websites. Two strategies are available: - **Capture**: `display_manager.py` captures screenshots in a background thread and writes to shared `screenshots/` directory
- CDP injection via DevTools websocket using `websocket-client` and `requests`. The injector attempts to evaluate an auto-scroll script inside the page context. Default duration: 60s. - **Transmission**: `simclient.py` reads latest screenshot from shared directory and transmits via MQTT dashboard topic
- Local Chrome extension fallback (`src/chrome_autoscroll`) loaded with `--load-extension` when CDP handshake is blocked (403), ensuring reliable autoscroll operation. - **Sharing**: Volume-based sharing between display_manager (host OS) and simclient (container)
- New helper scripts for debugging DevTools handshake: `scripts/test_cdp.py` and `scripts/test_cdp_origins.py`.
- Updated `requirements.txt` to include `websocket-client`.
Runtime behavior adjustments: ### Capture Strategy (display_manager.py)
- HDMI-CEC is automatically disabled in development mode; both the Display Manager and the CEC integration test (option 5) honor this to protect developer setups from constant TV power cycling. - **Session Detection**: Automatically detects Wayland vs X11 session
- **Wayland Tools**: Tries `grim`, `gnome-screenshot`, `spectacle` (in order)
- **X11 Tools**: Tries `scrot`, `import` (ImageMagick), `xwd`+`convert` (in order)
- **Processing**: Downscales to max width (default 800px), JPEG compresses (default quality 70)
- **Output**: Creates timestamped files (`screenshot_YYYYMMDD_HHMMSS.jpg`) plus `latest.jpg` symlink
- **Rotation**: Keeps max N files (default 20), deletes older
- **Timing**: Only captures when display process is active (unless `SCREENSHOT_ALWAYS=1`)
Notes for Copilot: ### Transmission Strategy (simclient.py)
- When adding or modifying display logic, prefer Impressive for PDF presentations and avoid xdotool approaches for slideshow control. - **Source**: Prefers `screenshots/latest.jpg` if present, falls back to newest timestamped file
- For web events, ensure autoscroll is only activated for `event_type: "website"` and keep the CDP injection optional/fallback-only when feasible. - **Topic**: `infoscreen/{client_id}/dashboard`
- **Format**: JSON with base64-encoded image data
- **Payload Structure**:
```json
{
"timestamp": "ISO datetime",
"client_id": "UUID",
"status": "alive",
"screenshot": {
"filename": "latest.jpg",
"data": "base64...",
"timestamp": "ISO datetime",
"size": 12345
},
"system_info": {
"hostname": "...",
"ip": "...",
"uptime": 123456.78
}
}
```
- **Logging**: Logs publish success/failure with file size for monitoring
### Scalability Considerations
- **Client-side resize/compress**: Reduces bandwidth and broker load (recommended for 50+ clients)
- **Recommended production settings**: `SCREENSHOT_CAPTURE_INTERVAL=60`, `SCREENSHOT_MAX_WIDTH=800`, `SCREENSHOT_JPEG_QUALITY=60-70`
- **Future optimization**: Hash-based deduplication to skip identical screenshots
- **Alternative for large scale**: HTTP storage + MQTT metadata (200+ clients)
### Testing
- Install capture tools: `sudo apt install scrot imagemagick` (X11) or `sudo apt install grim gnome-screenshot` (Wayland)
- Force capture for testing: `export SCREENSHOT_ALWAYS=1`
- Check logs: `tail -f logs/display_manager.log logs/simclient.log`
- Verify files: `ls -lh src/screenshots/`
### Troubleshooting
- **No screenshots**: Check session type in logs, install appropriate tools
- **Large payloads**: Reduce `SCREENSHOT_MAX_WIDTH` or `SCREENSHOT_JPEG_QUALITY`
- **Stale screenshots**: Check `latest.jpg` symlink, verify display_manager is running
- **MQTT errors**: Check dashboard topic logs for publish return codes
### Testing & Troubleshooting
**Setup:**
- X11: `sudo apt install scrot imagemagick`
- Wayland: `sudo apt install grim gnome-screenshot`
- Force capture: `export SCREENSHOT_ALWAYS=1`
**Verify:**
```bash
tail -f logs/display_manager.log | grep screenshot # Check capture
tail -f logs/simclient.log | grep dashboard # Check transmission
ls -lh src/screenshots/ # Check files
```
**Common Issues:**
| Issue | Check | Fix |
|-------|-------|-----|
| No screenshots | Session type in logs | Install tools for X11/Wayland |
| Large payloads | File sizes | Reduce `SCREENSHOT_MAX_WIDTH` or `SCREENSHOT_JPEG_QUALITY` |
| Stale data | `latest.jpg` timestamp | Restart display_manager |
| MQTT errors | Publish return codes | Check broker connectivity |
---
## Development Notes
### Event Types (Scheduler Integration)
- **Supported**: `presentation`, `webuntis`, `webpage`, `website`
- **Auto-scroll**: Only for `event_type: "website"` (CDP injection + Chrome extension fallback)
- **Display manager**: Uses `event_type` to select renderer when available
### HDMI-CEC Behavior
- **Development mode**: CEC auto-disabled (prevents TV cycling during testing)
- **Test script**: `test-hdmi-cec.sh` option 5 skips integration test in dev mode
- **Manual testing**: Options 1-4 work regardless of mode
### Code Modification Guidelines
- **Presentations**: Always use Impressive (`--auto`, `--wrap`, `--autoquit`)
- **Web autoscroll**: Only for `event_type: "website"`, keep CDP optional
- **Screenshot changes**: Remember two-process architecture (capture ≠ transmission)
- **URL resolution**: Handle `server` host placeholder in file/video URLs

267
README.md
View File

@@ -1,6 +1,6 @@
# Infoscreen Client - Display Manager # Infoscreen Client - Display Manager
Digital signage system for Raspberry Pi that displays presentations, videos, and web content in kiosk mode. Centrally managed via MQTT with automatic client discovery and heartbeat monitoring. Digital signage system for Raspberry Pi that displays presentations, videos, and web content in kiosk mode. Centrally managed via MQTT with automatic client discovery, heartbeat monitoring, and screenshot-based dashboard monitoring.
## 🎯 Key Features ## 🎯 Key Features
@@ -11,6 +11,7 @@ Digital signage system for Raspberry Pi that displays presentations, videos, and
- **MQTT Integration** - Real-time event management from central server - **MQTT Integration** - Real-time event management from central server
- **Group Management** - Organize clients into groups for targeted content - **Group Management** - Organize clients into groups for targeted content
- **Heartbeat Monitoring** - Regular status updates and screenshot dashboard - **Heartbeat Monitoring** - Regular status updates and screenshot dashboard
- **Screenshot Dashboard** - Automatic screen capture with Wayland/X11 support, client-side compression
- **Multi-Content Support** - Presentations, videos, and web pages - **Multi-Content Support** - Presentations, videos, and web pages
- **Kiosk Mode** - Full-screen display with automatic startup - **Kiosk Mode** - Full-screen display with automatic startup
@@ -26,9 +27,9 @@ Digital signage system for Raspberry Pi that displays presentations, videos, and
- Raspberry Pi OS (Bookworm or newer) - Raspberry Pi OS (Bookworm or newer)
- Python 3.x - Python 3.x
- Impressive (PDF presenter with auto-advance) - Impressive (PDF presenter with auto-advance)
- (Optional legacy) LibreOffice ONLY if older workflow still sends raw PPTX; otherwise not required
- Chromium browser (for web content) - Chromium browser (for web content)
- VLC or MPV (for video playback) - VLC or MPV (for video playback)
- Screenshot tools: `scrot` or ImageMagick (X11) OR `grim` or `gnome-screenshot` (Wayland)
- CEC Utils (for HDMI-CEC TV control - optional) - CEC Utils (for HDMI-CEC TV control - optional)
## 🚀 Quick Start ## 🚀 Quick Start
@@ -40,14 +41,17 @@ Digital signage system for Raspberry Pi that displays presentations, videos, and
cd ~/ cd ~/
git clone <repository-url> infoscreen-dev git clone <repository-url> infoscreen-dev
cd infoscreen-dev cd infoscreen-dev
# Install system dependencies # Install system dependencies
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y \
python3 python3-pip python3-venv \ python3 python3-pip python3-venv \
libreoffice impressive \ impressive \
chromium-browser vlc \ chromium-browser vlc \
cec-utils cec-utils \
scrot imagemagick
# For Wayland systems, install screenshot tools:
# sudo apt-get install grim gnome-screenshot
# Create Python virtual environment # Create Python virtual environment
python3 -m venv venv python3 -m venv venv
@@ -67,10 +71,17 @@ ENV=production
LOG_LEVEL=INFO LOG_LEVEL=INFO
# MQTT Configuration # MQTT Configuration
MQTT_BROKER=192.168.1.100 # Timing (seconds)
MQTT_PORT=1883 HEARTBEAT_INTERVAL=30
MQTT_BROKER_FALLBACKS=192.168.1.101,192.168.1.102 SCREENSHOT_INTERVAL=60 # How often simclient transmits screenshots
SCREENSHOT_CAPTURE_INTERVAL=60 # How often display_manager captures screenshots
DISPLAY_CHECK_INTERVAL=5
# Screenshot Configuration
SCREENSHOT_MAX_WIDTH=800 # Downscale to this width (preserves aspect)
SCREENSHOT_JPEG_QUALITY=70 # JPEG quality 1-95 (lower = smaller files)
SCREENSHOT_MAX_FILES=20 # Keep this many screenshots (rotation)
SCREENSHOT_ALWAYS=0 # Set to 1 to force capture even when no event active
# Timing (seconds) # Timing (seconds)
HEARTBEAT_INTERVAL=30 HEARTBEAT_INTERVAL=30
SCREENSHOT_INTERVAL=60 SCREENSHOT_INTERVAL=60
@@ -116,11 +127,12 @@ Or use the startup script:
The system uses **Impressive** as the PDF presenter with native auto-advance and loop support: The system uses **Impressive** as the PDF presenter with native auto-advance and loop support:
1. **PPTX files** are converted to PDF server-side (client receives ready-made PDFs) 1. **Server-side rendering**: PPTX files are converted to PDF by the server using Gotenberg
2. **PDF files** are displayed directly with Impressive 2. **Client receives PDFs**: Events contain pre-rendered PDF files ready for display
3. **Auto-advance** uses Impressive's built-in `--auto` parameter 3. **Direct display**: PDF files are displayed directly with Impressive (no client-side conversion needed)
4. **Loop mode** uses Impressive's `--wrap` parameter (infinite loop) 4. **Auto-advance** uses Impressive's built-in `--auto` parameter
5. **Auto-quit** uses Impressive's `--autoquit` parameter (exit after last slide) 5. **Loop mode** uses Impressive's `--wrap` parameter (infinite loop)
6. **Auto-quit** uses Impressive's `--autoquit` parameter (exit after last slide)
### Event JSON Format ### Event JSON Format
@@ -277,13 +289,22 @@ Interactive menu for testing:
**Loop mode (infinite):** **Loop mode (infinite):**
```bash ```bash
./scripts/test-impressive-loop.sh ### Test Screenshot Capture
```
### Test MQTT Communication
```bash ```bash
./scripts/test-mqtt.sh ./scripts/test-screenshot.sh
```
Captures test screenshot for dashboard monitoring.
**Manual test:**
```bash
export SCREENSHOT_ALWAYS=1
export SCREENSHOT_CAPTURE_INTERVAL=5
python3 src/display_manager.py &
sleep 15
ls -lh src/screenshots/
```
``` ```
Verifies MQTT broker connectivity and topics. Verifies MQTT broker connectivity and topics.
@@ -362,29 +383,18 @@ which impressive
# If not found: sudo apt-get install impressive # If not found: sudo apt-get install impressive
``` ```
**Legacy LibreOffice (only if still receiving raw PPTX):**
```bash
which libreoffice
# Optional install (legacy only): sudo apt-get install libreoffice
```
**Check logs:** **Check logs:**
```bash ```bash
tail -f logs/display_manager.log tail -f logs/display_manager.log
``` ```
### Legacy PPTX Conversion (Deprecated)
Server performs all PPTX→PDF rendering. Use these commands only if supporting an older workflow still sending PPTX:
```bash
libreoffice --headless --convert-to pdf --outdir /tmp presentation.pptx
ls -l /tmp/*.pdf
```
**Check disk space:** **Check disk space:**
```bash ```bash
df -h df -h
``` ```
**Note:** PPTX conversion happens server-side via Gotenberg. The client only receives and displays pre-rendered PDF files.
### Slides don't auto-advance ### Slides don't auto-advance
**Verify event JSON:** **Verify event JSON:**
@@ -460,18 +470,41 @@ This is the fastest workaround if hardware decode is not required or not availab
./scripts/test-mqtt.sh ./scripts/test-mqtt.sh
``` ```
**Check broker status:** ### Screenshots not uploading
**Check which session type you're running:**
```bash ```bash
# On server echo $WAYLAND_DISPLAY # Set if Wayland
sudo systemctl status mosquitto echo $DISPLAY # Set if X11
``` ```
**Try fallback brokers:** **Install appropriate screenshot tool:**
Edit `.env` and add `MQTT_BROKER_FALLBACKS` ```bash
# For X11:
sudo apt-get install scrot imagemagick
#### Client auto-reconnect and heartbeat behavior (Nov 2025) # For Wayland:
- The MQTT client now uses Paho v2 callbacks and `loop_start()` for automatic reconnection with exponential backoff. sudo apt-get install grim gnome-screenshot
- All topic subscriptions are restored in `on_connect` and a discovery message is re-sent on reconnect to re-register the client. ```
**Test screenshot capture:**
```bash
export SCREENSHOT_ALWAYS=1 # Force capture even without active event
./scripts/test-screenshot.sh
ls -lh src/screenshots/
```
**Check logs for session detection:**
```bash
tail -f logs/display_manager.log | grep -i screenshot
# Should show: "Screenshot session=wayland" or "Screenshot session=x11"
```
**Verify simclient is reading screenshots:**
```bash
tail -f logs/simclient.log | grep -i screenshot
# Should show: "Dashboard heartbeat sent with screenshot: latest.jpg"
```ll topic subscriptions are restored in `on_connect` and a discovery message is re-sent on reconnect to re-register the client.
- Heartbeats are sent only when connected; if publish occurs during a brief reconnect window, Paho may return rc=4 (NO_CONN). The client performs a short retry and logs the outcome. - Heartbeats are sent only when connected; if publish occurs during a brief reconnect window, Paho may return rc=4 (NO_CONN). The client performs a short retry and logs the outcome.
- Occasional `Heartbeat publish failed with code: 4` after broker restart or transient network hiccups is expected and not dangerous. It indicates "not connected at this instant"; the next heartbeat typically succeeds. - Occasional `Heartbeat publish failed with code: 4` after broker restart or transient network hiccups is expected and not dangerous. It indicates "not connected at this instant"; the next heartbeat typically succeeds.
- When to investigate: repeated rc=4 with no succeeding "Heartbeat sent" entries over multiple intervals. - When to investigate: repeated rc=4 with no succeeding "Heartbeat sent" entries over multiple intervals.
@@ -622,13 +655,159 @@ CEC_POWER_OFF_WAIT=2
### Testing ### Testing
```bash ```bash
# Interactive test menu ## 📸 Screenshot System
./scripts/test-hdmi-cec.sh
# Note: In development mode, option 5 (integration test) is skipped on purpose. The system includes automatic screenshot capture for dashboard monitoring with support for both X11 and Wayland display servers.
# Use options 14 for manual commands, or set ENV=production to run the integration test.
# Manual commands ### Architecture
**Two-process design:**
1. **display_manager.py** - Captures screenshots on host OS (has access to display)
2. **simclient.py** - Transmits screenshots via MQTT (runs in container)
3. **Shared directory** - `src/screenshots/` volume-mounted between processes
### Screenshot Capture (display_manager.py)
## Recent changes (Nov 2025)
The following notable changes were added after the previous release and are included in this branch:
### Screenshot System Implementation
- **Screenshot capture** added to `display_manager.py` with background thread
- **Session detection**: Automatic Wayland vs X11 detection with appropriate tool selection
- **Wayland support**: `grim`, `gnome-screenshot`, `spectacle` (in order)
- **X11 support**: `scrot`, `import` (ImageMagick), `xwd`+`convert` (in order)
- **Image processing**: Client-side downscaling and JPEG compression for bandwidth optimization
- **File management**: Timestamped screenshots plus `latest.jpg` symlink, automatic rotation
- **Transmission**: Enhanced `simclient.py` to prefer `latest.jpg`, added detailed logging
- **Dashboard topic**: Structured JSON payload with screenshot, system info, and client status
- **Configuration**: New environment variables for capture interval, quality, max width, file rotation
- **Testing mode**: `SCREENSHOT_ALWAYS=1` forces capture even without active display
### Previous Changes (Oct 2025)
- **Wayland**: `grim``gnome-screenshot``spectacle`
- **X11**: `scrot``import` (ImageMagick)`xwd`+`convert`
**Processing pipeline:**
1. Capture full-resolution screenshot to PNG
2. Downscale to configured max width (default 800px, preserves aspect ratio)
3. Convert to JPEG with configured quality (default 70)
4. Save timestamped file: `screenshot_YYYYMMDD_HHMMSS.jpg`
5. Create/update `latest.jpg` symlink for easy access
6. Rotate old screenshots (keeps max 20 by default)
**Capture timing:**
- Only captures when a display process is active (presentation/video/web)
- Can be forced with `SCREENSHOT_ALWAYS=1` for testing
- Default interval: 60 seconds
### Screenshot Transmission (simclient.py)
**Source selection:**
- Prefers `latest.jpg` symlink (fastest, most recent)
- Falls back to newest timestamped file if symlink missing
**MQTT topic:**
```
infoscreen/{client_id}/dashboard
```
**Payload structure:**
```json
{
"timestamp": "2025-11-30T14:23:45.123456",
"client_id": "abc123-def456-789",
"status": "alive",
"screenshot": {
"filename": "latest.jpg",
"data": "<base64-encoded-image>",
"timestamp": "2025-11-30T14:23:40.000000",
"size": 45678
},
"system_info": {
"hostname": "infoscreen-pi-01",
"ip": "192.168.1.50",
"uptime": 1732977825.123456
}
}
```
### Configuration
```bash
# Capture settings (display_manager.py)
SCREENSHOT_CAPTURE_INTERVAL=60 # Seconds between captures
SCREENSHOT_MAX_WIDTH=800 # Downscale width (0 = no downscaling)
SCREENSHOT_JPEG_QUALITY=70 # JPEG quality 1-95
SCREENSHOT_MAX_FILES=20 # Number to keep before rotation
SCREENSHOT_ALWAYS=0 # Force capture even without active event
# Transmission settings (simclient.py)
SCREENSHOT_INTERVAL=60 # Seconds between MQTT publishes
```
### Scalability Recommendations
**Small deployments (<10 clients):**
- Default settings work well
- `SCREENSHOT_CAPTURE_INTERVAL=30`, `SCREENSHOT_MAX_WIDTH=800`, `SCREENSHOT_JPEG_QUALITY=70`
**Medium deployments (10-50 clients):**
- Reduce capture frequency: `SCREENSHOT_CAPTURE_INTERVAL=60`
- Lower quality: `SCREENSHOT_JPEG_QUALITY=60-65`
- Ensure broker has adequate bandwidth
**Large deployments (50+ clients):**
- Further reduce frequency: `SCREENSHOT_CAPTURE_INTERVAL=120`
- Aggressive compression: `SCREENSHOT_JPEG_QUALITY=50-60`, `SCREENSHOT_MAX_WIDTH=640`
- Consider implementing hash-based deduplication (skip if unchanged)
- Monitor MQTT broker load and consider retained message limits
**Very large deployments (200+ clients):**
- Consider HTTP storage + MQTT metadata pattern instead of base64-over-MQTT
- Implement screenshot upload to file server, publish only URL via MQTT
### Troubleshooting
**No screenshots being captured:**
```bash
# Check session type
echo "Wayland: $WAYLAND_DISPLAY" # Set if Wayland
echo "X11: $DISPLAY" # Set if X11
# Check logs for tool detection
tail -f logs/display_manager.log | grep screenshot
# Install appropriate tools
sudo apt install scrot imagemagick # X11
sudo apt install grim # Wayland
```
**Screenshots too large:**
```bash
# Reduce quality and size
SCREENSHOT_MAX_WIDTH=640
SCREENSHOT_JPEG_QUALITY=50
```
**Not transmitting over MQTT:**
```bash
# Check simclient logs
tail -f logs/simclient.log | grep -i dashboard
# Should see:
# "Dashboard heartbeat sent with screenshot: latest.jpg (45678 bytes)"
# If NO_CONN errors, check MQTT broker connectivity
```
---
**Last Updated:** November 2025
**Status:** ✅ Production Ready
**Tested On:** Raspberry Pi 5, Raspberry Pi OS (Bookworm)
## Recent changes (Nov 2025)
echo "on 0" | cec-client -s -d 1 # Turn on echo "on 0" | cec-client -s -d 1 # Turn on
echo "standby 0" | cec-client -s -d 1 # Turn off echo "standby 0" | cec-client -s -d 1 # Turn off
echo "pow 0" | cec-client -s -d 1 # Check status echo "pow 0" | cec-client -s -d 1 # Check status

View File

@@ -41,6 +41,12 @@ for env_path in env_paths:
# Configuration # Configuration
ENV = os.getenv("ENV", "development") ENV = os.getenv("ENV", "development")
LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG" if ENV == "development" else "INFO") LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG" if ENV == "development" else "INFO")
# Screenshot capture configuration (distinct from transmission interval used by simclient)
SCREENSHOT_CAPTURE_INTERVAL = int(os.getenv("SCREENSHOT_CAPTURE_INTERVAL", os.getenv("SCREENSHOT_INTERVAL", "30"))) # seconds
SCREENSHOT_MAX_WIDTH = int(os.getenv("SCREENSHOT_MAX_WIDTH", "800")) # Width to downscale (preserve aspect)
SCREENSHOT_JPEG_QUALITY = int(os.getenv("SCREENSHOT_JPEG_QUALITY", "70")) # JPEG quality 1-95
SCREENSHOT_MAX_FILES = int(os.getenv("SCREENSHOT_MAX_FILES", "20")) # Rotate old screenshots
SCREENSHOT_ALWAYS = os.getenv("SCREENSHOT_ALWAYS", "0").lower() in ("1","true","yes")
CHECK_INTERVAL = int(os.getenv("DISPLAY_CHECK_INTERVAL", "5")) # seconds CHECK_INTERVAL = int(os.getenv("DISPLAY_CHECK_INTERVAL", "5")) # seconds
PRESENTATION_DIR = os.path.join(os.path.dirname(__file__), "presentation") PRESENTATION_DIR = os.path.join(os.path.dirname(__file__), "presentation")
EVENT_FILE = os.path.join(os.path.dirname(__file__), "current_event.json") EVENT_FILE = os.path.join(os.path.dirname(__file__), "current_event.json")
@@ -479,6 +485,14 @@ class DisplayManager:
# Setup signal handlers for graceful shutdown # Setup signal handlers for graceful shutdown
signal.signal(signal.SIGTERM, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGINT, self._signal_handler)
# Screenshot directory (shared with simclient container via volume)
self.screenshot_dir = os.path.join(os.path.dirname(__file__), 'screenshots')
os.makedirs(self.screenshot_dir, exist_ok=True)
# Start background screenshot thread
self._screenshot_thread = threading.Thread(target=self._screenshot_loop, daemon=True)
self._screenshot_thread.start()
def _signal_handler(self, signum, frame): def _signal_handler(self, signum, frame):
"""Handle shutdown signals""" """Handle shutdown signals"""
@@ -1395,6 +1409,7 @@ class DisplayManager:
logging.info("Display Manager starting...") logging.info("Display Manager starting...")
logging.info(f"Monitoring event file: {EVENT_FILE}") logging.info(f"Monitoring event file: {EVENT_FILE}")
logging.info(f"Check interval: {CHECK_INTERVAL}s") logging.info(f"Check interval: {CHECK_INTERVAL}s")
logging.info(f"Screenshot capture interval: {SCREENSHOT_CAPTURE_INTERVAL}s (max width {SCREENSHOT_MAX_WIDTH}px, quality {SCREENSHOT_JPEG_QUALITY})")
# Log timezone information for debugging # Log timezone information for debugging
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
@@ -1414,6 +1429,190 @@ class DisplayManager:
logging.info("Display Manager stopped") logging.info("Display Manager stopped")
# -------------------------------------------------------------
# Screenshot capture subsystem
# -------------------------------------------------------------
def _screenshot_loop(self):
"""Background loop that captures screenshots periodically while an event is active.
Runs in a daemon thread. Only captures when a display process is active and running.
"""
last_capture = 0
while self.running:
try:
if SCREENSHOT_CAPTURE_INTERVAL <= 0:
time.sleep(60)
continue
now = time.time()
if now - last_capture >= SCREENSHOT_CAPTURE_INTERVAL:
if SCREENSHOT_ALWAYS or (self.current_process and self.current_process.is_running()):
self._capture_screenshot()
last_capture = now
else:
# When no active process we can optionally capture blank screen once every 5 intervals
# but for now skip to avoid noise.
pass
time.sleep(1)
except Exception as e:
logging.debug(f"Screenshot loop error: {e}")
time.sleep(5)
def _capture_screenshot(self):
"""Capture a screenshot of the current display and store it in the shared screenshots directory.
Strategy:
1. Prefer 'scrot' if available (fast, simple)
2. Fallback to ImageMagick 'import -window root'
3. Fallback to Pillow-based X11 grab using xwd pipe if available
4. If none available, log warning once.
Downscale and JPEG-compress for bandwidth savings.
Maintains a 'latest.jpg' file and rotates older screenshots beyond SCREENSHOT_MAX_FILES.
"""
try:
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
raw_path = os.path.join(self.screenshot_dir, f'screenshot_{ts}.png')
final_path = os.path.join(self.screenshot_dir, f'screenshot_{ts}.jpg')
captured = False
def command_exists(cmd: str) -> bool:
try:
subprocess.run(['which', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return True
except Exception:
return False
# Detect session type
session = 'wayland' if os.environ.get('WAYLAND_DISPLAY') else 'x11'
display_env = os.environ.get('DISPLAY')
logging.debug(f"Screenshot session={session} DISPLAY={display_env} WAYLAND_DISPLAY={os.environ.get('WAYLAND_DISPLAY')}")
if session == 'wayland':
# 1W: grim (wayland/sway, wlroots) captures root
if command_exists('grim'):
cmd = ['grim', raw_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
if result.returncode == 0 and os.path.exists(raw_path):
captured = True
else:
logging.debug(f"grim failed rc={result.returncode}: {result.stderr.decode(errors='ignore')[:120]}")
# 2W: gnome-screenshot (GNOME Wayland)
if not captured and command_exists('gnome-screenshot'):
cmd = ['gnome-screenshot', '-f', raw_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20)
if result.returncode == 0 and os.path.exists(raw_path):
captured = True
else:
logging.debug(f"gnome-screenshot failed rc={result.returncode}: {result.stderr.decode(errors='ignore')[:120]}")
# 3W: spectacle (KDE) if available
if not captured and command_exists('spectacle'):
cmd = ['spectacle', '--noninteractive', '--fullscreen', '--output', raw_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=20)
if result.returncode == 0 and os.path.exists(raw_path):
captured = True
else:
logging.debug(f"spectacle failed rc={result.returncode}: {result.stderr.decode(errors='ignore')[:120]}")
else:
# X11 path
# 1X: scrot
if command_exists('scrot'):
cmd = ['scrot', raw_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
if result.returncode == 0 and os.path.exists(raw_path):
captured = True
else:
logging.debug(f"scrot failed rc={result.returncode}: {result.stderr.decode(errors='ignore')[:120]}")
# 2X: import (ImageMagick)
if not captured and command_exists('import'):
cmd = ['import', '-window', 'root', raw_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
if result.returncode == 0 and os.path.exists(raw_path):
captured = True
else:
logging.debug(f"import failed rc={result.returncode}: {result.stderr.decode(errors='ignore')[:120]}")
# 3X: Try xwd pipe -> convert via ImageMagick if available
if not captured and command_exists('xwd') and command_exists('convert'):
xwd_file = os.path.join(self.screenshot_dir, f'xwd_{ts}.xwd')
try:
r1 = subprocess.run(['xwd', '-root', '-silent', '-out', xwd_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
if r1.returncode == 0 and os.path.exists(xwd_file):
r2 = subprocess.run(['convert', xwd_file, raw_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30)
if r2.returncode == 0 and os.path.exists(raw_path):
captured = True
if os.path.exists(xwd_file):
try:
os.remove(xwd_file)
except Exception:
pass
except Exception as e:
logging.debug(f"xwd/convert pipeline failed: {e}")
if not captured:
# Warn only occasionally
logging.warning("No screenshot tool available for current session. For X11, install 'scrot' or ImageMagick. For Wayland, install 'grim' or 'gnome-screenshot'.")
return
# Open image and downscale/compress
try:
from PIL import Image
with Image.open(raw_path) as im:
width, height = im.size
if width > SCREENSHOT_MAX_WIDTH > 0:
new_height = int(height * (SCREENSHOT_MAX_WIDTH / width))
im = im.resize((SCREENSHOT_MAX_WIDTH, new_height), Image.LANCZOS)
im = im.convert('RGB') # Ensure JPEG compatible
im.save(final_path, 'JPEG', quality=SCREENSHOT_JPEG_QUALITY, optimize=True)
except Exception as e:
logging.debug(f"Resize/compress error: {e}; keeping raw PNG")
final_path = raw_path # Fallback
# Remove raw PNG if we produced JPEG
if final_path != raw_path and os.path.exists(raw_path):
try:
os.remove(raw_path)
except Exception:
pass
# Maintain latest.jpg symlink/copy
latest_link = os.path.join(self.screenshot_dir, 'latest.jpg')
try:
if os.path.islink(latest_link) or os.path.exists(latest_link):
try:
os.remove(latest_link)
except Exception:
pass
# Prefer symlink if possible
try:
os.symlink(os.path.basename(final_path), latest_link)
except Exception:
# Fallback copy
shutil.copyfile(final_path, latest_link)
except Exception as e:
logging.debug(f"Could not update latest.jpg: {e}")
# Rotate old screenshots
try:
files = sorted([f for f in os.listdir(self.screenshot_dir) if f.lower().startswith('screenshot_')], reverse=True)
if len(files) > SCREENSHOT_MAX_FILES:
for old in files[SCREENSHOT_MAX_FILES:]:
try:
os.remove(os.path.join(self.screenshot_dir, old))
except Exception:
pass
except Exception:
pass
size = None
try:
size = os.path.getsize(final_path)
except Exception:
pass
logging.info(f"Screenshot captured: {os.path.basename(final_path)} ({size or 'unknown'} bytes)")
except Exception as e:
logging.debug(f"Screenshot capture failure: {e}")
def main(): def main():
"""Entry point""" """Entry point"""

View File

@@ -474,6 +474,22 @@ def get_latest_screenshot():
screenshot_dir = os.path.join(os.path.dirname(__file__), "screenshots") screenshot_dir = os.path.join(os.path.dirname(__file__), "screenshots")
if not os.path.exists(screenshot_dir): if not os.path.exists(screenshot_dir):
return None return None
# Prefer 'latest.jpg' symlink/copy if present (written by display_manager)
preferred_path = os.path.join(screenshot_dir, "latest.jpg")
if os.path.exists(preferred_path):
try:
with open(preferred_path, "rb") as f:
screenshot_data = base64.b64encode(f.read()).decode('utf-8')
file_stats = os.stat(preferred_path)
logging.debug(f"Using preferred latest.jpg for screenshot ({file_stats.st_size} bytes)")
return {
"filename": os.path.basename(preferred_path),
"data": screenshot_data,
"timestamp": datetime.fromtimestamp(file_stats.st_mtime).isoformat(),
"size": file_stats.st_size
}
except Exception as e:
logging.debug(f"Could not read latest.jpg, falling back to newest file: {e}")
# Find the most recent screenshot file # Find the most recent screenshot file
screenshot_files = [f for f in os.listdir(screenshot_dir) screenshot_files = [f for f in os.listdir(screenshot_dir)
@@ -495,12 +511,14 @@ def get_latest_screenshot():
# Get file info # Get file info
file_stats = os.stat(screenshot_path) file_stats = os.stat(screenshot_path)
return { info = {
"filename": latest_file, "filename": latest_file,
"data": screenshot_data, "data": screenshot_data,
"timestamp": datetime.fromtimestamp(file_stats.st_mtime).isoformat(), "timestamp": datetime.fromtimestamp(file_stats.st_mtime).isoformat(),
"size": file_stats.st_size "size": file_stats.st_size
} }
logging.debug(f"Selected latest screenshot: {latest_file} ({file_stats.st_size} bytes)")
return info
except Exception as e: except Exception as e:
logging.error(f"Error reading screenshot: {e}") logging.error(f"Error reading screenshot: {e}")
@@ -526,12 +544,17 @@ def send_screenshot_heartbeat(client, client_id):
# Send to dashboard monitoring topic # Send to dashboard monitoring topic
dashboard_topic = f"infoscreen/{client_id}/dashboard" dashboard_topic = f"infoscreen/{client_id}/dashboard"
client.publish(dashboard_topic, json.dumps(heartbeat_data)) payload = json.dumps(heartbeat_data)
res = client.publish(dashboard_topic, payload, qos=0)
if screenshot_info: if res.rc == mqtt.MQTT_ERR_SUCCESS:
logging.info(f"Screenshot heartbeat sent: {screenshot_info['filename']} ({screenshot_info['size']} bytes)") if screenshot_info:
logging.info(f"Dashboard heartbeat sent with screenshot: {screenshot_info['filename']} ({screenshot_info['size']} bytes)")
else:
logging.info("Dashboard heartbeat sent (no screenshot available)")
elif res.rc == mqtt.MQTT_ERR_NO_CONN:
logging.warning("Dashboard heartbeat publish returned NO_CONN; will retry on next interval")
else: else:
logging.debug("Heartbeat sent without screenshot") logging.warning(f"Dashboard heartbeat publish failed with code: {res.rc}")
except Exception as e: except Exception as e:
logging.error(f"Error sending screenshot heartbeat: {e}") logging.error(f"Error sending screenshot heartbeat: {e}")