Compare commits
5 Commits
706af95557
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e9ea1aaa | |||
|
|
4c3ddbef73 | ||
|
|
87e059e0f4 | ||
|
|
46d9fcf6e3 | ||
|
|
f1a84d05d5 |
1
.player_stop_requested
Normal file
1
.player_stop_requested
Normal file
@@ -0,0 +1 @@
|
|||||||
|
User requested exit via password
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"server_ip": "digiserver",
|
"server_ip": "digi-signage.moto-adv.com",
|
||||||
"port": "80",
|
"port": "443",
|
||||||
"screen_name": "rpi-tvholba1",
|
"screen_name": "tv-terasa",
|
||||||
"quickconnect_key": "8887779",
|
"quickconnect_key": "8887779",
|
||||||
"orientation": "Landscape",
|
"orientation": "Landscape",
|
||||||
"touch": "True",
|
"touch": "True",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
53
setup_wifi_control.sh
Normal file
53
setup_wifi_control.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setup script to allow passwordless sudo for WiFi control commands
|
||||||
|
|
||||||
|
echo "Setting up passwordless sudo for WiFi control..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create sudoers file for WiFi commands
|
||||||
|
SUDOERS_FILE="/etc/sudoers.d/kiwy-signage-wifi"
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "This script must be run as root (use sudo)"
|
||||||
|
echo "Usage: sudo bash setup_wifi_control.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the username who invoked sudo
|
||||||
|
ACTUAL_USER="${SUDO_USER:-$USER}"
|
||||||
|
|
||||||
|
echo "Configuring passwordless sudo for user: $ACTUAL_USER"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create sudoers entry
|
||||||
|
cat > "$SUDOERS_FILE" << EOF
|
||||||
|
# Allow $ACTUAL_USER to control WiFi without password for Kiwy Signage Player
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: /usr/sbin/rfkill block wifi
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: /usr/sbin/rfkill unblock wifi
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/ifconfig wlan0 down
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/ifconfig wlan0 up
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/dhclient wlan0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set correct permissions
|
||||||
|
chmod 0440 "$SUDOERS_FILE"
|
||||||
|
|
||||||
|
echo "✓ Created sudoers file: $SUDOERS_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Validate the sudoers file
|
||||||
|
if visudo -c -f "$SUDOERS_FILE"; then
|
||||||
|
echo "✓ Sudoers file validated successfully"
|
||||||
|
echo ""
|
||||||
|
echo "Setup complete! User '$ACTUAL_USER' can now control WiFi without password."
|
||||||
|
echo ""
|
||||||
|
echo "Test with:"
|
||||||
|
echo " sudo rfkill block wifi"
|
||||||
|
echo " sudo rfkill unblock wifi"
|
||||||
|
else
|
||||||
|
echo "✗ Error: Sudoers file validation failed"
|
||||||
|
echo "Removing invalid file..."
|
||||||
|
rm -f "$SUDOERS_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -271,8 +271,53 @@ def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, kee
|
|||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
logger.info(f"🗑️ Deleted old playlist: {f}")
|
logger.info(f"🗑️ Deleted old playlist: {f}")
|
||||||
|
|
||||||
# TODO: Clean up unused media files
|
# Clean up unused media files
|
||||||
logger.info(f"✅ Cleanup complete (kept {keep_versions} latest versions)")
|
logger.info("🔍 Checking for unused media files...")
|
||||||
|
|
||||||
|
# Get list of media files referenced in current playlist
|
||||||
|
current_playlist_file = os.path.join(playlist_dir, f'server_playlist_v{current_version}.json')
|
||||||
|
referenced_files = set()
|
||||||
|
|
||||||
|
if os.path.exists(current_playlist_file):
|
||||||
|
try:
|
||||||
|
with open(current_playlist_file, 'r') as f:
|
||||||
|
playlist_data = json.load(f)
|
||||||
|
for item in playlist_data.get('playlist', []):
|
||||||
|
file_name = item.get('file_name', '')
|
||||||
|
if file_name:
|
||||||
|
referenced_files.add(file_name)
|
||||||
|
|
||||||
|
logger.info(f"📋 Current playlist references {len(referenced_files)} media files")
|
||||||
|
|
||||||
|
# Get all files in media directory (excluding edited_media subfolder)
|
||||||
|
if os.path.exists(media_dir):
|
||||||
|
media_files = [f for f in os.listdir(media_dir)
|
||||||
|
if os.path.isfile(os.path.join(media_dir, f))]
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
for media_file in media_files:
|
||||||
|
# Skip if file is in current playlist
|
||||||
|
if media_file in referenced_files:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Delete unreferenced file
|
||||||
|
media_path = os.path.join(media_dir, media_file)
|
||||||
|
try:
|
||||||
|
os.remove(media_path)
|
||||||
|
logger.info(f"🗑️ Deleted unused media: {media_file}")
|
||||||
|
deleted_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Could not delete {media_file}: {e}")
|
||||||
|
|
||||||
|
if deleted_count > 0:
|
||||||
|
logger.info(f"✅ Deleted {deleted_count} unused media files")
|
||||||
|
else:
|
||||||
|
logger.info("✅ No unused media files to delete")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error reading playlist for media cleanup: {e}")
|
||||||
|
|
||||||
|
logger.info(f"✅ Cleanup complete (kept {keep_versions} latest playlist versions)")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error during cleanup: {e}")
|
logger.error(f"❌ Error during cleanup: {e}")
|
||||||
|
|||||||
325
src/main.py
325
src/main.py
@@ -56,6 +56,7 @@ from get_playlists_v2 import (
|
|||||||
send_player_error_feedback
|
send_player_error_feedback
|
||||||
)
|
)
|
||||||
from keyboard_widget import KeyboardWidget
|
from keyboard_widget import KeyboardWidget
|
||||||
|
from network_monitor import NetworkMonitor
|
||||||
from kivy.graphics import Color, Line, Ellipse
|
from kivy.graphics import Color, Line, Ellipse
|
||||||
from kivy.uix.floatlayout import FloatLayout
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
from kivy.uix.slider import Slider
|
from kivy.uix.slider import Slider
|
||||||
@@ -382,14 +383,27 @@ class EditPopup(Popup):
|
|||||||
self.user_card_data = user_card_data # Store card data to send to server on save
|
self.user_card_data = user_card_data # Store card data to send to server on save
|
||||||
self.drawing_layer = None
|
self.drawing_layer = None
|
||||||
|
|
||||||
# Pause playback
|
# Pause playback (without auto-resume timer)
|
||||||
self.was_paused = self.player.is_paused
|
self.was_paused = self.player.is_paused
|
||||||
if not self.was_paused:
|
if not self.was_paused:
|
||||||
self.player.is_paused = True
|
self.player.is_paused = True
|
||||||
Clock.unschedule(self.player.next_media)
|
Clock.unschedule(self.player.next_media)
|
||||||
|
|
||||||
|
# Cancel auto-resume timer if one exists (don't want auto-resume during editing)
|
||||||
|
if self.player.auto_resume_event:
|
||||||
|
Clock.unschedule(self.player.auto_resume_event)
|
||||||
|
self.player.auto_resume_event = None
|
||||||
|
Logger.info("EditPopup: Cancelled auto-resume timer")
|
||||||
|
|
||||||
|
# Update button icon to play (to show it's paused)
|
||||||
|
self.player.ids.play_pause_btn.background_normal = self.player.resources_path + '/play.png'
|
||||||
|
self.player.ids.play_pause_btn.background_down = self.player.resources_path + '/play.png'
|
||||||
|
|
||||||
if self.player.current_widget and isinstance(self.player.current_widget, Video):
|
if self.player.current_widget and isinstance(self.player.current_widget, Video):
|
||||||
self.player.current_widget.state = 'pause'
|
self.player.current_widget.state = 'pause'
|
||||||
|
|
||||||
|
Logger.info("EditPopup: ⏸ Paused playback (no auto-resume) for editing")
|
||||||
|
|
||||||
# Show cursor
|
# Show cursor
|
||||||
try:
|
try:
|
||||||
Window.show_cursor = True
|
Window.show_cursor = True
|
||||||
@@ -703,6 +717,32 @@ class EditPopup(Popup):
|
|||||||
# Export only the visible content (image + drawings, no toolbars)
|
# Export only the visible content (image + drawings, no toolbars)
|
||||||
self.content.export_to_png(output_path)
|
self.content.export_to_png(output_path)
|
||||||
|
|
||||||
|
Logger.info(f"EditPopup: Saved edited image to {output_path}")
|
||||||
|
|
||||||
|
# ALSO overwrite the original image with edited content
|
||||||
|
Logger.info(f"EditPopup: Overwriting original image at {self.image_path}")
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Get original file info before overwrite
|
||||||
|
orig_size = os.path.getsize(self.image_path)
|
||||||
|
orig_mtime = os.path.getmtime(self.image_path)
|
||||||
|
|
||||||
|
# Overwrite the file
|
||||||
|
shutil.copy2(output_path, self.image_path)
|
||||||
|
|
||||||
|
# Force file system sync to ensure data is written to disk
|
||||||
|
os.sync()
|
||||||
|
|
||||||
|
# Verify the overwrite
|
||||||
|
new_size = os.path.getsize(self.image_path)
|
||||||
|
new_mtime = os.path.getmtime(self.image_path)
|
||||||
|
|
||||||
|
Logger.info(f"EditPopup: ✓ File overwritten:")
|
||||||
|
Logger.info(f" - Size: {orig_size} -> {new_size} bytes (changed: {new_size != orig_size})")
|
||||||
|
Logger.info(f" - Modified time: {orig_mtime} -> {new_mtime} (changed: {new_mtime > orig_mtime})")
|
||||||
|
Logger.info(f"EditPopup: ✓ File synced to disk")
|
||||||
|
|
||||||
# Restore toolbars
|
# Restore toolbars
|
||||||
self.top_toolbar.opacity = 1
|
self.top_toolbar.opacity = 1
|
||||||
self.right_sidebar.opacity = 1
|
self.right_sidebar.opacity = 1
|
||||||
@@ -711,9 +751,7 @@ class EditPopup(Popup):
|
|||||||
json_filename = self._save_metadata(edited_dir, new_name, base_name,
|
json_filename = self._save_metadata(edited_dir, new_name, base_name,
|
||||||
new_version if version_match else 1, output_filename)
|
new_version if version_match else 1, output_filename)
|
||||||
|
|
||||||
Logger.info(f"EditPopup: Saved edited image to {output_path}")
|
# Upload to server in background (continues after popup closes)
|
||||||
|
|
||||||
# Upload to server asynchronously (non-blocking)
|
|
||||||
import threading
|
import threading
|
||||||
upload_thread = threading.Thread(
|
upload_thread = threading.Thread(
|
||||||
target=self._upload_to_server,
|
target=self._upload_to_server,
|
||||||
@@ -721,10 +759,42 @@ class EditPopup(Popup):
|
|||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
upload_thread.start()
|
upload_thread.start()
|
||||||
|
Logger.info(f"EditPopup: Background upload thread started")
|
||||||
|
|
||||||
# Show confirmation
|
# NOW show saving popup AFTER everything is done
|
||||||
self.title = f'Saved as {output_filename}'
|
def show_saving_and_dismiss(dt):
|
||||||
Clock.schedule_once(lambda dt: self.dismiss(), 1)
|
from kivy.uix.popup import Popup
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
|
||||||
|
# Create label with background
|
||||||
|
save_label = Label(
|
||||||
|
text='✓ Saved! Reloading player...',
|
||||||
|
font_size='36sp',
|
||||||
|
color=(1, 1, 1, 1),
|
||||||
|
bold=True
|
||||||
|
)
|
||||||
|
|
||||||
|
saving_popup = Popup(
|
||||||
|
title='',
|
||||||
|
content=save_label,
|
||||||
|
size_hint=(0.8, 0.3),
|
||||||
|
auto_dismiss=False,
|
||||||
|
separator_height=0,
|
||||||
|
background_color=(0.2, 0.7, 0.2, 0.95) # Green background
|
||||||
|
)
|
||||||
|
saving_popup.open()
|
||||||
|
Logger.info("EditPopup: Saving confirmation popup opened")
|
||||||
|
|
||||||
|
# Dismiss both popups after 2 seconds
|
||||||
|
def dismiss_all(dt):
|
||||||
|
saving_popup.dismiss()
|
||||||
|
Logger.info(f"EditPopup: Dismissing to resume playback...")
|
||||||
|
self.dismiss()
|
||||||
|
|
||||||
|
Clock.schedule_once(dismiss_all, 2.0)
|
||||||
|
|
||||||
|
# Small delay to ensure UI is ready, then show popup
|
||||||
|
Clock.schedule_once(show_saving_and_dismiss, 0.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"EditPopup: Error in export: {e}")
|
Logger.error(f"EditPopup: Error in export: {e}")
|
||||||
@@ -734,6 +804,8 @@ class EditPopup(Popup):
|
|||||||
# Restore toolbars
|
# Restore toolbars
|
||||||
self.top_toolbar.opacity = 1
|
self.top_toolbar.opacity = 1
|
||||||
self.right_sidebar.opacity = 1
|
self.right_sidebar.opacity = 1
|
||||||
|
# Still dismiss on error after brief delay
|
||||||
|
Clock.schedule_once(lambda dt: self.dismiss(), 1)
|
||||||
|
|
||||||
Clock.schedule_once(do_export, 0.1)
|
Clock.schedule_once(do_export, 0.1)
|
||||||
return
|
return
|
||||||
@@ -851,17 +923,36 @@ class EditPopup(Popup):
|
|||||||
self.dismiss()
|
self.dismiss()
|
||||||
|
|
||||||
def on_popup_dismiss(self, *args):
|
def on_popup_dismiss(self, *args):
|
||||||
"""Resume playback when popup closes"""
|
"""Resume playback when popup closes - reload current image and continue"""
|
||||||
# Resume playback if it wasn't paused before
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
# Force remove current widget immediately
|
||||||
|
if self.player.current_widget:
|
||||||
|
Logger.info("EditPopup: Removing current widget to force reload")
|
||||||
|
self.player.ids.content_area.remove_widget(self.player.current_widget)
|
||||||
|
self.player.current_widget = None
|
||||||
|
Logger.info("EditPopup: ✓ Widget removed, ready for fresh load")
|
||||||
|
|
||||||
|
# Resume playback if it wasn't paused before editing
|
||||||
if not self.was_paused:
|
if not self.was_paused:
|
||||||
self.player.is_paused = False
|
self.player.is_paused = False
|
||||||
self.player.play_current_media()
|
|
||||||
|
# Update button icon to pause (to show it's playing)
|
||||||
|
self.player.ids.play_pause_btn.background_normal = self.player.resources_path + '/pause.png'
|
||||||
|
self.player.ids.play_pause_btn.background_down = self.player.resources_path + '/pause.png'
|
||||||
|
|
||||||
|
# Add delay to ensure file write is complete and synced
|
||||||
|
def reload_media(dt):
|
||||||
|
Logger.info("EditPopup: ▶ Resuming playback and reloading edited image (force_reload=True)")
|
||||||
|
self.player.play_current_media(force_reload=True)
|
||||||
|
|
||||||
|
Clock.schedule_once(reload_media, 0.5)
|
||||||
|
else:
|
||||||
|
Logger.info("EditPopup: Dismissed, keeping paused state")
|
||||||
|
|
||||||
# Restart control hide timer
|
# Restart control hide timer
|
||||||
self.player.schedule_hide_controls()
|
self.player.schedule_hide_controls()
|
||||||
|
|
||||||
Logger.info("EditPopup: Dismissed, playback resumed")
|
|
||||||
|
|
||||||
# Custom keyboard container with close button
|
# Custom keyboard container with close button
|
||||||
class KeyboardContainer(BoxLayout):
|
class KeyboardContainer(BoxLayout):
|
||||||
def __init__(self, vkeyboard, **kwargs):
|
def __init__(self, vkeyboard, **kwargs):
|
||||||
@@ -1126,8 +1217,8 @@ class SettingsPopup(Popup):
|
|||||||
self.ids.edit_enabled_checkbox.active = self.player.config.get('edit_feature_enabled', True)
|
self.ids.edit_enabled_checkbox.active = self.player.config.get('edit_feature_enabled', True)
|
||||||
|
|
||||||
# Update status info
|
# Update status info
|
||||||
self.ids.playlist_info.text = f'Playlist Version: {self.player.playlist_version}'
|
self.ids.playlist_info.text = f'Playlist: v{self.player.playlist_version}'
|
||||||
self.ids.media_count_info.text = f'Media Count: {len(self.player.playlist)}'
|
self.ids.media_count_info.text = f'Media: {len(self.player.playlist)}'
|
||||||
self.ids.status_info.text = f'Status: {"Playing" if self.player.is_playing else "Paused" if self.player.is_paused else "Idle"}'
|
self.ids.status_info.text = f'Status: {"Playing" if self.player.is_playing else "Paused" if self.player.is_paused else "Idle"}'
|
||||||
|
|
||||||
# Bind to dismiss event to manage cursor visibility and resume playback
|
# Bind to dismiss event to manage cursor visibility and resume playback
|
||||||
@@ -1267,6 +1358,93 @@ class SettingsPopup(Popup):
|
|||||||
else:
|
else:
|
||||||
self.ids.connection_status.color = (1, 0, 0, 1) # Red
|
self.ids.connection_status.color = (1, 0, 0, 1) # Red
|
||||||
|
|
||||||
|
def reset_player_auth(self):
|
||||||
|
"""Reset player authentication by deleting player_auth.json"""
|
||||||
|
try:
|
||||||
|
auth_file = os.path.join(os.path.dirname(__file__), 'player_auth.json')
|
||||||
|
if os.path.exists(auth_file):
|
||||||
|
os.remove(auth_file)
|
||||||
|
Logger.info(f"SettingsPopup: Deleted authentication file: {auth_file}")
|
||||||
|
self._show_temp_message('✓ Authentication reset - will reauthenticate on restart', (0, 1, 0, 1))
|
||||||
|
else:
|
||||||
|
Logger.info("SettingsPopup: No authentication file found")
|
||||||
|
self._show_temp_message('No authentication file found', (1, 0.7, 0, 1))
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"SettingsPopup: Error resetting authentication: {e}")
|
||||||
|
self._show_temp_message(f'✗ Error: {str(e)}', (1, 0, 0, 1))
|
||||||
|
|
||||||
|
def reset_playlist_version(self):
|
||||||
|
"""Reset playlist version to 0 and rename playlist file"""
|
||||||
|
try:
|
||||||
|
playlists_dir = os.path.join(self.player.base_dir, 'playlists')
|
||||||
|
|
||||||
|
# Find current playlist file
|
||||||
|
import glob
|
||||||
|
playlist_files = glob.glob(os.path.join(playlists_dir, 'server_playlist_v*.json'))
|
||||||
|
|
||||||
|
if playlist_files:
|
||||||
|
# Get the first (should be only one) playlist file
|
||||||
|
current_playlist = playlist_files[0]
|
||||||
|
new_playlist = os.path.join(playlists_dir, 'server_playlist_v0.json')
|
||||||
|
|
||||||
|
# Rename to v0
|
||||||
|
os.rename(current_playlist, new_playlist)
|
||||||
|
Logger.info(f"SettingsPopup: Renamed {os.path.basename(current_playlist)} to server_playlist_v0.json")
|
||||||
|
|
||||||
|
# Update player's playlist version
|
||||||
|
self.player.playlist_version = 0
|
||||||
|
|
||||||
|
# Update display
|
||||||
|
self.ids.playlist_info.text = f'Playlist: v{self.player.playlist_version}'
|
||||||
|
|
||||||
|
self._show_temp_message('✓ Playlist reset to v0 - will resync from server', (0, 1, 0, 1))
|
||||||
|
else:
|
||||||
|
Logger.info("SettingsPopup: No playlist file found")
|
||||||
|
self._show_temp_message('No playlist file found', (1, 0.7, 0, 1))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"SettingsPopup: Error resetting playlist: {e}")
|
||||||
|
self._show_temp_message(f'✗ Error: {str(e)}', (1, 0, 0, 1))
|
||||||
|
|
||||||
|
def restart_player(self):
|
||||||
|
"""Restart playlist from the first item"""
|
||||||
|
try:
|
||||||
|
Logger.info("SettingsPopup: Restarting player from first item")
|
||||||
|
|
||||||
|
# Reset to first item
|
||||||
|
self.player.current_index = 0
|
||||||
|
|
||||||
|
# Show message
|
||||||
|
self._show_temp_message('✓ Restarting playlist from beginning...', (0, 1, 0, 1))
|
||||||
|
|
||||||
|
# Close settings after a short delay
|
||||||
|
def close_and_restart(dt):
|
||||||
|
self.dismiss()
|
||||||
|
# Start playing from first item
|
||||||
|
self.player.play_current_media()
|
||||||
|
|
||||||
|
Clock.schedule_once(close_and_restart, 2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"SettingsPopup: Error restarting player: {e}")
|
||||||
|
self._show_temp_message(f'✗ Error: {str(e)}', (1, 0, 0, 1))
|
||||||
|
|
||||||
|
def _show_temp_message(self, message, color):
|
||||||
|
"""Show a temporary popup message for 2 seconds"""
|
||||||
|
from kivy.uix.popup import Popup
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
|
||||||
|
popup = Popup(
|
||||||
|
title='',
|
||||||
|
content=Label(text=message, color=color),
|
||||||
|
size_hint=(0.6, 0.3),
|
||||||
|
auto_dismiss=True
|
||||||
|
)
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
# Auto-close after 2 seconds
|
||||||
|
Clock.schedule_once(lambda dt: popup.dismiss(), 2)
|
||||||
|
|
||||||
def save_and_close(self):
|
def save_and_close(self):
|
||||||
"""Save configuration and close popup"""
|
"""Save configuration and close popup"""
|
||||||
# Update config
|
# Update config
|
||||||
@@ -1310,6 +1488,7 @@ class SignagePlayer(Widget):
|
|||||||
self.current_widget = None
|
self.current_widget = None
|
||||||
self.is_playing = False
|
self.is_playing = False
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
|
self.auto_resume_event = None # Track scheduled auto-resume
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.playlist_version = None
|
self.playlist_version = None
|
||||||
self.consecutive_errors = 0 # Track consecutive playback errors
|
self.consecutive_errors = 0 # Track consecutive playback errors
|
||||||
@@ -1318,6 +1497,8 @@ class SignagePlayer(Widget):
|
|||||||
# Card reader for authentication
|
# Card reader for authentication
|
||||||
self.card_reader = None
|
self.card_reader = None
|
||||||
self._pending_edit_image = None
|
self._pending_edit_image = None
|
||||||
|
# Network monitor
|
||||||
|
self.network_monitor = None
|
||||||
# Paths
|
# Paths
|
||||||
self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
self.config_dir = os.path.join(self.base_dir, 'config')
|
self.config_dir = os.path.join(self.base_dir, 'config')
|
||||||
@@ -1364,6 +1545,9 @@ class SignagePlayer(Widget):
|
|||||||
# Load configuration
|
# Load configuration
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
|
||||||
|
# Initialize network monitor
|
||||||
|
self.start_network_monitoring()
|
||||||
|
|
||||||
# Play intro video first
|
# Play intro video first
|
||||||
self.play_intro_video()
|
self.play_intro_video()
|
||||||
|
|
||||||
@@ -1405,6 +1589,30 @@ class SignagePlayer(Widget):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.error(f"SignagePlayer: Error saving config: {e}")
|
Logger.error(f"SignagePlayer: Error saving config: {e}")
|
||||||
|
|
||||||
|
def start_network_monitoring(self):
|
||||||
|
"""Initialize and start network monitoring"""
|
||||||
|
try:
|
||||||
|
if self.config and 'server_ip' in self.config:
|
||||||
|
server_url = self.config.get('server_ip', '')
|
||||||
|
|
||||||
|
# Initialize network monitor with:
|
||||||
|
# - Check every 30-45 minutes
|
||||||
|
# - Restart WiFi for 20 minutes on connection failure
|
||||||
|
self.network_monitor = NetworkMonitor(
|
||||||
|
server_url=server_url,
|
||||||
|
check_interval_min=30,
|
||||||
|
check_interval_max=45,
|
||||||
|
wifi_restart_duration=20
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start monitoring
|
||||||
|
self.network_monitor.start_monitoring()
|
||||||
|
Logger.info("SignagePlayer: Network monitoring started")
|
||||||
|
else:
|
||||||
|
Logger.warning("SignagePlayer: Cannot start network monitoring - no server configured")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"SignagePlayer: Error starting network monitoring: {e}")
|
||||||
|
|
||||||
async def async_playlist_update_loop(self):
|
async def async_playlist_update_loop(self):
|
||||||
"""Async coroutine to check for playlist updates without blocking UI"""
|
"""Async coroutine to check for playlist updates without blocking UI"""
|
||||||
Logger.info("SignagePlayer: Starting async playlist update loop")
|
Logger.info("SignagePlayer: Starting async playlist update loop")
|
||||||
@@ -1568,8 +1776,17 @@ class SignagePlayer(Widget):
|
|||||||
self.current_index = 0
|
self.current_index = 0
|
||||||
self.play_current_media()
|
self.play_current_media()
|
||||||
|
|
||||||
def play_current_media(self):
|
def play_current_media(self, force_reload=False):
|
||||||
"""Play the current media item"""
|
"""Play the current media item
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_reload: If True, clears image cache before loading (for edited images)
|
||||||
|
"""
|
||||||
|
# Don't play if paused (unless we're explicitly resuming)
|
||||||
|
if self.is_paused:
|
||||||
|
Logger.debug(f"SignagePlayer: Skipping play_current_media - player is paused")
|
||||||
|
return
|
||||||
|
|
||||||
if not self.playlist or self.current_index >= len(self.playlist):
|
if not self.playlist or self.current_index >= len(self.playlist):
|
||||||
# End of playlist, restart
|
# End of playlist, restart
|
||||||
self.restart_playlist()
|
self.restart_playlist()
|
||||||
@@ -1626,7 +1843,7 @@ class SignagePlayer(Widget):
|
|||||||
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']:
|
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']:
|
||||||
# Image file
|
# Image file
|
||||||
Logger.info(f"SignagePlayer: Media type: IMAGE")
|
Logger.info(f"SignagePlayer: Media type: IMAGE")
|
||||||
self.play_image(media_path, duration)
|
self.play_image(media_path, duration, force_reload=force_reload)
|
||||||
else:
|
else:
|
||||||
Logger.warning(f"SignagePlayer: ❌ Unsupported media type: {file_extension}")
|
Logger.warning(f"SignagePlayer: ❌ Unsupported media type: {file_extension}")
|
||||||
Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif/.webp")
|
Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif/.webp")
|
||||||
@@ -1726,9 +1943,29 @@ class SignagePlayer(Widget):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.warning(f"SignagePlayer: Could not log video info: {e}")
|
Logger.warning(f"SignagePlayer: Could not log video info: {e}")
|
||||||
|
|
||||||
def play_image(self, image_path, duration):
|
def play_image(self, image_path, duration, force_reload=False):
|
||||||
"""Play an image file"""
|
"""Play an image file"""
|
||||||
try:
|
try:
|
||||||
|
# Log file info before loading
|
||||||
|
file_size = os.path.getsize(image_path)
|
||||||
|
file_mtime = os.path.getmtime(image_path)
|
||||||
|
Logger.info(f"SignagePlayer: Loading image: {os.path.basename(image_path)}")
|
||||||
|
Logger.info(f" - Size: {file_size:,} bytes, Modified: {file_mtime}")
|
||||||
|
|
||||||
|
if force_reload:
|
||||||
|
Logger.info(f"SignagePlayer: Force reload - using Image widget (no async cache)")
|
||||||
|
# Use regular Image widget instead of AsyncImage to bypass all caching
|
||||||
|
from kivy.uix.image import Image
|
||||||
|
self.current_widget = Image(
|
||||||
|
source=image_path,
|
||||||
|
allow_stretch=True,
|
||||||
|
keep_ratio=True,
|
||||||
|
size_hint=(1, 1),
|
||||||
|
pos_hint={'center_x': 0.5, 'center_y': 0.5}
|
||||||
|
)
|
||||||
|
# Force reload the texture
|
||||||
|
self.current_widget.reload()
|
||||||
|
else:
|
||||||
Logger.info(f"SignagePlayer: Creating AsyncImage widget...")
|
Logger.info(f"SignagePlayer: Creating AsyncImage widget...")
|
||||||
self.current_widget = AsyncImage(
|
self.current_widget = AsyncImage(
|
||||||
source=image_path,
|
source=image_path,
|
||||||
@@ -1753,7 +1990,7 @@ class SignagePlayer(Widget):
|
|||||||
def next_media(self, dt=None):
|
def next_media(self, dt=None):
|
||||||
"""Move to next media item"""
|
"""Move to next media item"""
|
||||||
if self.is_paused:
|
if self.is_paused:
|
||||||
Logger.debug(f"SignagePlayer: Skipping next_media - player is paused")
|
Logger.info(f"SignagePlayer: ⏸ Blocked next_media - player is paused")
|
||||||
return
|
return
|
||||||
|
|
||||||
Logger.info(f"SignagePlayer: Transitioning to next media (was index {self.current_index})")
|
Logger.info(f"SignagePlayer: Transitioning to next media (was index {self.current_index})")
|
||||||
@@ -1773,15 +2010,52 @@ class SignagePlayer(Widget):
|
|||||||
self.play_current_media()
|
self.play_current_media()
|
||||||
|
|
||||||
def toggle_pause(self, instance=None):
|
def toggle_pause(self, instance=None):
|
||||||
"""Toggle pause/play"""
|
"""Toggle pause/play with auto-resume after 5 minutes"""
|
||||||
self.is_paused = not self.is_paused
|
self.is_paused = not self.is_paused
|
||||||
|
|
||||||
if self.is_paused:
|
if self.is_paused:
|
||||||
self.ids.play_pause_btn.text = '▶'
|
# Paused - change icon to play and schedule auto-resume
|
||||||
|
Logger.info("SignagePlayer: ⏸ PAUSING - is_paused = True")
|
||||||
|
self.ids.play_pause_btn.background_normal = self.resources_path + '/play.png'
|
||||||
|
self.ids.play_pause_btn.background_down = self.resources_path + '/play.png'
|
||||||
Clock.unschedule(self.next_media)
|
Clock.unschedule(self.next_media)
|
||||||
|
|
||||||
|
# Cancel any existing auto-resume
|
||||||
|
if self.auto_resume_event:
|
||||||
|
Clock.unschedule(self.auto_resume_event)
|
||||||
|
|
||||||
|
# Schedule auto-resume after 5 minutes (300 seconds)
|
||||||
|
self.auto_resume_event = Clock.schedule_once(self.auto_resume_playback, 300)
|
||||||
|
Logger.info("SignagePlayer: Auto-resume scheduled in 5 minutes")
|
||||||
else:
|
else:
|
||||||
self.ids.play_pause_btn.text = '⏸'
|
# Playing - change icon to pause and cancel auto-resume
|
||||||
# Resume by playing current media
|
# Note: is_paused is already set to False by the toggle above
|
||||||
|
Logger.info("SignagePlayer: ▶ RESUMING - is_paused = False")
|
||||||
|
self.ids.play_pause_btn.background_normal = self.resources_path + '/pause.png'
|
||||||
|
self.ids.play_pause_btn.background_down = self.resources_path + '/pause.png'
|
||||||
|
|
||||||
|
# Cancel auto-resume if manually resumed
|
||||||
|
if self.auto_resume_event:
|
||||||
|
Clock.unschedule(self.auto_resume_event)
|
||||||
|
self.auto_resume_event = None
|
||||||
|
|
||||||
|
# Resume by playing current media (is_paused is now False)
|
||||||
|
self.play_current_media()
|
||||||
|
|
||||||
|
def auto_resume_playback(self, dt):
|
||||||
|
"""Automatically resume playback after 5 minutes of pause"""
|
||||||
|
Logger.info("SignagePlayer: Auto-resuming playback after 5 minutes")
|
||||||
|
self.auto_resume_event = None
|
||||||
|
|
||||||
|
if self.is_paused:
|
||||||
|
# Reset pause state FIRST (before calling play_current_media)
|
||||||
|
self.is_paused = False
|
||||||
|
|
||||||
|
# Update icon to pause
|
||||||
|
self.ids.play_pause_btn.background_normal = self.resources_path + '/pause.png'
|
||||||
|
self.ids.play_pause_btn.background_down = self.resources_path + '/pause.png'
|
||||||
|
|
||||||
|
# Resume playback (is_paused is now False so it will work)
|
||||||
self.play_current_media()
|
self.play_current_media()
|
||||||
|
|
||||||
def restart_playlist(self):
|
def restart_playlist(self):
|
||||||
@@ -2119,6 +2393,11 @@ class SignagePlayerApp(App):
|
|||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
Logger.info("SignagePlayerApp: Application stopped")
|
Logger.info("SignagePlayerApp: Application stopped")
|
||||||
|
|
||||||
|
# Stop network monitoring
|
||||||
|
if hasattr(self.root, 'network_monitor') and self.root.network_monitor:
|
||||||
|
self.root.network_monitor.stop_monitoring()
|
||||||
|
Logger.info("SignagePlayerApp: Network monitoring stopped")
|
||||||
|
|
||||||
# Cancel all async tasks
|
# Cancel all async tasks
|
||||||
try:
|
try:
|
||||||
pending = asyncio.all_tasks()
|
pending = asyncio.all_tasks()
|
||||||
|
|||||||
235
src/network_monitor.py
Normal file
235
src/network_monitor.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
"""
|
||||||
|
Network Monitoring Module
|
||||||
|
Checks server connectivity and manages WiFi restart on connection failure
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkMonitor:
|
||||||
|
"""Monitor network connectivity and manage WiFi restart"""
|
||||||
|
|
||||||
|
def __init__(self, server_url, check_interval_min=30, check_interval_max=45, wifi_restart_duration=20):
|
||||||
|
"""
|
||||||
|
Initialize network monitor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_url (str): Server URL to check connectivity (e.g., 'https://digi-signage.moto-adv.com')
|
||||||
|
check_interval_min (int): Minimum minutes between checks (default: 30)
|
||||||
|
check_interval_max (int): Maximum minutes between checks (default: 45)
|
||||||
|
wifi_restart_duration (int): Minutes to keep WiFi off during restart (default: 20)
|
||||||
|
"""
|
||||||
|
self.server_url = server_url.rstrip('/')
|
||||||
|
self.check_interval_min = check_interval_min * 60 # Convert to seconds
|
||||||
|
self.check_interval_max = check_interval_max * 60 # Convert to seconds
|
||||||
|
self.wifi_restart_duration = wifi_restart_duration * 60 # Convert to seconds
|
||||||
|
self.is_monitoring = False
|
||||||
|
self.scheduled_event = None
|
||||||
|
self.consecutive_failures = 0
|
||||||
|
self.max_failures_before_restart = 3 # Restart WiFi after 3 consecutive failures
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Start the network monitoring loop"""
|
||||||
|
if not self.is_monitoring:
|
||||||
|
self.is_monitoring = True
|
||||||
|
Logger.info("NetworkMonitor: Starting network monitoring")
|
||||||
|
self._schedule_next_check()
|
||||||
|
|
||||||
|
def stop_monitoring(self):
|
||||||
|
"""Stop the network monitoring"""
|
||||||
|
self.is_monitoring = False
|
||||||
|
if self.scheduled_event:
|
||||||
|
self.scheduled_event.cancel()
|
||||||
|
self.scheduled_event = None
|
||||||
|
Logger.info("NetworkMonitor: Stopped network monitoring")
|
||||||
|
|
||||||
|
def _schedule_next_check(self):
|
||||||
|
"""Schedule the next connectivity check at a random interval"""
|
||||||
|
if not self.is_monitoring:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Random interval between min and max
|
||||||
|
next_check_seconds = random.randint(self.check_interval_min, self.check_interval_max)
|
||||||
|
next_check_minutes = next_check_seconds / 60
|
||||||
|
|
||||||
|
Logger.info(f"NetworkMonitor: Next check scheduled in {next_check_minutes:.1f} minutes")
|
||||||
|
|
||||||
|
# Schedule using Kivy Clock
|
||||||
|
self.scheduled_event = Clock.schedule_once(
|
||||||
|
lambda dt: self._check_connectivity(),
|
||||||
|
next_check_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_connectivity(self):
|
||||||
|
"""Check network connectivity to server"""
|
||||||
|
Logger.info("NetworkMonitor: Checking server connectivity...")
|
||||||
|
|
||||||
|
if self._test_server_connection():
|
||||||
|
Logger.info("NetworkMonitor: ✓ Server connection successful")
|
||||||
|
self.consecutive_failures = 0
|
||||||
|
else:
|
||||||
|
self.consecutive_failures += 1
|
||||||
|
Logger.warning(f"NetworkMonitor: ✗ Server connection failed (attempt {self.consecutive_failures}/{self.max_failures_before_restart})")
|
||||||
|
|
||||||
|
if self.consecutive_failures >= self.max_failures_before_restart:
|
||||||
|
Logger.error("NetworkMonitor: Multiple connection failures detected - initiating WiFi restart")
|
||||||
|
self._restart_wifi()
|
||||||
|
self.consecutive_failures = 0 # Reset counter after restart
|
||||||
|
|
||||||
|
# Schedule next check
|
||||||
|
self._schedule_next_check()
|
||||||
|
|
||||||
|
def _test_server_connection(self):
|
||||||
|
"""
|
||||||
|
Test connection to the server using ping only
|
||||||
|
This works in closed networks where the server is local
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if server is reachable, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract hostname from server URL (remove http:// or https://)
|
||||||
|
hostname = self.server_url.replace('https://', '').replace('http://', '').split('/')[0]
|
||||||
|
|
||||||
|
Logger.info(f"NetworkMonitor: Pinging server: {hostname}")
|
||||||
|
|
||||||
|
# Ping the server hostname with 3 attempts
|
||||||
|
result = subprocess.run(
|
||||||
|
['ping', '-c', '3', '-W', '3', hostname],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
Logger.info(f"NetworkMonitor: ✓ Server {hostname} is reachable")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
Logger.warning(f"NetworkMonitor: ✗ Cannot reach server {hostname}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
Logger.warning(f"NetworkMonitor: ✗ Ping timeout to server")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"NetworkMonitor: Error pinging server: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _restart_wifi(self):
|
||||||
|
"""
|
||||||
|
Restart WiFi by turning it off for a specified duration then back on
|
||||||
|
This runs in a separate thread to not block the main application
|
||||||
|
"""
|
||||||
|
def wifi_restart_thread():
|
||||||
|
try:
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
Logger.info("NetworkMonitor: INITIATING WIFI RESTART SEQUENCE")
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
|
||||||
|
# Turn off WiFi using rfkill (more reliable on Raspberry Pi)
|
||||||
|
Logger.info("NetworkMonitor: Turning WiFi OFF using rfkill...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['sudo', 'rfkill', 'block', 'wifi'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
Logger.info("NetworkMonitor: ✓ WiFi turned OFF successfully (rfkill)")
|
||||||
|
Logger.info("NetworkMonitor: WiFi is now DISABLED and will remain OFF")
|
||||||
|
else:
|
||||||
|
Logger.error(f"NetworkMonitor: rfkill failed, trying ifconfig...")
|
||||||
|
Logger.error(f"NetworkMonitor: rfkill error: {result.stderr}")
|
||||||
|
|
||||||
|
# Fallback to ifconfig
|
||||||
|
result2 = subprocess.run(
|
||||||
|
['sudo', 'ifconfig', 'wlan0', 'down'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result2.returncode == 0:
|
||||||
|
Logger.info("NetworkMonitor: ✓ WiFi turned OFF successfully (ifconfig)")
|
||||||
|
else:
|
||||||
|
Logger.error(f"NetworkMonitor: Failed to turn WiFi off: {result2.stderr}")
|
||||||
|
Logger.error(f"NetworkMonitor: Return code: {result2.returncode}")
|
||||||
|
Logger.error(f"NetworkMonitor: STDOUT: {result2.stdout}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wait for the specified duration with WiFi OFF
|
||||||
|
wait_minutes = self.wifi_restart_duration / 60
|
||||||
|
Logger.info(f"NetworkMonitor: ====================================")
|
||||||
|
Logger.info(f"NetworkMonitor: WiFi will remain OFF for {wait_minutes:.0f} minutes")
|
||||||
|
Logger.info(f"NetworkMonitor: Waiting period started at: {datetime.now().strftime('%H:%M:%S')}")
|
||||||
|
Logger.info(f"NetworkMonitor: ====================================")
|
||||||
|
|
||||||
|
# Sleep while WiFi is OFF
|
||||||
|
time.sleep(self.wifi_restart_duration)
|
||||||
|
|
||||||
|
Logger.info(f"NetworkMonitor: Wait period completed at: {datetime.now().strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
|
# Turn WiFi back on after the wait period
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
Logger.info("NetworkMonitor: Now turning WiFi back ON...")
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
|
||||||
|
# Unblock WiFi using rfkill
|
||||||
|
result = subprocess.run(
|
||||||
|
['sudo', 'rfkill', 'unblock', 'wifi'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
Logger.info("NetworkMonitor: ✓ WiFi unblocked successfully (rfkill)")
|
||||||
|
else:
|
||||||
|
Logger.error(f"NetworkMonitor: rfkill unblock failed: {result.stderr}")
|
||||||
|
|
||||||
|
# Also bring interface up
|
||||||
|
result2 = subprocess.run(
|
||||||
|
['sudo', 'ifconfig', 'wlan0', 'up'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if result2.returncode == 0:
|
||||||
|
Logger.info("NetworkMonitor: ✓ WiFi interface brought UP successfully")
|
||||||
|
|
||||||
|
# Wait a bit for connection to establish
|
||||||
|
Logger.info("NetworkMonitor: Waiting 10 seconds for WiFi to initialize...")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# Try to restart DHCP
|
||||||
|
Logger.info("NetworkMonitor: Requesting IP address...")
|
||||||
|
subprocess.run(
|
||||||
|
['sudo', 'dhclient', 'wlan0'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
Logger.info("NetworkMonitor: WIFI RESTART SEQUENCE COMPLETED")
|
||||||
|
Logger.info("NetworkMonitor: ====================================")
|
||||||
|
else:
|
||||||
|
Logger.error(f"NetworkMonitor: Failed to turn WiFi on: {result.stderr}")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
Logger.error("NetworkMonitor: WiFi restart command timeout")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.error(f"NetworkMonitor: Error during WiFi restart: {e}")
|
||||||
|
|
||||||
|
# Run in separate thread to not block the application
|
||||||
|
import threading
|
||||||
|
thread = threading.Thread(target=wifi_restart_thread, daemon=True)
|
||||||
|
thread.start()
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"hostname": "rpi-tvholba1",
|
"hostname": "tv-terasa",
|
||||||
"auth_code": "73XSgIh2iBu3jaU1VOWSrYSS7c9fCPuZuRd7ygYDcjc",
|
"auth_code": "vkrxEO6eOTxkzXJBtoN4OuXc8eaX2mC3AB9ZePrnick",
|
||||||
"player_id": 1,
|
"player_id": 1,
|
||||||
"player_name": "Tv-Anunturi Hol Ba1",
|
"player_name": "TV-acasa",
|
||||||
"playlist_id": 1,
|
"playlist_id": 1,
|
||||||
"orientation": "Landscape",
|
"orientation": "Landscape",
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"server_url": "http://digiserver"
|
"server_url": "http://digi-signage.moto-adv.com"
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
id: play_pause_btn
|
id: play_pause_btn
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: dp(50), dp(50)
|
size: dp(50), dp(50)
|
||||||
background_normal: root.resources_path + '/play.png'
|
background_normal: root.resources_path + '/pause.png'
|
||||||
background_down: root.resources_path + '/pause.png'
|
background_down: root.resources_path + '/pause.png'
|
||||||
border: (0, 0, 0, 0)
|
border: (0, 0, 0, 0)
|
||||||
on_press: root.toggle_pause()
|
on_press: root.toggle_pause()
|
||||||
@@ -526,6 +526,42 @@
|
|||||||
Widget:
|
Widget:
|
||||||
size_hint_y: 0.05
|
size_hint_y: 0.05
|
||||||
|
|
||||||
|
# Reset Buttons Section
|
||||||
|
Label:
|
||||||
|
text: 'Reset Options:'
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(30)
|
||||||
|
text_size: self.size
|
||||||
|
halign: 'left'
|
||||||
|
valign: 'middle'
|
||||||
|
bold: True
|
||||||
|
font_size: sp(16)
|
||||||
|
|
||||||
|
# Reset Buttons Row
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(50)
|
||||||
|
spacing: dp(10)
|
||||||
|
|
||||||
|
Button:
|
||||||
|
id: reset_auth_btn
|
||||||
|
text: 'Reset Player Auth'
|
||||||
|
background_color: 0.8, 0.4, 0.2, 1
|
||||||
|
on_press: root.reset_player_auth()
|
||||||
|
|
||||||
|
Button:
|
||||||
|
id: reset_playlist_btn
|
||||||
|
text: 'Reset Playlist to v0'
|
||||||
|
background_color: 0.8, 0.4, 0.2, 1
|
||||||
|
on_press: root.reset_playlist_version()
|
||||||
|
|
||||||
|
Button:
|
||||||
|
id: restart_player_btn
|
||||||
|
text: 'Restart Player'
|
||||||
|
background_color: 0.2, 0.6, 0.8, 1
|
||||||
|
on_press: root.restart_player()
|
||||||
|
|
||||||
# Test Connection Button
|
# Test Connection Button
|
||||||
Button:
|
Button:
|
||||||
id: test_connection_btn
|
id: test_connection_btn
|
||||||
@@ -549,36 +585,39 @@
|
|||||||
Widget:
|
Widget:
|
||||||
size_hint_y: 0.05
|
size_hint_y: 0.05
|
||||||
|
|
||||||
# Status information
|
# Status information row
|
||||||
Label:
|
BoxLayout:
|
||||||
id: playlist_info
|
orientation: 'horizontal'
|
||||||
text: 'Playlist Version: N/A'
|
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: dp(30)
|
height: dp(30)
|
||||||
|
spacing: dp(10)
|
||||||
|
|
||||||
|
Label:
|
||||||
|
id: playlist_info
|
||||||
|
text: 'Playlist: N/A'
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
halign: 'left'
|
halign: 'center'
|
||||||
valign: 'middle'
|
valign: 'middle'
|
||||||
|
font_size: sp(12)
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
id: media_count_info
|
id: media_count_info
|
||||||
text: 'Media Count: 0'
|
text: 'Media: 0'
|
||||||
size_hint_y: None
|
|
||||||
height: dp(30)
|
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
halign: 'left'
|
halign: 'center'
|
||||||
valign: 'middle'
|
valign: 'middle'
|
||||||
|
font_size: sp(12)
|
||||||
|
|
||||||
Label:
|
Label:
|
||||||
id: status_info
|
id: status_info
|
||||||
text: 'Status: Idle'
|
text: 'Status: Idle'
|
||||||
size_hint_y: None
|
|
||||||
height: dp(30)
|
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
halign: 'left'
|
halign: 'center'
|
||||||
valign: 'middle'
|
valign: 'middle'
|
||||||
|
font_size: sp(12)
|
||||||
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint_y: 0.2
|
size_hint_y: 0.05
|
||||||
|
|
||||||
# Action buttons
|
# Action buttons
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
|
|||||||
61
src/test_network_monitor.py
Normal file
61
src/test_network_monitor.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for network monitor functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.clock import Clock
|
||||||
|
from network_monitor import NetworkMonitor
|
||||||
|
|
||||||
|
class TestMonitorApp(App):
|
||||||
|
"""Minimal Kivy app to test network monitor"""
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
"""Build the app"""
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
return Label(text='Network Monitor Test Running\nCheck terminal for output')
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
"""Start monitoring when app starts"""
|
||||||
|
server_url = "https://digi-signage.moto-adv.com"
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Network Monitor Test")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print(f"Server URL: {server_url}")
|
||||||
|
print("Check interval: 0.5 minutes (30 seconds for testing)")
|
||||||
|
print("WiFi restart duration: 1 minute (for testing)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create monitor with short intervals for testing
|
||||||
|
self.monitor = NetworkMonitor(
|
||||||
|
server_url=server_url,
|
||||||
|
check_interval_min=0.5, # 30 seconds
|
||||||
|
check_interval_max=0.5, # 30 seconds
|
||||||
|
wifi_restart_duration=1 # 1 minute
|
||||||
|
)
|
||||||
|
|
||||||
|
# Perform immediate test
|
||||||
|
print("Performing immediate connectivity test...")
|
||||||
|
self.monitor._check_connectivity()
|
||||||
|
|
||||||
|
# Start monitoring for future checks
|
||||||
|
print("\nStarting periodic network monitoring...")
|
||||||
|
self.monitor.start_monitoring()
|
||||||
|
|
||||||
|
print("\nMonitoring is active. Press Ctrl+C to stop.")
|
||||||
|
print("Next check will occur in ~30 seconds.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def on_stop(self):
|
||||||
|
"""Stop monitoring when app stops"""
|
||||||
|
if hasattr(self, 'monitor'):
|
||||||
|
self.monitor.stop_monitoring()
|
||||||
|
print("\nNetwork monitoring stopped")
|
||||||
|
print("Test completed!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
TestMonitorApp().run()
|
||||||
Reference in New Issue
Block a user