Compare commits
71 Commits
0.1.0
...
multi-prin
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e3cc46a41 | |||
|
|
bbfe1936da | ||
|
|
8134c5e892 | ||
|
|
934f766cf3 | ||
|
|
eb9e1ec200 | ||
|
|
bc035508cd | ||
|
|
cba34744f6 | ||
|
|
0c8c40098c | ||
|
|
3b640dc549 | ||
|
|
2daafe28f2 | ||
|
|
c50922790d | ||
|
|
e8ec9b74c0 | ||
|
|
9dee67c333 | ||
|
|
42bf6d6496 | ||
|
|
a38088bd05 | ||
|
|
cb3e0d900f | ||
|
|
c5a8019fbe | ||
|
|
e926ee9163 | ||
|
|
3f915a1b25 | ||
|
|
a06086521a | ||
|
|
ee27c62d0f | ||
|
|
2a11239c1e | ||
|
|
bd9888caf7 | ||
| a95190690b | |||
|
|
b0e394f9d1 | ||
|
|
6254d60429 | ||
|
|
f408c47c27 | ||
|
|
a1cfb7a3ba | ||
|
|
d2f181cb22 | ||
|
|
cc16704651 | ||
|
|
36ae95c26f | ||
|
|
dc7495cd15 | ||
|
|
f2d9006a12 | ||
|
|
52e0a09552 | ||
|
|
16c1ef4d72 | ||
|
|
002dc2eb8e | ||
|
|
85c10a47b0 | ||
|
|
641b8a2d1f | ||
|
|
07dbe9be84 | ||
|
|
6888a69ee7 | ||
|
|
0f9135707a | ||
|
|
9bdd1b4569 | ||
|
|
4fd2d55cbd | ||
| d41114b5a2 | |||
|
|
44a5f6ddad | ||
|
|
e437beac59 | ||
|
|
3afd679148 | ||
|
|
cdba783f45 | ||
|
|
67b7de11e9 | ||
|
|
39d0c56672 | ||
|
|
11a5dc3587 | ||
|
|
5207fa5b4e | ||
|
|
20a22b379d | ||
|
|
13968ac7bc | ||
|
|
7131b68dbd | ||
|
|
000c7e9eec | ||
|
|
8157c5cb9d | ||
|
|
1a1c4e2fb3 | ||
|
|
d4a9a059bf | ||
|
|
aa6e11c537 | ||
|
|
0601fe8190 | ||
|
|
3dc6a41724 | ||
|
|
abaf506d56 | ||
|
|
866d89eb09 | ||
|
|
7df902df52 | ||
|
|
80b16f260e | ||
|
|
1735e468aa | ||
|
|
38b3acfb89 | ||
|
|
3d8c22598d | ||
|
|
b3ac0960ae | ||
| 4ced780d54 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
CACHEDIR.TAG
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
69
README.md
69
README.md
@@ -19,48 +19,94 @@ To make this project work, you will need :
|
||||
- Some electric wires.
|
||||
- Some knowledge of the command line,
|
||||
- Some knowledge of Python.
|
||||
- You will need to install [Poetry](https://python-poetry.org/) to manage the depedencies of the projet.
|
||||
- 3h of your time, 5h if things need debugging.
|
||||
- `git`, `virtualenv`,`pip` and `python` >= 3.8.6.
|
||||
- You will need to following packages: `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.
|
||||
|
||||
## Context
|
||||
|
||||
### Testing your printer
|
||||
For the EPSON TM-T20III, you can get the CUPS driver from [here](https://download.epson-biz.com/modules/pos/index.php?page=soft&scat=32). It's not specific to the printer I've been using, so you can try with other printers.
|
||||
|
||||
With that, you can try out your printer and print normal text, images or pdf documents for example. LittlePrynter itself does not require any other software than the ones installed with it, i.e the ones listed in the `requirements.txt` file.
|
||||
|
||||
For the Adafruit printer, start by following the guide [here](https://learn.adafruit.com/networked-thermal-printer-using-cups-and-raspberry-pi) to install the CUPS software needed to print images. If you want, you can install it via the command line, [following this guide](https://help.ubuntu.com/lts/serverguide/cups.html).
|
||||
With that, you can try out your printer and print normal text, images or pdf documents for example. For the Adafruit printer, start by following the guide [here](https://learn.adafruit.com/networked-thermal-printer-using-cups-and-raspberry-pi) to install the CUPS software needed to print images. If you want, you can install it via the command line, [following this guide](https://help.ubuntu.com/lts/serverguide/cups.html).
|
||||
|
||||
You can also get some information from [here](https://learn.adafruit.com/mini-thermal-receipt-printer) and [here](https://learn.adafruit.com/instant-camera-using-raspberry-pi-and-thermal-printer) if you're stuck.
|
||||
|
||||
### How LittlePrynter works
|
||||
|
||||
LittlePrynter itself does not require any other software than the ones installed with it, i.e the ones listed in the `pyproject.toml` file.
|
||||
|
||||
The version for the EPSON TM-T20III uses a library called `python-escpos`, which doesn't need a CUPS driver or anything else. It's included in the Python dependencies.
|
||||
|
||||
The project only supports the EPSON printer, but you can try to adapt it for other printers using the `adafruit-thermal` branch, but I won't offer support for it.
|
||||
The project only supports the EPSON printer, but you can try to adapt it for other printers. For example, using `adafruit-thermal`, or `brother-ql`.
|
||||
|
||||
### Install & setup the project :
|
||||
|
||||
Theses commands will copy the software on your computer, go into the directory, then activate a virtual environnement and install all of the project's dependecies.
|
||||
|
||||
```
|
||||
$ git clone https://git.n07070.xyz/n07070/LittlePrynter
|
||||
$ virtualenv LittlePrynter
|
||||
$ cd LittlePrynter
|
||||
$ source bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ eval "$(poetry env activate)"
|
||||
$ poetry install
|
||||
```
|
||||
|
||||
> tip : when you're done, you can get out of the virtualenv either by closing your terminal, or by running `deactivate`.
|
||||
> tip : when you're done, you can get out of the poetry environnement either by closing your terminal, or by running `deactivate`.
|
||||
|
||||
### Configure LittlePrynter
|
||||
|
||||
You should see a folder named `configuration`. Enter it, and duplicate the file named `config.toml.sample`, and rename the copy to `config.toml`. Now, edit this file by following the comments in the file itself.
|
||||
|
||||
You should also setup the proper `udev` permissions to access the printers via USB, following the configuration found [here](https://python-escpos.readthedocs.io/en/latest/user/installation.html#setup-udev-for-usb-printers).
|
||||
|
||||
You can now start the web server with
|
||||
|
||||
```
|
||||
$ export FLASK_APP=src/main.py
|
||||
$ flask run
|
||||
$ flask run --cert=adhoc
|
||||
```
|
||||
|
||||
This command should start a web server with which you can test your configuration. If you plan on exposing your printer to the Internet, and give it an IP / URL, _please, please, please_, don't run it this way. Look at Flask's documentation and read about running a production server. It's a little more work, but it will prevent your computer/server being hacked in too easily.
|
||||
The `--cert=adhoc` argument will make LittlePrinter accessible in HTTPS with a [self-signed SSL certificate](https://en.wikipedia.org/wiki/Self-signed_certificate). This provides a free and easy way to use HTTPS, but does not provide any trust value. It is, on the other hand, a good way to access the webcam in developpement mode or on a local network, because a HTTPS connexion is required by browsers to access the needed APIs.
|
||||
|
||||
This command should start a web server with which you can test your configuration. If you plan on exposing your printer to the Internet, and give it an IP / URL, _please, please, please_, don't run it this way. Look at Flask's documentation and read about running a production server. It's a little more work, but it will prevent your computer/server being hacked in too easily.
|
||||
|
||||
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.
|
||||
|
||||
### Code structure
|
||||
|
||||
The app is written about the Flask framework. You can start by looking at the code in the `src/` folder, in the `main.py` file. There, you will see that a few classes are initialized. In general, they are two parts to the program :
|
||||
|
||||
The Web pages and the API, which are the user-facing parts. This is with what the users will interact, and define how the program is going to be used. The web pages are renderer from the `include/` folder where Jinj2 templates are defined.
|
||||
|
||||
The Worker and Printer Queue are the internal parts. When a new thing needs to be printed, usually sent from the Web or API interfaces, a new Task in the type of the document is created, and added to a print queue. Then, a Worker thread looks up the state of the queue every so often and picks jobs to execute on the printers connected to the system.
|
||||
|
||||
The last part of the program is the Raspberry Pi class, that handles to Photomaton mode, which handles button presses, and LED indicator and a flash.
|
||||
|
||||
### Linting
|
||||
|
||||
If you want to contribute code, please make sure to lint the project before commiting. This helps the code keep a general structure, and avoids some commons erros and mistakes.
|
||||
|
||||
To do so, you can run the following command, which will modify your files to be in a certain coding style ;
|
||||
|
||||
```
|
||||
black src/
|
||||
```
|
||||
|
||||
Beware that this command *will* re-write files, so doing `git add <file>` and then `black src/` and then `git diff` to see what the linter has done is a good idea.
|
||||
|
||||
Then, you can run another command, called Pylint ( [documentation](https://www.pylint.org/) ) which will rate your code. Try to get 10/10 : an excellent code works better and make you a better programmer !
|
||||
|
||||
```
|
||||
pylint src/
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
@@ -83,6 +129,7 @@ If you liked this project, feel free to support my work !
|
||||
- [Github repo with CUPS drivers for the Adafruit Thermal Printer ( zj-58 )](https://github.com/klirichek/zj-58)
|
||||
- [A link to buy one in Europe](https://rlx.sk/sk/various-boards/1829-mini-thermal-receipt-printer-adafruit-597.html)
|
||||
- [Another link to buy one, direct from factory](https://www.cashinotech.com/csn-a2-58mm-mini-panel-thermal-receipt-printer_p11.html)
|
||||
- [Diagnostickoeur](https://framagit.org/stickoeur/diagnostickoeur), another printer software mainly around Brother QL printers.
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
# Configuration file the LittlePrynter
|
||||
|
||||
[defaults]
|
||||
signature = "Anonymous"
|
||||
|
||||
# Printer settings
|
||||
[printer]
|
||||
vendor_id = 0x04b8
|
||||
device_id = 0x0e28
|
||||
upload_folder = "src/static/uploads"
|
||||
|
||||
# Raspberry Pi Configuration
|
||||
[rpi]
|
||||
button_gpio_port_number = 2
|
||||
indicator_gpio_port_number = 22
|
||||
flash_gpio_port_number = 16
|
||||
flash = true
|
||||
|
||||
# Users = Password
|
||||
[users]
|
||||
admin = "admin"
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
# Configuration file the LittlePrynter
|
||||
|
||||
[defaults]
|
||||
signature = "Anonymous"
|
||||
|
||||
# Printer settings
|
||||
[printer]
|
||||
vendor_id = "0x04b8"
|
||||
device_id = "0x0e28"
|
||||
|
||||
# Raspberry Pi Configuration
|
||||
[rpi]
|
||||
button_gpio_port_number = 17
|
||||
indicator_gpio_port_number = 18
|
||||
flash = true
|
||||
|
||||
# Users = Password
|
||||
[users]
|
||||
admin = "admin"
|
||||
|
||||
5
docs/Thermal Receipt/CMakeLists.txt
Normal file
5
docs/Thermal Receipt/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
#cmake_minimum_required(VERSION 2.8)
|
||||
add_executable(rastertotmtr
|
||||
filter/TmThermalReceipt.c
|
||||
)
|
||||
target_link_libraries(rastertotmtr cupsimage cups)
|
||||
171
docs/Thermal Receipt/EULA
Normal file
171
docs/Thermal Receipt/EULA
Normal file
@@ -0,0 +1,171 @@
|
||||
SEIKO EPSON CORPORATION
|
||||
SOFTWARE LICENSE AGREEMENT
|
||||
|
||||
IMPORTANT! READ THIS SOFTWARE LICENSE AGREEMENT CAREFULLY. The
|
||||
computer software product, fontware, typefaces and/or data, including
|
||||
any accompanying explanatory written materials (the "Software") should
|
||||
only be installed or used by the Licensee ("you") on the condition you
|
||||
agree with SEIKO EPSON CORPORATION ("EPSON") to the terms and
|
||||
conditions set forth in this Agreement. By installing or using the
|
||||
Software, you are representing to agree all the terms and conditions
|
||||
set forth in this Agreement. You should read this Agreement carefully
|
||||
before installing or using the Software. If you do not agree with the
|
||||
terms and conditions of this Agreement, you are not permitted to
|
||||
install or use the Software.
|
||||
|
||||
1. License. EPSON and its suppliers grant you a personal,
|
||||
nonexclusive, royalty-free, non-sublicensable limited license to
|
||||
install and use the Software solely for the purpose of using EPSON
|
||||
printer products ("Purpose") on any single computer or computers
|
||||
that you intend to use directly or via network. You may allow other
|
||||
users of the computers connected to the network to use the Software,
|
||||
provided that you (a) ensure that all such users agree and are bound
|
||||
by the terms and conditions of this Agreement, (b) ensure that all
|
||||
such users use the Software only in conjunction with the computers and
|
||||
in relation to the network of which they form part, and (c) indemnify
|
||||
and keep whole EPSON and its suppliers against all damages, losses,
|
||||
costs, expenses and liabilities which EPSON or its suppliers may incur
|
||||
as a consequence of such users failing to observe and perform the
|
||||
terms and conditions of this Agreement. You may also make copies of
|
||||
the Software as necessary for backup and archival purposes, provided
|
||||
that the copyright notice is reproduced in its entirety on the backup
|
||||
copy. The term "Software" shall include the software components,
|
||||
media, all copies made by you and any upgrades, modified versions,
|
||||
updates, additions and copies of the Software licensed to you by EPSON
|
||||
or its suppliers. EPSON and its suppliers reserve all rights not
|
||||
granted herein.
|
||||
|
||||
2. Other Rights and Limitations. You agree not to modify, adapt or
|
||||
translate the Software. You also agree not to attempt to reverse
|
||||
engineer, decompile, disassemble or otherwise attempt to discover the
|
||||
source code of the Software. You may not use the Software for any
|
||||
purposes other than the Purpose. You may not share, rent, lease,
|
||||
encumber, sublicense or lend the Software. You may, however, transfer
|
||||
all your rights to use the Software to another person or legal entity
|
||||
provided that you transfer this Agreement, the Software, including all
|
||||
copies, updates and prior versions, to such person or entity, and that
|
||||
you retain no copies, including copies stored on a computer. Some
|
||||
states or jurisdictions, however, do not allow the restriction or
|
||||
limitation on transfer of the Software, so the above limitations may
|
||||
not apply to you.
|
||||
|
||||
3. Ownership. Title, ownership rights, and intellectual property
|
||||
rights in and to the Software and any copies thereof shall remain with
|
||||
EPSON or its suppliers. There is no transfer to you of any title to
|
||||
or ownership of the Software and this License shall not be construed
|
||||
as a sale of any rights in the Software. The Software is protected by
|
||||
Japanese Copyright Law and international copyright treaties, as well
|
||||
as other intellectual property laws and treaties. Except as otherwise
|
||||
provided in this Agreement, you may not copy the Software. You also
|
||||
agree not to remove or alter any copyright and other proprietary
|
||||
notices on any copies of the Software.
|
||||
|
||||
4. LGPL. The Software uses the open source software programs which
|
||||
apply the GNU Lesser General Public License Version 2 or later version
|
||||
("LGPL"). Notwithstanding any provision of this Agreement, you may
|
||||
make modification of the Software for your own use and reverse
|
||||
engineering for debugging such modifications according to the terms
|
||||
and conditions of the LGPL.
|
||||
|
||||
5. Protection and Security. You agree to use your best efforts and
|
||||
take all reasonable steps to safeguard the Software to ensure that no
|
||||
unauthorized person has access to them and that no unauthorized copy,
|
||||
publication, disclosure or distribution of any of the Software is
|
||||
made. You acknowledge that the Software contains valuable,
|
||||
confidential information and trade secrets, that unauthorized use and
|
||||
copying are harmful to EPSON and its suppliers, and that you have a
|
||||
confidentiality obligation as to such valuable information and trade
|
||||
secrets.
|
||||
|
||||
6. Limited Warranty. In case of that you obtained the Software by
|
||||
media from EPSON or a dealer, EPSON warrants that the media on which
|
||||
the Software is recorded will be free from defects in workmanship and
|
||||
materials under normal use for a period of 90 days from the date of
|
||||
delivery to you. If the media is returned to EPSON or the dealer from
|
||||
which the media was obtained within 90 days of the date of delivery to
|
||||
you, and if EPSON determines the media to be defective and provided
|
||||
the media was not subject to misuse, abuse, misapplication or use in
|
||||
defective equipment, EPSON will replace the media, upon your return to
|
||||
EPSON of the Software, including all copies of any portions thereof.
|
||||
ALL IMPLIED WARRANTIES ON THE MEDIA, INCLUDING IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE LIMITED TO
|
||||
THE DURATION OF THE EXPRESS WARRANTY SET FORTH ABOVE.
|
||||
|
||||
You acknowledge and agree that the use of the Software is at
|
||||
your sole risk. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT
|
||||
ANY WARRANTY OF ANY KIND. EPSON AND ITS SUPPLIERS DO NOT AND
|
||||
CANNOT WARRANT THE PERFORMANCE OR RESULTS YOU MAY OBTAIN BY
|
||||
USING THE SOFTWARE. THE FOREGOING STATES THE SOLE AND
|
||||
EXCLUSIVE REMEDIES FOR EPSON'S AND ITS SUPPLIERS' BREACH OF
|
||||
WARRANTY. EXCEPT FOR THE FOREGOING LIMITED WARRANTY, EPSON
|
||||
AND ITS SUPPLIERS MAKE NO WARRANTIES, EXPRESS OR IMPLIED, AS
|
||||
TO NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR ANY
|
||||
PARTICULAR PURPOSE. Some states or jurisdictions do not allow
|
||||
the exclusion of implied warranties or limitations on how long
|
||||
an implied warranty may last, so the above limitations may not
|
||||
apply to you. This warranty gives you specific legal rights.
|
||||
You may have other rights which vary from state to state or
|
||||
jurisdiction to jurisdiction.
|
||||
|
||||
IN NO EVENT WILL EPSON OR ITS SUPPLIERS BE LIABLE TO YOU,
|
||||
WHETHER ARISING UNDER CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY, BREACH OR WARRANTY, MISREPRESENTATION OR
|
||||
OTHERWISE, FOR ANY DIRECT, CONSEQUENTIAL, INCIDENTAL OR
|
||||
SPECIAL DAMAGES, INCLUDING ANY LOST PROFITS OR LOST SAVINGS,
|
||||
EVEN IF EPSON, ITS SUPPLIERS OR ANY REPRESENTATIVE HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR FOR ANY CLAIM
|
||||
BY ANY THIRD PARTY. Some states or jurisdictions, however, do
|
||||
not allow the exclusion or limitation of incidental,
|
||||
consequential or special damages, so the above limitations may
|
||||
not apply to you.
|
||||
|
||||
7. Termination. Without prejudice to any other rights EPSON has, this
|
||||
Agreement shall automatically terminate upon failure by you to comply
|
||||
with its terms. You may also terminate this Agreement at any time by
|
||||
uninstalling and destroying the Software and all copies thereof.
|
||||
|
||||
8. Export Restriction. You agree not to transfer, export or re-export
|
||||
the Software and any data or information which you obtained from EPSON
|
||||
or use the Software without a proper license under Japanese law,
|
||||
restrictions and regulations, or the laws of the jurisdiction in which
|
||||
the Software is obtained.
|
||||
|
||||
9. Governing Law and General Provisions. This Agreement shall be
|
||||
governed and construed under by the laws of Japan without regard to
|
||||
its conflicts of law rules. This Agreement is the entire agreement
|
||||
between the parties with respect to the Software, and supersedes any
|
||||
purchase order, communication, advertisement, or representation
|
||||
concerning the Software. This Agreement shall be binding upon, and
|
||||
inure to the benefit of, the parties hereto and their respective
|
||||
successors, assigns and legal representatives. If any provision
|
||||
herein is found void or unenforceable, it will not affect the validity
|
||||
of the balance of the Agreement, which shall remain valid and
|
||||
enforceable according to its terms. This Agreement may only be
|
||||
modified in writing signed by an authorized officer of EPSON.
|
||||
|
||||
10. U.S. Government End Users. If you are acquiring the Software on
|
||||
behalf of any unit or agency of the United States Government, the
|
||||
following provisions apply. The Government agrees: (i) if the
|
||||
Software is supplied to the Department of Defense (DoD), the Software
|
||||
is classified as "Commercial Computer Software" and the Government is
|
||||
acquiring only "restricted rights" in the Software and its
|
||||
documentation as that term is defined in Clause 252.227-7013(c)(1) of
|
||||
the DFARS; and (ii) if the Software is supplied to any unit or agency
|
||||
of the United States Government other than DoD, the Government's
|
||||
rights in the Software and its documentation will be as defined in
|
||||
Clause 52.227-19(c)(2) of the FAR or, in the case of NASA, in Clause
|
||||
18-52.227-86(d) of the NASA supplement to the FAR.
|
||||
|
||||
11. Internet Connection. The Software may have the ability to connect
|
||||
over the Internet to transmit data and/or information to and from your
|
||||
computer regarding the EPSON hardware and/or software that you use
|
||||
("EPSON Products") including, but not limited to, EPSON Products model
|
||||
information, the country/region where you live, the condition of EPSON
|
||||
Products, etc. EPSON may alter the items of such data and/or
|
||||
information without your prior approval. EPSON does not collect any
|
||||
personally identifiable information without your permission. EPSON
|
||||
may, however, use non personally identifiable information for
|
||||
statistical purposes to improve the level of service we provide to our
|
||||
users If you agree to install the Software, any transmissions to or
|
||||
from the Internet will be in accordance with EPSON's then-current
|
||||
Privacy Policy provided in EPSON Internet site.
|
||||
340
docs/Thermal Receipt/LISENSE
Normal file
340
docs/Thermal Receipt/LISENSE
Normal file
@@ -0,0 +1,340 @@
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
96
docs/Thermal Receipt/README
Normal file
96
docs/Thermal Receipt/README
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
EPSON TM Series Printer Driver for Linux Version 3.0.0
|
||||
|
||||
Copyright (C) Seiko Epson Corporation 2019.
|
||||
|
||||
|
||||
1. GENERAL
|
||||
----------
|
||||
This software is a printer driver for printing on an Epson
|
||||
TM series printer from Linux using CUPS.
|
||||
|
||||
1.1) Features
|
||||
+ A raster type printer driver for TM series printers.
|
||||
+ Can instantly print out images, texts and drawings displayed by
|
||||
an application.
|
||||
+ Various printer controls on paper cut timing, cash drawers,
|
||||
printing speed, blank line saving, inverted printing, etc.
|
||||
|
||||
2. ENVIRONMENT
|
||||
--------------
|
||||
2.1) Supported printers
|
||||
+ EPSON TM-m30
|
||||
+ EPSON TM-T88VI
|
||||
+ EPSON TM-H6000V
|
||||
|
||||
2.2) Confirmed distributions
|
||||
+ Ubuntu 18.04
|
||||
+ CentOS 7 1810
|
||||
+ openSUSE 13.1
|
||||
|
||||
3. FILES
|
||||
--------
|
||||
+ README .......... This file
|
||||
+ EULA ............ EPSON SOFTWARE LICENSE AGREEMENT
|
||||
+ LISENSE ......... GNU GENERAL PUBLIC LICENSE
|
||||
+ build.sh ........ Build script
|
||||
+ install.sh ...... Installation script
|
||||
+ CMakeList.txt ... input file of cmake
|
||||
+ /filter ......... source code of filter driver
|
||||
+ /ppd ............ ppd files
|
||||
|
||||
4. HOW TO BUILD & INSTALL
|
||||
-------------------------
|
||||
Ensure that you have following packages pre-installed
|
||||
+ Ubuntu ..... cmake, libcupsimage2-dev development
|
||||
+ CentOS ..... cmake, gcc, gcc-c++, cups-devel development
|
||||
+ openSUSE ... cmake, gcc, gcc-c++, cups-devel development
|
||||
|
||||
4.1) Execute build script
|
||||
#sudo ./build.sh
|
||||
|
||||
*Temporary folder build will be made when run script.
|
||||
|
||||
!!! You must the following install script with root privileges. !!!
|
||||
|
||||
4.2) Execute installation script
|
||||
#sudo ./install.sh
|
||||
|
||||
*Filter driver and ppd files will be copied to appropriate folders.
|
||||
|
||||
5. HOW TO PRINT
|
||||
---------------
|
||||
5.1 By command line
|
||||
1) Turn on printer
|
||||
Turn the printer on before registering a printer.
|
||||
2) Register a printer
|
||||
!!! You must run lpadmin command with root privilege. !!!
|
||||
#lpadmin -p <destination> -v <device-uri> -P <ppd-file> -E
|
||||
-p <destination>
|
||||
-v <device-uri>
|
||||
-P <ppd-file>
|
||||
-E
|
||||
|
||||
example)
|
||||
#lpadmin -p TM-m10 -v usb://EPSON/TM-m10 -P tm-ba-thermal-rastertotmtr.ppd -E
|
||||
|
||||
3) Print a file
|
||||
!!! You must always specify media options !!!
|
||||
$lpr -o <option> -P <printer> <file>
|
||||
example)
|
||||
$lpr -o media=RP80x2000 -P TM-m10 sample.pdf
|
||||
|
||||
5.2 by GUI
|
||||
Add a queue using OS tool & test print by GUI
|
||||
http://localhost:631 or http://127.0.0.1:631
|
||||
|
||||
6. LIMITATIONS
|
||||
--------------
|
||||
+ Support USB printer class only.
|
||||
Please configure printer class support manually.
|
||||
|
||||
7. HISTORY
|
||||
----------
|
||||
+ 2019/2/15 Version 3.0.0
|
||||
|
||||
[EOF]
|
||||
11
docs/Thermal Receipt/build.sh
Executable file
11
docs/Thermal Receipt/build.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
#build in directory
|
||||
if [ -d build ]
|
||||
then
|
||||
rm -R build
|
||||
fi
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
1069
docs/Thermal Receipt/filter/TmThermalReceipt.c
Normal file
1069
docs/Thermal Receipt/filter/TmThermalReceipt.c
Normal file
File diff suppressed because it is too large
Load Diff
123
docs/Thermal Receipt/install.sh
Executable file
123
docs/Thermal Receipt/install.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "EPSON TM series CUPS driver installer"
|
||||
echo "---------------------------------------"
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
ROOT_UID=0
|
||||
|
||||
if [ 0 -ne `id -u` ]
|
||||
then
|
||||
echo "This script requires root user access."
|
||||
echo "Re-run as root user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVERROOT=$(grep '^ServerRoot' /etc/cups/cupsd.conf | awk '{print $2}')
|
||||
|
||||
if [ -z $FILTERDIR ] || [ -z $PPDDIR ]
|
||||
then
|
||||
echo "Searching for ServerRoot, ServerBin, and DataDir tags in /etc/cups/cupsd.conf"
|
||||
echo ""
|
||||
|
||||
if [ -z $FILTERDIR ]
|
||||
then
|
||||
SERVERBIN=$(grep '^ServerBin' /etc/cups/cupsd.conf | awk '{print $2}')
|
||||
|
||||
if [ -z $SERVERBIN ]
|
||||
then
|
||||
echo "ServerBin tag not present in cupsd.conf - using default"
|
||||
FILTERDIR=/usr/lib/cups/filter
|
||||
elif [ ${SERVERBIN:0:1} = "/" ]
|
||||
then
|
||||
echo "ServerBin tag is present as an absolute path"
|
||||
FILTERDIR=$SERVERBIN/filter
|
||||
else
|
||||
echo "ServerBin tag is present as a relative path - appending to ServerRoot"
|
||||
FILTERDIR=$SERVERROOT/$SERVERBIN/filter
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ -z $PPDDIR ]
|
||||
then
|
||||
DATADIR=$(grep '^DataDir' /etc/cups/cupsd.conf | awk '{print $2}')
|
||||
|
||||
if [ -z $DATADIR ]
|
||||
then
|
||||
echo "DataDir tag not present in cupsd.conf - using default"
|
||||
PPDDIR=/usr/share/cups/model/EPSON
|
||||
elif [ ${DATADIR:0:1} = "/" ]
|
||||
then
|
||||
echo "DataDir tag is present as an absolute path"
|
||||
PPDDIR=$DATADIR/model/EPSON
|
||||
else
|
||||
echo "DataDir tag is present as a relative path - appending to ServerRoot"
|
||||
PPDDIR=$SERVERROOT/$DATADIR/model/EPSON
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "SERVERBIN = $SERVERBIN"
|
||||
echo "FILTERDIR = $FILTERDIR"
|
||||
echo "PPDDIR = $PPDDIR"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
INSTALL=/usr/bin/install
|
||||
|
||||
echo "Installing filter driver ..."
|
||||
$INSTALL -s ./build/rastertotmtr $FILTERDIR
|
||||
echo ""
|
||||
|
||||
echo "Installing ppd files ..."
|
||||
$INSTALL -m 755 -d $PPDDIR
|
||||
$INSTALL -m 755 ./ppd/*.ppd $PPDDIR
|
||||
echo ""
|
||||
|
||||
if [ -z $RPMBUILD ]
|
||||
then
|
||||
echo "Restarting CUPS"
|
||||
if [ -x /etc/software/init.d/cups ]
|
||||
then
|
||||
/etc/software/init.d/cups stop
|
||||
/etc/software/init.d/cups start
|
||||
elif [ -x /etc/rc.d/init.d/cups ]
|
||||
then
|
||||
/etc/rc.d/init.d/cups stop
|
||||
/etc/rc.d/init.d/cups start
|
||||
elif [ -x /etc/init.d/cups ]
|
||||
then
|
||||
/etc/init.d/cups stop
|
||||
/etc/init.d/cups start
|
||||
elif [ -x /sbin/init.d/cups ]
|
||||
then
|
||||
/sbin/init.d/cups stop
|
||||
/sbin/init.d/cups start
|
||||
elif [ -x /etc/software/init.d/cupsys ]
|
||||
then
|
||||
/etc/software/init.d/cupsys stop
|
||||
/etc/software/init.d/cupsys start
|
||||
elif [ -x /etc/rc.d/init.d/cupsys ]
|
||||
then
|
||||
/etc/rc.d/init.d/cupsys stop
|
||||
/etc/rc.d/init.d/cupsys start
|
||||
elif [ -x /etc/init.d/cupsys ]
|
||||
then
|
||||
/etc/init.d/cupsys stop
|
||||
/etc/init.d/cupsys start
|
||||
elif [ -x /sbin/init.d/cupsys ]
|
||||
then
|
||||
/sbin/init.d/cupsys stop
|
||||
/sbin/init.d/cupsys start
|
||||
else
|
||||
echo "Could not restart CUPS"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Installation Completed"
|
||||
echo "Add a printer queue using OS tool, http://localhost:631, or http://127.0.0.1:631"
|
||||
echo ""
|
||||
|
||||
120
docs/Thermal Receipt/ppd/tm-ba-thermal-rastertotmtr-180.ppd
Normal file
120
docs/Thermal Receipt/ppd/tm-ba-thermal-rastertotmtr-180.ppd
Normal file
@@ -0,0 +1,120 @@
|
||||
*PPD-Adobe: "4.3"
|
||||
*% Copyright (C) Seiko Epson Corporation 2018. All rights reserved.
|
||||
*%
|
||||
*% PPD file for TM Series Linux CUPS Printer Driver.
|
||||
|
||||
*FormatVersion: "4.3"
|
||||
*FileVersion: "2.0"
|
||||
*LanguageVersion: English
|
||||
*LanguageEncoding: ISOLatin1
|
||||
*PCFileName: "EPTMBATH.PPD"
|
||||
*Manufacturer: "EPSON"
|
||||
*Product: "(ThermalPrinter)"
|
||||
*ModelName:"EPSON TM Thermal"
|
||||
*ShortNickName:"TM Thermal"
|
||||
*NickName: "EPSON TM Thermal (180dpi)"
|
||||
*PSVersion: "(3010.000) 0"
|
||||
*LanguageLevel: "3"
|
||||
*ColorDevice: False
|
||||
*DefaultColorSpace: Gray
|
||||
*FileSystem: False
|
||||
*Throughput: "1"
|
||||
*LandscapeOrientation: Plus90
|
||||
*VariablePaperSize: True
|
||||
*TTRasterizer: Type42
|
||||
*cupsVersion: 1.2
|
||||
*cupsManualCopies: True
|
||||
*cupsModelNumber: 100
|
||||
*cupsFilter: "application/vnd.cups-raster 0 rastertotmtr"
|
||||
*cupsLanguages: "en"
|
||||
|
||||
|
||||
*% Printer option settings
|
||||
*OpenGroup: General/General
|
||||
|
||||
*% Page size settings.
|
||||
*OpenUI *PageSize/Media Size: PickOne
|
||||
*OrderDependency: 10 AnySetup *PageSize
|
||||
*DefaultPageSize:RP80x200
|
||||
*PageSize RP80x200/Roll paper 80 x 200 mm: "<</PageSize[205.0 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP80x2000/Roll paper 80 x 2000 mm: "<</PageSize[205.0 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP58x200/Roll paper 58 x 200 mm: "<</PageSize[144.0 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP58x2000/Roll paper 58 x 2000 mm: "<</PageSize[144.0 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*CloseUI: *PageSize
|
||||
|
||||
*OpenUI *PageRegion: PickOne
|
||||
*OrderDependency: 10 AnySetup *PageRegion
|
||||
*DefaultPageRegion:RP80x200
|
||||
*PageRegion RP80x200/Roll paper 80 x 200 mm: "<</PageSize[205.0 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP80x2000/Roll paper 80 x 2000 mm: "<</PageSize[205.0 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP58x200/Roll paper 58 x 200 mm: "<</PageSize[144.0 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP58x2000/Roll paper 58 x 2000 mm: "<</PageSize[144.0 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*CloseUI: *PageRegion
|
||||
|
||||
*DefaultImageableArea: RP80x200
|
||||
*ImageableArea RP80x200/Roll paper 80 x 200 mm: "0.0 0.0 205.0 566.9"
|
||||
*ImageableArea RP80x2000/Roll paper 80 x 2000 mm: "0.0 0.0 205.0 5669.3"
|
||||
*ImageableArea RP58x200/Roll paper 58 x 200 mm: "0.0 0.0 144.0 566.9"
|
||||
*ImageableArea RP58x2000/Roll paper 58 x 2000 mm: "0.0 0.0 144.0 5669.3"
|
||||
|
||||
*DefaultPaperDimension: RP80x200
|
||||
*PaperDimension RP80x200/Roll paper 80 x 200 mm: "205.0 566.9"
|
||||
*PaperDimension RP80x2000/Roll paper 80 x 2000 mm: "205.0 5669.3"
|
||||
*PaperDimension RP58x200/Roll paper 58 x 200 mm: "144.0 566.9"
|
||||
*PaperDimension RP58x2000/Roll paper 58 x 2000 mm: "144.0 5669.3"
|
||||
|
||||
*% Custom page size settings.
|
||||
*MaxMediaWidth: "205.0"
|
||||
*MaxMediaHeight: "5669.3"
|
||||
*NonUIOrderDependency: 100 AnySetup *CustomPageSize
|
||||
*CustomPageSize True: "pop pop pop <</PageSize [ 5 -2 roll ]/ImagingBBox null>>setpagedevice"
|
||||
*ParamCustomPageSize Width: 1 points 72 205.0
|
||||
*ParamCustomPageSize Height: 2 points 72 5669.3
|
||||
*ParamCustomPageSize WidthOffset: 3 points 0 0
|
||||
*ParamCustomPageSize HeightOffset: 4 points 0 0
|
||||
*ParamCustomPageSize Orientation: 5 int 0 0
|
||||
|
||||
*% Resolution settings.
|
||||
*OpenUI *Resolution/Resolution: PickOne
|
||||
*OrderDependency: 20 AnySetup *Resolution
|
||||
*DefaultResolution: 180x180dpi
|
||||
*Resolution 180x180dpi/180 x 180 dpi: "<</HWResolution[180 180]/cupsRowCount 24/cupsBitsPerColor 1>>setpagedevice"
|
||||
*CloseUI: *Resolution
|
||||
|
||||
*% Horizontal and Vertical motion units.
|
||||
*TmxMotionUnitHori: "180"
|
||||
*TmxMotionUnitVert: "180"
|
||||
|
||||
*% Paper reduction settings.
|
||||
*OpenUI *TmxPaperReduction/Paper Reduction: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxPaperReduction
|
||||
*DefaultTmxPaperReduction: Bottom
|
||||
*TmxPaperReduction Off/None: ""
|
||||
*TmxPaperReduction Top/Top: ""
|
||||
*TmxPaperReduction Bottom/Bottom: ""
|
||||
*TmxPaperReduction Both/Top & Bottom: ""
|
||||
*CloseUI: *TmxPaperReduction
|
||||
|
||||
*% Buzzer / Cash Drawer settings.
|
||||
*OpenUI *TmxBuzzerAndDrawer/Buzzer/ Cash Drawer: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxBuzzerAndDrawer
|
||||
*DefaultTmxBuzzerAndDrawer: NotUsed
|
||||
*TmxBuzzerAndDrawer NotUsed/Not used: ""
|
||||
*TmxBuzzerAndDrawer InternalBuzzer/Internal buzzer: ""
|
||||
*TmxBuzzerAndDrawer ExternalBuzzer/External buzzer: ""
|
||||
*TmxBuzzerAndDrawer OpenDrawer1/Open drawer #1: ""
|
||||
*TmxBuzzerAndDrawer OpenDrawer2/Open drawer #2: ""
|
||||
*CloseUI: *TmxBuzzerAndDrawer
|
||||
|
||||
*% Paper source settings.
|
||||
*OpenUI *TmxPaperCut/Paper Cut: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxPaperCut
|
||||
*DefaultTmxPaperCut: NoCut
|
||||
*TmxPaperCut NoCut/No cut: ""
|
||||
*TmxPaperCut CutPerJob/Cut per job: ""
|
||||
*TmxPaperCut CutPerPage/Cut per page: ""
|
||||
*CloseUI: *TmxPaperCut
|
||||
|
||||
*CloseGroup: General
|
||||
|
||||
*% End
|
||||
120
docs/Thermal Receipt/ppd/tm-ba-thermal-rastertotmtr-203.ppd
Normal file
120
docs/Thermal Receipt/ppd/tm-ba-thermal-rastertotmtr-203.ppd
Normal file
@@ -0,0 +1,120 @@
|
||||
*PPD-Adobe: "4.3"
|
||||
*% Copyright (C) Seiko Epson Corporation 2018. All rights reserved.
|
||||
*%
|
||||
*% PPD file for TM Series Linux CUPS Printer Driver.
|
||||
|
||||
*FormatVersion: "4.3"
|
||||
*FileVersion: "2.0"
|
||||
*LanguageVersion: English
|
||||
*LanguageEncoding: ISOLatin1
|
||||
*PCFileName: "EPTMBATH.PPD"
|
||||
*Manufacturer: "EPSON"
|
||||
*Product: "(ThermalPrinter)"
|
||||
*ModelName:"EPSON TM Thermal"
|
||||
*ShortNickName:"TM Thermal"
|
||||
*NickName: "EPSON TM Thermal (203dpi)"
|
||||
*PSVersion: "(3010.000) 0"
|
||||
*LanguageLevel: "3"
|
||||
*ColorDevice: False
|
||||
*DefaultColorSpace: Gray
|
||||
*FileSystem: False
|
||||
*Throughput: "1"
|
||||
*LandscapeOrientation: Plus90
|
||||
*VariablePaperSize: True
|
||||
*TTRasterizer: Type42
|
||||
*cupsVersion: 1.2
|
||||
*cupsManualCopies: True
|
||||
*cupsModelNumber: 100
|
||||
*cupsFilter: "application/vnd.cups-raster 0 rastertotmtr"
|
||||
*cupsLanguages: "en"
|
||||
|
||||
|
||||
*% Printer option settings
|
||||
*OpenGroup: General/General
|
||||
|
||||
*% Page size settings.
|
||||
*OpenUI *PageSize/Media Size: PickOne
|
||||
*OrderDependency: 10 AnySetup *PageSize
|
||||
*DefaultPageSize:RP80x200
|
||||
*PageSize RP80x200/Roll paper 80 x 200 mm: "<</PageSize[204.3 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP80x2000/Roll paper 80 x 2000 mm: "<</PageSize[204.3 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP58x200/Roll paper 58 x 200 mm: "<</PageSize[149.1 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageSize RP58x2000/Roll paper 58 x 2000 mm: "<</PageSize[149.1 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*CloseUI: *PageSize
|
||||
|
||||
*OpenUI *PageRegion: PickOne
|
||||
*OrderDependency: 10 AnySetup *PageRegion
|
||||
*DefaultPageRegion:RP80x200
|
||||
*PageRegion RP80x200/Roll paper 80 x 200 mm: "<</PageSize[204.3 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP80x2000/Roll paper 80 x 2000 mm: "<</PageSize[204.3 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP58x200/Roll paper 58 x 200 mm: "<</PageSize[149.1 566.9]/ImagingBBox null>>setpagedevice"
|
||||
*PageRegion RP58x2000/Roll paper 58 x 2000 mm: "<</PageSize[149.1 5669.3]/ImagingBBox null>>setpagedevice"
|
||||
*CloseUI: *PageRegion
|
||||
|
||||
*DefaultImageableArea: RP80x200
|
||||
*ImageableArea RP80x200/Roll paper 80 x 200 mm: "0.0 0.0 204.3 566.9"
|
||||
*ImageableArea RP80x2000/Roll paper 80 x 2000 mm: "0.0 0.0 204.3 5669.3"
|
||||
*ImageableArea RP58x200/Roll paper 58 x 200 mm: "0.0 0.0 149.1 566.9"
|
||||
*ImageableArea RP58x2000/Roll paper 58 x 2000 mm: "0.0 0.0 149.1 5669.3"
|
||||
|
||||
*DefaultPaperDimension: RP80x200
|
||||
*PaperDimension RP80x200/Roll paper 80 x 200 mm: "204.3 566.9"
|
||||
*PaperDimension RP80x2000/Roll paper 80 x 2000 mm: "204.3 5669.3"
|
||||
*PaperDimension RP58x200/Roll paper 58 x 200 mm: "149.1 566.9"
|
||||
*PaperDimension RP58x2000/Roll paper 58 x 2000 mm: "149.1 5669.3"
|
||||
|
||||
*% Custom page size settings.
|
||||
*MaxMediaWidth: "204.3"
|
||||
*MaxMediaHeight: "5669.3"
|
||||
*NonUIOrderDependency: 100 AnySetup *CustomPageSize
|
||||
*CustomPageSize True: "pop pop pop <</PageSize [ 5 -2 roll ]/ImagingBBox null>>setpagedevice"
|
||||
*ParamCustomPageSize Width: 1 points 72 204.3
|
||||
*ParamCustomPageSize Height: 2 points 72 5669.3
|
||||
*ParamCustomPageSize WidthOffset: 3 points 0 0
|
||||
*ParamCustomPageSize HeightOffset: 4 points 0 0
|
||||
*ParamCustomPageSize Orientation: 5 int 0 0
|
||||
|
||||
*% Resolution settings.
|
||||
*OpenUI *Resolution/Resolution: PickOne
|
||||
*OrderDependency: 20 AnySetup *Resolution
|
||||
*DefaultResolution: 203x203dpi
|
||||
*Resolution 203x203dpi/203 x 203 dpi: "<</HWResolution[203 203]/cupsRowCount 24/cupsBitsPerColor 1>>setpagedevice"
|
||||
*CloseUI: *Resolution
|
||||
|
||||
*% Horizontal and Vertical motion units.
|
||||
*TmxMotionUnitHori: "203"
|
||||
*TmxMotionUnitVert: "203"
|
||||
|
||||
*% Paper reduction settings.
|
||||
*OpenUI *TmxPaperReduction/Paper Reduction: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxPaperReduction
|
||||
*DefaultTmxPaperReduction: Bottom
|
||||
*TmxPaperReduction Off/None: ""
|
||||
*TmxPaperReduction Top/Top: ""
|
||||
*TmxPaperReduction Bottom/Bottom: ""
|
||||
*TmxPaperReduction Both/Top & Bottom: ""
|
||||
*CloseUI: *TmxPaperReduction
|
||||
|
||||
*% Buzzer / Cash Drawer settings.
|
||||
*OpenUI *TmxBuzzerAndDrawer/Buzzer/ Cash Drawer: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxBuzzerAndDrawer
|
||||
*DefaultTmxBuzzerAndDrawer: NotUsed
|
||||
*TmxBuzzerAndDrawer NotUsed/Not used: ""
|
||||
*TmxBuzzerAndDrawer InternalBuzzer/Internal buzzer: ""
|
||||
*TmxBuzzerAndDrawer ExternalBuzzer/External buzzer: ""
|
||||
*TmxBuzzerAndDrawer OpenDrawer1/Open drawer #1: ""
|
||||
*TmxBuzzerAndDrawer OpenDrawer2/Open drawer #2: ""
|
||||
*CloseUI: *TmxBuzzerAndDrawer
|
||||
|
||||
*% Paper source settings.
|
||||
*OpenUI *TmxPaperCut/Paper Cut: PickOne
|
||||
*OrderDependency: 30 AnySetup *TmxPaperCut
|
||||
*DefaultTmxPaperCut: NoCut
|
||||
*TmxPaperCut NoCut/No cut: ""
|
||||
*TmxPaperCut CutPerJob/Cut per job: ""
|
||||
*TmxPaperCut CutPerPage/Cut per page: ""
|
||||
*CloseUI: *TmxPaperCut
|
||||
|
||||
*CloseGroup: General
|
||||
|
||||
*% End
|
||||
BIN
docs/User Library-Printer TM-T20II-i.STL
Normal file
BIN
docs/User Library-Printer TM-T20II-i.STL
Normal file
Binary file not shown.
BIN
docs/User Library-Raspberry Pi 3-2.STL
Normal file
BIN
docs/User Library-Raspberry Pi 3-2.STL
Normal file
Binary file not shown.
12
littleprynter.service
Normal file
12
littleprynter.service
Normal 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 --no-reload
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1 +0,0 @@
|
||||
{"last_check":"2018-10-10T18:33:22Z","pypi_version":"18.1"}
|
||||
1886
poetry.lock
generated
Normal file
1886
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
pyproject.toml
Normal file
56
pyproject.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[project]
|
||||
name = "littleprynter"
|
||||
version = "0.1.0"
|
||||
description = "A web interface and API to print on thermal printers"
|
||||
authors = [
|
||||
{name = "n07070",email = "contact@n07070.xyz"}
|
||||
]
|
||||
license = "AGPLv3"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"flask (>=3.1.3,<4.0.0)",
|
||||
"numpy (>=2.3.4)",
|
||||
"toml (>=0.10.2,<0.11.0)",
|
||||
"flask-socketio (>=5.6.1,<6.0.0)",
|
||||
"flask-limiter (>=4.1.1,<5.0.0)",
|
||||
"gpiozero (>=2.0.1)",
|
||||
"cryptography (>=48.0.0,<49.0.0)",
|
||||
"pyusb (>=1.3.1,<2.0.0)",
|
||||
"pyserial (>=3.5,<4.0)",
|
||||
"qrcode (<9.0)",
|
||||
"python-barcode (>=0.16.1)",
|
||||
"setuptools (==81)",
|
||||
"cffi (>=2.0.0,<3.0.0)",
|
||||
"configobj (>=5.0.9,<6.0.0)",
|
||||
"future (>=1.0.0,<2.0.0)",
|
||||
"h11 (>=0.16.0,<0.17.0)",
|
||||
"idna (>=3.15,<4.0)",
|
||||
"itsdangerous (>=2.2.0,<3.0.0)",
|
||||
"jinja2 (>=3.1.6,<4.0.0)",
|
||||
"jsonpointer (>=3.1.1,<4.0.0)",
|
||||
"jsonschema (>=4.26.0,<5.0.0)",
|
||||
"limits (>=5.8.0,<6.0.0)",
|
||||
"markupsafe (>=3.0.3,<4.0.0)",
|
||||
"pillow (>=12.2.0,<13.0.0)",
|
||||
"pycryptodomex (>=3.23.0,<4.0.0)",
|
||||
"python-dateutil (>=2.9.0.post0,<3.0.0)",
|
||||
"python-engineio (>=4.13.1,<5.0.0)",
|
||||
"python-socketio (>=5.16.1,<6.0.0)",
|
||||
"pyyaml (>=6.0.3,<7.0.0)",
|
||||
"requests (>=2.34.2,<3.0.0)",
|
||||
"python-escpos (>=3.1,<4.0)",
|
||||
"pep8 (>=1.7.1,<2.0.0)",
|
||||
"pylint (>=4.0.5,<5.0.0)",
|
||||
"brother-ql-inventree (>=1.3,<2.0)",
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
package-mode = false
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python-escpos = {allow-prereleases = true}
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -1,31 +0,0 @@
|
||||
Adafruit-Thermal~=1.1.0
|
||||
appdirs~=1.4.4
|
||||
argcomplete~=2.0.0
|
||||
click~=8.1.3
|
||||
commonmark~=0.9.1
|
||||
Deprecated~=1.2.13
|
||||
escpos~=1.9
|
||||
Flask~=2.1.2
|
||||
Flask-Limiter~=2.4.5.1
|
||||
future~=0.18.2
|
||||
itsdangerous~=2.1.2
|
||||
Jinja2~=3.1.2
|
||||
limits~=2.6.1
|
||||
MarkupSafe~=2.1.1
|
||||
packaging~=21.3
|
||||
Pillow~=9.1.0
|
||||
Pygments~=2.12.0
|
||||
pyparsing~=3.0.8
|
||||
pyserial~=3.5
|
||||
python-barcode~=0.13.1
|
||||
pyusb~=1.2.1
|
||||
PyYAML~=6.0
|
||||
qrcode~=7.3.1
|
||||
rich~=12.4.1
|
||||
six~=1.16.0
|
||||
toml~=0.10.2
|
||||
typing_extensions~=4.2.0
|
||||
Unidecode~=1.3.4
|
||||
viivakoodi~=0.8.0
|
||||
Werkzeug~=2.1.2
|
||||
wrapt~=1.14.1
|
||||
7
run.sh
7
run.sh
@@ -1,7 +0,0 @@
|
||||
virtualenv .
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
export FLASK_APP=src/main.py
|
||||
export FLASK_ENV=development
|
||||
flask run --host 192.168.0.42 --debugger --eager-loading
|
||||
417
src/main.py
417
src/main.py
@@ -1,84 +1,229 @@
|
||||
# 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.
|
||||
# It also exposes a API, making it possible to print and interface with much of the printer's abilities.
|
||||
"""
|
||||
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.
|
||||
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,
|
||||
# Then we build the API around Flask,
|
||||
# Then we build the web interface, using the simple Jinja2 templating.
|
||||
We first define the connection to the printer itself,
|
||||
Then we build the API around Flask,
|
||||
Then we build the web interface, using the simple Jinja2 templating.
|
||||
|
||||
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 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,
|
||||
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_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
from printer import Printer # The wrapper for the printer class
|
||||
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
|
||||
from printer import Printer # The wrapper for the printer class
|
||||
from raspberry import Raspberry # The Raspberry pi control Class
|
||||
from web import Web # Wrapper for the web routes and API
|
||||
from print_queue import PrintQueue
|
||||
from worker import PrintWorker
|
||||
|
||||
# We create the main Flask object
|
||||
app = Flask(__name__)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
# Global variables
|
||||
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||
|
||||
# Load the configuration file
|
||||
try:
|
||||
app.logger.debug("Loading config file...")
|
||||
configuration_file = toml.load("configuration/config.toml")
|
||||
except TypeError :
|
||||
app.logger.error("Unable to load the config file: invalid type or is a list containing invalid types")
|
||||
exit(-1)
|
||||
except toml.TomlDecodeError:
|
||||
app.logger.error("An error occured while decoding the file")
|
||||
exit(-1)
|
||||
except Exception as e:
|
||||
app.logger.error("Error while loading file : " + str(e))
|
||||
exit(-1)
|
||||
with open("configuration/config.toml", "r", encoding="utf-8") as f:
|
||||
configuration_file = toml.load(f)
|
||||
except TypeError:
|
||||
app.logger.error(
|
||||
"Unable to load the config file: invalid type or is a list containing invalid types"
|
||||
)
|
||||
sys.exit(-1)
|
||||
except toml.TomlDecodeError as e:
|
||||
app.logger.error(
|
||||
"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 !")
|
||||
|
||||
# Define the USB connections here.
|
||||
vendor_id = configuration_file["printer"]["vendor_id"]
|
||||
device_id = configuration_file["printer"]["device_id"]
|
||||
UPLOAD_FOLDER = str(configuration_file["printer"]["upload_folder"])
|
||||
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
try:
|
||||
os.mkdir(UPLOAD_FOLDER)
|
||||
app.logger.debug("Directory %s created successfully.", UPLOAD_FOLDER)
|
||||
except FileExistsError:
|
||||
app.logger.debug("Directory %s already exists.", UPLOAD_FOLDER)
|
||||
except PermissionError:
|
||||
app.logger.error("Permission denied: Unable to create %s", UPLOAD_FOLDER)
|
||||
sys.exit(77)
|
||||
|
||||
# Output the config file
|
||||
if os.getenv('LIPY_DEBUG') == True:
|
||||
if os.getenv("FLASK_DEBUG"):
|
||||
pprint.pprint(configuration_file)
|
||||
|
||||
# We define the app module used by Flask
|
||||
app.secret_key = configuration_file["secrets"]["flask_secret_key"]
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['ALLOWED_EXTENSIONS'] = ALLOWED_EXTENSIONS
|
||||
app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000 # Maximum 3Mb for a file upload
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
app.config["ALLOWED_EXTENSIONS"] = ALLOWED_EXTENSIONS
|
||||
app.config["MAX_CONTENT_LENGTH"] = 10 * 1000 * 1000 # Maximum 3Mb for a file upload
|
||||
app.config["TEMPLATES_AUTO_RELOAD"] = True
|
||||
|
||||
# Printer connection
|
||||
# Uses the class defined in the printer.py file
|
||||
printer = Printer(app,0x04b8, 0x0e28)
|
||||
printer = Printer(app, 0x04B8, 0x0E28)
|
||||
printer.init_printer()
|
||||
|
||||
# Web routes
|
||||
web = Web(app, printer)
|
||||
|
||||
|
||||
limiter = Limiter(
|
||||
# Find out if we are running on a Raspberry Pi
|
||||
rpi = Raspberry(
|
||||
printer,
|
||||
app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["1500 per day", "500 per hour"]
|
||||
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"],
|
||||
)
|
||||
|
||||
@app.route('/')
|
||||
RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi()
|
||||
|
||||
# Queue creation
|
||||
print_queue = PrintQueue(app)
|
||||
|
||||
# Web & API management
|
||||
web = Web(app, print_queue)
|
||||
|
||||
# Start worker thread
|
||||
worker = PrintWorker(app, print_queue, printer, socketio)
|
||||
worker.start()
|
||||
|
||||
# The rate limit
|
||||
limiter = Limiter(
|
||||
get_remote_address, app=app, default_limits=["1500 per day", "500 per hour"]
|
||||
)
|
||||
|
||||
# General routes
|
||||
@app.route("/")
|
||||
@limiter.limit("1/second", override_defaults=False)
|
||||
def index():
|
||||
"""Return the web interface 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)
|
||||
def webcam():
|
||||
"""Returns the webcam web interface"""
|
||||
app.logger.debug("Loading webcam interface")
|
||||
return render_template('webcam.html')
|
||||
return render_template("webcam.html")
|
||||
|
||||
|
||||
# Form treatement
|
||||
@app.route("/web/print/sms", methods=["POST"])
|
||||
@limiter.limit("6/minute", override_defaults=False)
|
||||
def web_print_sms():
|
||||
"""Prints a short message on a printer"""
|
||||
app.logger.debug("Printing an sms via web method")
|
||||
|
||||
try:
|
||||
txt = request.form["txt"]
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.error("Whoops, we are missing the txt input field. : %s ", str(e))
|
||||
flash("Whoops, no forms submitted or missing signature : " + str(e), "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
# comment: We try to get a signature
|
||||
sign = request.form["signature"]
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.warning(
|
||||
"No signature found for this print, using default signature : %s ", str(e)
|
||||
)
|
||||
sign = configuration_file["defaults"]["signature"]
|
||||
|
||||
try:
|
||||
web.print_sms(txt, sign)
|
||||
except RuntimeError as e:
|
||||
app.logger.error("Whoops, we could not print an SMS because : %s ", str(e))
|
||||
flash("Whoops, we could not print an SMS because :" + str(e), "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
# end try
|
||||
flash("The SMS has been added to the print queue !", "info")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/web/print/img", methods=["POST"])
|
||||
@limiter.limit("1/second", override_defaults=False)
|
||||
def web_print_img():
|
||||
"""Prints an image on a printer"""
|
||||
app.logger.debug("Printing an image with web method")
|
||||
|
||||
try:
|
||||
# comment: We try to get a signature
|
||||
sign = request.form["signature"]
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.warning(
|
||||
"No signature found for this print, using default signature : %s", str(e)
|
||||
)
|
||||
sign = configuration_file["defaults"]["signature"]
|
||||
|
||||
# check if the post request has the file part
|
||||
if "img" not in request.files:
|
||||
app.logger.error("Whoops, no images submitted : %s ", str(e))
|
||||
app.logger.error("Error getting the files : %s", str(e))
|
||||
flash("Whoops, no images submitted : " + str(e), "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
file = request.files["img"]
|
||||
# If the user does not select a file, the browser submits an
|
||||
# empty file without a filename.
|
||||
if file.filename == "":
|
||||
app.logger.error("Submitted file has no filename !")
|
||||
flash("Submitted file has no filename !", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
app.logger.debug("Sending the image to the printer.")
|
||||
web.print_image(file, sign)
|
||||
except RuntimeError as e:
|
||||
app.logger.error("The image could not be printed because : %s ", str(e))
|
||||
flash("The image could not be printed because : " + str(e), "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
flash("Picture added to the print queue !", "info")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
# API routes
|
||||
# The api has the following methods
|
||||
@@ -86,98 +231,176 @@ def webcam():
|
||||
# api/auth/{login,logout}
|
||||
# api/status/{paper,ping,stats}
|
||||
|
||||
|
||||
# If you just call the api route, you get a help back.
|
||||
@app.route('/api')
|
||||
@app.route('/api/print')
|
||||
@app.route("/api")
|
||||
@app.route("/api/print")
|
||||
@limiter.limit("1/second", override_defaults=False)
|
||||
def api_index():
|
||||
"""Returns a how-to for the API"""
|
||||
app.logger.debug("Loading API")
|
||||
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)
|
||||
def api_print_sms():
|
||||
app.logger.debug("Printing an sms")
|
||||
"""Prints a short message on a printer"""
|
||||
app.logger.debug("Printing an sms via API")
|
||||
try:
|
||||
txt = request.form["txt"]
|
||||
sign = request.form["signature"]
|
||||
except Exception as e:
|
||||
flash(e,'error')
|
||||
redirect(url_for('index'))
|
||||
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.error("Whoops, we are missing the txt input field. : %s ", str(e))
|
||||
return str(e), 400
|
||||
try:
|
||||
web.print_sms(txt,sign)
|
||||
except Exception as e:
|
||||
pass
|
||||
# comment: We try to get a signature
|
||||
sign = request.form["signature"]
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.warning(
|
||||
"No signature found for this print, using default signature. : %s", str(e)
|
||||
)
|
||||
sign = configuration_file["defaults"]["signature"]
|
||||
try:
|
||||
# comment: We try to print the SMS
|
||||
web.print_sms(txt, sign)
|
||||
except RuntimeError as e:
|
||||
return str(e), 500
|
||||
# end try
|
||||
return "OK", 200
|
||||
|
||||
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)
|
||||
def api_print_image():
|
||||
app.logger.debug("Printing an image")
|
||||
"""Prints an image on a printer"""
|
||||
app.logger.debug("Printing an image via API")
|
||||
|
||||
try:
|
||||
# comment: We try to get a signature
|
||||
sign = request.form["signature"]
|
||||
except Exception as e:
|
||||
flash(str(e),'error')
|
||||
app.logger.error(str(e) + " - Whoops, no forms submitted or missing signature.")
|
||||
return redirect(url_for('index'))
|
||||
except werkzeug.exceptions.BadRequestKeyError as e:
|
||||
app.logger.warning(
|
||||
"No signature found for this print, using default signature. %s", str(e)
|
||||
)
|
||||
sign = configuration_file["defaults"]["signature"]
|
||||
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
# check if the post request has the file part
|
||||
if "img" not in request.files:
|
||||
app.logger.error("Whoops, no images submitted.")
|
||||
return "No image submitted", 400
|
||||
|
||||
file = request.files["img"]
|
||||
# If the user submits an empty file without a filename.
|
||||
if file.filename == "":
|
||||
app.logger.error("Submitted file has no filename !")
|
||||
return "Submitted file has no filename !", 400
|
||||
|
||||
try:
|
||||
app.logger.debug("Sending the image to the printer.")
|
||||
web.print_image(file, sign)
|
||||
except RuntimeError as e:
|
||||
return str(e), 500
|
||||
|
||||
return "OK", 200
|
||||
|
||||
|
||||
@app.route("/api/camera/picture", methods=["GET"])
|
||||
def camera_picture():
|
||||
"""Returns a picture taken by the camera on a raspberry pi"""
|
||||
if RASPBERRY_PI_CONNECTED:
|
||||
try:
|
||||
if 'img' not in request.files:
|
||||
flash('No file found. Did you use the good form ?', 'error')
|
||||
app.logger.error("No file found. Did you use the good form ?")
|
||||
return redirect(url_for("index"))
|
||||
else:
|
||||
file = request.files['img']
|
||||
except Exception as e:
|
||||
if sign is not None and photo is not None:
|
||||
pass
|
||||
else:
|
||||
flash(str(e), 'error')
|
||||
app.logger.error("Couldn't get an image nor signature : " + str(e))
|
||||
|
||||
# If the user does not select a file, the browser submits an
|
||||
# empty file without a filename.
|
||||
if file.filename == '':
|
||||
app.logger.error("Submitted file has no filename !")
|
||||
flash('No file submitted, please select a file','error')
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
app.logger.debug("Sending the image to the printer.")
|
||||
web.print_image(file, sign)
|
||||
except Exception as e:
|
||||
pass
|
||||
return rpi.camera_picture()
|
||||
except RuntimeError as e:
|
||||
return jsonify({"message": "Error getting the stream : " + e}), 500
|
||||
else:
|
||||
flash('Cannot access to page with this method.','error')
|
||||
app.logger.debug('Bad access type to this API.')
|
||||
return jsonify({"message": "No camera present"}), 500
|
||||
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route('/login')
|
||||
@app.route("/api/queue", methods=["GET"])
|
||||
def api_queue_status():
|
||||
"""API endpoint for entire queue"""
|
||||
return jsonify(web.get_queue_state())
|
||||
|
||||
|
||||
@app.route("/api/worker", methods=["GET"])
|
||||
def api_worker_state():
|
||||
"""API endpoint to get the worker state"""
|
||||
return jsonify(worker.current_state())
|
||||
|
||||
|
||||
@app.route("/api/worker/start")
|
||||
def api_worker_start():
|
||||
"""
|
||||
Enable to worker. This starts to process the print queue.
|
||||
"""
|
||||
worker.start_worker()
|
||||
return jsonify(worker.current_state())
|
||||
|
||||
|
||||
@app.route("/api/worker/stop")
|
||||
def api_worker_stop():
|
||||
"""
|
||||
Stops the print queue. This stops the processing of the print queue.
|
||||
"""
|
||||
worker.stop_worker()
|
||||
return jsonify(worker.current_state())
|
||||
|
||||
|
||||
## Authentification
|
||||
|
||||
|
||||
@app.route("/login")
|
||||
@limiter.limit("1/second", override_defaults=False)
|
||||
def login_page():
|
||||
"""Unsued, logins"""
|
||||
# web.login(username,password)
|
||||
return redirect(url_for("index"))
|
||||
return redirect(url_for("index")), 501
|
||||
|
||||
@app.route('/logout')
|
||||
|
||||
@app.route("/logout")
|
||||
@limiter.limit("1/second", override_defaults=False)
|
||||
def logout_page():
|
||||
"""Unused, logout"""
|
||||
# web.logout(username, password)
|
||||
return redirect(url_for("index"))
|
||||
return redirect(url_for("index")), 501
|
||||
|
||||
|
||||
@app.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
flash("Rate limit reached, please slow down :) ( Currently at "+ e.description + ")", 'error')
|
||||
"""Handle rate limits"""
|
||||
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"))
|
||||
|
||||
|
||||
@app.route("/ping")
|
||||
@limiter.exempt
|
||||
def ping():
|
||||
flash("🏓 Pong !",'info')
|
||||
"""Returns a pong"""
|
||||
flash("🏓 Pong !", "info")
|
||||
app.logger.debug("🏓 Pong !")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@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():
|
||||
"""Returns camera status to a socket"""
|
||||
app.logger.debug("Client asked if we had a camera")
|
||||
if RASPBERRY_PI_CONNECTED:
|
||||
socketio.emit("camera_status", True)
|
||||
else:
|
||||
socketio.emit("camera_status", False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, use_reloader=False, host="0.0.0.0", ssl_context="adhoc")
|
||||
|
||||
135
src/print_queue.py
Normal file
135
src/print_queue.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
This class has the method by which we manage the Tasks
|
||||
It's a printing queue, so we need to add, remove and get information on where
|
||||
the queue is
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
# Because actually printing and adding new print job requests happen at
|
||||
# diffrent times, the print queue is managed by it's own thread.
|
||||
import threading
|
||||
|
||||
from datetime import datetime
|
||||
from task import TaskType
|
||||
|
||||
|
||||
class PrintQueue:
|
||||
"""
|
||||
A Double-ended Queue to manage the printing Tasks
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self._queue = deque()
|
||||
self._lock = threading.Lock()
|
||||
self._completed_tasks = {} # Store completed task info
|
||||
self._task_counter = 0
|
||||
self.app.logger.debug("Created a new PrintQueue")
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._queue)
|
||||
|
||||
def enqueue(self, task):
|
||||
"""Add task to right of the queue and return position"""
|
||||
with self._lock:
|
||||
try:
|
||||
self.app.logger.info("Add task %s to queue ", task.task_id)
|
||||
self._queue.append(task)
|
||||
position = self._queue.index(task)
|
||||
# We return the current position of the task if it was added
|
||||
self.app.logger.debug(
|
||||
"Added a new task %s to the queue at position %s",
|
||||
task.task_id,
|
||||
position,
|
||||
)
|
||||
return position
|
||||
except Exception as e:
|
||||
self.app.logger.error("Could not add a task to the queue : %s ", e)
|
||||
raise e
|
||||
|
||||
def dequeue(self):
|
||||
"""Remove and return next task ( from the left of the queue ) (thread-safe)"""
|
||||
with self._lock:
|
||||
return self._queue.popleft() if len(self._queue) > 0 else None
|
||||
|
||||
def get_position(self, task):
|
||||
"""Get current position of task in queue (1-indexed)"""
|
||||
with self._lock:
|
||||
if task.task_id in self._completed_tasks:
|
||||
return None # Task already completed
|
||||
|
||||
try:
|
||||
# Try to get the position of a Task
|
||||
return self._queue.index(task)
|
||||
except ValueError as e:
|
||||
raise e
|
||||
# end try
|
||||
|
||||
def is_empty(self):
|
||||
"""Check if queue is empty"""
|
||||
with self._lock:
|
||||
self.app.logger.debug("Checking if queue is empty")
|
||||
return len(self._queue) == 0
|
||||
|
||||
def get_queue_state(self):
|
||||
"""Return current queue state"""
|
||||
with self._lock:
|
||||
self.app.logger.debug("Return current queue state")
|
||||
return [{"task_id": t.task_id, "status": t.status} for t in self._queue]
|
||||
|
||||
def get_status(self, task_id):
|
||||
"""Get full status info for a task"""
|
||||
with self._lock:
|
||||
|
||||
if task_id in self._completed_tasks:
|
||||
return self._completed_tasks[task_id]
|
||||
|
||||
# Check in queue if it exists
|
||||
for index, task in enumerate(self._queue):
|
||||
if task.task_id == task_id:
|
||||
# Depending on it's type, we return more info
|
||||
if task.task_type == TaskType.IMAGE:
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": task.status,
|
||||
"type": task.task_type,
|
||||
"position": index,
|
||||
"in_queue": True,
|
||||
"content": task.content,
|
||||
"signature": task.signature,
|
||||
}
|
||||
|
||||
if task.task_type == TaskType.TEXT:
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": task.status,
|
||||
"type": task.task_type,
|
||||
"position": index,
|
||||
"in_queue": True,
|
||||
"image_path": str(task.image_path),
|
||||
"signature": task.signature,
|
||||
"process": str(task.process),
|
||||
}
|
||||
|
||||
if task.task_type == TaskType.CUT:
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": task.status,
|
||||
"type": task.task_type,
|
||||
"position": index,
|
||||
"in_queue": True,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def mark_completed(self, task_id, task_status):
|
||||
"""Mark task as completed and remove from queue"""
|
||||
with self._lock:
|
||||
self._completed_tasks[task_id] = {
|
||||
"task_id": task_id,
|
||||
"status": task_status,
|
||||
"position": None,
|
||||
"in_queue": False,
|
||||
"completed_at": datetime.now().isoformat(),
|
||||
}
|
||||
357
src/printer.py
357
src/printer.py
@@ -1,12 +1,18 @@
|
||||
# Importing the module to mage the connection to the printer.
|
||||
from flask import flash
|
||||
from escpos.printer import Usb, USBNotFoundError
|
||||
from time import sleep, gmtime, strftime
|
||||
"""
|
||||
This class manages connexion to a Printer
|
||||
"""
|
||||
# import brother_ql
|
||||
from time import sleep
|
||||
import os.path
|
||||
from PIL import Image
|
||||
|
||||
from PIL import Image, ImageEnhance
|
||||
import numpy as np
|
||||
|
||||
# Importing the module to manage the connection to the printer.
|
||||
import escpos.printer
|
||||
|
||||
|
||||
class Printer(object):
|
||||
class Printer():
|
||||
"""
|
||||
# The connection is based on the ESC/POS library
|
||||
|
||||
@@ -27,152 +33,315 @@ class Printer(object):
|
||||
ready = False
|
||||
|
||||
def __init__(self, app, device_id, vendor_id):
|
||||
super(Printer, self).__init__()
|
||||
super().__init__()
|
||||
self.app = app
|
||||
self.ready = False
|
||||
self.printer = None
|
||||
self.device_id = device_id
|
||||
self.vendor_id = vendor_id
|
||||
self.usb_args = {}
|
||||
self.usb_args['idVendor'] = self.device_id
|
||||
self.usb_args['idProduct'] = self.vendor_id
|
||||
self.usb_args["idVendor"] = self.device_id
|
||||
self.usb_args["idProduct"] = self.vendor_id
|
||||
|
||||
def check_paper(self) -> bool:
|
||||
# Let's check paper status
|
||||
self.app.logger.debug('Checking paper status...')
|
||||
"""
|
||||
On printers that support it, we check that the printer has paper
|
||||
"""
|
||||
self.app.logger.debug("Checking paper status...")
|
||||
self.printer.open(self.usb_args)
|
||||
status = self.printer.paper_status()
|
||||
match status:
|
||||
case 0:
|
||||
self.app.logger.error('Printer has no more paper, aborting...')
|
||||
flash("No more paper on the printer. Sorry.",category='error')
|
||||
self.app.logger.error("Printer has no more paper, aborting...")
|
||||
self.printer.close()
|
||||
return False
|
||||
raise RuntimeError("No more paper in the printer")
|
||||
case 1:
|
||||
self.app.logger.warning('Printer needs paper to be changed very soon ! ')
|
||||
flash('Printer needs paper to be changed very soon ! ', category='info')
|
||||
self.app.logger.warning(
|
||||
"Printer needs paper to be changed very soon ! "
|
||||
)
|
||||
self.printer.close()
|
||||
return True
|
||||
case 2:
|
||||
self.app.logger.debug('Printer has paper, good to go')
|
||||
self.app.logger.debug("Printer has paper, good to go")
|
||||
self.printer.close()
|
||||
return True
|
||||
|
||||
def init_printer(self):
|
||||
"""
|
||||
Check if the printer online ? Is the communication with the printer successfull ?
|
||||
"""
|
||||
|
||||
# Is the printer online ? Is the communication with the printer successfull ?
|
||||
waiting_elapsed = 30
|
||||
self.app.logger.debug('Waiting for printer to get online...')
|
||||
# TODO: This could happen directly when creating a new Printer class
|
||||
if os.getenv("FLASK_DEBUG"):
|
||||
waiting_elapsed = 3
|
||||
else:
|
||||
waiting_elapsed = 10
|
||||
|
||||
self.app.logger.debug("Waiting for printer to get online...")
|
||||
|
||||
while not self.ready:
|
||||
try:
|
||||
# This also calls open(), which we need to close()
|
||||
# or else the device will appear as busy.
|
||||
p = Usb(self.device_id, self.vendor_id, 0, profile="TM-P80")
|
||||
except USBNotFoundError as e:
|
||||
self.app.logger.error("The USB device is not plugged in, trying again : " + str(e))
|
||||
pass
|
||||
p = escpos.printer.Usb(
|
||||
self.device_id, self.vendor_id, 0, profile="TM-P80"
|
||||
)
|
||||
except RuntimeError as e:
|
||||
self.app.logger.error(
|
||||
"The USB device is not plugged in, trying again %s : %s",
|
||||
waiting_elapsed,
|
||||
str(e),
|
||||
)
|
||||
|
||||
try:
|
||||
if p.is_online():
|
||||
self.ready = True
|
||||
self.app.logger.debug('Printer online !')
|
||||
except Exception as e:
|
||||
pass
|
||||
self.app.logger.debug("Printer online !")
|
||||
except RuntimeError as e:
|
||||
self.app.logger.error(
|
||||
"Error while getting the printer online %s : %s",
|
||||
waiting_elapsed,
|
||||
str(e),
|
||||
)
|
||||
|
||||
sleep(1)
|
||||
waiting_elapsed -= 1
|
||||
if waiting_elapsed < 1:
|
||||
self.app.logger.error('Printer took more than 30 seconds to get online, aborting...')
|
||||
waiting_elapsed = 30 # Reset the waiting time for the next print.
|
||||
self.app.logger.error(
|
||||
"Printer took more than 30 seconds to get online, aborting..."
|
||||
)
|
||||
waiting_elapsed = 1 # Reset the waiting time for the next print.
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
# Setting up the printing options.
|
||||
p.set(align='center', font='a', bold=False, underline=0, width=1, height=1, density=9, invert=False, smooth=False, flip=False, double_width=False, double_height=False, custom_size=False)
|
||||
p.set(
|
||||
align="center",
|
||||
font="a",
|
||||
bold=False,
|
||||
underline=0,
|
||||
width=1,
|
||||
height=1,
|
||||
density=9,
|
||||
invert=False,
|
||||
smooth=False,
|
||||
flip=False,
|
||||
double_width=False,
|
||||
double_height=False,
|
||||
custom_size=False,
|
||||
)
|
||||
# Beware : if we print every time the printer becomes ready, it means
|
||||
# we are printing before and after every print !
|
||||
self.printer = p
|
||||
self.ready = True;
|
||||
self.printer.close();
|
||||
self.ready = True
|
||||
self.printer.close()
|
||||
|
||||
if not self.check_paper():
|
||||
return False
|
||||
self.check_paper()
|
||||
|
||||
return True
|
||||
|
||||
def print_sms(self, msg, signature) -> bool:
|
||||
clean_msg = str(msg)
|
||||
def _print_sms(self, msg, signature="", bold=False):
|
||||
|
||||
if not isinstance(msg, str):
|
||||
self.app.logger.error(
|
||||
"It is not possible to print a " + str(type(msg)) + ", only strings."
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
# We make sure that the signature is not something too goofy
|
||||
clean_msg = str(msg) + "\n"
|
||||
clean_signature = str(signature)
|
||||
|
||||
if not self.check_paper():
|
||||
return False
|
||||
# Make checks on the size of the message being printed
|
||||
if len(clean_msg) > 4096:
|
||||
self.app.logger.warning(
|
||||
"Could not print message of this length: " + str(len(clean_msg))
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Could not print message of this length :"
|
||||
+ str(len(clean_msg))
|
||||
+ ", needs to be below 4096 caracters long."
|
||||
)
|
||||
|
||||
if len(clean_msg) > 256 or len(clean_msg) < 3 :
|
||||
self.app.logger.warning("Could not print message of this length: " + str(len(clean_msg)))
|
||||
flash("Could not print message of this length :" + str(len(clean_msg)) + ", needs to between 3 and 256 caracters long.",category='error')
|
||||
return False
|
||||
if len(signature) > 256:
|
||||
self.app.logger.warning(
|
||||
"Could not print signature of this length: " + str(len(clean_signature))
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Could not print signature of this length :"
|
||||
+ str(len(clean_signature))
|
||||
+ ", needs to be below 256 caracters long."
|
||||
)
|
||||
|
||||
if len(signature) > 256 or len(signature) < 3:
|
||||
self.app.logger.warning("Could not print message without a signature.")
|
||||
flash("Could not print message without a signature.",category='error')
|
||||
return False
|
||||
# Do the actual printing
|
||||
# We would pop the next element in the queue here, if it's a sms type
|
||||
try:
|
||||
self.printer.open(self.usb_args)
|
||||
self.printer.set(align="center", font="a", bold=bold)
|
||||
self.printer.textln(clean_msg)
|
||||
if clean_signature:
|
||||
self.printer.textln(clean_signature)
|
||||
self.printer.close()
|
||||
except Exception as e:
|
||||
self.app.logger.error("Unable to print because : " + str(e))
|
||||
raise RuntimeError(
|
||||
"Unable to print a SMS, the printer couldn't do it."
|
||||
) from e
|
||||
|
||||
if not os.getenv('LIPY_DEBUG') == True:
|
||||
try:
|
||||
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.textln(clean_msg)
|
||||
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.cut()
|
||||
self.printer.close
|
||||
except Exception as e:
|
||||
flash("Unable to print because : " + e)
|
||||
|
||||
flash("Message printed : " + clean_msg ,category='info')
|
||||
self.app.logger.info("Printed text")
|
||||
return True
|
||||
|
||||
def print_img(self, path, sign):
|
||||
clean_signature = str(sign)
|
||||
def _print_img(self, path, signature="", center=True, process=False):
|
||||
clean_signature = str(signature)
|
||||
|
||||
if len(signature) > 256:
|
||||
self.app.logger.warning(
|
||||
"Could not print signature of this length: " + str(len(clean_signature))
|
||||
)
|
||||
raise ValueError(
|
||||
"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)):
|
||||
self.app.logger.warning("File does not exist : " + str(path))
|
||||
flash('The file path for this image :' + str(path) + " wasn't found. Please try again.", 'error')
|
||||
return False
|
||||
raise OSError(
|
||||
"The file path for this image :"
|
||||
+ str(path)
|
||||
+ " wasn't found. Please try again."
|
||||
)
|
||||
|
||||
self.app.logger.debug("Printing file from " + str(path))
|
||||
|
||||
if process:
|
||||
try:
|
||||
self.app.logger.debug("Proccessing the image")
|
||||
path = _process_image(self, path)
|
||||
except RuntimeError as e:
|
||||
self.app.logger.error(
|
||||
"Error while processing the image, aborting print : %s", str(e)
|
||||
)
|
||||
raise e
|
||||
else:
|
||||
self.app.logger.debug("Printing file from " + str(path))
|
||||
|
||||
|
||||
|
||||
try:
|
||||
self.app.logger.debug("Resizing the image")
|
||||
with Image.open(path) as im:
|
||||
|
||||
basewidth = 575
|
||||
img = Image.open(path)
|
||||
wpercent = (basewidth/float(img.size[0]))
|
||||
hsize = int((float(img.size[1])*float(wpercent)))
|
||||
img = img.resize((basewidth,hsize), Image.ANTIALIAS)
|
||||
if img.height > 1000:
|
||||
flash("Image is too long, sorry ! Keep it below 500×1000 pixels.",'error')
|
||||
return False
|
||||
img.save(path)
|
||||
except Exception as e:
|
||||
flash(str(e))
|
||||
self.app.logger.error(str(e))
|
||||
self.app.logger.warning("Not proccessing the image")
|
||||
|
||||
try:
|
||||
self.printer.open(self.usb_args)
|
||||
self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
|
||||
self.printer.image(path)
|
||||
self.printer.cut()
|
||||
self.printer.image(path, center=center)
|
||||
self.printer.textln(signature)
|
||||
self.printer.close()
|
||||
self.app.logger.debug("Printed an image : " + str(path))
|
||||
return True
|
||||
except Exception as e:
|
||||
self.app.logger.error(str(e))
|
||||
raise RuntimeError("Could not print the picture") from e
|
||||
finally:
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
raise e
|
||||
|
||||
self.app.logger.debug("Removed image : " + str(path))
|
||||
|
||||
try:
|
||||
self.printer.close()
|
||||
except Exception as e:
|
||||
self.app.logger.error(
|
||||
"Could not close the printer connexion %s", str(e)
|
||||
)
|
||||
raise RuntimeError("Could not close the printer connexion. ") from e
|
||||
|
||||
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 RuntimeError 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:
|
||||
self.printer.close()
|
||||
flash(str(e),'error')
|
||||
return False
|
||||
self.app.logger.error(str(e))
|
||||
raise e
|
||||
|
||||
self.app.logger.info("Did a cut")
|
||||
return True
|
||||
|
||||
def print_task(self, task_type, data):
|
||||
"""Execute actual print based on task type"""
|
||||
match (task_type.value):
|
||||
case "text":
|
||||
self._print_sms(data["txt"], signature=data["sign"])
|
||||
case "image":
|
||||
self._print_img(
|
||||
data["img"], signature=data["sign"], process=data["process"]
|
||||
)
|
||||
case "cut":
|
||||
self._cut()
|
||||
case _:
|
||||
raise RuntimeError("This task type is not supported")
|
||||
|
||||
|
||||
def _process_image(self, path):
|
||||
brightness_factor = 1.5 # Used only if image is too dark
|
||||
brightness_threshold = 100 # Brightness threshold (0–255)
|
||||
contrast_factor = 0.6 # Less than 1.0 = lower contrast
|
||||
max_width = 575
|
||||
max_height = 1000
|
||||
|
||||
with Image.open(path) as original_img:
|
||||
# Convert to RGB if needed (JPEG doesn't support alpha)
|
||||
if original_img.mode in ("RGBA", "P"):
|
||||
self.app.logger.debug("Converting the image to RGB from RGBA")
|
||||
original_img = original_img.convert("RGB")
|
||||
|
||||
# Resize while maintaining aspect ratio
|
||||
original_img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||
self.app.logger.debug("Resized the image")
|
||||
|
||||
# # Convert to grayscale for dithering
|
||||
# dithered_img = original_img.convert("L").convert("1")
|
||||
# Dithering using default method (Floyd–Steinberg)
|
||||
# self.app.logger.debug("Dithered the image")
|
||||
|
||||
# Compute brightness of original image (grayscale average)
|
||||
grayscale = original_img.convert("L")
|
||||
avg_brightness = np.array(grayscale).mean()
|
||||
self.app.logger.debug(
|
||||
"Average brightness of the image : " + str(avg_brightness)
|
||||
)
|
||||
|
||||
# Dynamically compute brightness factor if too dark
|
||||
if avg_brightness < brightness_threshold:
|
||||
brightness_factor = (
|
||||
1 + (brightness_threshold - avg_brightness) / brightness_threshold
|
||||
)
|
||||
brightness_factor = min(
|
||||
max(brightness_factor, 1.1), 2.5
|
||||
) # Clamp between 1.1 and 2.5
|
||||
self.app.logger.debug(
|
||||
f"Image too dark, increasing brightness by a factor of {brightness_factor:.2f}"
|
||||
)
|
||||
enhancer = ImageEnhance.Brightness(original_img)
|
||||
original_img = enhancer.enhance(brightness_factor)
|
||||
|
||||
# # Reduce contrast
|
||||
# contrast_enhancer = ImageEnhance.Contrast(original_img)
|
||||
# original_img = contrast_enhancer.enhance(contrast_factor)
|
||||
|
||||
# Convert to JPEG and save
|
||||
jpeg_path = os.path.splitext(path)[0] + "_processed.jpg"
|
||||
original_img.save(jpeg_path, format="JPEG", quality=95, optimize=True)
|
||||
self.app.logger.debug("Processed and saved image.")
|
||||
|
||||
return jpeg_path
|
||||
|
||||
273
src/raspberry.py
Normal file
273
src/raspberry.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
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
|
||||
@@ -3,6 +3,36 @@
|
||||
margin-right: 20%;
|
||||
} */
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fullscreen-countdown {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* backdrop-filter: blur(10px); */
|
||||
color: black;
|
||||
box-sizing: initial;
|
||||
|
||||
}
|
||||
|
||||
#countdown-number {
|
||||
font-weight: bolder;
|
||||
font-size: 10em;
|
||||
text-shadow: 0px 0px 3em white, 1px 1px 1px rgba(255, 255, 255, 1);
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/*// Small devices (landscape phones, 576px and up)*/
|
||||
@media (min-width: 576px) {
|
||||
|
||||
|
||||
BIN
src/static/images/extase-club.png
Normal file
BIN
src/static/images/extase-club.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
src/static/images/requin.png
Normal file
BIN
src/static/images/requin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -1,39 +1,159 @@
|
||||
let streaming;
|
||||
var current_stream;
|
||||
var current_camera_is;
|
||||
var supports_facing_mode;
|
||||
var camera_options;
|
||||
|
||||
var width = document.getElementById("video").parentNode.parentElement.clientWidth;
|
||||
var height = width / (4 / 3);
|
||||
var socket = io();
|
||||
|
||||
function startup(){
|
||||
socket.on('connect', function() {
|
||||
socket.emit('ping', {data: 'I\'m connected!'});
|
||||
console.log("Sent a ping to the server :)");
|
||||
});
|
||||
|
||||
|
||||
socket.on('pong', () => {
|
||||
console.log('Received pong back ! ');
|
||||
});
|
||||
|
||||
socket.on('new_image', () => {
|
||||
console.log("Received new image event");
|
||||
const img = document.getElementById('snapshot');
|
||||
img.src = '/image?' + new Date().getTime(); // Bust cache
|
||||
});
|
||||
|
||||
|
||||
async function startup(){
|
||||
video = document.getElementById('video');
|
||||
canvas = document.getElementById('canvas');
|
||||
photo = document.getElementById('photo');
|
||||
switch_cameras = document.getElementById('flip')
|
||||
printButton = document.getElementById('print_button');
|
||||
|
||||
if (check_webcam() === true ){
|
||||
console.log("Checking for client webcam capabilities");
|
||||
// We have a camera_options dictionnary in return, or false if the device does not support webcams.
|
||||
let client_webcam_capabilities = await check_webcam_capabilies();
|
||||
|
||||
|
||||
if ( client_webcam_capabilities != false ){
|
||||
get_webcam(client_webcam_capabilities);
|
||||
setup_events();
|
||||
clear_canvas();
|
||||
} else {
|
||||
no_webcam_error();
|
||||
console.log("Seems like it's impossible to get a webcam.");
|
||||
console.log("Checking for server webcam capabilities");
|
||||
let server_webcam_capabilities = await check_server_camera();
|
||||
if (server_webcam_capabilities === true ){
|
||||
console.log("The server has a camera, using it.");
|
||||
console.log("TODO");
|
||||
}
|
||||
else {
|
||||
no_webcam_error();
|
||||
console.log("Seems like it's impossible to get a webcam from the client nor the server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function check_webcam(){
|
||||
console.log("Cheking for a camera...");
|
||||
if (get_front_webcam()) {
|
||||
console.log("Got front camera !");
|
||||
return true;
|
||||
async function check_webcam_capabilies(){
|
||||
console.log("Checking for a the capabilities of the client's media devices");
|
||||
|
||||
// We try to start with the front facing camera,
|
||||
// if we have no support, we switch back to a normal camera.
|
||||
try {
|
||||
// We first check if the navigator has a media device
|
||||
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||
console.log("enumerateDevices() not supported.");
|
||||
return false;
|
||||
} else {
|
||||
// List cameras and microphones.
|
||||
console.log("The device has the following media devices : ")
|
||||
await navigator.mediaDevices
|
||||
.enumerateDevices()
|
||||
.then((devices) => {
|
||||
devices.forEach((device) => {
|
||||
console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`${err.name}: ${err.message}`);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("The device does not seem to support webcams " + e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (get_any_webcam()) {
|
||||
console.log("Got a webcam !");
|
||||
return true;
|
||||
console.log("Checking for the supported constraints of the media devices ");
|
||||
try {
|
||||
const supports = navigator.mediaDevices.getSupportedConstraints();
|
||||
if (!supports['facingMode']) {
|
||||
throw new Error("This browser does not support facingMode!");
|
||||
} else {
|
||||
console.log("The device supports facing mode, selecting the front camera by default");
|
||||
supports_facing_mode = true;
|
||||
camera_options = {
|
||||
video: {
|
||||
facingMode: 'user', // Or 'environment' if we want a camera facing away
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
return camera_options;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Resetting to default camera : " + e);
|
||||
supports_facing_mode = false;
|
||||
camera_options = {
|
||||
video: true,
|
||||
audio: false
|
||||
};
|
||||
return camera_options
|
||||
}
|
||||
console.log("Nope");
|
||||
return false;
|
||||
}
|
||||
|
||||
async function get_webcam(options){
|
||||
stop_video_streams();
|
||||
|
||||
try {
|
||||
navigator.mediaDevices.getUserMedia(options)
|
||||
.then(function(stream) {
|
||||
// on success, stream it in video tag
|
||||
// the video tag is hidden, as is the canvas.
|
||||
console.log("Got the webcam stream");
|
||||
|
||||
printButton.removeAttribute("disabled","");
|
||||
current_stream = stream;
|
||||
video.srcObject = stream;
|
||||
video.setAttribute('autoplay', '');
|
||||
video.setAttribute('muted', '');
|
||||
video.setAttribute('playsinline', '')
|
||||
video.play();
|
||||
return true;
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Didn't manage to get a camera :" + err);
|
||||
return false;
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Didn't manage to get a camera :" + err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function check_server_camera(){
|
||||
console.log("Checking for a camera on the server");
|
||||
|
||||
await socket.emit('get_camera_status', (response) => {
|
||||
if (response){
|
||||
console.log("Server has a camera !");
|
||||
return true;
|
||||
} else {
|
||||
console.log("The server doesn't seem to have a camera");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function setup_events(){
|
||||
@@ -69,15 +189,15 @@ function setup_events(){
|
||||
flip_cameras();
|
||||
}, false );
|
||||
|
||||
printButton.addEventListener('click', function(ev){
|
||||
data = take_picture();
|
||||
printButton.addEventListener('click', async function(ev) {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await countDownToPicture(); // wait for countdown to finish
|
||||
var data = await take_picture(); // take_picture can be async
|
||||
print_picture(data);
|
||||
} catch (e) {
|
||||
alert("Failed to print a picture because : " + e);
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
}, false);
|
||||
|
||||
}
|
||||
@@ -92,6 +212,10 @@ function clear_canvas() {
|
||||
}
|
||||
|
||||
function dataURLtoFile(dataurl, filename) {
|
||||
if(dataurl === "") {
|
||||
console.log("We didn't receive data");
|
||||
}
|
||||
|
||||
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
|
||||
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||
while(n--){
|
||||
@@ -100,19 +224,18 @@ function dataURLtoFile(dataurl, filename) {
|
||||
return new File([u8arr], filename, {type:mime});
|
||||
}
|
||||
|
||||
function take_picture(){
|
||||
var context = canvas.getContext('2d');
|
||||
async function take_picture() {
|
||||
const context = canvas.getContext('2d');
|
||||
let data = null;
|
||||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
var data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
} else {
|
||||
clear_canvas();
|
||||
clear_canvas();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -124,7 +247,7 @@ function print_picture(data){
|
||||
let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
|
||||
|
||||
formData.set("img", picture, "picture.png");
|
||||
formData.set("signature", "Printed via the webcam @ " + time)
|
||||
formData.set("signature", "Webcam")
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST', // or 'PUT'
|
||||
@@ -132,32 +255,28 @@ function print_picture(data){
|
||||
// headers:{
|
||||
// 'Content-Type': 'multipart/form-data'
|
||||
// }
|
||||
}).then(function(response) { console.log('Success:', response); alert("Picture printed."); } , true)
|
||||
}).then(function(response) {
|
||||
console.log('Reponse:', response);
|
||||
if(response.status != 200 ){
|
||||
alert("The picture could not be printed be : " + response.statusText)
|
||||
}
|
||||
} , true)
|
||||
.catch(error => console.error('Error:', error), false);
|
||||
|
||||
}
|
||||
|
||||
function flip_cameras(){
|
||||
switch (current_camera_is) {
|
||||
case "front":
|
||||
try {
|
||||
get_any_webcam();
|
||||
} catch (e) {
|
||||
console.log("Could not get another camera");
|
||||
get_front_webcam();
|
||||
}
|
||||
break;
|
||||
case "any":
|
||||
try {
|
||||
get_front_webcam();
|
||||
} catch (e) {
|
||||
console.log("Could not get another camera");
|
||||
get_any_webcam();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Impossible to switch cameras : none is selected.");
|
||||
console.log("Current facing mode : " + camera_options.video.facingMode)
|
||||
if ( supports_facing_mode ) {
|
||||
console.log("Support facing mode, trying to switch !");
|
||||
if (camera_options.video.facingMode == 'user'){
|
||||
camera_options = { audio: false, video: { facingMode: "environment" },};
|
||||
} else {
|
||||
camera_options = { audio: false, video: { facingMode: "user" },};
|
||||
}
|
||||
}
|
||||
console.log("Switching to " + camera_options.video.facingMode );
|
||||
get_webcam(camera_options);
|
||||
}
|
||||
|
||||
function stop_video_streams(){
|
||||
@@ -177,81 +296,6 @@ function stop_video_streams(){
|
||||
}
|
||||
}
|
||||
|
||||
async function get_webcam(options){
|
||||
stop_video_streams();
|
||||
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia(options)
|
||||
.then(function(stream) {
|
||||
// on success, stream it in video tag
|
||||
// the video tag is hidden, as is the canvas.
|
||||
console.log("Got a camera ( generic )");
|
||||
|
||||
printButton.removeAttribute("disabled","");
|
||||
current_stream = stream;
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
return true;
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("Didn't manage to get a camera :" + err);
|
||||
return false;
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Didn't manage to get a camera :" + err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function get_any_webcam(){
|
||||
var camera_options = {
|
||||
video: {
|
||||
facingMode: 'environment', // Or 'environment' if we want a camera facing away
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
if(get_webcam(camera_options)){
|
||||
console.log("Got any camera, or environment camera.");
|
||||
current_camera_is = "any";
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function get_front_webcam(){
|
||||
// We try to start with the front facing camera,
|
||||
// if we have no support, we switch back to a normal camera.
|
||||
try {
|
||||
const supports = navigator.mediaDevices.getSupportedConstraints();
|
||||
if (!supports['facingMode']) {
|
||||
throw new Error("This browser does not support facingMode!");
|
||||
} else {
|
||||
var camera_options = {
|
||||
video: {
|
||||
facingMode: 'user', // Or 'environment' if we want a camera facing away
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Resetting to default camera : " + e);
|
||||
var camera_options = {
|
||||
video: true,
|
||||
audio: false
|
||||
};
|
||||
}
|
||||
|
||||
if(get_webcam(camera_options)){
|
||||
console.log("Got the front camera");
|
||||
current_camera_is = "front";
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function no_webcam_error(){
|
||||
console.log("Seems like they is no webcam available.")
|
||||
|
||||
@@ -272,4 +316,33 @@ function no_webcam_error(){
|
||||
throw new Error("Unable to get a video device, stopping the photobooth.");
|
||||
}
|
||||
|
||||
async function countDownToPicture(){
|
||||
return new Promise((resolve) => {
|
||||
console.log("Starting countdown");
|
||||
// We create full-page overlay that displays a number to countdown until the picture is taken.
|
||||
const countdown_div = document.getElementById("countdown");
|
||||
countdown_div.style.display = "block";
|
||||
|
||||
// Set the different styling attributes of the element
|
||||
countdown_div.setAttribute("class", "fullscreen-countdown");
|
||||
|
||||
// The loop must run for 3 seconds
|
||||
interval = 1000;
|
||||
var loops = 3;
|
||||
|
||||
var x = setInterval(() => {
|
||||
console.log(loops);
|
||||
// Update the content of the div
|
||||
document.getElementById("countdown-number").innerHTML = loops;
|
||||
if (loops == 0) {
|
||||
countdown_div.style.display = "none";
|
||||
// The timer has finished
|
||||
clearInterval(x);
|
||||
resolve();
|
||||
}
|
||||
loops--;
|
||||
}, interval);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', startup, false);
|
||||
|
||||
98
src/task.py
Normal file
98
src/task.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Here we define the types of tasks
|
||||
We are using Abstract Base Classes,
|
||||
like this we can define types of tasks ( text, images, ... )
|
||||
that all work with the same basic options
|
||||
|
||||
The tasks are going to be injected into a Queue.
|
||||
It's a usefull way of storing information in our
|
||||
program, while making sure that things are indeed printed.
|
||||
It's also a way to prevent two concurrent connexions creating
|
||||
a access conflict on a single printer, like two people wanting
|
||||
to print at the same time.
|
||||
|
||||
We can also delay and store printing tasks until a printer becomes
|
||||
available if none is online.
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
## See https://docs.python.org/3/library/abc.html to learn more about this
|
||||
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
|
||||
## You can expand this if you want to take other types of tasks into account
|
||||
class TaskType(Enum):
|
||||
"""
|
||||
The different tasks supported by the printers
|
||||
"""
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
CUT = "cut"
|
||||
|
||||
|
||||
class PrintTask(ABC):
|
||||
"""
|
||||
A print task holds information about what we are looking to print.
|
||||
"""
|
||||
|
||||
def __init__(self, task_type):
|
||||
self.task_id = self._generate_id()
|
||||
self.task_type = task_type
|
||||
self.status = "pending" # pending, processing, completed, failed
|
||||
|
||||
print("Created a new " + str(self.task_type) + " with ID " + self.task_id)
|
||||
|
||||
@abstractmethod
|
||||
def get_print_data(self):
|
||||
"""Return data formatted for printer"""
|
||||
|
||||
|
||||
def _generate_id(self):
|
||||
# Generate unique task ID
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class TextTask(PrintTask):
|
||||
"""
|
||||
This tasks represents a texte content, and it's signature.
|
||||
"""
|
||||
|
||||
def __init__(self, content, signature):
|
||||
super().__init__(TaskType.TEXT)
|
||||
self.content = content
|
||||
self.signature = signature
|
||||
|
||||
def get_print_data(self):
|
||||
return {"txt": self.content, "sign": self.signature}
|
||||
|
||||
|
||||
class ImageTask(PrintTask):
|
||||
"""
|
||||
This tasks represents a image content ( in the form of it's path ), and it's signature.
|
||||
"""
|
||||
|
||||
def __init__(self, image_path, signature, process):
|
||||
super().__init__(TaskType.IMAGE)
|
||||
self.image_path = image_path
|
||||
self.signature = signature
|
||||
self.process = process
|
||||
|
||||
def get_print_data(self):
|
||||
# Return image data in printer-compatible format
|
||||
return {"img": self.image_path, "sign": self.signature, "process": self.process}
|
||||
|
||||
|
||||
class CutTask(PrintTask):
|
||||
"""
|
||||
This class activates the cutter on the printer if it exists
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(TaskType.CUT)
|
||||
|
||||
# There is no print data,
|
||||
# the task existence in itself is indication of what to do
|
||||
def get_print_data(self):
|
||||
return None
|
||||
@@ -56,4 +56,6 @@
|
||||
<p class=" text-center">Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter">Source code - AGPLv3</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print a short message</h3>
|
||||
<div class="card-body">
|
||||
<form class="form-group" action="/api/print/sms" method="post">
|
||||
<form class="form-group" action="/web/print/sms" method="post">
|
||||
<input class="form-control" type="text" name="txt" placeholder="200 chars or less " maxlength="200" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200"><br>
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
@@ -18,9 +18,9 @@
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print an image</h3>
|
||||
<div class="card-body">
|
||||
<form enctype="multipart/form-data" class="form-group" action="/api/print/img" method="post">
|
||||
<form enctype="multipart/form-data" class="form-group" action="/web/print/img" method="post">
|
||||
<input class="form-control" type="file" name="img" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200"><br>
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,6 @@
|
||||
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1v6zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2z"/>
|
||||
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
||||
</svg>
|
||||
<b>&</b>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-printer" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"/>
|
||||
<path d="M5 1a2 2 0 0 0-2 2v2H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-1h1a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V3a2 2 0 0 0-2-2H5zM4 3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2H4V3zm1 5a2 2 0 0 0-2 2v1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v-1a2 2 0 0 0-2-2H5zm7 2v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="col-sm col-lg-3 offset-lg-2 btn btn-secondary justify-content-center" name="flip" id="flip" data-bs-toggle="tooltip" data-bs-placement="top" title="Change camera" disabled="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
|
||||
@@ -59,6 +54,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div id="countdown">
|
||||
<div id="countdown-number"></div>
|
||||
</div>
|
||||
|
||||
<style media="screen">
|
||||
canvas {
|
||||
display: none;
|
||||
@@ -104,6 +103,7 @@
|
||||
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static',filename='js/webcam.js') }}"></script>
|
||||
|
||||
<br>
|
||||
|
||||
72
src/user.py
72
src/user.py
@@ -1,39 +1,41 @@
|
||||
class User(object):
|
||||
"""docstring for User."""
|
||||
# class User(object):
|
||||
# """docstring for User."""
|
||||
|
||||
def __init__(self, arg):
|
||||
super(User, self).__init__()
|
||||
self.arg = arg
|
||||
# def __init__(self, arg):
|
||||
# super(User, self).__init__()
|
||||
# self.arg = arg
|
||||
|
||||
# @app.route('/login', methods=['POST','GET'])
|
||||
# @limiter.limit("100 per minute", error_message=error_handler_limiter)
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
if not session.get('logged_in'):
|
||||
if request.form['username'] and request.form['password']:
|
||||
# Get the json
|
||||
with open('users.json') as f:
|
||||
users_file = json.load(f)
|
||||
for user in users_file["users"]:
|
||||
if users_file["users"][user] == request.form['password']:
|
||||
session['logged_in'] = True
|
||||
session['user'] = request.form['username']
|
||||
|
||||
if not session.get('logged_in'):
|
||||
flash('Mot de passe ou pseudo invalide.','danger')
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return redirect(url_for('display_index_page'))
|
||||
else:
|
||||
flash('Incorrect logins')
|
||||
return render_template('password.html')
|
||||
else:
|
||||
return render_template('password.html')
|
||||
else:
|
||||
return render_template('password.html')
|
||||
# # @app.route('/login', methods=['POST','GET'])
|
||||
# # @limiter.limit("100 per minute", error_message=error_handler_limiter)
|
||||
# def login():
|
||||
# if request.method == "POST":
|
||||
# if not session.get("logged_in"):
|
||||
# if request.form["username"] and request.form["password"]:
|
||||
# # Get the json
|
||||
# with open("users.json") as f:
|
||||
# users_file = json.load(f)
|
||||
# for user in users_file["users"]:
|
||||
# if users_file["users"][user] == request.form["password"]:
|
||||
# session["logged_in"] = True
|
||||
# session["user"] = request.form["username"]
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session['logged_in'] = False
|
||||
flash('Tu est déconnecté', 'info')
|
||||
return redirect(url_for('login'))
|
||||
# if not session.get("logged_in"):
|
||||
# flash("Mot de passe ou pseudo invalide.", "danger")
|
||||
# return redirect(url_for("login"))
|
||||
# else:
|
||||
# return redirect(url_for("display_index_page"))
|
||||
# else:
|
||||
# flash("Incorrect logins")
|
||||
# return render_template("password.html")
|
||||
# else:
|
||||
# return render_template("password.html")
|
||||
# else:
|
||||
# return render_template("password.html")
|
||||
|
||||
|
||||
# @app.route("/logout")
|
||||
# def logout():
|
||||
# session["logged_in"] = False
|
||||
# flash("Tu est déconnecté", "info")
|
||||
# return redirect(url_for("login"))
|
||||
|
||||
133
src/web.py
133
src/web.py
@@ -1,68 +1,115 @@
|
||||
from flask import Flask, request
|
||||
from flask import flash
|
||||
from werkzeug.utils import secure_filename
|
||||
from printer import Printer
|
||||
import time
|
||||
import os
|
||||
from task import TextTask, ImageTask, CutTask
|
||||
|
||||
|
||||
class Web(object):
|
||||
"""docstring for web."""
|
||||
"""Web is the class that gets all of the information from web calls
|
||||
( API and Web page ) and provides checks before sending stuff to printing"""
|
||||
|
||||
def __init__(self, app, printer ):
|
||||
def __init__(self, app, print_queue):
|
||||
super(Web).__init__()
|
||||
self.printer = printer
|
||||
self.print_queue = print_queue
|
||||
self.app = app
|
||||
|
||||
def print_sms(self, texte, sign: str):
|
||||
# TODO: verify the texte before printing it here ?
|
||||
def print_sms(self, texte, sign: str) -> bool:
|
||||
"""
|
||||
Get text and a signature, prints the text and cuts after that.
|
||||
"""
|
||||
self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign))
|
||||
if not os.getenv('LIPY_DEBUG'):
|
||||
time.sleep(1)
|
||||
|
||||
return self.printer.print_sms(texte, sign)
|
||||
|
||||
def print_image(self, image, sign: str) -> bool:
|
||||
self.app.logger.debug("Uploading file")
|
||||
try:
|
||||
self.app.logger.debug("Uploading file from " + str(sign))
|
||||
if self.upload_file(image):
|
||||
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)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# We create two new tasks and add them directly to the queue
|
||||
# TODO: this might need to be improved because
|
||||
# !! there is no garantee !! that both the SMS task and the Cut task
|
||||
# are added back to back, another task could be
|
||||
# inserted between the two.
|
||||
sms = self.print_queue.enqueue(TextTask(content=texte, signature=sign))
|
||||
cut = self.print_queue.enqueue(CutTask())
|
||||
except Exception as e:
|
||||
self.app.logger.error(e)
|
||||
raise Exception
|
||||
raise RuntimeError("Could not add SMS to queue, " + str(e)) from e
|
||||
self.app.logger.info("Added two new tasks at position %s and %s", sms, cut)
|
||||
return True
|
||||
|
||||
else:
|
||||
flash("Could not upload file.",'error')
|
||||
return False
|
||||
def print_image(self, image, sign: str) -> bool:
|
||||
"""
|
||||
Get an image and a signature, prints the image and cuts after that.
|
||||
"""
|
||||
try:
|
||||
file_uploaded = self.upload_file(image)
|
||||
except Exception as e:
|
||||
self.app.logger.error(e)
|
||||
raise RuntimeError("Could not upload file") from e
|
||||
|
||||
def login(username: str,password: str) -> bool:
|
||||
pass
|
||||
if file_uploaded:
|
||||
self.app.logger.debug("File has been uploaded, printing...")
|
||||
try:
|
||||
img = self.print_queue.enqueue(
|
||||
ImageTask(
|
||||
os.path.join(
|
||||
self.app.config["UPLOAD_FOLDER"],
|
||||
secure_filename(image.filename),
|
||||
),
|
||||
signature=sign,
|
||||
process=True,
|
||||
)
|
||||
)
|
||||
|
||||
def logout(username: str, password: str) -> bool:
|
||||
pass
|
||||
cut = self.print_queue.enqueue(CutTask())
|
||||
except Exception as e:
|
||||
raise RuntimeError("Could not add IMG to queue" + str(e)) from e
|
||||
|
||||
self.app.logger.info("Added two new tasks at position %s and %s", img, cut)
|
||||
|
||||
return True
|
||||
|
||||
def login(self, username: str, password: str) -> bool:
|
||||
"""Not implemented"""
|
||||
return
|
||||
|
||||
def logout(self, username: str, password: str) -> bool:
|
||||
"""Not implemented"""
|
||||
return
|
||||
|
||||
def allowed_file(self, filename) -> bool:
|
||||
self.app.logger.debug("Is the filename allowed ?")
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in self.app.config['ALLOWED_EXTENSIONS']
|
||||
return (
|
||||
"." in filename
|
||||
and filename.rsplit(".", 1)[1].lower()
|
||||
in self.app.config["ALLOWED_EXTENSIONS"]
|
||||
)
|
||||
|
||||
def upload_file(self, image)-> bool:
|
||||
def upload_file(self, image) -> bool:
|
||||
self.app.logger.debug("Validating file")
|
||||
if image and self.allowed_file(image.filename):
|
||||
filename = secure_filename(image.filename)
|
||||
self.app.logger.debug("File valid")
|
||||
try:
|
||||
image.save(os.path.join(self.app.config['UPLOAD_FOLDER'], filename))
|
||||
self.app.logger.debug("File saved")
|
||||
except Exception as e:
|
||||
self.app.logger.error("Could not save file")
|
||||
flash(e,'error')
|
||||
return False
|
||||
if image:
|
||||
if self.allowed_file(image.filename):
|
||||
filename = secure_filename(image.filename)
|
||||
self.app.logger.debug("File valid")
|
||||
try:
|
||||
image.save(os.path.join(self.app.config["UPLOAD_FOLDER"], filename))
|
||||
except OSError as e:
|
||||
self.app.logger.error("Could not save file %s", e)
|
||||
return False
|
||||
|
||||
self.app.logger.debug("File saved to " + str(os.path.join(self.app.config['UPLOAD_FOLDER'], filename)))
|
||||
return True
|
||||
self.app.logger.debug(
|
||||
"File saved to "
|
||||
+ str(os.path.join(self.app.config["UPLOAD_FOLDER"], filename))
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self.app.logger.error(
|
||||
"Could not save file because the filename is forbidden"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
self.app.logger.error("Could not save file " + str(filename))
|
||||
self.app.logger.error(
|
||||
"Could not save file, it seems to be null ? : " + str(filename)
|
||||
)
|
||||
return False
|
||||
|
||||
def get_queue_state(self):
|
||||
"""Return current queue state"""
|
||||
return self.print_queue.get_queue_state()
|
||||
|
||||
107
src/worker.py
Normal file
107
src/worker.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# This is the main printing thread
|
||||
# As explained in the task file, this is where we command
|
||||
# printing to happen.
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class PrintWorker(threading.Thread):
|
||||
def __init__(self, app, print_queue, printer, socketio=None):
|
||||
super().__init__(daemon=True)
|
||||
self.app = app
|
||||
self.print_queue = print_queue
|
||||
self.printer = printer
|
||||
self.socketio = socketio # Optional
|
||||
self.running = True
|
||||
self.state = "idle" # idle, printing, dead, drinking-a-beer
|
||||
|
||||
self.app.logger.debug("Ho great, I'm alive... I'm ready to work another day...")
|
||||
|
||||
def run(self):
|
||||
"""Background thread that processes queue items"""
|
||||
self.app.logger.info("Worker started working.")
|
||||
while True:
|
||||
if not self.running or not self.printer.ready:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
try:
|
||||
task = self.print_queue.dequeue()
|
||||
except Exception as e:
|
||||
self.app.logger.error("Could not get a new task ! %s ", str(e))
|
||||
raise RuntimeError(
|
||||
"We could not get a new task because " + str(e)
|
||||
) from e
|
||||
|
||||
if task:
|
||||
try:
|
||||
self.app.logger.info("Got a new task")
|
||||
self.app.logger.debug("Got task %s", task.task_id)
|
||||
self.state = "printing"
|
||||
task.status = "processing"
|
||||
self._emit_status(task.task_id, "processing")
|
||||
|
||||
print_data = task.get_print_data()
|
||||
try:
|
||||
self.printer.print_task(task.task_type, print_data)
|
||||
except RuntimeError as e:
|
||||
self.app.logger.error("Could not print : %s", str(e))
|
||||
raise e
|
||||
|
||||
task.status = "completed"
|
||||
self.print_queue.mark_completed(task.task_id, "completed")
|
||||
self._emit_status(task.task_id, "completed")
|
||||
|
||||
except RuntimeError as e:
|
||||
task.status = "failed"
|
||||
self.print_queue.mark_completed(task.task_id, "failed")
|
||||
self._emit_status(task.task_id, "failed", error=str(e))
|
||||
print(f"Print task {task.task_id} failed: {e}")
|
||||
else:
|
||||
# When they are no new tasks to handle, we put the thread to sleep.
|
||||
self.state = "idle"
|
||||
time.sleep(0.1)
|
||||
|
||||
def _emit_status(self, task_id, status, error=None):
|
||||
"""Emit status update via Socket.IO if available"""
|
||||
if not self.socketio:
|
||||
return
|
||||
|
||||
room = f"task_{task_id}"
|
||||
data = {
|
||||
"task_id": task_id,
|
||||
"status": status,
|
||||
"position": None, # Task no longer in queue
|
||||
}
|
||||
|
||||
if error:
|
||||
data["error"] = error
|
||||
|
||||
self.socketio.emit("task_status", data, room=room)
|
||||
|
||||
def stop_worker(self):
|
||||
"""
|
||||
Give the worker a break
|
||||
"""
|
||||
self.app.logger.debug("Giving the worker a break")
|
||||
self.state = "drinking-a-beer"
|
||||
self.running = False
|
||||
|
||||
def start_worker(self):
|
||||
"""
|
||||
Get the worker back to it
|
||||
"""
|
||||
self.app.logger.debug("Time to work !")
|
||||
self.state = "idle"
|
||||
self.running = True
|
||||
|
||||
def current_state(self):
|
||||
"""
|
||||
Return the worker state
|
||||
"""
|
||||
return {
|
||||
"is_running": self.running,
|
||||
"queue_size": len(self.print_queue),
|
||||
"state": self.state,
|
||||
}
|
||||
Reference in New Issue
Block a user