From af1e671c7f41f9dc05af0794c0d662c707303c1d Mon Sep 17 00:00:00 2001 From: ske087 Date: Mon, 8 Dec 2025 14:05:04 +0200 Subject: [PATCH] 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 --- config/resources/access-card.png | Bin 0 -> 36970 bytes requirements.txt | 3 +- src/main.py | 298 ++++++++++++++++++-- working_files/CARD_READER_AUTHENTICATION.md | 182 ++++++++++++ working_files/test_card_reader.py | 114 ++++++++ 5 files changed, 577 insertions(+), 20 deletions(-) create mode 100644 config/resources/access-card.png create mode 100644 working_files/CARD_READER_AUTHENTICATION.md create mode 100644 working_files/test_card_reader.py diff --git a/config/resources/access-card.png b/config/resources/access-card.png new file mode 100644 index 0000000000000000000000000000000000000000..b080ee9a10f193218188fb760465884b0d205cba GIT binary patch literal 36970 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

F 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%0DjhT_IWW&Le4u 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

%PM}^L=A;M?}WE-d;Io*PZ5w01kzh!<4Ux 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 0c88956..f1d6cf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ ffpyplayer requests==2.32.4 bcrypt==4.2.1 aiohttp==3.9.1 -asyncio==3.4.3 \ No newline at end of file +asyncio==3.4.3 +evdev>=1.6.0 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 95e5ac9..2ebdd5b 100644 --- a/src/main.py +++ b/src/main.py @@ -35,6 +35,16 @@ from kivy.clock import Clock from kivy.core.window import Window from kivy.properties import BooleanProperty from kivy.logger import Logger + +# Try to import evdev for card reader support (after Logger is available) +try: + import evdev + from evdev import InputDevice, categorize, ecodes + EVDEV_AVAILABLE = True + Logger.info("CardReader: evdev library loaded successfully") +except ImportError: + EVDEV_AVAILABLE = False + Logger.warning("CardReader: evdev not available - card reader functionality will use fallback mode") from kivy.animation import Animation from kivy.lang import Builder from kivy.graphics import Rectangle @@ -105,13 +115,226 @@ class DrawingLayer(Widget): Logger.debug(f"DrawingLayer: Thickness set to {value}") +class CardReader: + """USB Card Reader Handler for user authentication""" + def __init__(self): + self.device = None + self.card_data = "" + self.reading = False + self.last_read_time = 0 + self.read_timeout = 5 # seconds + self.evdev_available = EVDEV_AVAILABLE + + def find_card_reader(self): + """Find USB card reader device""" + if not self.evdev_available: + Logger.error("CardReader: evdev library not available") + return False + + try: + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + # Look for devices that might be card readers (keyboards, HID devices) + for device in devices: + # Card readers typically show up as keyboard input devices + if 'keyboard' in device.name.lower() or 'card' in device.name.lower() or 'reader' in device.name.lower(): + Logger.info(f"CardReader: Found potential card reader: {device.name} at {device.path}") + self.device = device + return True + + # If no specific card reader found, use first keyboard device + for device in devices: + capabilities = device.capabilities() + if ecodes.EV_KEY in capabilities: + Logger.info(f"CardReader: Using keyboard device as card reader: {device.name}") + self.device = device + return True + + Logger.warning("CardReader: No suitable input device found") + return False + except Exception as e: + Logger.error(f"CardReader: Error finding device: {e}") + return False + + def read_card_async(self, callback): + """Start reading card data asynchronously""" + if not self.evdev_available: + Logger.warning("CardReader: evdev not available - waiting for manual timeout/cancel") + # Fallback: In development mode without evdev, don't auto-authenticate + # Let the popup timeout or user cancel manually + # This allows testing the popup behavior + # To enable auto-authentication for testing, uncomment the next line: + # Clock.schedule_once(lambda dt: callback("DEFAULT_USER_12345"), 0.5) + return + + if not self.device: + if not self.find_card_reader(): + Logger.error("CardReader: Cannot start reading - no device found") + callback(None) + return + + self.reading = True + self.card_data = "" + self.last_read_time = time.time() + + # Start reading in a separate thread + thread = threading.Thread(target=self._read_card_thread, args=(callback,)) + thread.daemon = True + thread.start() + + def _read_card_thread(self, callback): + """Thread function to read card data""" + try: + Logger.info("CardReader: Waiting for card swipe...") + for event in self.device.read_loop(): + if not self.reading: + break + + # Check for timeout + if time.time() - self.last_read_time > self.read_timeout: + Logger.warning("CardReader: Read timeout") + self.reading = False + callback(None) + break + + 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': + Logger.info(f"CardReader: Card read complete: {self.card_data}") + self.reading = False + callback(self.card_data) + break + + # Build card data string + elif key_code.startswith('KEY_'): + char = key_code.replace('KEY_', '') + if len(char) == 1: # Single character + self.card_data += char + self.last_read_time = time.time() + + except Exception as e: + Logger.error(f"CardReader: Error reading card: {e}") + self.reading = False + callback(None) + + def stop_reading(self): + """Stop reading card data""" + self.reading = False + + +class CardSwipePopup(Popup): + """Popup that shows card swipe prompt with 5 second timeout""" + def __init__(self, callback, resources_path, **kwargs): + super(CardSwipePopup, self).__init__(**kwargs) + self.callback = callback + self.timeout_event = None + + # Popup settings + self.title = 'Card Authentication Required' + self.size_hint = (0.5, 0.4) + self.auto_dismiss = False + self.separator_height = 2 + + # Main layout + layout = BoxLayout(orientation='vertical', padding=20, spacing=20) + + # Card swipe icon (using image) + icon_path = os.path.join(resources_path, 'access-card.png') + icon_image = Image( + source=icon_path, + size_hint=(1, 0.4), + allow_stretch=True, + keep_ratio=True + ) + layout.add_widget(icon_image) + + # Message + self.message_label = Label( + text='Please swipe your card...', + font_size='20sp', + size_hint=(1, 0.2) + ) + layout.add_widget(self.message_label) + + # Countdown timer display + self.countdown_label = Label( + text='5', + font_size='48sp', + color=(0.9, 0.6, 0.2, 1), + size_hint=(1, 0.2) + ) + layout.add_widget(self.countdown_label) + + # Cancel button + cancel_btn = Button( + text='Cancel', + size_hint=(1, 0.2), + background_color=(0.9, 0.3, 0.2, 1) + ) + cancel_btn.bind(on_press=self.cancel) + layout.add_widget(cancel_btn) + + self.content = layout + + # Start countdown + self.remaining_time = 5 + self.countdown_event = Clock.schedule_interval(self.update_countdown, 1) + + # Schedule timeout + self.timeout_event = Clock.schedule_once(self.on_timeout, 5) + + def update_countdown(self, dt): + """Update countdown display""" + self.remaining_time -= 1 + self.countdown_label.text = str(self.remaining_time) + if self.remaining_time <= 0: + Clock.unschedule(self.countdown_event) + + def on_timeout(self, dt): + """Called when timeout occurs""" + Logger.warning("CardSwipePopup: Timeout - no card swiped") + self.message_label.text = 'Timeout - No card detected' + Clock.schedule_once(lambda dt: self.finish(None), 0.5) + + def card_received(self, card_data): + """Called when card data is received""" + Logger.info(f"CardSwipePopup: Card received: {card_data}") + self.message_label.text = 'āœ“ Card detected' + self.countdown_label.text = 'āœ“' + self.countdown_label.color = (0.2, 0.9, 0.3, 1) + Clock.schedule_once(lambda dt: self.finish(card_data), 0.5) + + def cancel(self, instance): + """Cancel button pressed""" + Logger.info("CardSwipePopup: Cancelled by user") + self.finish(None) + + def finish(self, card_data): + """Clean up and call callback""" + # Cancel scheduled events + if self.timeout_event: + Clock.unschedule(self.timeout_event) + if self.countdown_event: + Clock.unschedule(self.countdown_event) + + # Dismiss popup + self.dismiss() + + # Call callback with result + if self.callback: + self.callback(card_data) + + class EditPopup(Popup): """Popup for editing/annotating images""" - def __init__(self, player_instance, image_path, authenticated_user=None, **kwargs): + def __init__(self, player_instance, image_path, user_card_data=None, **kwargs): super(EditPopup, self).__init__(**kwargs) self.player = player_instance self.image_path = image_path - self.authenticated_user = authenticated_user or "player_1" # Default to player_1 + self.user_card_data = user_card_data # Store card data to send to server on save self.drawing_layer = None # Pause playback @@ -487,7 +710,7 @@ class EditPopup(Popup): 'new_name': output_filename, 'original_path': self.image_path, 'version': version, - 'user': self.authenticated_user + 'user_card_data': self.user_card_data # Card data from reader (or None) } # Save metadata JSON @@ -496,7 +719,7 @@ class EditPopup(Popup): with open(json_path, 'w') as f: json.dump(metadata, f, indent=2) - Logger.info(f"EditPopup: Saved metadata to {json_path}") + Logger.info(f"EditPopup: Saved metadata to {json_path} (user_card_data: {self.user_card_data})") return json_path def _upload_to_server(self, image_path, metadata_path): @@ -1039,6 +1262,9 @@ class SignagePlayer(Widget): self.consecutive_errors = 0 # Track consecutive playback errors self.max_consecutive_errors = 10 # Maximum errors before stopping self.intro_played = False # Track if intro has been played + # Card reader for authentication + self.card_reader = None + self._pending_edit_image = None # Paths self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) self.config_dir = os.path.join(self.base_dir, 'config') @@ -1608,7 +1834,16 @@ class SignagePlayer(Widget): popup.open() def show_edit_interface(self, instance=None): - """Show edit interface for current image""" + """ + Show edit interface for current image + + Workflow: + 1. Validate media type (must be image) + 2. Check edit_on_player permission from server + 3. Prompt for card swipe (5 second timeout) + 4. Open edit interface with card data stored + 5. When saved, card data is included in metadata and sent to server + """ # Check if current media is an image if not self.playlist or self.current_index >= len(self.playlist): Logger.warning("SignagePlayer: No media to edit") @@ -1637,29 +1872,54 @@ class SignagePlayer(Widget): Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) return - # Check 3: Verify user authentication - # TODO: Implement card swipe authentication system - authenticated_user = "player_1" # Placeholder - will be replaced with card authentication + # Store image info + self._pending_edit_image = file_name - if not authenticated_user: - Logger.warning(f"SignagePlayer: User not authenticated for editing") - # Show error message briefly - self.ids.status_label.text = 'User authentication required' - self.ids.status_label.opacity = 1 - Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) - return + # Show card swipe popup with 5 second timeout + self._card_swipe_popup = CardSwipePopup(callback=self._on_card_swipe_result, resources_path=self.resources_path) + self._card_swipe_popup.open() - # Get full path to current image + # Initialize card reader if not already created + if not self.card_reader: + self.card_reader = CardReader() + + # Start reading card asynchronously + self.card_reader.read_card_async(self._on_card_data_received) + + def _on_card_data_received(self, card_data): + """Called when card reader gets data""" + if hasattr(self, '_card_swipe_popup') and self._card_swipe_popup: + self._card_swipe_popup.card_received(card_data) + + def _on_card_swipe_result(self, card_data): + """Handle result from card swipe popup (card data or None if timeout/cancelled)""" + # Get image path + file_name = self._pending_edit_image image_path = os.path.join(self.media_dir, file_name) if not os.path.exists(image_path): Logger.error(f"SignagePlayer: Image not found: {image_path}") + self.ids.status_label.text = 'Image file not found' + self.ids.status_label.opacity = 1 + Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) return - Logger.info(f"SignagePlayer: Opening edit interface for {file_name} (user: {authenticated_user})") + # If no card data (timeout or cancelled), don't open edit interface + if not card_data: + Logger.info("SignagePlayer: Edit cancelled - no card data") + self.ids.status_label.text = 'Edit cancelled - card required' + self.ids.status_label.opacity = 1 + Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) + return - # Open edit popup with authenticated user - popup = EditPopup(player_instance=self, image_path=image_path, authenticated_user=authenticated_user) + # Store the card data (will be sent to server when save is done) + user_card_data = card_data.strip() + + Logger.info(f"SignagePlayer: Card data captured: {user_card_data}") + Logger.info(f"SignagePlayer: Opening edit interface for {file_name}") + + # Open edit popup with card data (will be used when saving) + popup = EditPopup(player_instance=self, image_path=image_path, user_card_data=user_card_data) popup.open() def show_exit_popup(self, instance=None): diff --git a/working_files/CARD_READER_AUTHENTICATION.md b/working_files/CARD_READER_AUTHENTICATION.md new file mode 100644 index 0000000..d33653b --- /dev/null +++ b/working_files/CARD_READER_AUTHENTICATION.md @@ -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) diff --git a/working_files/test_card_reader.py b/working_files/test_card_reader.py new file mode 100644 index 0000000..4adda28 --- /dev/null +++ b/working_files/test_card_reader.py @@ -0,0 +1,114 @@ +#!/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()] + + for i, device in enumerate(devices): + 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") + + 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()] + + 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 + device = None + for dev in devices: + if 'keyboard' in dev.name.lower() or 'card' in dev.name.lower() or 'reader' in dev.name.lower(): + device = dev + print(f"Found potential card reader: {dev.name}") + break + + if not device and devices: + # Use first keyboard device + for dev in devices: + capabilities = dev.capabilities() + if ecodes.EV_KEY in capabilities: + device = dev + print(f"Using keyboard device: {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()