12 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
nono
8ecb93656c Change signature 2025-10-16 18:39:29 +02:00
nono
eac737b889 Remove flash from the printer class, update web class 2025-10-16 18:36:16 +02:00
nono
dfeb1be0f0 update raspberry pi code 2025-10-16 18:35:57 +02:00
nono
0eefafdeb2 Add configuration ports for the flash 2025-10-16 18:35:35 +02:00
nono
ce70d498ca Add requirements 2025-10-16 18:35:27 +02:00
nono
991f6794c7 Update readme with more deps 2025-10-16 18:35:16 +02:00
13 changed files with 669 additions and 239 deletions

View File

@@ -20,7 +20,7 @@ To make this project work, you will need :
- Some knowledge of the command line, - Some knowledge of the command line,
- Some knowledge of Python. - Some knowledge of Python.
- 3h of your time, 5h if things need debugging. - 3h of your time, 5h if things need debugging.
- `git`, `virtualenv`,`pip` and `python` >= 3.8.6. - `fswebcam`, `libjpeg-dev` ,`zlib1g-dev`,`libffi-dev`,`git`, `virtualenv`,`pip` and `python` >= 3.8.6.
- A webcam for the webcam page to work. Will work on a smartphone. Not required. - A webcam for the webcam page to work. Will work on a smartphone. Not required.
## Context ## Context
@@ -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)

View File

@@ -8,8 +8,9 @@ upload_folder = "src/static/uploads"
# Raspberry Pi Configuration # Raspberry Pi Configuration
[rpi] [rpi]
button_gpio_port_number = 17 button_gpio_port_number = 2
indicator_gpio_port_number = 18 indicator_gpio_port_number = 22
flash_gpio_port_number = 16
flash = true flash = true
# Users = Password # Users = Password

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,36 +1,261 @@
Adafruit_Thermal>=1.1.0 appdirs==1.4.4
appdirs>=1.4.4 apt-listchanges==4.8
argcomplete>=2.0.6 argcomplete==3.6.2
cffi>=1.17.1 arrow==1.3.0
click>=8.1.8 attrs==25.3.0
commonmark>=0.9.1 babel==2.17.0
cryptography>=45.0.4 bcrypt==4.2.0
Deprecated>=1.2.18 bidict==0.23.1
escpos>=2.0.0 blinker==1.9.0
Flask>=2.1.3 certifi==2025.1.31
Flask-Limiter>=2.4.5.1 cffi==1.17.1
future>=0.18.3 chardet==5.2.0
itsdangerous>=2.1.2 charset-normalizer==3.4.2
Jinja2>=3.1.6 click==8.3.0
limits>=2.6.3 cloud-init==25.2
MarkupSafe>=2.1.5 colorzero==2.0
numpy>=2.3.0 configobj==5.0.9
packaging>=21.3 cryptography==43.0.0
pillow>=11.2.1 Deprecated==1.2.18
pycparser>=2.22 distlib==0.3.9
Pygments>=2.12.0 distro==1.9.0
pyparsing>=3.0.9 escpos==2.0.0
pyserial>=3.5 filelock==3.18.0
python-barcode>=0.13.1 Flask==3.1.2
pyusb>=1.2.1 Flask-Limiter==4.0.0
PyYAML>=6.0.2 Flask-SocketIO==5.5.1
qrcode>=7.3.1 fqdn==1.5.1
rich>=12.4.4 future==1.0.0
setuptools>=80.9.0 gpiod==2.2.0
six>=1.16.0 gpiozero==2.0.1
toml>=0.10.2 h11==0.16.0
typing_extensions>=4.2.0 idna==3.10
Unidecode>=1.3.8 isoduration==20.11.0
viivakoodi>=0.8.0 itsdangerous==2.2.0
Werkzeug>=2.1.2 Jinja2==3.1.6
wrapt>=1.14.1 jsonpatch==1.32
jsonpointer==2.4
jsonschema==4.19.2
jsonschema-specifications==2023.12.1
lgpio==0.2.2.0
limits==5.6.0
linkify-it-py==2.0.3
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
numpy==2.3.4
oauthlib==3.2.2
olefile==0.47
ordered-set==4.1.0
packaging==25.0
pillow==11.1.0
platformdirs==4.3.7
ply==3.11
pycparser==2.22
pycryptodomex==3.20.0
Pygments==2.18.0
PyJWT==2.10.1
pyserial==3.5
python-apt==3.0.0
python-barcode==0.16.1
python-dateutil==2.9.0
python-engineio==4.12.3
python-socketio==5.14.1
pyusb==1.3.1
PyYAML==6.0.2
qrcode==8.2
referencing==0.36.2
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rfc3987==1.3.8
rich==13.9.4
rpds-py==0.21.0
rpi-keyboard-config==1.0
rpi-lgpio==0.6
setuptools==80.9.0
simple-websocket==1.1.0
six==1.17.0
smbus2==0.4.3
spidev==3.6
ssh-import-id==5.10
toml==0.10.2
types-aiofiles==24.1
types-antlr4-python3-runtime==4.13
types-assertpy==1.1
types-atheris==2.3
types-aws-xray-sdk==2.14
types-beautifulsoup4==4.12
types-bleach==6.2
types-boltons==24.1
types-braintree==4.31
types-cachetools==5.5
types-caldav==1.3
types-capturer==3.0
types-cffi==1.16
types-chevron==0.14
types-click-default-group==1.2
types-click-spinner==0.1
types-colorama==0.4
types-commonmark==0.9
types-console-menu==0.8
types-corus==0.10
types-croniter==5.0.1
types-dateparser==1.2
types-decorator==5.1
types-defusedxml==0.7
types-Deprecated==1.2.15
types-docker==7.1
types-dockerfile-parse==2.0
types-docutils==0.21
types-editdistance==0.8
types-entrypoints==0.4
types-ExifRead==3.0
types-fanstatic==1.4
types-first==2.0
types-flake8==7.1
types-flake8-bugbear==24.12.12
types-flake8-builtins==2.5
types-flake8-docstrings==1.7
types-flake8-rst-docstrings==0.3
types-flake8-simplify==0.21
types-flake8-typing-imports==1.16
types-Flask-Cors==5.0
types-Flask-Migrate==4.0
types-Flask-SocketIO==5.4
types-fpdf2==2.8.2
types-gdb==15.0
types-gevent==24.11
types-google-cloud-ndb==2.3
types-greenlet==3.1
types-hdbcli==2.23
types-html5lib==1.1
types-httplib2==0.22
types-humanfriendly==10.0
types-hvac==2.3
types-ibm-db==3.2.4
types-icalendar==6.1
types-influxdb-client==1.45
types-inifile==0.4
types-JACK-Client==0.5
types-Jetson.GPIO==2.1
types-jmespath==1.0
types-jsonschema==4.23
types-jwcrypto==1.5
types-keyboard==0.13
types-ldap3==2.9
types-libsass==0.23
types-lupa==2.2
types-lzstring==1.0
types-m3u8==6.0
types-Markdown==3.7
types-mock==5.1
types-mypy-extensions==1.0
types-mysqlclient==2.2
types-nanoid==2.0.0
types-netaddr==1.3
types-netifaces==0.11
types-networkx==3.4.2
types-oauthlib==3.2
types-objgraph==3.6
types-olefile==0.47
types-openpyxl==3.1.5
types-opentracing==2.4
types-paramiko==3.5
types-parsimonious==0.10
types-passlib==1.7
types-passpy==1.0
types-peewee==3.17.8
types-pep8-naming==0.14
types-pexpect==4.9
types-pika-ts==1.3
types-polib==1.2
types-portpicker==1.6
types-protobuf==5.29.1
types-psutil==6.1
types-psycopg2==2.9.10
types-pyasn1==0.6
types-pyaudio==0.2
types-PyAutoGUI==0.9
types-pycocotools==2.0
types-pycurl==7.45.4
types-pyfarmhash==0.4
types-pyflakes==3.2
types-pygit2==1.15
types-Pygments==2.18
types-pyinstaller==6.11
types-pyjks==20.0
types-PyMySQL==1.1
types-pynput==1.7.7
types-pyOpenSSL==24.1
types-pyRFC3339==2.0.1
types-PyScreeze==1.0.1
types-pyserial==3.5
types-pysftp==0.2
types-pytest-lazy-fixture==0.6
types-python-crontab==3.2
types-python-datemath==3.0.1
types-python-dateutil==2.9
types-python-http-client==3.3.7
types-python-jenkins==1.8
types-python-jose==3.3
types-python-nmap==0.7
types-python-xlib==0.33
types-pytz==2024.2
types-pywin32==308
types-pyxdg==0.28
types-PyYAML==6.0
types-qrbill==1.1
types-qrcode==8.0
types-regex==2024.11.6
types-reportlab==4.2.5
types-requests==2.32
types-requests-oauthlib==2.0
types-retry==0.9
types-RPi.GPIO==0.7
types-s2clientprotocol==5
types-seaborn==0.13.2
types-Send2Trash==1.8
types-setuptools==75.6
types-shapely==2.0
types-simplejson==3.19
types-singledispatch==4.1
types-six==1.17
types-slumber==0.7
types-str2bool==1.1
types-tabulate==0.9
types-tensorflow==2.18.0
types-TgCrypto==1.2
types-toml==0.10
types-toposort==1.10
types-tqdm==4.67
types-translationstring==1.4
types-tree-sitter-languages==1.10
types-ttkthemes==3.2
types-ujson==5.10
types-unidiff==0.7
types-untangle==1.2
types-usersettings==1.1
types-uWSGI==2.0
types-vobject==0.9.9
types-waitress==3.0.1
types-WebOb==1.8
types-whatthepatch==1.0
types-workalendar==17.0
types-WTForms==3.2.1
types-wurlitzer==3.1
types-xdgenvpy==3.0
types-xmltodict==0.14
types-zstd==1.5
types-zxcvbn==4.4
typing_extensions==4.15.0
uc-micro-py==1.0.3
uritemplate==4.1.1
urllib3==2.3.0
viivakoodi==0.8.0
virtualenv==20.31.2
webcolors==1.13
Werkzeug==3.1.3
wheel==0.46.1
wrapt==1.17.3
wsproto==1.2.0

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:
configuration_file = toml.load(f)
except TypeError: except TypeError:
app.logger.error("Unable to load the config file: invalid type or is a list containing invalid types") app.logger.error(
exit(-1) "Unable to load the config file: invalid type or is a list containing invalid types"
except toml.TomlDecodeError: )
app.logger.error("An error occured while decoding the file") sys.exit(-1)
exit(-1) except toml.TomlDecodeError as e:
except Exception as e: app.logger.error(
app.logger.error("Error while loading file : " + str(e)) "An error occured while decoding the file %s , error at %s:%s",
exit(-1) 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,65 +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, app, socketio, configuration_file['rpi']['button_gpio_port_number'], configuration_file['rpi']['indicator_gpio_port_number'], configuration_file['rpi']['flash'] ) rpi = Raspberry(
printer,
app,
socketio,
configuration_file["rpi"]["button_gpio_port_number"],
configuration_file["rpi"]["indicator_gpio_port_number"],
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()
if RASPBERRY_PI_CONNECTED:
rpi.initialise_gpio()
#############################################################
# Web & API routes # Web & API routes
#############################################################
web = Web(app, printer) web = Web(app, printer)
if __name__ == "__main__": if __name__ == "__main__":
app.run(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
@@ -117,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:
@@ -189,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

@@ -1,5 +1,4 @@
# Importing the module to mage the connection to the printer. # Importing the module to mage the connection to the printer.
from flask import flash
from escpos.printer import Usb, USBNotFoundError from escpos.printer import Usb, USBNotFoundError
from time import sleep, gmtime, strftime from time import sleep, gmtime, strftime
import os.path import os.path
@@ -46,12 +45,10 @@ class Printer(object):
match status: match status:
case 0: case 0:
self.app.logger.error('Printer has no more paper, aborting...') self.app.logger.error('Printer has no more paper, aborting...')
flash("No more paper on the printer. Sorry.",category='error')
self.printer.close() self.printer.close()
raise Exception("No more paper in the printer") raise Exception("No more paper in the printer")
case 1: case 1:
self.app.logger.warning('Printer needs paper to be changed very soon ! ') self.app.logger.warning('Printer needs paper to be changed very soon ! ')
flash('Printer needs paper to be changed very soon ! ', category='info')
self.printer.close() self.printer.close()
case 2: case 2:
self.app.logger.debug('Printer has paper, good to go') self.app.logger.debug('Printer has paper, good to go')
@@ -101,47 +98,37 @@ class Printer(object):
return True return True
def print_sms(self, msg, signature) -> 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) < 3: 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 )
self.printer.textln("") if 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 )
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
flash("Message printed : " + clean_msg ,category='info') 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) < 3: 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))
@@ -149,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.textln("Printed by LittlePrinter ") self.printer.image(path,center=center)
self.printer.textln("n07070.xyz/articles/littleprynter")
self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
self.printer.image(path)
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)
@@ -191,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")
@@ -208,13 +225,13 @@ 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:
flash("Image is too long, sorry! Keep it below 575×1000 pixels.", 'error') raise ValueError("Image is too long, sorry! Keep it below 575×1000 pixels.")
self.app.logger.error("Image is too long, sorry! Keep it below 575×1000 pixels.") self.app.logger.error("Image is too long, sorry! Keep it below 575×1000 pixels.")
return False return False

View File

@@ -1,8 +1,10 @@
from flask_socketio import SocketIO from flask_socketio import SocketIO
from gpiozero import Button, LED 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):
""" """
@@ -13,24 +15,17 @@ class Raspberry(object):
- Flash an indicator light - Flash an indicator light
""" """
def __init__(self, printer, app, socketio, button_gpio_port_number, indicator_gpio_port_number, is_flash_present,): def __init__(self, printer, app, socketio, button_gpio_port_number, indicator_gpio_port_number, flash_gpio_port_number, is_flash_present,):
self.printer = printer self.printer = printer
self.socketio = socketio self.socketio = socketio
self.app = app self.app = app
self.flash_gpio = flash_gpio_port_number
self.is_flash_present = is_flash_present self.is_flash_present = is_flash_present
self.button_gpio = button_gpio_port_number self.button_gpio = button_gpio_port_number
self.led_gpio = indicator_gpio_port_number self.led_gpio = indicator_gpio_port_number
self.image_path = self.app.config['UPLOAD_FOLDER'] + '/image.jpg' self.image_path = self.app.config['UPLOAD_FOLDER'] + '/image.jpg'
def initialise_gpio(self):
self.button = Button(self.button_gpio)
self.led = LED(self.led_gpio)
button.when_pressed = self.on_button_pressed()
def is_raspberry_pi(self, raise_on_errors=False): def is_raspberry_pi(self, raise_on_errors=False):
# Check if we are running on a raspberry pi # Check if we are running on a raspberry pi
try: try:
@@ -59,48 +54,177 @@ class Raspberry(object):
return False return False
self.app.logger.debug('It seems we are on a Raspberry Pi') self.app.logger.debug('It seems we are on a Raspberry Pi')
return True
def take_picture(self):
# return a path to a picture
try: try:
subprocess.run(['fswebcam', '--no-banner', self.image_path]) self.initialise_gpio()
except Exception as e: except Exception as e:
self.app.logger.error('Unable to take a picture :' + e) self.app.logger.debug('Could not init GPIO : ' + str(e))
raise e raise e
return True return True
def flash(state): def initialise_gpio(self):
if state: self.app.logger.debug('Initializing GPIO')
# turn on the flash
pass
else:
# turn off the flash
pass
def flash_indicator(self): self.led = LED(self.led_gpio)
# The indicator will flash three times before return true self.app.logger.debug('Activated indicator LED')
self.led.on() self.indicator_countdown(iters=3)
sleep(0.1) self.button = Button(self.button_gpio, pull_up=True, bounce_time=0.1)
self.led.off() self.button.when_pressed = self.on_button_pressed
sleep(1) self.app.logger.debug('Activated button')
self.led.on()
sleep(0.1) # The "flash" is a relay-controlled device ( light bulb for example )
self.led.off() self.flash = DigitalOutputDevice(self.flash_gpio)
sleep(1) self.flash_toggle()
self.led.on() self.app.logger.debug('Activated flash')
sleep(0.1)
self.led.off() 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
def on_button_pressed():
self.socketio.emit('button_pressed') # Notify clients that a button has been pressed on the raspberry pi
self.flash_indicator() # The indicator will flash a countdown LED
self.flash(True)
try: try:
self.take_picture() result = subprocess.run(
self.printer.print_image(self.image_path) ['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: except Exception as e:
app.logger.error("Could not take a picture after the button press : " + 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):
self.app.logger.debug("Button has been pressed")
self.led.on() self.led.on()
self.flash(False) self.app.logger.debug("Counting down")
self.socketio.emit('picture_taken') # Notify clients that a picture has been taken 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 Exception 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.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -244,7 +244,7 @@ function print_picture(data){
let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds(); let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
formData.set("img", picture, "picture.png"); formData.set("img", picture, "picture.png");
formData.set("signature", " Accès Libre ~ 1 An ~ Fête de la Musique Libre @ " + time) formData.set("signature", "Webcam")
fetch(url, { fetch(url, {
method: 'POST', // or 'PUT' method: 'POST', // or 'PUT'

View File

@@ -17,20 +17,22 @@ 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))
flash("You message 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))
@@ -57,7 +59,7 @@ class Web(object):
self.app.logger.debug("File saved") self.app.logger.debug("File saved")
except Exception as e: except Exception as e:
self.app.logger.error("Could not save file") self.app.logger.error("Could not save file")
flash(e,'error') flash(str(e),'error')
return False return False
self.app.logger.debug("File saved to " + str(os.path.join(self.app.config['UPLOAD_FOLDER'], filename))) self.app.logger.debug("File saved to " + str(os.path.join(self.app.config['UPLOAD_FOLDER'], filename)))