Compare commits
21 Commits
dbc1f51e9a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e9ea1aaa | |||
|
|
4c3ddbef73 | ||
|
|
87e059e0f4 | ||
|
|
46d9fcf6e3 | ||
|
|
f1a84d05d5 | ||
|
|
706af95557 | ||
| 02227a12e5 | |||
| 9d32f43ac7 | |||
| af1e671c7f | |||
|
|
9664ad541b | ||
|
|
89e5ad86dd | ||
|
|
f573af0505 | ||
|
|
fba2007bdf | ||
|
|
72d382b96b | ||
|
|
07b7e96edd | ||
| 744681bb20 | |||
| 68ed5b8534 | |||
| 3f9674517d | |||
| 493f307599 | |||
| da1d515cc5 | |||
| 4d803d4fe9 |
1
.player_stop_requested
Normal file
1
.player_stop_requested
Normal file
@@ -0,0 +1 @@
|
||||
User requested exit via password
|
||||
76
check_player_status.sh
Executable file
76
check_player_status.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Player Status Checker
|
||||
# Check if the player is running and healthy
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
|
||||
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
|
||||
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Kivy Signage Player - Status Check"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check for stop flag
|
||||
if [ -f "$STOP_FLAG_FILE" ]; then
|
||||
echo "🛑 Stop Flag: PRESENT (user requested exit)"
|
||||
echo " Watchdog will not restart player"
|
||||
echo " To restart: ./start.sh"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if player process is running
|
||||
PLAYER_PID=$(pgrep -f "python3 main.py" | head -1)
|
||||
|
||||
if [ -z "$PLAYER_PID" ]; then
|
||||
echo "Status: ❌ NOT RUNNING"
|
||||
echo ""
|
||||
echo "Player process is not running"
|
||||
else
|
||||
echo "Status: ✓ RUNNING"
|
||||
echo "PID: $PLAYER_PID"
|
||||
echo ""
|
||||
|
||||
# Check heartbeat
|
||||
if [ -f "$HEARTBEAT_FILE" ]; then
|
||||
last_update=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null || echo 0)
|
||||
current_time=$(date +%s)
|
||||
diff=$((current_time - last_update))
|
||||
|
||||
echo "Heartbeat: $(date -d @${last_update} '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "Last update: ${diff}s ago"
|
||||
|
||||
if [ $diff -lt 60 ]; then
|
||||
echo "Health: ✓ HEALTHY"
|
||||
else
|
||||
echo "Health: ⚠️ STALE (may be frozen)"
|
||||
fi
|
||||
else
|
||||
echo "Heartbeat: Not found (player may be starting)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Show watchdog status
|
||||
if pgrep -f "start.sh" > /dev/null; then
|
||||
echo "Watchdog: ✓ ACTIVE"
|
||||
else
|
||||
echo "Watchdog: ❌ NOT RUNNING"
|
||||
echo ""
|
||||
echo "To start with watchdog: ./start.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Show last few log entries
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo "=========================================="
|
||||
echo "Recent Log Entries (last 10):"
|
||||
echo "=========================================="
|
||||
tail -10 "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"server_ip": "172.18.0.1",
|
||||
"port": "5000",
|
||||
"screen_name": "rpi-tvholba1",
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "443",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"orientation": "Landscape",
|
||||
"touch": "True",
|
||||
"max_resolution": "1920x1080"
|
||||
"max_resolution": "1920x1080",
|
||||
"edit_feature_enabled": true
|
||||
}
|
||||
BIN
config/resources/access-card.png
Normal file
BIN
config/resources/access-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
config/resources/edit-pen.png
Normal file
BIN
config/resources/edit-pen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
config/resources/intro1.mp4
Normal file
BIN
config/resources/intro1.mp4
Normal file
Binary file not shown.
BIN
config/resources/pencil.png
Normal file
BIN
config/resources/pencil.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
install.sh
13
install.sh
@@ -83,13 +83,18 @@ if [ "$OFFLINE_MODE" = true ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A $WHEELS_DIR/
|
||||
echo "Installing from offline Python wheels..."
|
||||
echo "Wheel files found: $(ls -1 $WHEELS_DIR/*.whl 2>/dev/null | wc -l)"
|
||||
|
||||
pip3 install --no-index --find-links="$WHEELS_DIR" -r requirements.txt
|
||||
|
||||
echo "Python packages installed from offline repository"
|
||||
if pip3 install --break-system-packages --no-index --find-links="$WHEELS_DIR" -r requirements.txt 2>&1 | tee /tmp/pip_install.log; then
|
||||
echo "Python packages installed from offline repository"
|
||||
else
|
||||
echo "Warning: Offline installation failed (possibly due to Python version mismatch)"
|
||||
echo "Falling back to online installation..."
|
||||
pip3 install --break-system-packages -r requirements.txt
|
||||
echo "Python packages installed from PyPI"
|
||||
fi
|
||||
else
|
||||
# Online: Use pip from PyPI
|
||||
echo "Installing from PyPI..."
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install --break-system-packages -r requirements.txt
|
||||
|
||||
echo "Python packages installed successfully"
|
||||
fi
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
BIN
repo/python-wheels/certifi-2025.11.12-py3-none-any.whl
Normal file
BIN
repo/python-wheels/certifi-2025.11.12-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
repo/python-wheels/docutils-0.22.3-py3-none-any.whl
Normal file
BIN
repo/python-wheels/docutils-0.22.3-py3-none-any.whl
Normal file
Binary file not shown.
BIN
repo/python-wheels/filetype-1.2.0-py2.py3-none-any.whl
Normal file
BIN
repo/python-wheels/filetype-1.2.0-py2.py3-none-any.whl
Normal file
Binary file not shown.
BIN
repo/python-wheels/requests-2.32.5-py3-none-any.whl
Normal file
BIN
repo/python-wheels/requests-2.32.5-py3-none-any.whl
Normal file
Binary file not shown.
BIN
repo/python-wheels/setuptools-80.9.0-py3-none-any.whl
Normal file
BIN
repo/python-wheels/setuptools-80.9.0-py3-none-any.whl
Normal file
Binary file not shown.
@@ -1,5 +1,7 @@
|
||||
kivy==2.1.0
|
||||
kivy>=2.3.0
|
||||
ffpyplayer
|
||||
requests==2.32.4
|
||||
bcrypt==4.2.1
|
||||
aiohttp==3.9.1
|
||||
asyncio==3.4.3
|
||||
asyncio==3.4.3
|
||||
evdev>=1.6.0
|
||||
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
|
||||
Binary file not shown.
100
src/edit_drowing.py
Normal file
100
src/edit_drowing.py
Normal file
@@ -0,0 +1,100 @@
|
||||
Kiwy drawing
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.graphics import Color, Line
|
||||
from kivy.core.window import Window
|
||||
|
||||
class DrawLayer(Widget):
|
||||
def init(self, **kwargs):
|
||||
super().init(**kwargs)
|
||||
self.strokes = [] # store all drawn lines
|
||||
self.current_color = (1, 0, 0) # default red
|
||||
self.current_width = 2 # default thickness
|
||||
self.drawing_enabled = False # drawing toggle
|
||||
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
if not self.drawing_enabled:
|
||||
return False
|
||||
|
||||
with self.canvas:
|
||||
Color(*self.current_color)
|
||||
new_line = Line(points=[touch.x, touch.y], width=self.current_width)
|
||||
self.strokes.append(new_line)
|
||||
return True
|
||||
|
||||
def on_touch_move(self, touch):
|
||||
if self.strokes and self.drawing_enabled:
|
||||
self.strokes[-1].points += [touch.x, touch.y]
|
||||
|
||||
# ==========================
|
||||
# UNDO LAST LINE
|
||||
# ==========================
|
||||
def undo(self):
|
||||
if self.strokes:
|
||||
last = self.strokes.pop()
|
||||
self.canvas.remove(last)
|
||||
|
||||
# ==========================
|
||||
# CHANGE COLOR
|
||||
# ==========================
|
||||
def set_color(self, color_tuple):
|
||||
self.current_color = color_tuple
|
||||
|
||||
# ==========================
|
||||
# CHANGE LINE WIDTH
|
||||
# ==========================
|
||||
def set_thickness(self, value):
|
||||
self.current_width = value
|
||||
class EditorUI(BoxLayout):
|
||||
def init(self, **kwargs):
|
||||
super().init(orientation="vertical", **kwargs)
|
||||
|
||||
|
||||
# Background image
|
||||
self.img = Image(source="graph.png", allow_stretch=True)
|
||||
self.add_widget(self.img)
|
||||
|
||||
# Drawing layer above image
|
||||
self.draw = DrawLayer()
|
||||
self.add_widget(self.draw)
|
||||
|
||||
# Toolbar
|
||||
toolbar = BoxLayout(size_hint_y=0.15)
|
||||
|
||||
toolbar.add_widget(Button(text="Draw On", on_press=self.toggle_draw))
|
||||
toolbar.add_widget(Button(text="Red", on_press=lambda x: self.draw.set_color((1,0,0))))
|
||||
toolbar.add_widget(Button(text="Blue", on_press=lambda x: self.draw.set_color((0,0,1))))
|
||||
toolbar.add_widget(Button(text="Green", on_press=lambda x: self.draw.set_color((0,1,0))))
|
||||
|
||||
toolbar.add_widget(Button(text="Thin", on_press=lambda x: self.draw.set_thickness(2)))
|
||||
toolbar.add_widget(Button(text="Thick", on_press=lambda x: self.draw.set_thickness(6)))
|
||||
|
||||
toolbar.add_widget(Button(text="Undo", on_press=lambda x: self.draw.undo()))
|
||||
toolbar.add_widget(Button(text="Save", on_press=lambda x: self.save_image()))
|
||||
|
||||
self.add_widget(toolbar)
|
||||
|
||||
# ==========================
|
||||
# TOGGLE DRAWING MODE
|
||||
# ==========================
|
||||
def toggle_draw(self, btn):
|
||||
self.draw.drawing_enabled = not self.draw.drawing_enabled
|
||||
btn.text = "Draw Off" if self.draw.drawing_enabled else "Draw On"
|
||||
|
||||
# ==========================
|
||||
# SAVE MERGED IMAGE
|
||||
# ==========================
|
||||
def save_image(self):
|
||||
self.export_to_png("edited_graph.png")
|
||||
print("Saved as edited_graph.png")
|
||||
class AnnotatorApp(App):
|
||||
def build(self):
|
||||
return EditorUI()
|
||||
|
||||
AnnotatorApp().run()
|
||||
|
||||
@@ -237,7 +237,8 @@ def download_media_files(playlist, media_dir):
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
|
||||
'duration': duration
|
||||
'duration': duration,
|
||||
'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag
|
||||
}
|
||||
updated_playlist.append(updated_media)
|
||||
|
||||
@@ -270,8 +271,53 @@ def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, kee
|
||||
os.remove(filepath)
|
||||
logger.info(f"🗑️ Deleted old playlist: {f}")
|
||||
|
||||
# TODO: Clean up unused media files
|
||||
logger.info(f"✅ Cleanup complete (kept {keep_versions} latest versions)")
|
||||
# Clean up unused media files
|
||||
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:
|
||||
logger.error(f"❌ Error during cleanup: {e}")
|
||||
|
||||
160
src/keyboard_widget.py
Normal file
160
src/keyboard_widget.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Custom Keyboard Widget for Signage Player
|
||||
Provides an on-screen keyboard for text input
|
||||
"""
|
||||
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.lang import Builder
|
||||
from kivy.animation import Animation
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.window import Window
|
||||
from kivy.graphics import Color, RoundedRectangle
|
||||
|
||||
class KeyboardWidget(FloatLayout):
|
||||
"""Custom on-screen keyboard widget"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(KeyboardWidget, self).__init__(**kwargs)
|
||||
self.target_input = None
|
||||
self.size_hint = (None, None)
|
||||
|
||||
# Calculate size - half screen width
|
||||
self.width = Window.width * 0.5
|
||||
self.height = self.width / 3
|
||||
|
||||
# Position at bottom center
|
||||
self.x = (Window.width - self.width) / 2
|
||||
self.y = 0
|
||||
|
||||
# Start hidden
|
||||
self.opacity = 0
|
||||
|
||||
# Create the keyboard UI
|
||||
self._build_keyboard()
|
||||
|
||||
# Bind window resize
|
||||
Window.bind(on_resize=self._on_window_resize)
|
||||
|
||||
Logger.info(f"KeyboardWidget: Initialized at ({self.x}, {self.y}) with size {self.width}x{self.height}")
|
||||
|
||||
def _build_keyboard(self):
|
||||
"""Build the keyboard UI"""
|
||||
# Background
|
||||
with self.canvas.before:
|
||||
Color(0.1, 0.1, 0.1, 0.95)
|
||||
self.bg_rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[15, 15, 0, 0])
|
||||
|
||||
self.bind(pos=self._update_bg, size=self._update_bg)
|
||||
|
||||
# Main layout
|
||||
main_layout = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
||||
main_layout.size = self.size
|
||||
main_layout.pos = self.pos
|
||||
|
||||
# Close button bar
|
||||
close_bar = BoxLayout(orientation='horizontal', size_hint=(1, None), height=40, padding=[5, 0])
|
||||
close_bar.add_widget(Widget())
|
||||
close_btn = Button(text='✕', size_hint=(None, 1), width=40,
|
||||
background_color=(0.8, 0.2, 0.2, 0.9), font_size='20sp', bold=True)
|
||||
close_btn.bind(on_press=lambda x: self.hide_keyboard())
|
||||
close_bar.add_widget(close_btn)
|
||||
main_layout.add_widget(close_bar)
|
||||
|
||||
# Number row
|
||||
self._add_key_row(main_layout, ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'])
|
||||
|
||||
# Top letter row
|
||||
self._add_key_row(main_layout, ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'])
|
||||
|
||||
# Middle letter row (with offset)
|
||||
middle_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
|
||||
for letter in ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L']:
|
||||
btn = Button(text=letter)
|
||||
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
|
||||
middle_row.add_widget(btn)
|
||||
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
|
||||
main_layout.add_widget(middle_row)
|
||||
|
||||
# Bottom letter row (with offset)
|
||||
bottom_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||
bottom_row.add_widget(Widget(size_hint=(1, 1)))
|
||||
for letter in ['Z', 'X', 'C', 'V', 'B', 'N', 'M']:
|
||||
btn = Button(text=letter)
|
||||
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
|
||||
bottom_row.add_widget(btn)
|
||||
bottom_row.add_widget(Widget(size_hint=(1, 1)))
|
||||
main_layout.add_widget(bottom_row)
|
||||
|
||||
# Space and backspace row
|
||||
last_row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||
backspace_btn = Button(text='←', size_hint=(0.3, 1), font_size='24sp')
|
||||
backspace_btn.bind(on_press=lambda x: self.key_pressed('backspace'))
|
||||
last_row.add_widget(backspace_btn)
|
||||
space_btn = Button(text='Space', size_hint=(0.7, 1))
|
||||
space_btn.bind(on_press=lambda x: self.key_pressed(' '))
|
||||
last_row.add_widget(space_btn)
|
||||
main_layout.add_widget(last_row)
|
||||
|
||||
self.add_widget(main_layout)
|
||||
|
||||
def _add_key_row(self, parent, keys):
|
||||
"""Add a row of keys"""
|
||||
row = BoxLayout(size_hint=(1, 1), spacing=3)
|
||||
for key in keys:
|
||||
btn = Button(text=key)
|
||||
btn.bind(on_press=lambda x, k=key: self.key_pressed(k.lower() if k.isalpha() else k))
|
||||
row.add_widget(btn)
|
||||
parent.add_widget(row)
|
||||
|
||||
def _update_bg(self, *args):
|
||||
"""Update background rectangle"""
|
||||
self.bg_rect.pos = self.pos
|
||||
self.bg_rect.size = self.size
|
||||
|
||||
def _on_window_resize(self, window, width, height):
|
||||
"""Handle window resize"""
|
||||
self.width = width * 0.5
|
||||
self.height = self.width / 3
|
||||
self.x = (width - self.width) / 2
|
||||
self.y = 0
|
||||
|
||||
def show_keyboard(self, target_input):
|
||||
"""Show the keyboard for a specific TextInput"""
|
||||
self.target_input = target_input
|
||||
Logger.info(f"KeyboardWidget: Showing keyboard for {target_input}")
|
||||
|
||||
# Animate keyboard appearing
|
||||
anim = Animation(opacity=1, duration=0.2)
|
||||
anim.start(self)
|
||||
|
||||
def hide_keyboard(self):
|
||||
"""Hide the keyboard"""
|
||||
Logger.info("KeyboardWidget: Hiding keyboard")
|
||||
|
||||
# Animate keyboard disappearing
|
||||
anim = Animation(opacity=0, duration=0.2)
|
||||
anim.start(self)
|
||||
|
||||
# Clear target
|
||||
if self.target_input:
|
||||
self.target_input.focus = False
|
||||
self.target_input = None
|
||||
|
||||
def key_pressed(self, key):
|
||||
"""Handle key press"""
|
||||
if not self.target_input:
|
||||
return
|
||||
|
||||
if key == 'backspace':
|
||||
# Remove last character
|
||||
if self.target_input.text:
|
||||
self.target_input.text = self.target_input.text[:-1]
|
||||
else:
|
||||
# Add character
|
||||
self.target_input.text += key
|
||||
|
||||
Logger.debug(f"KeyboardWidget: Key pressed '{key}', current text: {self.target_input.text}")
|
||||
1737
src/main.py
1737
src/main.py
File diff suppressed because it is too large
Load Diff
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",
|
||||
"auth_code": "aDHIMS2yx_HhfR0dWKy9VHaM_h0CKemfcsqv4Zgp0IY",
|
||||
"player_id": 2,
|
||||
"player_name": "Tv-Anunturi",
|
||||
"group_id": null,
|
||||
"hostname": "tv-terasa",
|
||||
"auth_code": "vkrxEO6eOTxkzXJBtoN4OuXc8eaX2mC3AB9ZePrnick",
|
||||
"player_id": 1,
|
||||
"player_name": "TV-acasa",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://172.18.0.1:5000"
|
||||
"server_url": "http://digi-signage.moto-adv.com"
|
||||
}
|
||||
@@ -275,6 +275,8 @@ class PlayerAuth:
|
||||
feedback_url = f"{server_url}/api/player-feedback"
|
||||
headers = {'Authorization': f'Bearer {auth_code}'}
|
||||
payload = {
|
||||
'hostname': self.auth_data.get('hostname'),
|
||||
'quickconnect_code': self.auth_data.get('quickconnect_code'),
|
||||
'message': message,
|
||||
'status': status,
|
||||
'playlist_version': playlist_version,
|
||||
|
||||
@@ -1,5 +1,193 @@
|
||||
#:kivy 2.1.0
|
||||
|
||||
# Custom On-Screen Keyboard Widget
|
||||
<CustomKeyboard@FloatLayout>:
|
||||
size_hint: None, None
|
||||
width: root.parent.width * 0.5 if root.parent else dp(600)
|
||||
height: self.width / 3
|
||||
pos: (root.parent.width - self.width) / 2 if root.parent else 0, 0
|
||||
opacity: 0
|
||||
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0.1, 0.1, 0.1, 0.95
|
||||
RoundedRectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
radius: [dp(15), dp(15), 0, 0]
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: dp(5)
|
||||
spacing: dp(5)
|
||||
|
||||
# Close button bar
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: dp(40)
|
||||
padding: [dp(5), 0]
|
||||
|
||||
Widget:
|
||||
|
||||
Button:
|
||||
text: '✕'
|
||||
size_hint: None, 1
|
||||
width: dp(40)
|
||||
background_color: 0.8, 0.2, 0.2, 0.9
|
||||
font_size: sp(20)
|
||||
bold: True
|
||||
on_press: root.parent.hide_keyboard() if root.parent and hasattr(root.parent, 'hide_keyboard') else None
|
||||
|
||||
# Number row
|
||||
BoxLayout:
|
||||
size_hint: 1, 1
|
||||
spacing: dp(3)
|
||||
Button:
|
||||
text: '1'
|
||||
on_press: root.parent.key_pressed('1') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '2'
|
||||
on_press: root.parent.key_pressed('2') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '3'
|
||||
on_press: root.parent.key_pressed('3') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '4'
|
||||
on_press: root.parent.key_pressed('4') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '5'
|
||||
on_press: root.parent.key_pressed('5') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '6'
|
||||
on_press: root.parent.key_pressed('6') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '7'
|
||||
on_press: root.parent.key_pressed('7') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '8'
|
||||
on_press: root.parent.key_pressed('8') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '9'
|
||||
on_press: root.parent.key_pressed('9') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: '0'
|
||||
on_press: root.parent.key_pressed('0') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
|
||||
# Top letter row (QWERTYUIOP)
|
||||
BoxLayout:
|
||||
size_hint: 1, 1
|
||||
spacing: dp(3)
|
||||
Button:
|
||||
text: 'Q'
|
||||
on_press: root.parent.key_pressed('q') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'W'
|
||||
on_press: root.parent.key_pressed('w') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'E'
|
||||
on_press: root.parent.key_pressed('e') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'R'
|
||||
on_press: root.parent.key_pressed('r') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'T'
|
||||
on_press: root.parent.key_pressed('t') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'Y'
|
||||
on_press: root.parent.key_pressed('y') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'U'
|
||||
on_press: root.parent.key_pressed('u') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'I'
|
||||
on_press: root.parent.key_pressed('i') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'O'
|
||||
on_press: root.parent.key_pressed('o') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'P'
|
||||
on_press: root.parent.key_pressed('p') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
|
||||
# Middle letter row (ASDFGHJKL)
|
||||
BoxLayout:
|
||||
size_hint: 1, 1
|
||||
spacing: dp(3)
|
||||
Widget:
|
||||
size_hint: 0.5, 1
|
||||
Button:
|
||||
text: 'A'
|
||||
on_press: root.parent.key_pressed('a') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'S'
|
||||
on_press: root.parent.key_pressed('s') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'D'
|
||||
on_press: root.parent.key_pressed('d') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'F'
|
||||
on_press: root.parent.key_pressed('f') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'G'
|
||||
on_press: root.parent.key_pressed('g') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'H'
|
||||
on_press: root.parent.key_pressed('h') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'J'
|
||||
on_press: root.parent.key_pressed('j') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'K'
|
||||
on_press: root.parent.key_pressed('k') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'L'
|
||||
on_press: root.parent.key_pressed('l') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Widget:
|
||||
size_hint: 0.5, 1
|
||||
|
||||
# Bottom letter row (ZXCVBNM)
|
||||
BoxLayout:
|
||||
size_hint: 1, 1
|
||||
spacing: dp(3)
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
Button:
|
||||
text: 'Z'
|
||||
on_press: root.parent.key_pressed('z') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'X'
|
||||
on_press: root.parent.key_pressed('x') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'C'
|
||||
on_press: root.parent.key_pressed('c') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'V'
|
||||
on_press: root.parent.key_pressed('v') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'B'
|
||||
on_press: root.parent.key_pressed('b') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'N'
|
||||
on_press: root.parent.key_pressed('n') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'M'
|
||||
on_press: root.parent.key_pressed('m') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
|
||||
# Bottom row (Space, Backspace)
|
||||
BoxLayout:
|
||||
size_hint: 1, 1
|
||||
spacing: dp(3)
|
||||
Button:
|
||||
text: '←'
|
||||
size_hint: 0.3, 1
|
||||
font_size: sp(24)
|
||||
on_press: root.parent.key_pressed('backspace') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
Button:
|
||||
text: 'Space'
|
||||
size_hint: 0.7, 1
|
||||
on_press: root.parent.key_pressed(' ') if root.parent and hasattr(root.parent, 'key_pressed') else None
|
||||
|
||||
<SignagePlayer@FloatLayout>:
|
||||
size: root.screen_width, root.screen_height
|
||||
canvas.before:
|
||||
@@ -36,12 +224,12 @@
|
||||
pos: self.pos
|
||||
radius: [dp(15)]
|
||||
|
||||
# New control panel overlay (bottom center, 1/6 width, 90% transparent)
|
||||
# New control panel overlay (bottom center, width for 6 buttons, 90% transparent)
|
||||
BoxLayout:
|
||||
id: controls_layout
|
||||
orientation: 'horizontal'
|
||||
size_hint: None, None
|
||||
width: root.width / 6 if root.width > 0 else dp(260)
|
||||
width: dp(370)
|
||||
height: dp(70)
|
||||
pos: (root.width - self.width) / 2, dp(10)
|
||||
opacity: 1
|
||||
@@ -68,11 +256,19 @@
|
||||
id: play_pause_btn
|
||||
size_hint: None, None
|
||||
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'
|
||||
border: (0, 0, 0, 0)
|
||||
on_press: root.toggle_pause()
|
||||
|
||||
Button:
|
||||
id: edit_btn
|
||||
size_hint: None, None
|
||||
size: dp(50), dp(50)
|
||||
background_normal: root.resources_path + '/pencil.png'
|
||||
background_down: root.resources_path + '/pencil.png'
|
||||
border: (0, 0, 0, 0)
|
||||
on_press: root.show_edit_interface()
|
||||
Button:
|
||||
id: settings_btn
|
||||
size_hint: None, None
|
||||
@@ -126,6 +322,9 @@
|
||||
font_size: sp(16)
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
write_tab: False
|
||||
readonly: True
|
||||
on_focus: root.on_input_focus(self, self.focus)
|
||||
|
||||
Label:
|
||||
id: error_label
|
||||
@@ -180,6 +379,8 @@
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Screen name
|
||||
BoxLayout:
|
||||
@@ -200,6 +401,8 @@
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Quickconnect key
|
||||
BoxLayout:
|
||||
@@ -220,6 +423,8 @@
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Orientation
|
||||
BoxLayout:
|
||||
@@ -240,6 +445,8 @@
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Touch
|
||||
BoxLayout:
|
||||
@@ -260,6 +467,8 @@
|
||||
size_hint_x: 0.7
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Resolution
|
||||
BoxLayout:
|
||||
@@ -281,40 +490,134 @@
|
||||
multiline: False
|
||||
font_size: sp(14)
|
||||
hint_text: '1920x1080 or auto'
|
||||
write_tab: False
|
||||
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
|
||||
|
||||
# Edit Feature Enable/Disable
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
text: 'Enable Edit Feature:'
|
||||
size_hint_x: 0.3
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
|
||||
CheckBox:
|
||||
id: edit_enabled_checkbox
|
||||
size_hint_x: None
|
||||
width: dp(40)
|
||||
active: True
|
||||
on_active: root.on_edit_feature_toggle(self.active)
|
||||
|
||||
Label:
|
||||
text: '(Allow editing images on this player)'
|
||||
size_hint_x: 0.4
|
||||
font_size: sp(12)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.1
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Status information
|
||||
# Reset Buttons Section
|
||||
Label:
|
||||
id: playlist_info
|
||||
text: 'Playlist Version: N/A'
|
||||
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)
|
||||
|
||||
Label:
|
||||
id: media_count_info
|
||||
text: 'Media Count: 0'
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
Button:
|
||||
id: reset_auth_btn
|
||||
text: 'Reset Player Auth'
|
||||
background_color: 0.8, 0.4, 0.2, 1
|
||||
on_press: root.reset_player_auth()
|
||||
|
||||
Label:
|
||||
id: status_info
|
||||
text: 'Status: Idle'
|
||||
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
|
||||
Button:
|
||||
id: test_connection_btn
|
||||
text: 'Test Server Connection'
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
height: dp(50)
|
||||
background_color: 0.2, 0.4, 0.8, 1
|
||||
on_press: root.test_connection()
|
||||
|
||||
# Connection Status Label
|
||||
Label:
|
||||
id: connection_status
|
||||
text: 'Click button to test connection'
|
||||
size_hint_y: None
|
||||
height: dp(40)
|
||||
text_size: self.size
|
||||
halign: 'left'
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
color: 0.7, 0.7, 0.7, 1
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.2
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Status information row
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: dp(30)
|
||||
spacing: dp(10)
|
||||
|
||||
Label:
|
||||
id: playlist_info
|
||||
text: 'Playlist: N/A'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Label:
|
||||
id: media_count_info
|
||||
text: 'Media: 0'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Label:
|
||||
id: status_info
|
||||
text: 'Status: Idle'
|
||||
text_size: self.size
|
||||
halign: 'center'
|
||||
valign: 'middle'
|
||||
font_size: sp(12)
|
||||
|
||||
Widget:
|
||||
size_hint_y: 0.05
|
||||
|
||||
# Action buttons
|
||||
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()
|
||||
160
start.sh
160
start.sh
@@ -1,58 +1,152 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Kivy Signage Player Startup Script
|
||||
# This script activates the virtual environment and starts the player
|
||||
# Kivy Signage Player Startup Script with Watchdog
|
||||
# This script monitors and auto-restarts the player if it crashes
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Starting Kivy Signage Player"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
# Configuration
|
||||
MAX_RETRIES=999999 # Effectively unlimited retries
|
||||
RESTART_DELAY=5 # Seconds to wait before restart
|
||||
HEALTH_CHECK_INTERVAL=30 # Seconds between health checks
|
||||
HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
|
||||
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
|
||||
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
|
||||
|
||||
# Function to log messages
|
||||
log_message() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Function to check if player is healthy
|
||||
check_health() {
|
||||
# Check if heartbeat file exists and is recent (within last 60 seconds)
|
||||
if [ -f "$HEARTBEAT_FILE" ]; then
|
||||
local last_update=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null || echo 0)
|
||||
local current_time=$(date +%s)
|
||||
local diff=$((current_time - last_update))
|
||||
|
||||
if [ $diff -lt 60 ]; then
|
||||
return 0 # Healthy
|
||||
else
|
||||
log_message "⚠️ Player heartbeat stale (${diff}s old)"
|
||||
return 1 # Unhealthy
|
||||
fi
|
||||
else
|
||||
# If heartbeat file doesn't exist yet, assume player is starting
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
log_message "🛑 Watchdog received stop signal"
|
||||
rm -f "$HEARTBEAT_FILE"
|
||||
rm -f "$STOP_FLAG_FILE"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Trap signals for graceful shutdown
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
log_message "=========================================="
|
||||
log_message "🚀 Kivy Signage Player Watchdog Started"
|
||||
log_message "=========================================="
|
||||
log_message "Project directory: $SCRIPT_DIR"
|
||||
log_message "Max retries: Unlimited"
|
||||
log_message "Restart delay: ${RESTART_DELAY}s"
|
||||
log_message ""
|
||||
|
||||
# Remove old stop flag if exists (fresh start)
|
||||
rm -f "$STOP_FLAG_FILE"
|
||||
|
||||
# Change to the project directory
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Project directory: $SCRIPT_DIR"
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ -d ".venv" ]; then
|
||||
echo "Activating virtual environment..."
|
||||
log_message "✓ Virtual environment found"
|
||||
source .venv/bin/activate
|
||||
echo "✓ Virtual environment activated"
|
||||
else
|
||||
echo "Warning: Virtual environment not found at .venv/"
|
||||
echo "Creating virtual environment..."
|
||||
log_message "⚠️ Creating virtual environment..."
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
echo "Installing dependencies..."
|
||||
log_message "📦 Installing dependencies..."
|
||||
pip3 install -r requirements.txt
|
||||
echo "✓ Virtual environment created and dependencies installed"
|
||||
log_message "✓ Virtual environment ready"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if configuration exists
|
||||
if [ ! -f "config/app_config.txt" ]; then
|
||||
echo "=========================================="
|
||||
echo "⚠ WARNING: Configuration file not found!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Please configure the player before running:"
|
||||
echo " 1. Copy config/app_config.txt.example to config/app_config.txt"
|
||||
echo " 2. Edit the configuration file with your server details"
|
||||
echo ""
|
||||
read -p "Press Enter to continue anyway, or Ctrl+C to exit..."
|
||||
echo ""
|
||||
if [ ! -f "config/app_config.json" ]; then
|
||||
log_message "⚠️ WARNING: Configuration file not found!"
|
||||
log_message "Player may not function correctly without configuration"
|
||||
fi
|
||||
|
||||
# Change to src directory and start the application
|
||||
echo "Starting application..."
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
# Main watchdog loop
|
||||
retry_count=0
|
||||
while true; do
|
||||
retry_count=$((retry_count + 1))
|
||||
|
||||
log_message ""
|
||||
log_message "=========================================="
|
||||
log_message "▶️ Starting player (attempt #${retry_count})"
|
||||
log_message "=========================================="
|
||||
|
||||
# Clean old heartbeat
|
||||
rm -f "$HEARTBEAT_FILE"
|
||||
|
||||
# Start the player
|
||||
cd "$SCRIPT_DIR/src"
|
||||
python3 main.py &
|
||||
PLAYER_PID=$!
|
||||
|
||||
log_message "Player PID: $PLAYER_PID"
|
||||
|
||||
# Monitor the player
|
||||
while true; do
|
||||
sleep $HEALTH_CHECK_INTERVAL
|
||||
|
||||
# Check if process is still running
|
||||
if ! kill -0 $PLAYER_PID 2>/dev/null; then
|
||||
log_message "❌ Player process crashed or stopped (PID: $PLAYER_PID)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Check health via heartbeat
|
||||
if ! check_health; then
|
||||
log_message "❌ Player health check failed - may be frozen"
|
||||
kill $PLAYER_PID 2>/dev/null
|
||||
sleep 2
|
||||
kill -9 $PLAYER_PID 2>/dev/null
|
||||
break
|
||||
fi
|
||||
|
||||
# Player is healthy, continue monitoring
|
||||
done
|
||||
|
||||
# Player stopped or crashed
|
||||
# Check if user requested intentional exit
|
||||
if [ -f "$STOP_FLAG_FILE" ]; then
|
||||
log_message "✋ Stop flag detected - user requested exit via password"
|
||||
log_message "Watchdog will NOT restart the player"
|
||||
log_message "To restart, run ./start.sh again"
|
||||
rm -f "$HEARTBEAT_FILE"
|
||||
break
|
||||
fi
|
||||
|
||||
log_message "⏳ Waiting ${RESTART_DELAY}s before restart..."
|
||||
sleep $RESTART_DELAY
|
||||
|
||||
# Cleanup any zombie processes
|
||||
pkill -9 -f "python3 main.py" 2>/dev/null
|
||||
|
||||
done
|
||||
|
||||
cd src
|
||||
python3 main.py
|
||||
log_message ""
|
||||
log_message "=========================================="
|
||||
log_message "Watchdog stopped"
|
||||
log_message "=========================================="
|
||||
|
||||
# Deactivate virtual environment when app exits
|
||||
# Deactivate virtual environment (this line is never reached in watchdog mode)
|
||||
deactivate
|
||||
|
||||
34
stop_player.sh
Executable file
34
stop_player.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Stop the player and watchdog
|
||||
# Use this to gracefully shutdown the player
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Stopping Kivy Signage Player"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Kill watchdog (start.sh)
|
||||
echo "Stopping watchdog..."
|
||||
pkill -f "bash.*start.sh"
|
||||
|
||||
# Kill player
|
||||
echo "Stopping player..."
|
||||
pkill -f "python3 main.py"
|
||||
|
||||
# Give processes time to exit gracefully
|
||||
sleep 2
|
||||
|
||||
# Force kill if still running
|
||||
pkill -9 -f "bash.*start.sh" 2>/dev/null
|
||||
pkill -9 -f "python3 main.py" 2>/dev/null
|
||||
|
||||
# Clean up heartbeat and stop flag files
|
||||
rm -f "$SCRIPT_DIR/.player_heartbeat"
|
||||
rm -f "$SCRIPT_DIR/.player_stop_requested"
|
||||
|
||||
echo ""
|
||||
echo "✓ Player and watchdog stopped"
|
||||
echo ""
|
||||
182
working_files/CARD_READER_AUTHENTICATION.md
Normal file
182
working_files/CARD_READER_AUTHENTICATION.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# USB Card Reader Authentication
|
||||
|
||||
This document describes the USB card reader authentication feature for the Kiwy Signage Player.
|
||||
|
||||
## Overview
|
||||
|
||||
The player now supports user authentication via USB card readers when accessing the edit/drawing interface. When a user clicks the edit button (pencil icon), they must swipe their card to authenticate before being allowed to edit the image.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Edit Button Click**: User clicks the pencil icon to edit the current image
|
||||
2. **Validation Checks**:
|
||||
- Verify current media is an image (not video)
|
||||
- Check if editing is allowed for this media (`edit_on_player` permission from server)
|
||||
3. **Card Reader Prompt**:
|
||||
- Display "Please swipe your card..." message
|
||||
- Wait for card swipe (5 second timeout)
|
||||
- Read card data from USB card reader
|
||||
- Store the card data (no validation required)
|
||||
4. **Open Edit Interface**: Edit interface opens with card data stored
|
||||
5. **Save & Upload**: When user saves the edited image:
|
||||
- Card data is included in the metadata JSON
|
||||
- Both image and metadata (with card data) are uploaded to server
|
||||
- Server receives `user_card_data` field for tracking who edited the image
|
||||
|
||||
## Card Reader Setup
|
||||
|
||||
### Hardware Requirements
|
||||
- USB card reader (HID/keyboard emulation type)
|
||||
- Compatible cards (magnetic stripe or RFID depending on reader)
|
||||
|
||||
### Software Requirements
|
||||
The player requires the `evdev` Python library to interface with USB input devices:
|
||||
|
||||
```bash
|
||||
# Install via apt (recommended for Raspberry Pi)
|
||||
sudo apt-get install python3-evdev
|
||||
|
||||
# Or via pip
|
||||
pip3 install evdev
|
||||
```
|
||||
|
||||
### Fallback Mode
|
||||
If `evdev` is not available, the player will:
|
||||
- Log a warning message
|
||||
- Use a default card value (`DEFAULT_USER_12345`) for testing
|
||||
- This allows development and testing without hardware
|
||||
|
||||
## Card Data Storage
|
||||
|
||||
The card data is captured as a raw string and stored without validation or mapping:
|
||||
|
||||
- **No preprocessing**: Card data is stored exactly as received from the reader
|
||||
- **Format**: Whatever the card reader sends (typically numeric or alphanumeric)
|
||||
- **Sent to server**: Raw card data is included in the `user_card_data` field of the metadata JSON
|
||||
- **Server-side processing**: The server can validate, map, or process the card data as needed
|
||||
|
||||
### Metadata JSON Format
|
||||
When an image is saved, the metadata includes:
|
||||
```json
|
||||
{
|
||||
"time_of_modification": "2025-12-08T10:30:00",
|
||||
"original_name": "image.jpg",
|
||||
"new_name": "image_e_v1.jpg",
|
||||
"original_path": "/path/to/image.jpg",
|
||||
"version": 1,
|
||||
"user_card_data": "123456789"
|
||||
}
|
||||
```
|
||||
|
||||
If no card is swiped (timeout), `user_card_data` will be `null`.
|
||||
|
||||
## Testing the Card Reader
|
||||
|
||||
A test utility is provided to verify card reader functionality:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
The test tool will:
|
||||
1. List all available input devices
|
||||
2. Auto-detect the card reader (or let you select manually)
|
||||
3. Listen for card swipes and display the data received
|
||||
4. Show how the data will be processed
|
||||
|
||||
### Test Output Example
|
||||
```
|
||||
✓ Card data received: '123456789'
|
||||
Length: 9 characters
|
||||
Processed ID: card_123456789
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Main Components
|
||||
|
||||
1. **CardReader Class** (`main.py`)
|
||||
- Handles USB device detection
|
||||
- Reads input events from card reader
|
||||
- Provides async callback interface
|
||||
- Includes timeout handling (5 seconds)
|
||||
|
||||
2. **Card Read Flow** (`show_edit_interface()` method)
|
||||
- Validates media type and permissions
|
||||
- Initiates card read
|
||||
- Stores raw card data
|
||||
- Opens edit popup
|
||||
|
||||
3. **Metadata Creation** (`_save_metadata()` method)
|
||||
- Includes card data in metadata JSON
|
||||
- No processing or validation of card data
|
||||
- Sent to server as-is
|
||||
|
||||
### Card Data Format
|
||||
|
||||
Card readers typically send data as keyboard input:
|
||||
- Each character is sent as a key press event
|
||||
- Data ends with an ENTER key press
|
||||
- Reader format: `[CARD_DATA][ENTER]`
|
||||
|
||||
The CardReader class:
|
||||
- Captures key press events
|
||||
- Builds the card data string character by character
|
||||
- Completes reading when ENTER is detected
|
||||
- Returns the complete card data to the callback
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **Server-Side Validation**: Card validation should be implemented on the server
|
||||
2. **Timeout**: 5-second timeout prevents infinite waiting for card swipe
|
||||
3. **Logging**: All card reads are logged with the raw card data
|
||||
4. **Permissions**: Edit permission must be enabled on the server (`edit_on_player`)
|
||||
5. **Raw Data**: Card data is sent as-is; server is responsible for validation and authorization
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Card Reader Not Detected
|
||||
- Check USB connection
|
||||
- Run `ls /dev/input/` to see available devices
|
||||
- Run the test script to verify detection
|
||||
- Check `evdev` is installed: `python3 -c "import evdev"`
|
||||
|
||||
### Card Swipes Not Recognized
|
||||
- Verify card reader sends keyboard events
|
||||
- Test with the `test_card_reader.py` utility
|
||||
- Check card format is compatible with reader
|
||||
- Ensure card is swiped smoothly at proper speed
|
||||
|
||||
### Card Data Not Captured
|
||||
- Check card data format in logs
|
||||
- Enable debug logging to see raw card data
|
||||
- Test in fallback mode (without evdev) to isolate hardware issues
|
||||
- Verify card swipe completes within 5-second timeout
|
||||
|
||||
### Permission Denied Errors
|
||||
- User may need to be in the `input` group:
|
||||
```bash
|
||||
sudo usermod -a -G input $USER
|
||||
```
|
||||
- Reboot after adding user to group
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for the card reader system:
|
||||
|
||||
1. **Server Validation**: Server validates cards against database and returns authorization
|
||||
2. **Card Enrollment**: Server-side UI for registering new cards
|
||||
3. **Multiple Card Types**: Support for different card formats (barcode, RFID, magnetic)
|
||||
4. **Client-side Validation**: Add optional local card validation before opening edit
|
||||
5. **Audit Trail**: Server tracks all card usage with timestamps
|
||||
6. **RFID Support**: Test and optimize for RFID readers
|
||||
7. **Barcode Scanners**: Support USB barcode scanners as alternative
|
||||
8. **Retry Logic**: Allow re-swipe if card read fails
|
||||
|
||||
## Related Files
|
||||
|
||||
- `/src/main.py` - Main implementation (CardReader class, authentication flow)
|
||||
- `/src/edit_drowing.py` - Drawing/editing interface (uses authenticated user)
|
||||
- `/working_files/test_card_reader.py` - Card reader test utility
|
||||
- `/requirements.txt` - Dependencies (includes evdev)
|
||||
170
working_files/CARD_READER_FIX.md
Normal file
170
working_files/CARD_READER_FIX.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Card Reader Fix - Multi-USB Device Support
|
||||
|
||||
## Problem Description
|
||||
|
||||
When a USB touchscreen was connected to the Raspberry Pi, the card reader authentication was not working. The system reported "no authentication was received" even though the card reader was physically connected on a different USB port.
|
||||
|
||||
### Root Cause
|
||||
|
||||
The original `find_card_reader()` function used overly broad matching criteria:
|
||||
1. It would select the **first** device with "keyboard" in its name
|
||||
2. USB touchscreens often register as HID keyboard devices (for touch input)
|
||||
3. The touchscreen would be detected first, blocking the actual card reader
|
||||
4. No exclusion logic existed to filter out touch devices
|
||||
|
||||
## Solution
|
||||
|
||||
The fix implements a **priority-based device selection** with **exclusion filters**:
|
||||
|
||||
### 1. Device Exclusion List
|
||||
Devices containing these keywords are now skipped:
|
||||
- `touch`, `touchscreen`
|
||||
- `mouse`, `mice`
|
||||
- `trackpad`, `touchpad`
|
||||
- `pen`, `stylus`
|
||||
- `video`, `button`, `lid`
|
||||
|
||||
### 2. Three-Priority Device Search
|
||||
|
||||
**Priority 1: Explicit Card Readers**
|
||||
- Devices with "card", "reader", "rfid", or "hid" in their name
|
||||
- Must have keyboard capabilities (EV_KEY)
|
||||
- Excludes any device matching exclusion keywords
|
||||
|
||||
**Priority 2: USB Keyboards**
|
||||
- Devices with both "usb" AND "keyboard" in their name
|
||||
- Card readers typically appear as "USB Keyboard" or similar
|
||||
- Excludes touch devices and other non-card peripherals
|
||||
|
||||
**Priority 3: Fallback to Any Keyboard**
|
||||
- Any keyboard device not in the exclusion list
|
||||
- Used only if no card reader or USB keyboard is found
|
||||
|
||||
### 3. Enhanced Logging
|
||||
|
||||
The system now logs:
|
||||
- All detected input devices at startup
|
||||
- Which devices are being skipped and why
|
||||
- Which device is ultimately selected as the card reader
|
||||
|
||||
## Testing
|
||||
|
||||
### Using the Test Script
|
||||
|
||||
Run the enhanced test script to identify your card reader:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. List all input devices with helpful indicators:
|
||||
- `** LIKELY CARD READER **` - devices with "card" or "reader" in name
|
||||
- `(Excluded: ...)` - devices that will be skipped
|
||||
- `(USB Keyboard - could be card reader)` - potential card readers
|
||||
|
||||
2. Auto-detect the card reader using the same logic as the main app
|
||||
|
||||
3. Allow manual selection by device number if auto-detection is wrong
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
=== Available Input Devices ===
|
||||
|
||||
[0] /dev/input/event0
|
||||
Name: USB Touchscreen Controller
|
||||
Phys: usb-0000:01:00.0-1.1/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
(Excluded: appears to be touch/mouse/other non-card device)
|
||||
|
||||
[1] /dev/input/event1
|
||||
Name: HID 08ff:0009
|
||||
Phys: usb-0000:01:00.0-1.2/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
** LIKELY CARD READER **
|
||||
|
||||
[2] /dev/input/event2
|
||||
Name: Logitech USB Keyboard
|
||||
Phys: usb-0000:01:00.0-1.3/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
(USB Keyboard - could be card reader)
|
||||
```
|
||||
|
||||
### Verifying the Fix
|
||||
|
||||
1. **Check Logs**: When the main app starts, check the logs for device detection:
|
||||
```bash
|
||||
tail -f /path/to/logfile
|
||||
```
|
||||
|
||||
Look for messages like:
|
||||
```
|
||||
CardReader: Scanning input devices...
|
||||
CardReader: Skipping excluded device: USB Touchscreen Controller
|
||||
CardReader: Found card reader: HID 08ff:0009 at /dev/input/event1
|
||||
```
|
||||
|
||||
2. **Test Card Swipe**:
|
||||
- Start the signage player
|
||||
- Click the edit button (pencil icon)
|
||||
- Swipe a card
|
||||
- Should successfully authenticate
|
||||
|
||||
3. **Multiple USB Devices**: Test with various USB configurations:
|
||||
- Touchscreen + card reader
|
||||
- Mouse + keyboard + card reader
|
||||
- Multiple USB hubs
|
||||
|
||||
## Configuration
|
||||
|
||||
### If Auto-Detection Fails
|
||||
|
||||
If the automatic detection still selects the wrong device, you can:
|
||||
|
||||
1. **Check device names**: Run `test_card_reader.py` to see all devices
|
||||
2. **Identify your card reader**: Note the exact name of your card reader
|
||||
3. **Add custom exclusions**: If needed, add more keywords to the exclusion list
|
||||
4. **Manual override**: Modify the priority logic to match your specific hardware
|
||||
|
||||
### Permissions
|
||||
|
||||
Ensure the user running the app has permission to access input devices:
|
||||
|
||||
```bash
|
||||
# Add user to input group
|
||||
sudo usermod -a -G input $USER
|
||||
|
||||
# Logout and login again for changes to take effect
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **src/main.py**
|
||||
- Updated `CardReader.find_card_reader()` method
|
||||
- Added exclusion keyword list
|
||||
- Implemented priority-based search
|
||||
- Enhanced logging
|
||||
|
||||
2. **working_files/test_card_reader.py**
|
||||
- Updated `list_input_devices()` to show device classifications
|
||||
- Updated `test_card_reader()` to use same logic as main app
|
||||
- Added visual indicators for device types
|
||||
|
||||
## Compatibility
|
||||
|
||||
This fix is backward compatible:
|
||||
- Works with single-device setups (no touchscreen)
|
||||
- Works with multiple USB devices
|
||||
- Fallback behavior unchanged for systems without card readers
|
||||
- No changes to card data format or server communication
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for specific use cases:
|
||||
|
||||
1. **Configuration file**: Allow specifying device path or name pattern
|
||||
2. **Device caching**: Remember the working device path to avoid re-scanning
|
||||
3. **Hot-plug support**: Detect when card reader is plugged in after app starts
|
||||
4. **Multi-reader support**: Support for multiple card readers simultaneously
|
||||
211
working_files/DEBUGGING_MEDIA_SKIPS.md
Normal file
211
working_files/DEBUGGING_MEDIA_SKIPS.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Debugging Media File Skips - Guide
|
||||
|
||||
## Summary
|
||||
Your playlist has been analyzed and all 3 media files are present and valid:
|
||||
- ✅ music.jpg (36,481 bytes) - IMAGE - 15s
|
||||
- ✅ 130414-746934884.mp4 (6,474,921 bytes) - VIDEO - 23s
|
||||
- ✅ IMG_0386.jpeg (592,162 bytes) - IMAGE - 15s
|
||||
|
||||
## Enhanced Logging Added
|
||||
The application has been updated with detailed logging to track:
|
||||
- When each media file starts playing
|
||||
- File path validation
|
||||
- File size and existence checks
|
||||
- Media type detection
|
||||
- Widget creation steps
|
||||
- Scheduling of next media
|
||||
- Any errors or skips
|
||||
|
||||
## How to See Detailed Logs
|
||||
|
||||
### Method 1: Run with log output
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
source .venv/bin/activate
|
||||
cd src
|
||||
python3 main.py 2>&1 | tee playback.log
|
||||
```
|
||||
|
||||
### Method 2: Check Kivy logs location
|
||||
Kivy logs are typically stored in:
|
||||
- Linux: `~/.kivy/logs/`
|
||||
- Check with: `ls -lth ~/.kivy/logs/ | head`
|
||||
|
||||
## Common Reasons Media Files Get Skipped
|
||||
|
||||
### 1. **File Not Found**
|
||||
**Symptom**: Log shows "❌ Media file not found"
|
||||
**Cause**: File doesn't exist at expected path
|
||||
**Solution**: Run diagnostic tool
|
||||
```bash
|
||||
python3 diagnose_playlist.py
|
||||
```
|
||||
|
||||
### 2. **Unsupported File Type**
|
||||
**Symptom**: Log shows "❌ Unsupported media type"
|
||||
**Supported formats**:
|
||||
- Videos: .mp4, .avi, .mkv, .mov, .webm
|
||||
- Images: .jpg, .jpeg, .png, .bmp, .gif
|
||||
**Solution**: Convert files or check extension
|
||||
|
||||
### 3. **Video Codec Issues**
|
||||
**Symptom**: Video file exists but doesn't play
|
||||
**Cause**: Video codec not supported by ffpyplayer
|
||||
**Check**: Look for error in logs about codec
|
||||
**Solution**: Re-encode video with H.264 codec:
|
||||
```bash
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset fast -crf 23 output.mp4
|
||||
```
|
||||
|
||||
### 4. **Corrupted Media Files**
|
||||
**Symptom**: File exists but throws error when loading
|
||||
**Check**: Try playing file with external player
|
||||
```bash
|
||||
# For images
|
||||
feh media/music.jpg
|
||||
|
||||
# For videos
|
||||
vlc media/130414-746934884.mp4
|
||||
# or
|
||||
ffplay media/130414-746934884.mp4
|
||||
```
|
||||
|
||||
### 5. **Memory/Performance Issues**
|
||||
**Symptom**: First few files play, then skipping increases
|
||||
**Cause**: Memory leak or performance degradation
|
||||
**Check**: Look for "consecutive_errors" in logs
|
||||
**Solution**:
|
||||
- Reduce resolution setting in settings popup
|
||||
- Optimize video files (lower bitrate/resolution)
|
||||
|
||||
### 6. **Timing Issues**
|
||||
**Symptom**: Files play too fast or skip immediately
|
||||
**Cause**: Duration set too low or scheduler issues
|
||||
**Check**: Verify durations in playlist.json
|
||||
**Current durations**: 15s (images), 23s (video)
|
||||
|
||||
### 7. **Permission Issues**
|
||||
**Symptom**: "Permission denied" in logs
|
||||
**Check**: File permissions
|
||||
```bash
|
||||
ls -la media/
|
||||
```
|
||||
**Solution**: Fix permissions
|
||||
```bash
|
||||
chmod 644 media/*
|
||||
```
|
||||
|
||||
## What to Look For in Logs
|
||||
|
||||
### Successful Playback Pattern:
|
||||
```
|
||||
SignagePlayer: ===== Playing item 1/3 =====
|
||||
SignagePlayer: File: music.jpg
|
||||
SignagePlayer: Duration: 15s
|
||||
SignagePlayer: Full path: /path/to/media/music.jpg
|
||||
SignagePlayer: ✓ File exists (size: 36,481 bytes)
|
||||
SignagePlayer: Extension: .jpg
|
||||
SignagePlayer: Media type: IMAGE
|
||||
SignagePlayer: Creating AsyncImage widget...
|
||||
SignagePlayer: Adding image widget to content area...
|
||||
SignagePlayer: Scheduled next media in 15s
|
||||
SignagePlayer: ✓ Image displayed successfully
|
||||
SignagePlayer: ✓ Media started successfully (consecutive_errors reset to 0)
|
||||
```
|
||||
|
||||
### Skip Pattern (File Not Found):
|
||||
```
|
||||
SignagePlayer: ===== Playing item 2/3 =====
|
||||
SignagePlayer: File: missing.mp4
|
||||
SignagePlayer: Full path: /path/to/media/missing.mp4
|
||||
SignagePlayer: ❌ Media file not found: /path/to/media/missing.mp4
|
||||
SignagePlayer: Skipping to next media...
|
||||
SignagePlayer: Transitioning to next media (was index 1)
|
||||
```
|
||||
|
||||
### Video Loading Error:
|
||||
```
|
||||
SignagePlayer: Loading video file.mp4 for 23s
|
||||
SignagePlayer: Video provider: ffpyplayer
|
||||
[ERROR ] [Video ] Error reading video
|
||||
[ERROR ] SignagePlayer: Error playing video: ...
|
||||
```
|
||||
|
||||
## Testing Tools Provided
|
||||
|
||||
### 1. Diagnostic Tool
|
||||
```bash
|
||||
python3 diagnose_playlist.py
|
||||
```
|
||||
Checks:
|
||||
- Playlist file exists and is valid
|
||||
- All media files exist
|
||||
- File types are supported
|
||||
- No case sensitivity issues
|
||||
|
||||
### 2. Playback Simulation
|
||||
```bash
|
||||
python3 test_playback_logging.py
|
||||
```
|
||||
Simulates the playback sequence without running the GUI
|
||||
|
||||
## Monitoring Live Playback
|
||||
|
||||
To see live logs while the app is running:
|
||||
```bash
|
||||
# Terminal 1: Start the app
|
||||
./run_player.sh
|
||||
|
||||
# Terminal 2: Monitor logs
|
||||
tail -f ~/.kivy/logs/kivy_*.txt
|
||||
```
|
||||
|
||||
## Quick Fixes to Try
|
||||
|
||||
### 1. Clear any stuck state
|
||||
```bash
|
||||
rm -f src/*.pyc
|
||||
rm -rf src/__pycache__
|
||||
```
|
||||
|
||||
### 2. Test with simpler playlist
|
||||
Create `playlists/test_playlist_v9.json`:
|
||||
```json
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "music.jpg",
|
||||
"url": "media/music.jpg",
|
||||
"duration": 5
|
||||
}
|
||||
],
|
||||
"version": 9
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check video compatibility
|
||||
```bash
|
||||
# Install ffmpeg tools if not present
|
||||
sudo apt-get install ffmpeg
|
||||
|
||||
# Check video info
|
||||
ffprobe media/130414-746934884.mp4
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
When reporting issues, please provide:
|
||||
1. Output from `python3 diagnose_playlist.py`
|
||||
2. Last 100 lines of Kivy log file
|
||||
3. Any error messages from console
|
||||
4. What you observe (which files skip? pattern?)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Run the app** and observe the console output
|
||||
2. **Check logs** for error patterns
|
||||
3. **Run diagnostic** if files are skipping
|
||||
4. **Test individual files** with external players if needed
|
||||
5. **Re-encode videos** if codec issues found
|
||||
|
||||
The enhanced logging will now tell you exactly why each file is being skipped!
|
||||
182
working_files/INVESTIGATION_RESULTS.md
Normal file
182
working_files/INVESTIGATION_RESULTS.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Investigation Results: Media File Skipping
|
||||
|
||||
## Diagnostic Summary
|
||||
✅ **All 3 media files are present and valid:**
|
||||
- music.jpg (36,481 bytes) - IMAGE
|
||||
- 130414-746934884.mp4 (6,474,921 bytes) - VIDEO (H.264, 1920x1080, compatible)
|
||||
- IMG_0386.jpeg (592,162 bytes) - IMAGE
|
||||
|
||||
✅ **No file system issues found:**
|
||||
- All files exist
|
||||
- Correct permissions
|
||||
- No case sensitivity problems
|
||||
- Supported file types
|
||||
|
||||
✅ **Video codec is compatible:**
|
||||
- H.264 codec (fully supported by ffpyplayer)
|
||||
- 1920x1080 @ 29.97fps
|
||||
- Reasonable bitrate (2.3 Mbps)
|
||||
|
||||
## Potential Root Causes Identified
|
||||
|
||||
### 1. **Video Widget Not Properly Stopping** (Most Likely)
|
||||
When transitioning from video to the next media, the video widget may not be properly stopped before removal. This could cause:
|
||||
- The video to continue playing in background
|
||||
- Race conditions with scheduling
|
||||
- Next media appearing to "skip"
|
||||
|
||||
**Location**: `play_current_media()` line 417-420
|
||||
```python
|
||||
if self.current_widget:
|
||||
self.ids.content_area.remove_widget(self.current_widget)
|
||||
self.current_widget = None
|
||||
```
|
||||
|
||||
**Fix**: Stop video before removing widget
|
||||
|
||||
### 2. **Multiple Scheduled Events**
|
||||
The `Clock.schedule_once(self.next_media, duration)` could be called multiple times if widget loading triggers multiple events.
|
||||
|
||||
**Location**: Lines 510, 548
|
||||
|
||||
**Fix**: Add `Clock.unschedule()` before scheduling
|
||||
|
||||
### 3. **Video Loading Callback Issues**
|
||||
The video `loaded` callback might not fire or might fire multiple times, causing state confusion.
|
||||
|
||||
**Location**: `_on_video_loaded()` line 516
|
||||
|
||||
### 4. **Pause State Not Properly Checked**
|
||||
If the player gets paused/unpaused during media transition, scheduling could get confused.
|
||||
|
||||
**Location**: `next_media()` line 551
|
||||
|
||||
## What Enhanced Logging Will Show
|
||||
|
||||
With the new logging, you'll see patterns like:
|
||||
|
||||
### If Videos Are Being Skipped:
|
||||
```
|
||||
===== Playing item 2/3 =====
|
||||
File: 130414-746934884.mp4
|
||||
Extension: .mp4
|
||||
Media type: VIDEO
|
||||
Loading video...
|
||||
Creating Video widget...
|
||||
[SHORT PAUSE OR ERROR]
|
||||
Transitioning to next media (was index 1)
|
||||
===== Playing item 3/3 =====
|
||||
```
|
||||
|
||||
### If Duration Is Too Short:
|
||||
```
|
||||
Creating Video widget...
|
||||
Scheduled next media in 23s
|
||||
[Only 1-2 seconds pass]
|
||||
Transitioning to next media
|
||||
```
|
||||
|
||||
## Recommended Fixes
|
||||
|
||||
I've added comprehensive logging. Here are additional fixes to try:
|
||||
|
||||
### Fix 1: Properly Stop Video Widget Before Removal
|
||||
Add this to `play_current_media()` before removing widget:
|
||||
|
||||
```python
|
||||
# Remove previous media widget
|
||||
if self.current_widget:
|
||||
# Stop video if it's playing
|
||||
if isinstance(self.current_widget, Video):
|
||||
self.current_widget.state = 'stop'
|
||||
self.current_widget.unload()
|
||||
self.ids.content_area.remove_widget(self.current_widget)
|
||||
self.current_widget = None
|
||||
```
|
||||
|
||||
### Fix 2: Ensure Scheduled Events Don't Overlap
|
||||
Modify scheduling in both `play_video()` and `play_image()`:
|
||||
|
||||
```python
|
||||
# Unschedule any pending transitions before scheduling new one
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, duration)
|
||||
```
|
||||
|
||||
### Fix 3: Add Video State Monitoring
|
||||
Track when video actually starts playing vs when widget is created.
|
||||
|
||||
## How to Test
|
||||
|
||||
### 1. Run with Enhanced Logging
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
source .venv/bin/activate
|
||||
cd src
|
||||
python3 main.py 2>&1 | tee ../playback_debug.log
|
||||
```
|
||||
|
||||
Watch the console output. You should see:
|
||||
- Each media file being loaded
|
||||
- Timing information
|
||||
- Any errors or skips
|
||||
|
||||
### 2. Check Timing
|
||||
If media skips, check the log for timing:
|
||||
- Does "Scheduled next media in Xs" appear?
|
||||
- How long until "Transitioning to next media" appears?
|
||||
- Is it immediate (< 1 second) = scheduling bug
|
||||
- Is it after full duration = normal operation
|
||||
|
||||
### 3. Look for Error Patterns
|
||||
Search the log for:
|
||||
```bash
|
||||
grep "❌" playback_debug.log
|
||||
grep "Error" playback_debug.log
|
||||
grep "consecutive_errors" playback_debug.log
|
||||
```
|
||||
|
||||
## Quick Test Scenario
|
||||
|
||||
Create a test with just one file to isolate the issue:
|
||||
|
||||
```json
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "music.jpg",
|
||||
"url": "media/music.jpg",
|
||||
"duration": 10
|
||||
}
|
||||
],
|
||||
"version": 99
|
||||
}
|
||||
```
|
||||
|
||||
If this single image repeats correctly every 10s, the issue is with video playback or transitions.
|
||||
|
||||
## What to Report
|
||||
|
||||
When you run the app, please capture:
|
||||
|
||||
1. **Console output** - especially the pattern around skipped files
|
||||
2. **Which files skip?** - Is it always videos? Always after videos?
|
||||
3. **Timing** - Do files play for full duration before skipping?
|
||||
4. **Pattern** - First loop OK then skips? Always skips certain file?
|
||||
|
||||
## Tools Created
|
||||
|
||||
1. **diagnose_playlist.py** - Check file system issues
|
||||
2. **test_playback_logging.py** - Simulate playback logic
|
||||
3. **check_video_codecs.py** - Verify video compatibility
|
||||
4. **Enhanced main.py** - Detailed logging throughout
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. ✅ Run `diagnose_playlist.py` - **PASSED**
|
||||
2. ✅ Run `check_video_codecs.py` - **PASSED**
|
||||
3. ⏳ Run app with logging and observe pattern
|
||||
4. ⏳ Apply video widget fixes if needed
|
||||
5. ⏳ Report findings for further diagnosis
|
||||
|
||||
The enhanced logging will pinpoint exactly where and why files are being skipped!
|
||||
35
working_files/analyze_playlist.py
Normal file
35
working_files/analyze_playlist.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze what's happening with the playlist download."""
|
||||
|
||||
import json
|
||||
|
||||
# Check the saved playlist
|
||||
playlist_file = 'playlists/server_playlist_v8.json'
|
||||
print("=" * 80)
|
||||
print("SAVED PLAYLIST ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
with open(playlist_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"\nVersion: {data.get('version', 'N/A')}")
|
||||
print(f"Items in playlist: {len(data.get('playlist', []))}")
|
||||
|
||||
print("\nPlaylist items:")
|
||||
for idx, item in enumerate(data.get('playlist', []), 1):
|
||||
print(f"\n{idx}. File: {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("\n⚠️ ISSUE: Server has 5 files, but only 3 are saved!")
|
||||
print("\nPossible reasons:")
|
||||
print("1. Server sent only 3 files")
|
||||
print("2. 2 files failed to download and were skipped")
|
||||
print("3. Download function has a bug")
|
||||
print("\nThe download_media_files() function in get_playlists_v2.py:")
|
||||
print("- Downloads from the 'url' field in the playlist")
|
||||
print("- If download fails, it SKIPS the file (continues)")
|
||||
print("- Only successfully downloaded files are added to updated_playlist")
|
||||
print("\nThis means 2 files likely had invalid URLs or download errors!")
|
||||
print("=" * 80)
|
||||
123
working_files/check_video_codecs.py
Normal file
123
working_files/check_video_codecs.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check video files for codec compatibility with ffpyplayer
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def check_video_codec(video_path):
|
||||
"""Check video codec using ffprobe"""
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'-v', 'quiet',
|
||||
'-print_format', 'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
video_path
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None, "ffprobe failed"
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
video_streams = [s for s in data.get('streams', []) if s.get('codec_type') == 'video']
|
||||
audio_streams = [s for s in data.get('streams', []) if s.get('codec_type') == 'audio']
|
||||
|
||||
if not video_streams:
|
||||
return None, "No video stream found"
|
||||
|
||||
video_stream = video_streams[0]
|
||||
|
||||
info = {
|
||||
'codec': video_stream.get('codec_name', 'unknown'),
|
||||
'codec_long': video_stream.get('codec_long_name', 'unknown'),
|
||||
'width': video_stream.get('width', 0),
|
||||
'height': video_stream.get('height', 0),
|
||||
'fps': eval(video_stream.get('r_frame_rate', '0/1')),
|
||||
'duration': float(data.get('format', {}).get('duration', 0)),
|
||||
'bitrate': int(data.get('format', {}).get('bit_rate', 0)),
|
||||
'audio_codec': audio_streams[0].get('codec_name', 'none') if audio_streams else 'none',
|
||||
'size': int(data.get('format', {}).get('size', 0))
|
||||
}
|
||||
|
||||
return info, None
|
||||
|
||||
except FileNotFoundError:
|
||||
return None, "ffprobe not installed (run: sudo apt-get install ffmpeg)"
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
|
||||
print("=" * 80)
|
||||
print("VIDEO CODEC COMPATIBILITY CHECKER")
|
||||
print("=" * 80)
|
||||
|
||||
# Supported codecs by ffpyplayer
|
||||
supported_codecs = ['h264', 'h265', 'hevc', 'vp8', 'vp9', 'mpeg4']
|
||||
|
||||
# Find video files
|
||||
video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
video_files = []
|
||||
|
||||
if os.path.exists(media_dir):
|
||||
for filename in os.listdir(media_dir):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext in video_extensions:
|
||||
video_files.append(filename)
|
||||
|
||||
if not video_files:
|
||||
print("\n✓ No video files found in media directory")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(video_files)} video file(s):\n")
|
||||
|
||||
for filename in video_files:
|
||||
video_path = os.path.join(media_dir, filename)
|
||||
print(f"📹 {filename}")
|
||||
print(f" Path: {video_path}")
|
||||
|
||||
info, error = check_video_codec(video_path)
|
||||
|
||||
if error:
|
||||
print(f" ❌ ERROR: {error}")
|
||||
continue
|
||||
|
||||
# Display video info
|
||||
print(f" Video Codec: {info['codec']} ({info['codec_long']})")
|
||||
print(f" Resolution: {info['width']}x{info['height']}")
|
||||
print(f" Frame Rate: {info['fps']:.2f} fps")
|
||||
print(f" Duration: {info['duration']:.1f}s")
|
||||
print(f" Bitrate: {info['bitrate'] / 1000:.0f} kbps")
|
||||
print(f" Audio Codec: {info['audio_codec']}")
|
||||
print(f" File Size: {info['size'] / (1024*1024):.2f} MB")
|
||||
|
||||
# Check compatibility
|
||||
if info['codec'] in supported_codecs:
|
||||
print(f" ✅ COMPATIBLE - Codec '{info['codec']}' is supported by ffpyplayer")
|
||||
else:
|
||||
print(f" ⚠️ WARNING - Codec '{info['codec']}' may not be supported")
|
||||
print(f" Supported codecs: {', '.join(supported_codecs)}")
|
||||
print(f" Consider re-encoding to H.264:")
|
||||
print(f" ffmpeg -i \"{filename}\" -c:v libx264 -preset fast -crf 23 \"{os.path.splitext(filename)[0]}_h264.mp4\"")
|
||||
|
||||
# Performance warnings
|
||||
if info['width'] > 1920 or info['height'] > 1080:
|
||||
print(f" ⚠️ High resolution ({info['width']}x{info['height']}) may cause performance issues")
|
||||
print(f" Consider downscaling to 1920x1080 or lower")
|
||||
|
||||
if info['bitrate'] > 5000000: # 5 Mbps
|
||||
print(f" ⚠️ High bitrate ({info['bitrate'] / 1000000:.1f} Mbps) may cause playback issues")
|
||||
print(f" Consider reducing bitrate to 2-4 Mbps")
|
||||
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
167
working_files/diagnose_playlist.py
Normal file
167
working_files/diagnose_playlist.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Diagnostic script to check why media files might be skipped
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
# Paths
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
playlists_dir = os.path.join(base_dir, 'playlists')
|
||||
|
||||
# Supported extensions
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
||||
SUPPORTED_EXTENSIONS = VIDEO_EXTENSIONS + IMAGE_EXTENSIONS
|
||||
|
||||
def check_playlist():
|
||||
"""Check playlist for issues"""
|
||||
print("=" * 80)
|
||||
print("PLAYLIST DIAGNOSTIC TOOL")
|
||||
print("=" * 80)
|
||||
|
||||
# Find latest playlist file
|
||||
playlist_files = [f for f in os.listdir(playlists_dir)
|
||||
if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
|
||||
if not playlist_files:
|
||||
print("\n❌ ERROR: No playlist files found!")
|
||||
return
|
||||
|
||||
# Sort by version and get latest
|
||||
versions = [(int(f.split('_v')[-1].split('.json')[0]), f) for f in playlist_files]
|
||||
versions.sort(reverse=True)
|
||||
latest_file = versions[0][1]
|
||||
playlist_path = os.path.join(playlists_dir, latest_file)
|
||||
|
||||
print(f"\n📋 Latest Playlist: {latest_file}")
|
||||
print(f" Path: {playlist_path}")
|
||||
|
||||
# Load playlist
|
||||
try:
|
||||
with open(playlist_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
playlist = data.get('playlist', [])
|
||||
version = data.get('version', 0)
|
||||
|
||||
print(f" Version: {version}")
|
||||
print(f" Total items: {len(playlist)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR loading playlist: {e}")
|
||||
return
|
||||
|
||||
# Check media directory
|
||||
print(f"\n📁 Media Directory: {media_dir}")
|
||||
if not os.path.exists(media_dir):
|
||||
print(" ❌ ERROR: Media directory doesn't exist!")
|
||||
return
|
||||
|
||||
media_files = os.listdir(media_dir)
|
||||
print(f" Files found: {len(media_files)}")
|
||||
for f in media_files:
|
||||
print(f" - {f}")
|
||||
|
||||
# Check each playlist item
|
||||
print("\n" + "=" * 80)
|
||||
print("CHECKING PLAYLIST ITEMS")
|
||||
print("=" * 80)
|
||||
|
||||
valid_count = 0
|
||||
missing_count = 0
|
||||
unsupported_count = 0
|
||||
|
||||
for idx, item in enumerate(playlist, 1):
|
||||
file_name = item.get('file_name', '')
|
||||
duration = item.get('duration', 0)
|
||||
media_path = os.path.join(media_dir, file_name)
|
||||
file_ext = os.path.splitext(file_name)[1].lower()
|
||||
|
||||
print(f"\n[{idx}/{len(playlist)}] {file_name}")
|
||||
print(f" Duration: {duration}s")
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.exists(media_path):
|
||||
print(f" ❌ STATUS: FILE NOT FOUND")
|
||||
print(f" Expected path: {media_path}")
|
||||
missing_count += 1
|
||||
continue
|
||||
|
||||
# Check file size
|
||||
file_size = os.path.getsize(media_path)
|
||||
print(f" ✓ File exists ({file_size:,} bytes)")
|
||||
|
||||
# Check if supported type
|
||||
if file_ext not in SUPPORTED_EXTENSIONS:
|
||||
print(f" ❌ STATUS: UNSUPPORTED FILE TYPE '{file_ext}'")
|
||||
print(f" Supported extensions: {', '.join(SUPPORTED_EXTENSIONS)}")
|
||||
unsupported_count += 1
|
||||
continue
|
||||
|
||||
# Check media type
|
||||
if file_ext in VIDEO_EXTENSIONS:
|
||||
media_type = "VIDEO"
|
||||
elif file_ext in IMAGE_EXTENSIONS:
|
||||
media_type = "IMAGE"
|
||||
else:
|
||||
media_type = "UNKNOWN"
|
||||
|
||||
print(f" ✓ Type: {media_type}")
|
||||
print(f" ✓ Extension: {file_ext}")
|
||||
print(f" ✓ STATUS: SHOULD PLAY OK")
|
||||
valid_count += 1
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Total items: {len(playlist)}")
|
||||
print(f"✓ Valid: {valid_count}")
|
||||
print(f"❌ Missing files: {missing_count}")
|
||||
print(f"❌ Unsupported: {unsupported_count}")
|
||||
|
||||
if valid_count == len(playlist):
|
||||
print("\n✅ All playlist items should play correctly!")
|
||||
else:
|
||||
print(f"\n⚠️ WARNING: {len(playlist) - valid_count} items may be skipped!")
|
||||
|
||||
# Additional checks
|
||||
print("\n" + "=" * 80)
|
||||
print("ADDITIONAL CHECKS")
|
||||
print("=" * 80)
|
||||
|
||||
# Check for files in media dir not in playlist
|
||||
playlist_files_set = {item.get('file_name', '') for item in playlist}
|
||||
orphaned_files = [f for f in media_files if f not in playlist_files_set]
|
||||
|
||||
if orphaned_files:
|
||||
print(f"\n⚠️ Files in media directory NOT in playlist:")
|
||||
for f in orphaned_files:
|
||||
print(f" - {f}")
|
||||
else:
|
||||
print("\n✓ All media files are in the playlist")
|
||||
|
||||
# Check for case sensitivity issues
|
||||
print("\n🔍 Checking for case sensitivity issues...")
|
||||
media_files_lower = {f.lower(): f for f in media_files}
|
||||
case_issues = []
|
||||
|
||||
for item in playlist:
|
||||
file_name = item.get('file_name', '')
|
||||
if file_name.lower() in media_files_lower:
|
||||
actual_name = media_files_lower[file_name.lower()]
|
||||
if actual_name != file_name:
|
||||
case_issues.append((file_name, actual_name))
|
||||
|
||||
if case_issues:
|
||||
print("⚠️ Case sensitivity mismatches found:")
|
||||
for playlist_name, actual_name in case_issues:
|
||||
print(f" Playlist: {playlist_name}")
|
||||
print(f" Actual: {actual_name}")
|
||||
else:
|
||||
print("✓ No case sensitivity issues found")
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_playlist()
|
||||
54
working_files/force_update.py
Normal file
54
working_files/force_update.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Force playlist update to download all files."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from get_playlists_v2 import update_playlist_if_needed
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("FORCING PLAYLIST UPDATE")
|
||||
print("=" * 80)
|
||||
|
||||
playlist_dir = 'playlists'
|
||||
media_dir = 'media'
|
||||
|
||||
print(f"\nConfiguration:")
|
||||
print(f" Playlist dir: {playlist_dir}")
|
||||
print(f" Media dir: {media_dir}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Updating playlist...")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
result = update_playlist_if_needed(config, playlist_dir, media_dir)
|
||||
|
||||
if result:
|
||||
print("\n" + "=" * 80)
|
||||
print("SUCCESS!")
|
||||
print("=" * 80)
|
||||
print(f"✓ Playlist updated to: {result}")
|
||||
|
||||
# Check media directory
|
||||
import os
|
||||
media_files = sorted([f for f in os.listdir(media_dir) if not f.startswith('.')])
|
||||
print(f"\n✓ Media files downloaded ({len(media_files)}):")
|
||||
for f in media_files:
|
||||
size = os.path.getsize(os.path.join(media_dir, f))
|
||||
print(f" - {f} ({size:,} bytes)")
|
||||
|
||||
else:
|
||||
print("\n" + "=" * 80)
|
||||
print("FAILED or already up to date")
|
||||
print("=" * 80)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
@@ -31,15 +31,21 @@ def send_player_feedback(config, message, status="active", playlist_version=None
|
||||
port = config.get("port", "")
|
||||
|
||||
# Construct server URL
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server):
|
||||
feedback_url = f'http://{server}:{port}/api/player-feedback'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
feedback_url = f'http://{server_clean}:{port}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
feedback_url = f'{server}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
|
||||
# Prepare feedback data
|
||||
feedback_data = {
|
||||
'player_name': host,
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick,
|
||||
'message': message,
|
||||
'status': status,
|
||||
@@ -165,11 +171,17 @@ def fetch_server_playlist(config):
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
try:
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server):
|
||||
server_url = f'http://{server}:{port}/api/playlists'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
server_url = f'http://{server_clean}:{port}/api/playlists'
|
||||
else:
|
||||
server_url = f'http://{server}/api/playlists'
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
server_url = f'{server}/api/playlists'
|
||||
else:
|
||||
server_url = f'http://{server}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
10
working_files/player_auth.json
Normal file
10
working_files/player_auth.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hostname": "tv-terasa",
|
||||
"auth_code": "iiSyZDLWGyqNIxeRt54XYREgvAio11RwwU1_oJev6WI",
|
||||
"player_id": 1,
|
||||
"player_name": "TV-acasa 1",
|
||||
"playlist_id": 1,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://digi-signage.moto-adv.com"
|
||||
}
|
||||
54
working_files/server_response_debug.json
Normal file
54
working_files/server_response_debug.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"count": 5,
|
||||
"player_id": 1,
|
||||
"player_name": "TV-acasa 1",
|
||||
"playlist": [
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "music.jpg",
|
||||
"id": 1,
|
||||
"position": 1,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/music.jpg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 23,
|
||||
"file_name": "130414-746934884.mp4",
|
||||
"id": 2,
|
||||
"position": 3,
|
||||
"type": "video",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/130414-746934884.mp4"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "IMG_0386.jpeg",
|
||||
"id": 4,
|
||||
"position": 4,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/IMG_0386.jpeg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "AGC_20250704_204105932.jpg",
|
||||
"id": 5,
|
||||
"position": 5,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/AGC_20250704_204105932.jpg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "50194.jpg",
|
||||
"id": 3,
|
||||
"position": 6,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/50194.jpg"
|
||||
}
|
||||
],
|
||||
"playlist_id": 1,
|
||||
"playlist_version": 9
|
||||
}
|
||||
162
working_files/test_card_reader.py
Normal file
162
working_files/test_card_reader.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for USB card reader functionality
|
||||
"""
|
||||
|
||||
import evdev
|
||||
from evdev import InputDevice, categorize, ecodes
|
||||
import time
|
||||
|
||||
def list_input_devices():
|
||||
"""List all available input devices"""
|
||||
print("\n=== Available Input Devices ===")
|
||||
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
||||
|
||||
# Exclusion keywords that help identify non-card-reader devices
|
||||
exclusion_keywords = [
|
||||
'touch', 'touchscreen', 'mouse', 'mice', 'trackpad',
|
||||
'touchpad', 'pen', 'stylus', 'video', 'button', 'lid'
|
||||
]
|
||||
|
||||
for i, device in enumerate(devices):
|
||||
device_name_lower = device.name.lower()
|
||||
is_excluded = any(keyword in device_name_lower for keyword in exclusion_keywords)
|
||||
is_likely_card = 'card' in device_name_lower or 'reader' in device_name_lower or 'rfid' in device_name_lower
|
||||
|
||||
print(f"\n[{i}] {device.path}")
|
||||
print(f" Name: {device.name}")
|
||||
print(f" Phys: {device.phys}")
|
||||
|
||||
capabilities = device.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
print(f" Type: Keyboard/HID Input Device")
|
||||
|
||||
# Add helpful hints
|
||||
if is_likely_card:
|
||||
print(f" ** LIKELY CARD READER **")
|
||||
elif is_excluded:
|
||||
print(f" (Excluded: appears to be touch/mouse/other non-card device)")
|
||||
elif 'usb' in device_name_lower and 'keyboard' in device_name_lower:
|
||||
print(f" (USB Keyboard - could be card reader)")
|
||||
|
||||
return devices
|
||||
|
||||
def test_card_reader(device_index=None):
|
||||
"""Test reading from a card reader device"""
|
||||
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
||||
|
||||
# Exclusion keywords (same as in main app)
|
||||
exclusion_keywords = [
|
||||
'touch', 'touchscreen', 'mouse', 'mice', 'trackpad',
|
||||
'touchpad', 'pen', 'stylus', 'video', 'button', 'lid'
|
||||
]
|
||||
|
||||
if device_index is not None:
|
||||
if device_index >= len(devices):
|
||||
print(f"Error: Device index {device_index} out of range")
|
||||
return
|
||||
device = devices[device_index]
|
||||
else:
|
||||
# Try to find a card reader automatically using same logic as main app
|
||||
device = None
|
||||
|
||||
# Priority 1: Explicit card readers
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
if 'card' in device_name_lower or 'reader' in device_name_lower or 'rfid' in device_name_lower or 'hid' in device_name_lower:
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Found card reader: {dev.name}")
|
||||
break
|
||||
|
||||
# Priority 2: USB keyboards
|
||||
if not device:
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
if 'usb' in device_name_lower and 'keyboard' in device_name_lower:
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Using USB keyboard as card reader: {dev.name}")
|
||||
break
|
||||
|
||||
# Priority 3: Any non-excluded keyboard
|
||||
if not device:
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Using keyboard device as card reader: {dev.name}")
|
||||
break
|
||||
|
||||
if not device:
|
||||
print("No suitable input device found!")
|
||||
return
|
||||
|
||||
print(f"\n=== Testing Card Reader ===")
|
||||
print(f"Device: {device.name}")
|
||||
print(f"Path: {device.path}")
|
||||
print("\nSwipe your card now (press Ctrl+C to exit)...\n")
|
||||
|
||||
card_data = ""
|
||||
|
||||
try:
|
||||
for event in device.read_loop():
|
||||
if event.type == ecodes.EV_KEY:
|
||||
key_event = categorize(event)
|
||||
|
||||
if key_event.keystate == 1: # Key down
|
||||
key_code = key_event.keycode
|
||||
|
||||
# Handle Enter key (card read complete)
|
||||
if key_code == 'KEY_ENTER':
|
||||
print(f"\n✓ Card data received: '{card_data}'")
|
||||
print(f" Length: {len(card_data)} characters")
|
||||
print(f" Processed ID: card_{card_data.strip().upper()}")
|
||||
print("\nReady for next card swipe...")
|
||||
card_data = ""
|
||||
|
||||
# Build card data string
|
||||
elif key_code.startswith('KEY_'):
|
||||
char = key_code.replace('KEY_', '')
|
||||
if len(char) == 1: # Single character
|
||||
card_data += char
|
||||
print(f"Reading: {card_data}", end='\r', flush=True)
|
||||
elif char.isdigit(): # Handle numeric keys
|
||||
card_data += char
|
||||
print(f"Reading: {card_data}", end='\r', flush=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nTest stopped by user")
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("USB Card Reader Test Tool")
|
||||
print("=" * 50)
|
||||
|
||||
devices = list_input_devices()
|
||||
|
||||
if not devices:
|
||||
print("\nNo input devices found!")
|
||||
exit(1)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
choice = input("\nEnter device number to test (or press Enter for auto-detect): ").strip()
|
||||
|
||||
if choice:
|
||||
try:
|
||||
device_index = int(choice)
|
||||
test_card_reader(device_index)
|
||||
except ValueError:
|
||||
print("Invalid device number!")
|
||||
else:
|
||||
test_card_reader()
|
||||
138
working_files/test_connection.py
Normal file
138
working_files/test_connection.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test server connection and playlist fetch."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from player_auth import PlayerAuth
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("SERVER CONNECTION TEST")
|
||||
print("=" * 80)
|
||||
|
||||
server_ip = config.get("server_ip", "")
|
||||
screen_name = config.get("screen_name", "")
|
||||
quickconnect_key = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
print(f"\nConfiguration:")
|
||||
print(f" Server: {server_ip}")
|
||||
print(f" Port: {port}")
|
||||
print(f" Screen Name: {screen_name}")
|
||||
print(f" QuickConnect: {quickconnect_key}")
|
||||
|
||||
# Build server URL
|
||||
if server_ip.startswith('http://') or server_ip.startswith('https://'):
|
||||
server_url = server_ip
|
||||
# If it has https but port 443 is specified, ensure port is included if non-standard
|
||||
if not ':' in server_ip.replace('https://', '').replace('http://', ''):
|
||||
if port and port != '443' and port != '80':
|
||||
server_url = f"{server_ip}:{port}"
|
||||
else:
|
||||
# Use https for port 443, http for others
|
||||
protocol = "https" if port == "443" else "http"
|
||||
server_url = f"{protocol}://{server_ip}:{port}"
|
||||
|
||||
print(f"\nServer URL: {server_url}")
|
||||
|
||||
# Test authentication
|
||||
print("\n" + "=" * 80)
|
||||
print("1. TESTING AUTHENTICATION")
|
||||
print("=" * 80)
|
||||
|
||||
auth = PlayerAuth('src/player_auth.json')
|
||||
|
||||
# Check if already authenticated
|
||||
if auth.is_authenticated():
|
||||
print("✓ Found existing authentication")
|
||||
valid, message = auth.verify_auth()
|
||||
if valid:
|
||||
print(f"✓ Auth is valid: {message}")
|
||||
else:
|
||||
print(f"✗ Auth expired: {message}")
|
||||
print("\nRe-authenticating...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=screen_name,
|
||||
quickconnect_code=quickconnect_key
|
||||
)
|
||||
if success:
|
||||
print(f"✓ Re-authentication successful!")
|
||||
else:
|
||||
print(f"✗ Re-authentication failed: {error}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("No existing authentication found. Authenticating...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=screen_name,
|
||||
quickconnect_code=quickconnect_key
|
||||
)
|
||||
if success:
|
||||
print(f"✓ Authentication successful!")
|
||||
else:
|
||||
print(f"✗ Authentication failed: {error}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test playlist fetch
|
||||
print("\n" + "=" * 80)
|
||||
print("2. TESTING PLAYLIST FETCH")
|
||||
print("=" * 80)
|
||||
|
||||
playlist_data = auth.get_playlist()
|
||||
|
||||
if playlist_data:
|
||||
print(f"✓ Playlist fetched successfully!")
|
||||
print(f"\nPlaylist Version: {playlist_data.get('playlist_version', 'N/A')}")
|
||||
print(f"Number of items: {len(playlist_data.get('playlist', []))}")
|
||||
|
||||
print("\n" + "-" * 80)
|
||||
print("PLAYLIST ITEMS:")
|
||||
print("-" * 80)
|
||||
|
||||
for idx, item in enumerate(playlist_data.get('playlist', []), 1):
|
||||
print(f"\n{idx}. File: {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
# Check if URL is relative or absolute
|
||||
url = item.get('url', '')
|
||||
if url.startswith('http://') or url.startswith('https://'):
|
||||
print(f" Type: Absolute URL")
|
||||
else:
|
||||
print(f" Type: Relative path (will fail to download!)")
|
||||
|
||||
# Save full response
|
||||
with open('server_response_debug.json', 'w') as f:
|
||||
json.dump(playlist_data, f, indent=2)
|
||||
print(f"\n✓ Full response saved to: server_response_debug.json")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Server has: {len(playlist_data.get('playlist', []))} files")
|
||||
print(f"Local has: 3 files (from playlists/server_playlist_v8.json)")
|
||||
|
||||
if len(playlist_data.get('playlist', [])) > 3:
|
||||
print(f"\n⚠️ PROBLEM: Server has {len(playlist_data.get('playlist', []))} files but only 3 were saved!")
|
||||
print("\nMissing files are likely:")
|
||||
local_files = ['music.jpg', '130414-746934884.mp4', 'IMG_0386.jpeg']
|
||||
server_files = [item.get('file_name', '') for item in playlist_data.get('playlist', [])]
|
||||
missing = [f for f in server_files if f not in local_files]
|
||||
for f in missing:
|
||||
print(f" - {f}")
|
||||
|
||||
else:
|
||||
print("✗ Failed to fetch playlist")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
55
working_files/test_direct_api.py
Normal file
55
working_files/test_direct_api.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Direct API test to check server playlist."""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Try with the saved auth
|
||||
auth_file = 'src/player_auth.json'
|
||||
with open(auth_file, 'r') as f:
|
||||
auth_data = json.load(f)
|
||||
|
||||
server_url = auth_data['server_url']
|
||||
auth_code = auth_data['auth_code']
|
||||
|
||||
print("=" * 80)
|
||||
print("DIRECT API TEST")
|
||||
print("=" * 80)
|
||||
print(f"Server: {server_url}")
|
||||
print(f"Auth code: {auth_code[:20]}...")
|
||||
print()
|
||||
|
||||
# Try to get playlist
|
||||
try:
|
||||
url = f"{server_url}/api/player/playlist"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {auth_code}'
|
||||
}
|
||||
|
||||
print(f"Fetching: {url}")
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"\nPlaylist version: {data.get('playlist_version', 'N/A')}")
|
||||
print(f"Number of items: {len(data.get('playlist', []))}")
|
||||
|
||||
print("\nPlaylist items:")
|
||||
for idx, item in enumerate(data.get('playlist', []), 1):
|
||||
print(f"\n {idx}. {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
# Save full response
|
||||
with open('server_playlist_full.json', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"\nFull response saved to: server_playlist_full.json")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
82
working_files/test_playback_logging.py
Normal file
82
working_files/test_playback_logging.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the enhanced logging without running the full GUI
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
def simulate_playback_check():
|
||||
"""Simulate the playback logic to see what would happen"""
|
||||
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
playlists_dir = os.path.join(base_dir, 'playlists')
|
||||
|
||||
# Supported extensions
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
||||
|
||||
# Load playlist
|
||||
playlist_file = os.path.join(playlists_dir, 'server_playlist_v8.json')
|
||||
with open(playlist_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
playlist = data.get('playlist', [])
|
||||
|
||||
print("=" * 80)
|
||||
print("SIMULATING PLAYBACK SEQUENCE")
|
||||
print("=" * 80)
|
||||
|
||||
for idx, media_item in enumerate(playlist):
|
||||
file_name = media_item.get('file_name', '')
|
||||
duration = media_item.get('duration', 10)
|
||||
|
||||
print(f"\n[STEP {idx + 1}] ===== Playing item {idx + 1}/{len(playlist)} =====")
|
||||
print(f" File: {file_name}")
|
||||
print(f" Duration: {duration}s")
|
||||
|
||||
# Construct path
|
||||
media_path = os.path.join(media_dir, file_name)
|
||||
print(f" Full path: {media_path}")
|
||||
|
||||
# Check existence
|
||||
if not os.path.exists(media_path):
|
||||
print(f" \u274c Media file not found: {media_path}")
|
||||
print(f" ACTION: Skipping to next media...")
|
||||
continue
|
||||
|
||||
file_size = os.path.getsize(media_path)
|
||||
print(f" \u2713 File exists (size: {file_size:,} bytes)")
|
||||
|
||||
# Check extension
|
||||
file_extension = os.path.splitext(file_name)[1].lower()
|
||||
print(f" Extension: {file_extension}")
|
||||
|
||||
if file_extension in VIDEO_EXTENSIONS:
|
||||
print(f" Media type: VIDEO")
|
||||
print(f" ACTION: play_video('{media_path}', {duration})")
|
||||
print(f" - Creating Video widget...")
|
||||
print(f" - Adding to content area...")
|
||||
print(f" - Scheduling next media in {duration}s")
|
||||
print(f" \u2713 Media started successfully")
|
||||
elif file_extension in IMAGE_EXTENSIONS:
|
||||
print(f" Media type: IMAGE")
|
||||
print(f" ACTION: play_image('{media_path}', {duration})")
|
||||
print(f" - Creating AsyncImage widget...")
|
||||
print(f" - Adding to content area...")
|
||||
print(f" - Scheduling next media in {duration}s")
|
||||
print(f" \u2713 Image displayed successfully")
|
||||
else:
|
||||
print(f" \u274c Unsupported media type: {file_extension}")
|
||||
print(f" Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif")
|
||||
print(f" ACTION: Skipping to next media...")
|
||||
continue
|
||||
|
||||
print(f"\n [After {duration}s] Transitioning to next media (was index {idx})")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("END OF PLAYLIST - Would restart from beginning")
|
||||
print("=" * 80)
|
||||
|
||||
if __name__ == '__main__':
|
||||
simulate_playback_check()
|
||||
45
working_files/test_server_playlist.py
Normal file
45
working_files/test_server_playlist.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to check what playlist the server is actually returning."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from get_playlists_v2 import fetch_server_playlist
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("TESTING SERVER PLAYLIST FETCH")
|
||||
print("=" * 80)
|
||||
|
||||
# Fetch playlist from server
|
||||
print("\n1. Fetching playlist from server...")
|
||||
server_data = fetch_server_playlist(config)
|
||||
|
||||
print(f"\n2. Server Response:")
|
||||
print(f" Version: {server_data.get('version', 'N/A')}")
|
||||
print(f" Playlist items: {len(server_data.get('playlist', []))}")
|
||||
|
||||
print(f"\n3. Detailed Playlist Items:")
|
||||
for idx, item in enumerate(server_data.get('playlist', []), 1):
|
||||
print(f"\n Item {idx}:")
|
||||
print(f" file_name: {item.get('file_name', 'N/A')}")
|
||||
print(f" url: {item.get('url', 'N/A')}")
|
||||
print(f" duration: {item.get('duration', 'N/A')}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"TOTAL: Server has {len(server_data.get('playlist', []))} files")
|
||||
print("=" * 80)
|
||||
|
||||
# Save to file for inspection
|
||||
output_file = 'server_response_debug.json'
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(server_data, f, indent=2)
|
||||
print(f"\nFull server response saved to: {output_file}")
|
||||
Reference in New Issue
Block a user