6 Commits

Author SHA1 Message Date
n07070
3769e17444 Lint project 2026-02-02 12:37:15 +01:00
n07070
98299cc281 Add information on contributions 2026-01-04 12:08:05 +01:00
n07070
d261bb0fdc Add 3d models of the printer and raspberry pi 2026-01-02 17:40:29 +01:00
n07070
46fda975d4 Ajout des logos de extase club 2025-11-13 16:59:52 +01:00
nono
670ab495d8 Update code to better handle the raspberry pi mode 2025-10-17 17:33:53 +02:00
nono
34bffe93af Add systemd service 2025-10-17 17:33:37 +02:00
10 changed files with 351 additions and 163 deletions

View File

@@ -61,6 +61,12 @@ This command should start a web server with which you can test your configuratio
Voilà ! Voilà !
## Contributions
Your contributions are very much welcome ! You can either request an account on git.n07070.xyz, or send me a patch by email ( see git-send-mail.io ). Please [squash](https://www.geeksforgeeks.org/git/use-of-git-squash-commits/) yours commits into one commit, and add as much information in the commit's description. The more you add comments and descriptions, the better it is.
Please also say if you had a printer to test your code, and which printer you've been using.
## Screenshots ## Screenshots
![](src/static/images/homepage.png) ![](src/static/images/homepage.png)

Binary file not shown.

Binary file not shown.

12
littleprynter.service Normal file
View File

@@ -0,0 +1,12 @@
[Unit]
Description=LittlePrynter
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/pi/littleprynter/
Environment=FLASK_APP=src/main.py
ExecStart=/home/pi/littleprynter/bin/flask run --host 0.0.0.0 --debug --no-reload
[Install]
WantedBy=multi-user.target

View File

@@ -1,46 +1,72 @@
# Welcome to the LittlePrynter's source code. """
# This program expose a web interface, with user authentification, that makes it possible to print messages from the web. Welcome to the LittlePrynter's source code.
# It also exposes a API, making it possible to print and interface with much of the printer's abilities. This program expose a web interface, with user authentification,
that makes it possible to print messages from the web.
It also exposes a API, making it possible to print and interface
with much of the printer's abilities.
# We first define the connection to the printer itself, We first define the connection to the printer itself,
# Then we build the API around Flask, Then we build the API around Flask,
# Then we build the web interface, using the simple Jinja2 templating. Then we build the web interface, using the simple Jinja2 templating.
# We support two modes : We support two modes :
# The first is a simple mode, where a computer, connected to a thermal printer, runs this program and exposes a web interface that makes use of the client's camera The first is a simple mode, where a computer, connected to a thermal printer,
# The seconde is booth mode, where a Raspberry Pi is connected to a thermal printer, a button and a flash. The web interface exists but may not be used, as the press of the button with take a picture and activate the flash while simply informing the web page. runs this program and exposes a web interface that makes use of the client's camera
The seconde is booth mode, where a Raspberry Pi is connected to a thermal printer,
a button and a flash.
The web interface exists but may not be used, as the press of the button with take
a picture and activate the flash while simply informing the web page.
"""
# Following are the librairies we import, # Following are the librairies we import,
from flask import Flask, request, render_template, flash, abort, redirect, url_for, make_response, jsonify # Used for the web framework import sys
import os # For VARS from the shell.
import pprint # To pretty print JSON
import toml # Used for the config file parsing
from flask import (
Flask,
request,
render_template,
flash,
redirect,
url_for,
jsonify,
) # Used for the web framework
import werkzeug.exceptions
from flask_socketio import SocketIO from flask_socketio import SocketIO
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from printer import Printer # The wrapper for the printer class from printer import Printer # The wrapper for the printer class
from raspberry import Raspberry # The Raspberry pi control Class from raspberry import Raspberry # The Raspberry pi control Class
from web import Web # Wrapper for the web routes and API from web import Web # Wrapper for the web routes and API
import toml # Used for the config file parsing
import pprint # To pretty print JSON
import time # To sleep
import os # For VARS from the shell.
# Variables # Variables
app = Flask(__name__) app = Flask(__name__)
socketio = SocketIO(app) socketio = SocketIO(app)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
# Load the configuration file # Load the configuration file
try: try:
app.logger.debug("Loading config file...") app.logger.debug("Loading config file...")
configuration_file = toml.load("configuration/config.toml") with open("configuration/config.toml", "r", encoding="utf-8") as f:
except TypeError : configuration_file = toml.load(f)
app.logger.error("Unable to load the config file: invalid type or is a list containing invalid types") except TypeError:
exit(-1) app.logger.error(
except toml.TomlDecodeError: "Unable to load the config file: invalid type or is a list containing invalid types"
app.logger.error("An error occured while decoding the file") )
exit(-1) sys.exit(-1)
except Exception as e: except toml.TomlDecodeError as e:
app.logger.error("Error while loading file : " + str(e)) app.logger.error(
exit(-1) "An error occured while decoding the file %s , error at %s:%s",
str(e.doc),
str(e.colno),
str(e.lineno),
)
sys.exit(-1)
except OSError as e:
app.logger.error("Error while loading file %s ", str(e))
sys.exit(-1)
app.logger.debug("Config file loaded !") app.logger.debug("Config file loaded !")
@@ -51,69 +77,69 @@ UPLOAD_FOLDER = str(configuration_file["printer"]["upload_folder"])
try: try:
os.mkdir(UPLOAD_FOLDER) os.mkdir(UPLOAD_FOLDER)
app.logger.debug(f"Directory '{UPLOAD_FOLDER}' created successfully.") app.logger.debug("Directory %s created successfully.", UPLOAD_FOLDER)
except FileExistsError: except FileExistsError:
app.logger.debug(f"Directory '{UPLOAD_FOLDER}' already exists.") app.logger.debug("Directory %s already exists.", UPLOAD_FOLDER)
except PermissionError: except PermissionError:
app.logger.error(f"Permission denied: Unable to create '{UPLOAD_FOLDER}'.") app.logger.error("Permission denied: Unable to create %s", UPLOAD_FOLDER)
except Exception as e:
app.logger.error(f"An error occurred: {e}")
# Output the config file # Output the config file
if os.getenv('LIPY_DEBUG') == True: if os.getenv("LIPY_DEBUG") is True:
pprint.pprint(configuration_file) pprint.pprint(configuration_file)
# We define the app module used by Flask # We define the app module used by Flask
app.secret_key = configuration_file["secrets"]["flask_secret_key"] app.secret_key = configuration_file["secrets"]["flask_secret_key"]
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config['ALLOWED_EXTENSIONS'] = ALLOWED_EXTENSIONS app.config["ALLOWED_EXTENSIONS"] = ALLOWED_EXTENSIONS
app.config['MAX_CONTENT_LENGTH'] = 10 * 1000 * 1000 # Maximum 3Mb for a file upload app.config["MAX_CONTENT_LENGTH"] = 10 * 1000 * 1000 # Maximum 3Mb for a file upload
app.config['TEMPLATES_AUTO_RELOAD'] = True app.config["TEMPLATES_AUTO_RELOAD"] = True
# Printer connection # Printer connection
# Uses the class defined in the printer.py file # Uses the class defined in the printer.py file
printer = Printer(app,0x04b8, 0x0e28) printer = Printer(app, 0x04B8, 0x0E28)
printer.init_printer() printer.init_printer()
# Find out if we are running on a Raspberry Pi # Find out if we are running on a Raspberry Pi
rpi = Raspberry(printer, rpi = Raspberry(
printer,
app, app,
socketio, socketio,
configuration_file['rpi']['button_gpio_port_number'], configuration_file['rpi']['indicator_gpio_port_number'], configuration_file["rpi"]["button_gpio_port_number"],
configuration_file['rpi']['flash_gpio_port_number'], configuration_file["rpi"]["indicator_gpio_port_number"],
configuration_file['rpi']['flash'] ) configuration_file["rpi"]["flash_gpio_port_number"],
configuration_file["rpi"]["flash"],
)
RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi() RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi()
#############################################################
# Web & API routes # Web & API routes
#############################################################
web = Web(app, printer) web = Web(app, printer)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, use_reloader=False, host='0.0.0.0', ssl_context='adhoc') app.run(debug=True, use_reloader=False, host="0.0.0.0", ssl_context="adhoc")
limiter = Limiter( limiter = Limiter(
get_remote_address, get_remote_address, app=app, default_limits=["1500 per day", "500 per hour"]
app=app, )
default_limits=["1500 per day", "500 per hour"]
)
@app.route('/')
@app.route("/")
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
def index(): def index():
"""Return the web interface index"""
app.logger.debug("Loading index") app.logger.debug("Loading index")
return render_template('index.html') return render_template("index.html")
@app.route('/webcam')
@app.route("/webcam")
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
def webcam(): def webcam():
"""Returns the webcam web interface"""
app.logger.debug("Loading webcam interface") app.logger.debug("Loading webcam interface")
return render_template('webcam.html') return render_template("webcam.html")
# API routes # API routes
# The api has the following methods # The api has the following methods
@@ -121,71 +147,71 @@ def webcam():
# api/auth/{login,logout} # api/auth/{login,logout}
# api/status/{paper,ping,stats} # api/status/{paper,ping,stats}
# If you just call the api route, you get a help back. # If you just call the api route, you get a help back.
@app.route('/api') @app.route("/api")
@app.route('/api/print') @app.route("/api/print")
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
def api_index(): def api_index():
"""Returns a how-to for the API"""
app.logger.debug("Loading API") app.logger.debug("Loading API")
return render_template("api.html") return render_template("api.html")
@app.route('/api/print/sms', methods=['POST']) @app.route("/api/print/sms", methods=["POST"])
@limiter.limit("6/minute", override_defaults=False) @limiter.limit("6/minute", override_defaults=False)
def api_print_sms(): def api_print_sms():
"""Prints a short message on a printer"""
app.logger.debug("Printing an sms") app.logger.debug("Printing an sms")
try: try:
txt = request.form["txt"] txt = request.form["txt"]
sign = request.form["signature"] sign = request.form["signature"]
except Exception as e: except werkzeug.exceptions.BadRequestKeyError as e:
app.logger.error("Whoops, no forms submitted or missing signature :" + str(e)) app.logger.error(
flash("Whoops, no forms submitted or missing signature : " + str(e)) "Whoops, no forms submitted or missing signature : %s ", str(e)
)
flash("Whoops, no forms submitted or missing signature : %s", str(e))
return redirect(url_for("index")) return redirect(url_for("index"))
web.print_sms(txt,sign) web.print_sms(txt, sign)
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route('/api/print/img', methods=['POST'])
@app.route("/api/print/img", methods=["POST"])
@limiter.limit("6/minute", override_defaults=False) @limiter.limit("6/minute", override_defaults=False)
def api_print_image(): def api_print_image():
"""Prints an image on a printer"""
app.logger.debug("Printing an image") app.logger.debug("Printing an image")
try: try:
sign = request.form["signature"] sign = request.form["signature"]
except Exception as e: except Exception as e:
app.logger.error("Whoops, no forms submitted or missing signature :" + str(e)) app.logger.error("Whoops, no forms submitted or missing signature : %s", str(e))
flash("Whoops, no forms submitted or missing signature : " + str(e)) flash("Whoops, no forms submitted or missing signature : %s ", str(e))
return redirect(url_for("index")) return redirect(url_for("index"))
if request.method == "POST":
if request.method == 'POST':
# check if the post request has the file part # check if the post request has the file part
try: if "img" not in request.files:
if 'img' not in request.files: app.logger.error("Whoops, no images submitted : %s ", str(e))
app.logger.error("Whoops, no images submitted :" + str(e)) app.logger.error("Error getting the files : %s", str(e))
flash("Whoops, no images submitted : " + str(e)) flash("Whoops, no images submitted : %s", str(e))
else:
file = request.files['img']
except Exception as e:
app.logger.error('Error getting the files :' + str(e))
flash('Error getting the files :' + str(e))
return redirect(url_for("index")) return redirect(url_for("index"))
file = request.files["img"]
# If the user does not select a file, the browser submits an # If the user does not select a file, the browser submits an
# empty file without a filename. # empty file without a filename.
if file.filename == '': if file.filename == "":
app.logger.error("Submitted file has no filename !") app.logger.error("Submitted file has no filename !")
flash("Submitted file has no filename !") flash("Submitted file has no filename !")
return redirect(url_for("index")) return redirect(url_for("index"))
try: try:
app.logger.debug("Sending the image to the printer.") app.logger.debug("Sending the image to the printer.")
web.print_image(file, sign) web.print_image(file, sign)
except Exception as e: except Exception as e:
app.logger.error("The image could not be printed because : " + str(e) ) app.logger.error("The image could not be printed because : %s ", str(e))
flash("The image could not be printed because : " + str(e)) flash("The image could not be printed because : %s ", str(e))
return redirect(url_for("index")) return redirect(url_for("index"))
else: else:
@@ -193,56 +219,69 @@ def api_print_image():
flash("Method not allowed") flash("Method not allowed")
return redirect(url_for("index")) return redirect(url_for("index"))
flash('Picture printed ! '), flash("Picture printed ! ")
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route('/api/camera/picture') @app.route("/api/camera/picture")
def camera_picture(): def camera_picture():
# Returns a picture taken by the camera """Returns a picture taken by the camera"""
if RASPBERRY_PI_CONNECTED: if RASPBERRY_PI_CONNECTED:
try: try:
return rpi.camera_picture() return rpi.camera_picture()
except Exception as e: except Exception as e:
return jsonify({'message': 'Error getting the stream : ' + e}), 500 return jsonify({"message": "Error getting the stream : " + e}), 500
else: else:
return jsonify({'message': 'No camera present'}), 500 return jsonify({"message": "No camera present"}), 500
@app.route("/login")
@app.route('/login')
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
def login_page(): def login_page():
"""Unsued, logins"""
# web.login(username,password) # web.login(username,password)
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route('/logout')
@app.route("/logout")
@limiter.limit("1/second", override_defaults=False) @limiter.limit("1/second", override_defaults=False)
def logout_page(): def logout_page():
"""Unused, logout"""
# web.logout(username, password) # web.logout(username, password)
return redirect(url_for("index")) return redirect(url_for("index"))
@app.errorhandler(429) @app.errorhandler(429)
def ratelimit_handler(e): def ratelimit_handler(e):
flash("Rate limit reached, please slow down :) ( Currently at "+ e.description + ")", 'error') """Handle rate limits"""
app.logger.debug('Rate limit reached ' + str(e.description)) flash(
"Rate limit reached, please slow down :) ( Currently at " + e.description + ")",
"error",
)
app.logger.debug("Rate limit reached %s " , str(e.description))
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route("/ping") @app.route("/ping")
@limiter.exempt @limiter.exempt
def ping(): def ping():
flash("🏓 Pong !",'info') """Returns a pong"""
app.logger.debug('🏓 Pong !') flash("🏓 Pong !", "info")
app.logger.debug("🏓 Pong !")
return redirect(url_for("index")) return redirect(url_for("index"))
@socketio.on('ping')
def handle_message(data):
app.logger.debug('Received : ' + str(data))
socketio.emit('pong',"Pong !")
@socketio.on('get_camera_status') @socketio.on("ping")
def handle_message(data):
"""Handle sockets pings"""
app.logger.debug("Received : %s " , str(data))
socketio.emit("pong", "Pong !")
@socketio.on("get_camera_status")
def camera_status(): def camera_status():
app.logger.debug('Client asked if we had a camera') """Returns camera status to a socket"""
app.logger.debug("Client asked if we had a camera")
if RASPBERRY_PI_CONNECTED: if RASPBERRY_PI_CONNECTED:
socketio.emit("camera_status", True) socketio.emit("camera_status", True)
else: else:

View File

@@ -98,46 +98,37 @@ class Printer(object):
return True return True
def print_sms(self, msg, signature="Guest") -> None: def print_sms(self, msg, signature="",bold=False):
clean_msg = str(msg) clean_msg = str(msg) + "\n"
clean_signature = str(signature) clean_signature = str(signature)
if len(clean_msg) > 4096 or len(clean_msg) < 3 : if len(clean_msg) > 4096:
self.app.logger.warning("Could not print message of this length: " + str(len(clean_msg))) self.app.logger.warning("Could not print message of this length: " + str(len(clean_msg)))
raise Exception("Could not print message of this length :" + str(len(clean_msg)) + ", needs to between 3 and 4096 caracters long.") raise Exception("Could not print message of this length :" + str(len(clean_msg)) + ", needs to be below 4096 caracters long.")
if len(signature) > 256 or len(signature) < 1: if len(signature) > 256:
self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature))) self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature)))
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to between 3 and 256 caracters long.") raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long.")
self.check_paper()
try: try:
self.printer.open(self.usb_args); self.printer.open(self.usb_args);
self.printer.set(align='left', font='a', bold=False, underline=0, width=1, height=1, density=8, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False) self.printer.set(align='center', font='a', bold=bold)
self.printer.set(align='left', font='a', bold=True, underline=1, width=1, height=1, density=6, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False) self.printer.textln(clean_msg )
self.printer.textln(clean_msg) if clean_signature:
self.printer.textln("") self.printer.textln(clean_signature )
self.printer.set(align='left', font='b', bold=False, underline=1, width=1, height=1, density=9, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False)
self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
self.printer.textln("")
self.printer.textln("Printed by LittlePrinter ")
self.printer.textln("n07070.xyz/articles/littleprynter")
self.printer.textln("")
self.printer.cut()
self.printer.close() self.printer.close()
except Exception as e: except Exception as e:
self.app.logger.error("Unable to print because : " + str(e)) self.app.logger.error("Unable to print because : " + str(e))
raise e
self.app.logger.info("Printed text")
return True return True
def print_img(self, path, sign) -> None: def print_img(self, path, sign="",center=True,process=False):
clean_signature = str(sign) clean_signature = str(sign)
if len(sign) > 256 or len(sign) < 1: if len(sign) > 256:
self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature))) self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature)))
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to between 3 and 256 caracters long.") raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long.")
if not os.path.isfile(str(path)): if not os.path.isfile(str(path)):
self.app.logger.warning("File does not exist : " + str(path)) self.app.logger.warning("File does not exist : " + str(path))
@@ -145,31 +136,61 @@ class Printer(object):
else: else:
self.app.logger.debug("Printing file from " + str(path)) self.app.logger.debug("Printing file from " + str(path))
if process:
try: try:
self.app.logger.debug("Proccessing the image") self.app.logger.debug("Proccessing the image")
path = process_image(self, path) path = process_image(self, path)
except Exception as e: except Exception as e:
self.app.logger.error(str(e)) self.app.logger.error(str(e))
raise e return False
else:
self.app.logger.warning("Not proccessing the image")
try: try:
self.check_paper()
self.printer.open(self.usb_args) self.printer.open(self.usb_args)
self.printer.image(path) self.printer.image(path,center=center)
self.printer.textln("Printed by LittlePrynter ")
self.printer.textln("n07070.xyz/articles/littleprynter")
self.printer.textln(clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
self.printer.cut()
self.printer.close() self.printer.close()
self.app.logger.debug("Printed an image : " + str(path)) self.app.logger.debug("Printed an image : " + str(path))
os.remove(path) os.remove(path)
self.app.logger.debug("Removed image.") self.app.logger.debug("Removed image : " + str(path))
except Exception as e:
self.printer.close()
self.app.logger.error(str(e))
return False
self.app.logger.info("Printed a picture")
return True
def qr(self, content):
try:
self.printer.open(self.usb_args)
self.printer.qr(content, center=True)
self.printer.close()
except Exception as e:
self.printer.close()
self.app.logger.error(str(e))
return False
self.app.logger.info("Printed a QR")
return True
def cut(self):
try:
self.printer.open(self.usb_args)
self.printer.cut()
self.printer.close()
except Exception as e: except Exception as e:
self.printer.close() self.printer.close()
self.app.logger.error(str(e)) self.app.logger.error(str(e))
raise e raise e
self.app.logger.info("Did a cut")
return True
def process_image(self, path): def process_image(self, path):
brightness_factor = 1.5 # Used only if image is too dark brightness_factor = 1.5 # Used only if image is too dark
brightness_threshold = 100 # Brightness threshold (0255) brightness_threshold = 100 # Brightness threshold (0255)
@@ -187,9 +208,9 @@ def process_image(self, path):
original_img.thumbnail((max_width, max_height), Image.LANCZOS) original_img.thumbnail((max_width, max_height), Image.LANCZOS)
self.app.logger.debug("Resized the image") self.app.logger.debug("Resized the image")
# Convert to grayscale for dithering # # Convert to grayscale for dithering
dithered_img = original_img.convert("L").convert("1") # Dithering using default method (FloydSteinberg) # dithered_img = original_img.convert("L").convert("1") # Dithering using default method (FloydSteinberg)
self.app.logger.debug("Dithered the image") # self.app.logger.debug("Dithered the image")
# Compute brightness of original image (grayscale average) # Compute brightness of original image (grayscale average)
grayscale = original_img.convert("L") grayscale = original_img.convert("L")
@@ -204,9 +225,9 @@ def process_image(self, path):
enhancer = ImageEnhance.Brightness(original_img) enhancer = ImageEnhance.Brightness(original_img)
original_img = enhancer.enhance(brightness_factor) original_img = enhancer.enhance(brightness_factor)
# Reduce contrast # # Reduce contrast
contrast_enhancer = ImageEnhance.Contrast(original_img) # contrast_enhancer = ImageEnhance.Contrast(original_img)
original_img = contrast_enhancer.enhance(contrast_factor) # original_img = contrast_enhancer.enhance(contrast_factor)
# Final resize check # Final resize check
if original_img.height > max_height: if original_img.height > max_height:

View File

@@ -1,8 +1,10 @@
from flask_socketio import SocketIO from flask_socketio import SocketIO
from gpiozero import Button, LED, DigitalOutputDevice from gpiozero import Button, LED, DigitalOutputDevice
from time import sleep from time import sleep, gmtime, strftime
from PIL import Image
import io # To check if we are on a Raspberry Pi import io # To check if we are on a Raspberry Pi
import subprocess import subprocess
import os
class Raspberry(object): class Raspberry(object):
""" """
@@ -65,24 +67,16 @@ class Raspberry(object):
self.led = LED(self.led_gpio) self.led = LED(self.led_gpio)
self.app.logger.debug('Activated indicator LED') 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 = Button(self.button_gpio, pull_up=True, bounce_time=0.1)
self.button.when_pressed = self.on_button_pressed self.button.when_pressed = self.on_button_pressed
self.app.logger.debug('Activated button') self.app.logger.debug('Activated button')
# The "flash" is a relay-controlled device ( light bulb for example ) # The "flash" is a relay-controlled device ( light bulb for example )
self.flash = DigitalOutputDevice(self.flash_gpio) self.flash = DigitalOutputDevice(self.flash_gpio)
self.flash_toggle()
self.app.logger.debug('Activated flash') self.app.logger.debug('Activated flash')
def take_picture(self):
try:
subprocess.run(['fswebcam', '--no-banner', self.image_path])
except Exception as e:
self.app.logger.error('Unable to take a picture :' + str(e))
raise e
return True
def indicator_countdown(self,iters=10,multi=10): def indicator_countdown(self,iters=10,multi=10):
for i in range(iters,0,-1): for i in range(iters,0,-1):
self.led.on() self.led.on()
@@ -106,17 +100,131 @@ class Raspberry(object):
self.flash.off() self.flash.off()
self.app.logger.debug("Flash turned 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 Exception 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 Exception 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 Exception as e:
self.app.logger.error(f"Error cropping image to square: {e}")
return False
return True
def on_button_pressed(self): def on_button_pressed(self):
self.app.logger.debug("Button has been pressed") self.app.logger.debug("Button has been pressed")
self.socketio.emit('button_pressed') # Notify clients that a button has been pressed on the raspberry pi self.led.on()
self.indicator_countdown(iters=5,multi=20) # The indicator will flash a countdown LED self.app.logger.debug("Counting down")
self.flash_toggle() self.indicator_countdown(iters=4,multi=20) # The indicator will flash a countdown LED
self.app.logger.debug("Taking picture")
try: try:
self.flash.on()
self.take_picture() self.take_picture()
self.printer.print_image(self.image_path)
self.socketio.emit('picture_taken') # Notify clients that a picture has been taken
except Exception as e: except Exception as e:
self.app.logger.error("Could not take a picture after the button press : " + str(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.printer.print_img('src/static/images/extase-club.png',process=True)
self.printer.print_img(self.image_path,process=True)
self.printer.print_sms("")
self.printer.print_sms("With Love From Société.Vide",signature="",bold=True)
self.printer.print_sms("Printed by LittlePrynter",signature="")
self.printer.print_sms("n07070.xyz",signature="")
self.printer.print_sms(strftime("%Y-%m-%d %H:%M", gmtime()),signature="")
self.printer.qr("https://n07070.xyz/articles/littleprynter")
self.printer.cut()
self.led.off()
self.app.logger.debug("Done printing picture")
return True return True

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -17,6 +17,7 @@ class Web(object):
self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign)) self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign))
try: try:
self.printer.print_sms(texte, sign) self.printer.print_sms(texte, sign)
self.printer.cut()
except Exception as e: except Exception as e:
self.app.logger.error(e) self.app.logger.error(e)
flash("Error while printing the SMS : "+ str(e)) flash("Error while printing the SMS : "+ str(e))
@@ -24,13 +25,14 @@ class Web(object):
flash("You message " + str( texte ) + " has been printed :)") flash("You message " + str( texte ) + " has been printed :)")
def print_image(self, image, sign: str): def print_image(self, image, sign):
self.app.logger.debug("Uploading file") self.app.logger.debug("Uploading file")
try: try:
self.app.logger.debug("Uploading file from " + str(sign)) self.app.logger.debug("Uploading file from " + str(sign))
if self.upload_file(image): if self.upload_file(image):
self.app.logger.debug("File has been uploaded, printing...") self.app.logger.debug("File has been uploaded, printing...")
self.printer.print_img(os.path.join(self.app.config['UPLOAD_FOLDER'], secure_filename(image.filename)), sign) self.printer.print_img(os.path.join(self.app.config['UPLOAD_FOLDER'], secure_filename(image.filename)), sign=sign,process=True)
self.printer.cut()
except Exception as e: except Exception as e:
self.app.logger.error(e) self.app.logger.error(e)
flash("Could not upload file." + str(e)) flash("Could not upload file." + str(e))