Compare commits

...

14 Commits

Author SHA1 Message Date
02e9ea1aaa updated to monitor the netowrk and reset wifi if is not working 2025-12-10 15:53:30 +02:00
Kiwy Signage Player
4c3ddbef73 updated to correctly play the playlist and reload images after edited 2025-12-10 00:09:20 +02:00
Kiwy Signage Player
87e059e0f4 updated to corect function the play pause function 2025-12-09 18:53:34 +02:00
Kiwy Signage Player
46d9fcf6e3 delete watch dog 2025-12-08 21:52:13 +02:00
Kiwy Signage Player
f1a84d05d5 updated buttons in settings 2025-12-08 21:52:03 +02:00
Kiwy Signage Player
706af95557 deleted unnecesary files 2025-12-08 18:17:09 +02:00
02227a12e5 updeated to read specific card 2025-12-08 15:45:37 +02:00
9d32f43ac7 Add edit feature enable/disable setting
- Added checkbox in Settings screen to enable/disable edit feature
- Setting stored in app_config.json as 'edit_feature_enabled'
- Edit workflow now validates: player setting, media type, server permission, card auth
- Shows appropriate error message when edit is blocked at any validation step
- Defaults to enabled (true) if not set
- All conditions must be met for edit interface to open
2025-12-08 14:30:12 +02:00
af1e671c7f Add USB card reader authentication for edit feature
- Implemented CardReader class to read data from USB card readers
- Added CardSwipePopup with 5-second timeout and visual feedback
- Card data is captured and included in edit metadata
- Card data sent to server when edited images are uploaded
- Added evdev dependency for USB input device handling
- Fallback mode when evdev not available (for development)
- Created test utility (test_card_reader.py) for card reader testing
- Added comprehensive documentation (CARD_READER_AUTHENTICATION.md)
- Added access-card.png icon for authentication popup
- Edit interface requires card swipe or times out after 5 seconds
2025-12-08 14:05:04 +02:00
Kiwy Signage Player
9664ad541b moved files 2025-12-06 00:12:37 +02:00
Kiwy Signage Player
89e5ad86dd Add media editing features: WebP support, edit permissions, user auth, server upload
- Migrated to get_playlists_v2 with improved auth system
- Added WebP image format support for playback and editing
- Implemented edit_on_player permission check from server playlist
- Added user authentication layer for edit function (placeholder: player_1)
- Implemented versioned saving with metadata (user, timestamp, version)
- Added server upload functionality for edited media
- Fixed playlist update after intro video completion
- Added hostname and quickconnect_code to player feedback
- Improved error handling for upload failures (non-blocking)
2025-12-06 00:07:48 +02:00
Kiwy Signage Player
f573af0505 update 2025-12-05 00:36:38 +02:00
Kiwy Signage Player
fba2007bdf Improve edit interface: full-screen layout with toolbars, versioned saving to edited_media folder 2025-12-05 00:25:53 +02:00
Kiwy Signage Player
72d382b96b Add image editing feature with drawing tools
- Added pencil edit button to player controls
- Created EditPopup with drawing layer for image annotation
- Drawing tools: color selection (red/blue/green/black), thickness control
- Features: undo last stroke, clear all strokes, save edited image
- Playback automatically pauses during editing
- Only images (.jpg, .jpeg, .png, .bmp) can be edited
- Edited images saved with '_edited' suffix in same directory
- Drawing layer with touch support for annotations
- Full toolbar with color, thickness, and action controls
2025-12-04 22:40:05 +02:00
21 changed files with 2353 additions and 68 deletions

View File

@@ -1 +0,0 @@
1763799978.6257727

1
.player_stop_requested Normal file
View File

@@ -0,0 +1 @@
User requested exit via password

View File

@@ -5,5 +5,6 @@
"quickconnect_key": "8887779",
"orientation": "Landscape",
"touch": "True",
"max_resolution": "1920x1080"
"max_resolution": "1920x1080",
"edit_feature_enabled": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
config/resources/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

View File

@@ -3,4 +3,5 @@ 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
View 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

100
src/edit_drowing.py Normal file
View 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()

View File

@@ -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}")

File diff suppressed because it is too large Load Diff

235
src/network_monitor.py Normal file
View 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()

View File

@@ -1,10 +1,10 @@
{
"hostname": "tv-terasa",
"auth_code": "iiSyZDLWGyqNIxeRt54XYREgvAio11RwwU1_oJev6WI",
"auth_code": "vkrxEO6eOTxkzXJBtoN4OuXc8eaX2mC3AB9ZePrnick",
"player_id": 1,
"player_name": "TV-acasa 1",
"player_name": "TV-acasa",
"playlist_id": 1,
"orientation": "Landscape",
"authenticated": true,
"server_url": "https://digi-signage.moto-adv.com:443"
"server_url": "http://digi-signage.moto-adv.com"
}

View File

@@ -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,

View File

@@ -224,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
@@ -256,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
@@ -485,9 +493,75 @@
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.05
# Reset Buttons Section
Label:
text: 'Reset Options:'
size_hint_y: None
height: dp(30)
text_size: self.size
halign: 'left'
valign: 'middle'
bold: True
font_size: sp(16)
# Reset Buttons Row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(50)
spacing: dp(10)
Button:
id: reset_auth_btn
text: 'Reset Player Auth'
background_color: 0.8, 0.4, 0.2, 1
on_press: root.reset_player_auth()
Button:
id: reset_playlist_btn
text: 'Reset Playlist to v0'
background_color: 0.8, 0.4, 0.2, 1
on_press: root.reset_playlist_version()
Button:
id: restart_player_btn
text: 'Restart Player'
background_color: 0.2, 0.6, 0.8, 1
on_press: root.restart_player()
# Test Connection Button
Button:
id: test_connection_btn
@@ -511,36 +585,39 @@
Widget:
size_hint_y: 0.05
# Status information
Label:
id: playlist_info
text: 'Playlist Version: N/A'
# Status information row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(30)
text_size: self.size
halign: 'left'
valign: 'middle'
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'
Label:
id: playlist_info
text: 'Playlist: N/A'
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
Label:
id: status_info
text: 'Status: Idle'
size_hint_y: None
height: dp(30)
text_size: self.size
halign: 'left'
valign: 'middle'
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.2
size_hint_y: 0.05
# Action buttons
BoxLayout:

View 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()

View 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)

View 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

View 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()