""" This class executes when we are on a raspberry Pi. It handles the press of a button via GPIO, activates a flash and prints out the picture that was taken via a USB Webcam. """ import io # To check if we are on a Raspberry Pi import subprocess import os from time import sleep, gmtime, strftime from flask_socketio import SocketIO from gpiozero import Button, LED, DigitalOutputDevice from PIL import Image from task import TextTask, ImageTask, CutTask class Raspberry(): """ This class will manage three things : - Connecting to a USB webcam - Managing a push button - Activating a flash ( or light ) - Flash an indicator light """ def __init__( self, print_queue, app, socketio, button_gpio_port_number, indicator_gpio_port_number, flash_gpio_port_number, is_flash_present, ): self.print_queue = print_queue self.socketio = socketio self.app = app self.flash_gpio = flash_gpio_port_number self.is_flash_present = is_flash_present self.button_gpio = button_gpio_port_number self.led_gpio = indicator_gpio_port_number self.image_path = self.app.config["UPLOAD_FOLDER"] + "/image.jpg" def is_raspberry_pi(self, raise_on_errors=False): """ Checking if we are on a Raspberry Pi by checking information on the /proc/cpuinfo file """ # Check if we are running on a raspberry pi try: with io.open("/proc/cpuinfo", "r", encoding="utf-8") as cpuinfo: found = False for line in cpuinfo: if line.startswith("Hardware"): found = True label, value = line.strip().split(":", 1) value = value.strip() if value not in ( "BCM2708", "BCM2709", "BCM2711", "BCM2835", "BCM2836", ): self.app.logger.debug( "This system does not appear to be a Raspberry Pi." ) return False if not found: self.app.logger.error( "Couldn't get sufficient hardware information from /proc/cpuinfo, Unable to determine if we are on a Raspberry Pi." ) return False except IOError: self.app.logger.error("Unable to open `/proc/cpuinfo`.") return False self.app.logger.debug("It seems we are on a Raspberry Pi") try: self.initialise_gpio() except Exception as e: self.app.logger.debug("Could not init GPIO : " + str(e)) raise e return True def initialise_gpio(self): self.app.logger.debug("Initializing GPIO") self.led = LED(self.led_gpio) self.app.logger.debug("Activated indicator LED") self.indicator_countdown(iters=3) self.button = Button(self.button_gpio, pull_up=True, bounce_time=0.1) self.button.when_pressed = self.on_button_pressed self.app.logger.debug("Activated button") # The "flash" is a relay-controlled device ( light bulb for example ) self.flash = DigitalOutputDevice(self.flash_gpio) self.flash_toggle() self.app.logger.debug("Activated flash") def indicator_countdown(self, iters=10, multi=10): for i in range(iters, 0, -1): self.led.on() sleep(i / multi) self.led.off() sleep(i / multi) def indicator_led(self, timing=0.2, l=5): for i in range(l): self.app.logger.debug("LED turned on") self.led.on() sleep(timing) self.led.off() self.app.logger.debug("LED turned off") sleep(timing) def flash_toggle(self): self.app.logger.debug("Flash turned on") self.flash.on() sleep(0.3) self.flash.off() self.app.logger.debug("Flash turned off") def take_picture(self): # Validate if the image path is valid if not os.path.isdir(os.path.dirname(self.image_path)): self.app.logger.error( f"Invalid directory for image path: {self.image_path}" ) return False try: result = subprocess.run( ["fswebcam", "--no-banner", "-r", "1920x1080", self.image_path], check=True, # Will raise CalledProcessError if the command fails stdout=subprocess.PIPE, # Capture standard output stderr=subprocess.PIPE, # Capture error output ) # Optionally log the command output self.app.logger.debug( f"Image captured successfully: {result.stdout.decode()}" ) except subprocess.CalledProcessError as e: # Log error output if available self.app.logger.error( f"Unable to take a picture. Error: {e.stderr.decode()}" ) return False except RuntimeError as e: # Catch any unexpected errors self.app.logger.error(f"Unexpected error while taking picture: {str(e)}") return False # # Overlay logo # logo_path = 'src/static/images/requin.png' # Update path as needed # if not self.overlay_logo(self.image_path, logo_path): # self.app.logger.warning("Picture taken but failed to overlay logo.") return True def overlay_logo( self, image_path, logo_path, output_path=None, position="bottom_right", margin=10, ): try: image = Image.open(image_path).convert("RGBA") logo = Image.open(logo_path).convert("RGBA") # Resize logo if it's too big (logo will be 30% the width of the image) logo_ratio = ( 0.30 # You can change the ratio if you want the logo bigger or smaller ) logo_width = int(image.width * logo_ratio) logo_height = int(logo.height * (logo_width / logo.width)) logo = logo.resize((logo_width, logo_height), Image.Resampling.LANCZOS) # Calculate position based on the chosen location if position == "bottom_right": x = image.width - logo.width - margin y = image.height - logo.height - margin elif position == "top_left": x, y = margin, margin elif position == "top_right": x = image.width - logo.width - margin y = margin elif position == "bottom_left": x = margin y = image.height - logo.height - margin else: raise ValueError( "Invalid position. Choose from 'bottom_right', 'top_left', 'top_right', or 'bottom_left'." ) # Composite the logo onto the image image.paste(logo, (x, y), logo) # Use logo as its own alpha mask # Save the result if not output_path: output_path = image_path # Overwrite the original image if no output path is given image.save(output_path) except RuntimeError as e: self.app.logger.error(f"Error overlaying logo: {e}") return False return True def crop_to_square(self, image_path, output_path=None): try: image = Image.open(image_path) width, height = image.size # Determine shorter side new_edge = min(width, height) # Calculate cropping box (centered) left = (width - new_edge) // 2 top = (height - new_edge) // 2 right = left + new_edge bottom = top + new_edge image = image.crop((left, top, right, bottom)) if not output_path: output_path = image_path # Overwrite original image.save(output_path) except RuntimeError as e: self.app.logger.error(f"Error cropping image to square: {e}") return False return True def on_button_pressed(self): self.app.logger.debug("Button has been pressed") self.led.on() self.app.logger.debug("Counting down") self.indicator_countdown( iters=4, multi=20 ) # The indicator will flash a countdown LED self.app.logger.debug("Taking picture") try: self.flash.on() self.take_picture() except RuntimeError as e: self.app.logger.error( "Could not take a picture after the button press : " + str(e) ) finally: self.flash.off() self.app.logger.debug("Printing picture") self.led.on() self.crop_to_square(self.image_path) self.print_queue.enqueue(ImageTask(self.image_path,signature="",process=True)) self.print_queue.enqueue(TextTask(content="Imprimé par LittlePrynter", signature="")) time = strftime("%Y-%m-%d %H:%M", gmtime()) self.print_queue.enqueue(TextTask(content=time, signature="")) self.print_queue.enqueue(CutTask()) self.led.off() self.app.logger.debug("Added a photomaton picture to the print queue") return True