Compare commits
14 Commits
07b7e96edd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e9ea1aaa | |||
|
|
4c3ddbef73 | ||
|
|
87e059e0f4 | ||
|
|
46d9fcf6e3 | ||
|
|
f1a84d05d5 | ||
|
|
706af95557 | ||
| 02227a12e5 | |||
| 9d32f43ac7 | |||
| af1e671c7f | |||
|
|
9664ad541b | ||
|
|
89e5ad86dd | ||
|
|
f573af0505 | ||
|
|
fba2007bdf | ||
|
|
72d382b96b |
@@ -1 +0,0 @@
|
||||
1763799978.6257727
|
||||
1
.player_stop_requested
Normal file
1
.player_stop_requested
Normal file
@@ -0,0 +1 @@
|
||||
User requested exit via password
|
||||
@@ -5,5 +5,6 @@
|
||||
"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/pencil.png
Normal file
BIN
config/resources/pencil.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
@@ -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
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
|
||||
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}")
|
||||
|
||||
1259
src/main.py
1259
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": "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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
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()
|
||||
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
|
||||
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()
|
||||
Reference in New Issue
Block a user