initial commit
This commit is contained in:
parent
bd4e69d0c3
commit
05f06f7817
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
venv
|
BIN
Eyoyo EY-009P Barcode Scanner.pdf
Normal file
BIN
Eyoyo EY-009P Barcode Scanner.pdf
Normal file
Binary file not shown.
BIN
Eyoyo EY-009P YH1705 Handbuch.pdf
Normal file
BIN
Eyoyo EY-009P YH1705 Handbuch.pdf
Normal file
Binary file not shown.
5
LICENSE.md
Normal file
5
LICENSE.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
184
README.md
184
README.md
@ -1,3 +1,183 @@
|
|||||||
# kugelstossmeeting-ticketing
|
# Scanner Service und Frontend Software (Kugelstoßmeeting Rochlitz)
|
||||||
|
- Geschrieben von Mario Voigt (2024 - 2025)
|
||||||
|
- License: MIT
|
||||||
|
|
||||||
https://kugelstossmeeting-rochlitz.de
|
## WARNUNG: UGLY SOFTWARE. Das ist ein "works for me" Projekt
|
||||||
|
|
||||||
|
## Konzept
|
||||||
|
- ein eigenständiger Scan-Service läuft permanent und speichert alle gescannten Barcodes, die von einem Eyoyo Barcode Scanner per USB-Anschluss kommen, in eine SQLite DB ab.
|
||||||
|
- der Scan-Service (usb-scanner.py) basiert auf https://github.com/vpatron/barcode_scanner_python/tree/master
|
||||||
|
- läuft nur, wenn der Scanner angeschlossen und aktiv ist. Anderfalls gibt es Fehlermeldungen
|
||||||
|
- wird als systemd Service installiert
|
||||||
|
- ein separat gestartetes Frontend (GUI) greift auf diese Datenbank zu und prüft die Eingaben bzw. reichert sie an
|
||||||
|
- Frontend (sqlite.py) basiert auf https://github.com/tonypdavis/Raspberry-Pi-Barcode-Scanner-/blob/master/ipad_bs_v1.2.py
|
||||||
|
- ruft die gleiche Datenbank auf und liest/schreibt Änderungen
|
||||||
|
|
||||||
|
**Achtung: Frontend und Scan-Service müssen auf die gleiche Datenbankdatei konfiguriert werden!**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Siehe auch https://pypi.org/project/cysystemd/
|
||||||
|
|
||||||
|
Fedora:
|
||||||
|
```
|
||||||
|
dnf install -y systemd-devel python3-venv
|
||||||
|
sudo usermod -G dialout -a $USER #den aktuellen User zu dialout hinzufügen
|
||||||
|
```
|
||||||
|
Ubuntu:
|
||||||
|
```
|
||||||
|
apt install build-essential libsystemd-dev systemd-dev python3-venv
|
||||||
|
sudo usermod -G dialout -a $USER #den aktuellen User zu dialout hinzufügen
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /opt/
|
||||||
|
git clone https://gitea.fablabchemnitz.de/vmario/kugelstossmeeting-ticketing.git
|
||||||
|
cd kugelstossmeeting-ticketing/
|
||||||
|
python3 -m venv venv
|
||||||
|
venv/bin/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## USB Scanner betriebsbereit machen
|
||||||
|
|
||||||
|
USB-Geräte anzeigen und nach Scanner prüfen:
|
||||||
|
```
|
||||||
|
lsusb -v
|
||||||
|
#idVendor 0x0581 Racal Data Group
|
||||||
|
#idProduct 0x0115 Tera 5100
|
||||||
|
lsusb
|
||||||
|
#Bus 003 Device 003: ID 0581:0115 Racal Data Group Tera 5100
|
||||||
|
```
|
||||||
|
|
||||||
|
USB-Gerät Berechtigungen anpassen:
|
||||||
|
```
|
||||||
|
vim /etc/udev/rules.d/55-barcode-scanner.rules
|
||||||
|
```
|
||||||
|
```
|
||||||
|
# Set permissions to let anyone use barcode scanner
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="0581", ATTR{idProduct}=="0115", MODE="666"
|
||||||
|
```
|
||||||
|
|
||||||
|
Änderungen übernehmen:
|
||||||
|
```
|
||||||
|
udevadm control --reload-rules && udevadm trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
Dienst starten und prüfen:
|
||||||
|
```
|
||||||
|
ln -sf /etc/systemd/system/usb-scanner.service /opt/kugelstossmeeting-ticketing/usb-scanner.service
|
||||||
|
systemctl enable usb-scanner.service --now
|
||||||
|
journalctl -f -u usb-scanner.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Codes scannen
|
||||||
|
Folgende Codes scannen wir ein, um den Scanner korrekt zu konfigurieren:
|
||||||
|

|
||||||
|
|
||||||
|
**Der Scanner kann zurückgesetzt werden, indem wir ihn per QR Code ausschalten und vom USB-Port trennen.**
|
||||||
|
|
||||||
|
## Datenbankoperationen
|
||||||
|
|
||||||
|
**SQLite installieren:**
|
||||||
|
|
||||||
|
Fedora:
|
||||||
|
```
|
||||||
|
sudo dnf install sqlite3
|
||||||
|
```
|
||||||
|
Ubuntu:
|
||||||
|
```
|
||||||
|
sudo apt install sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Datenbank per Shell leeren:**
|
||||||
|
|
||||||
|
```
|
||||||
|
sqlite3 kugelstossmeeting-prod.db "DELETE FROM tickets;"
|
||||||
|
sqlite3 kugelstossmeeting-prod.db "DELETE FROM scans;"
|
||||||
|
sqlite3 kugelstossmeeting-prod.db "VACUUM;"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Datenbank per DB Browser for SQLite bearbeiten/ansehen:**
|
||||||
|
|
||||||
|
Fedora:
|
||||||
|
```
|
||||||
|
sudo dnf install sqlitebrowser
|
||||||
|
```
|
||||||
|
Ubuntu:
|
||||||
|
```
|
||||||
|
sudo apt install sqlitebrowser
|
||||||
|
```
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Datenbankstruktur erzeugen:**
|
||||||
|
```
|
||||||
|
CREATE TABLE "tickets" (
|
||||||
|
"BarcodeContent" TEXT,
|
||||||
|
"Nr" INTEGER,
|
||||||
|
"VIP" TEXT,
|
||||||
|
"Ticketbeschriftung" INTEGER,
|
||||||
|
"BarcodeFile" TEXT,
|
||||||
|
"Medium" TEXT,
|
||||||
|
"Aushändigung" TEXT,
|
||||||
|
"Bestellnummer/Verwendungszweck" TEXT,
|
||||||
|
"Bezahlt am" TEXT,
|
||||||
|
"Hinweise" TEXT,
|
||||||
|
"Besteller" TEXT,
|
||||||
|
"Typ" TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
```
|
||||||
|
CREATE TABLE "scans" (
|
||||||
|
"unixtimestamp_created" INTEGER NOT NULL,
|
||||||
|
"barcode" TEXT NOT NULL,
|
||||||
|
"validated" INTEGER,
|
||||||
|
"hint" TEXT,
|
||||||
|
"unixtimestamp_checked" INTEGER,
|
||||||
|
"skipped" INTEGER
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Datenbankstruktur ändern (Spalten):**
|
||||||
|
|
||||||
|
Die Reihenfolge der Spalten in der Tabelle ist entscheidend, da diese statisch im Quellcode referenziert wird. Anpassungen (Löschen) zum Beispiel wie folgt:
|
||||||
|
```
|
||||||
|
ALTER TABLE tickets DROP COLUMN 'BarcodeFile';
|
||||||
|
ALTER TABLE tickets DROP COLUMN 'Ticketbeschriftung';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test-Scan mit Pseudo-Barcode und Zeitstempel erzeugen:**
|
||||||
|
|
||||||
|
```
|
||||||
|
INSERT
|
||||||
|
INTO scans(unixtimestamp_created,barcode,validated,hint,unixtimestamp_checked,skipped)
|
||||||
|
VALUES(CAST(unixepoch('now') AS FLOAT)*1e9,'12345',0,'None',CAST(unixepoch('now') AS FLOAT)*1e9,0)
|
||||||
|
;
|
||||||
|
```
|
||||||
|
|
||||||
|
## GUI starten
|
||||||
|
```
|
||||||
|
/opt/kugelstossmeeting-ticketing/venv/bin/python3 /opt/kugelstossmeeting-ticketing/sqlite.py
|
||||||
|
```
|
||||||
|
|
||||||
|
... oder
|
||||||
|
```
|
||||||
|
/opt/kugelstossmeeting-ticketing/sqlite.py #siehe python3 Header
|
||||||
|
```
|
||||||
|
|
||||||
|
... oder die .desktop-Verknüpfung benutzen
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Notwendig für den Betrieb
|
||||||
|
- Notebook mit Netzteil
|
||||||
|
- USB Scanner mit USB-C Kabel
|
||||||
|
- Stempel + Stempelfarbe
|
||||||
|
- Alle Tickets als PDF-Backup (falls es nicht klappt)
|
||||||
|
- eine aktuell befüllte SQ-Lite Datenbank
|
||||||
|
|
||||||
|
## Mögliche Verbesserungen der Software
|
||||||
|
- SQLite DB gegen netzwerkfähige MySQL/PGSQL tauschen und multimandantenfähig machen (2 Rechner, 2 Barcode Scanner)
|
||||||
|
- Programm so beschränken, dass es nicht mehrfach gestartet werden kann (PID Kontrolle o.ä.)
|
||||||
|
- siehe Code-Kommentare
|
||||||
|
- TK GUI modernisieren
|
||||||
|
9
hooks/pre-commit
Executable file
9
hooks/pre-commit
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script zum Leeren der Datenbank vor dem Commit - aus Datenschutzgründen
|
||||||
|
DB="kugelstossmeeting-prod.db"
|
||||||
|
sqlite3 $DB "DELETE FROM tickets;"
|
||||||
|
sqlite3 $DB "DELETE FROM scans;"
|
||||||
|
sqlite3 $DB "VACUUM;"
|
||||||
|
|
||||||
|
echo ">>> pre-commit hook: $DB geleert!"
|
44
icon.svg
Normal file
44
icon.svg
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512.00005 512.00005"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="icon.svg"
|
||||||
|
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
|
||||||
|
inkscape:export-filename="icon.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="false"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#ffffff"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.90175016"
|
||||||
|
inkscape:cx="-46.021617"
|
||||||
|
inkscape:cy="266.70359"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1008"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4"
|
||||||
|
inkscape:document-units="px" />
|
||||||
|
<path
|
||||||
|
id="text1"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.6096px;line-height:1.15;font-family:Helvetica-Condensed-Black-Se;-inkscape-font-specification:'Helvetica-Condensed-Black-Se, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:3.66128"
|
||||||
|
d="M 255.99642,0 C 397.46327,0 512,114.5296 512,255.99642 512,397.46327 397.46327,512 255.99642,512 114.5296,512 0,397.46327 0,255.99642 0,114.5296 114.5296,0 255.99642,0 Z m 0,10.376012 c -135.61224,0 -245.620408,110.008168 -245.620408,245.620408 0,135.61224 110.008168,245.62759 245.620408,245.62759 135.61224,0 245.62759,-110.01535 245.62759,-245.62759 0,-135.61224 -110.01535,-245.620408 -245.62759,-245.620408 z m 0,6.914957 c 37.57351,0 73.09522,8.702382 104.71118,24.155865 -16.34527,0.648061 -30.01649,10.21794 -43.82808,17.948857 -17.71475,5.909086 -32.56875,17.333516 -50.39267,22.975962 -17.40764,7.045727 -36.78951,8.576204 -52.6166,19.564967 -19.44459,10.96477 -23.94855,-8.743304 -28.80398,-23.40502 -6.15009,-22.250293 -32.82102,-17.549499 -43.93536,-2.6673 -9.36674,16.249905 5.62615,32.70821 15.64625,44.54319 -8.5639,9.6097 -27.33376,21.47859 -36.51269,35.60452 -11.28042,9.5916 -26.872467,21.78694 -25.514546,37.90712 12.701206,20.98422 40.833746,1.94549 56.120566,-6.75048 13.6099,-14.55091 26.42621,13.87527 30.97786,24.89241 5.59134,25.48546 -4.34744,51.22858 -9.72528,75.96441 -4.5263,27.63202 -22.19098,50.02404 -32.82995,75.27076 -8.91215,14.30207 -3.04529,37.08251 -15.76782,47.58951 -3.28793,9.30961 -13.6076,22.45031 -14.1088,33.4664 C 53.367895,400.67886 17.290969,332.58685 17.290969,255.99642 17.290969,124.12345 124.12345,17.290969 255.99642,17.290969 Z M 376.51833,49.95645 c 70.69576,41.430105 118.18355,118.14833 118.18355,206.03997 0,77.12655 -36.576,145.64499 -93.29827,189.27818 -0.67133,-6.07644 -8.92785,-14.48373 -17.04069,-15.99664 -22.05897,-10.01093 -29.66993,-31.58154 -31.34972,-54.5545 -5.62742,-19.8961 -7.65724,-42.38935 -21.30978,-58.79503 -15.20922,-22.41272 -30.08928,-45.19511 -44.66478,-68.06978 -11.36419,-13.72578 -22.0614,-27.74956 -21.96768,-46.61696 -0.22802,-18.46404 -11.51917,-33.50548 -14.72378,-51.09345 -0.24878,-11.19332 -13.73054,-29.6126 4.74107,-31.76447 13.01638,-5.14944 24.01956,-9.41492 36.67716,-14.25896 11.98855,-6.472944 22.25326,-16.26269 32.05051,-25.686173 10.52724,-16.885548 30.89066,-10.234284 47.12467,-15.488931 6.40237,-0.779801 7.47707,-7.242718 5.57774,-12.993256 z M 170.3997,129.05297 c 7.57145,-10e-4 13.70964,6.1369 13.70835,13.70835 0.001,7.57145 -6.1369,13.70965 -13.70835,13.70835 -7.56866,-0.003 -13.7025,-6.13969 -13.7012,-13.70835 -0.001,-7.56865 6.13254,-13.70569 13.7012,-13.70835 z m 64.0724,151.97889 c 18.39114,3.37109 25.88867,24.25284 41.88304,32.71554 10.93828,13.69484 34.83266,16.36416 36.32676,36.17659 4.50725,17.84513 9.68013,35.38815 22.94736,48.87667 11.03964,12.77816 4.23054,29.65366 7.29396,44.35011 17.19129,-3.7195 29.95474,8.45239 46.34522,8.30938 1.26681,0.14975 2.3768,0.19332 3.42531,0.20027 -38.72232,27.09794 -85.82834,43.0415 -136.69733,43.0415 -48.93847,0 -94.40976,-14.73581 -132.27089,-39.98089 3.21304,-0.2043 6.23632,-1.00934 8.48101,-3.3538 4.3738,-14.98189 11.8278,-27.17135 14.88825,-42.26918 25.27634,-45.03079 50.20496,-91.47947 87.37731,-128.06616 z"
|
||||||
|
sodipodi:nodetypes="ssssssssssscccccccccccccccsscsccccccccccccccccccccccccscccc" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
BIN
kugelstossmeeting-prod.db
Normal file
BIN
kugelstossmeeting-prod.db
Normal file
Binary file not shown.
7
kugelstossmeeting-ticket-scanner.desktop
Normal file
7
kugelstossmeeting-ticket-scanner.desktop
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Exec=/opt/kugelstossmeeting-ticketing/venv/bin/python3 /opt/kugelstossmeeting-ticketing/sqlite.py
|
||||||
|
Icon=/opt/kugelstossmeeting-ticketing/icon.svg
|
||||||
|
Type=Application
|
||||||
|
Name=kugelstossmeeting Ticket Scanner
|
||||||
|
GenericName=kugelstossmeeting Ticket Scanner
|
||||||
|
Categories=Utility;
|
BIN
qrcodes.png
Normal file
BIN
qrcodes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pyusb
|
||||||
|
systemd
|
||||||
|
pytz
|
388
sqlite.py
Executable file
388
sqlite.py
Executable file
@ -0,0 +1,388 @@
|
|||||||
|
#!/opt/kugelstossmeeting-ticketing/venv/bin/python3
|
||||||
|
'''
|
||||||
|
TODOS
|
||||||
|
- UI aufhübschen
|
||||||
|
- Suchfunktion einbauen
|
||||||
|
- usbscanner.py journal log automatisch öffnen per Button (soll separate Shell öffnen)
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from sqlite3 import Error
|
||||||
|
from contextlib import closing
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import pytz
|
||||||
|
import tkinter as tk
|
||||||
|
#from tkinter import *
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
import tkinter.font as font
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
db_file = os.path.join(BASE_DIR, "kugelstossmeeting-prod.db")
|
||||||
|
if os.path.isfile(db_file) is False:
|
||||||
|
error = "Error: Datenbankdatei '{}' konnte nicht gefunden werden!".format(db_file)
|
||||||
|
print(error)
|
||||||
|
messagebox.showerror("Error", error)
|
||||||
|
exit(1)
|
||||||
|
if os.path.getsize(db_file) == 0:
|
||||||
|
error = "Error: Datenbankdatei '{}' ist 0 Byte groß!".format(db_file)
|
||||||
|
print(error)
|
||||||
|
messagebox.showerror("Error", error)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
title = "Kugelstoßmeeting Rochlitz"
|
||||||
|
|
||||||
|
timeoffset = 20 #seconds
|
||||||
|
autoscroll = True
|
||||||
|
lastSelectedTicket = None #das letzte manuell ausgewählte. Müssen wir merken, sonst wird es alle 2 Sekunden deselektiert
|
||||||
|
|
||||||
|
window = tk.Tk()
|
||||||
|
#window.geometry('800x600+0+0')
|
||||||
|
window.attributes('-zoomed', True)
|
||||||
|
window.title(title)
|
||||||
|
window.iconphoto(False, tk.PhotoImage(file=os.path.join(BASE_DIR, "icon.png")))
|
||||||
|
|
||||||
|
currentScan = None
|
||||||
|
|
||||||
|
def treeview_sort_column(tv, col, reverse):
|
||||||
|
l = [(tv.set(k, col), k) for k in tv.get_children('')]
|
||||||
|
l.sort(key=lambda t: int(t[0]), reverse=reverse)
|
||||||
|
for index, (val, k) in enumerate(l):
|
||||||
|
tv.move(k, '', index)
|
||||||
|
tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))
|
||||||
|
|
||||||
|
|
||||||
|
def insertManualSelectionAsScan(mb):
|
||||||
|
sql = "INSERT INTO scans(unixtimestamp_created,barcode,validated,hint,unixtimestamp_checked,skipped) VALUES(?,?,?,?,?,?)"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
ts = time.time_ns()
|
||||||
|
res = cur.execute(sql,(int(ts),mb,0,"Manuelle Auswahl",None,0))
|
||||||
|
print(res)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def getLastScan():
|
||||||
|
#sql = "SELECT * FROM scans WHERE skipped = 0 AND validated = 0 ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
||||||
|
sql = "SELECT * FROM scans WHERE (julianday('now') - julianday(CAST(unixtimestamp_created AS float) / 1e9,'unixepoch'))*24*60*60 < ? ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
||||||
|
#sql = "SELECT * FROM scans ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql, (timeoffset, ))
|
||||||
|
#res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
if len(rows) > 0:
|
||||||
|
return rows[-1]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getValidatedCount():
|
||||||
|
sql = "SELECT COUNT(*) FROM scans WHERE validated = 1 and skipped = 0;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
validated=rows[-1][0]
|
||||||
|
sql = "SELECT COUNT(*) FROM tickets;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
total=rows[-1][0]
|
||||||
|
return "{}/{} eingecheckt".format(validated, total)
|
||||||
|
|
||||||
|
#Anzahl der Barcodes, die mehr als 1x eingescannt wurden (nur für Statistik)
|
||||||
|
def getScannedDuplicates():
|
||||||
|
sql = 'SELECT COUNT(*) FROM (SELECT COUNT(*) as "ct",* FROM scans GROUP BY barcode) WHERE ct > 1;'
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
total=rows[-1][0]
|
||||||
|
return "{} Barcodes mehr als 1x gescannt".format(total)
|
||||||
|
|
||||||
|
#Prüfen, ob Barcode in der Ticketliste existiert oder nicht
|
||||||
|
def exists(barcode):
|
||||||
|
sql = "SELECT * FROM tickets WHERE BarcodeContent = ? LIMIT 1;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql, (barcode, ))
|
||||||
|
rows = res.fetchall()
|
||||||
|
return True if len(rows) > 0 else False
|
||||||
|
|
||||||
|
def isAlreadyCheckedIn(barcode):
|
||||||
|
sql = "SELECT * FROM scans WHERE validated = 1 AND barcode = ? ORDER BY unixtimestamp_checked LIMIT 1;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql, (barcode, ))
|
||||||
|
rows = res.fetchall()
|
||||||
|
return True if len(rows) > 0 else False
|
||||||
|
|
||||||
|
def getScans(tv):
|
||||||
|
#sql = "SELECT * FROM scans WHERE skipped = 0 AND validated = 0;" # Scans, die nicht übersprungen und noch nicht validiert wurden
|
||||||
|
sql = "SELECT * FROM scans;" # alle Scans
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
for i in tv.get_children():
|
||||||
|
tv.delete(i)
|
||||||
|
k=1
|
||||||
|
for i in rows:
|
||||||
|
unixtimestamp_created_c = datetime.fromtimestamp(int(str(i[0])[0:10]), pytz.timezone("Europe/Berlin")).strftime('%d.%m.%Y %H:%M:%S')
|
||||||
|
try:
|
||||||
|
unixtimestamp_checked_c = datetime.fromtimestamp(int(str(i[4])[0:10]), pytz.timezone("Europe/Berlin")).strftime('%d.%m.%Y %H:%M:%S')
|
||||||
|
except:
|
||||||
|
unixtimestamp_checked_c = ""
|
||||||
|
barcode = i[1]
|
||||||
|
validated = "Ja" if i[2] == 1 else "Nein"
|
||||||
|
hint = "" if i[3] == None else i[3]
|
||||||
|
#unixtimestamp_skipped = i[4]
|
||||||
|
skipped = "Ja" if i[5] == 1 else "Nein"
|
||||||
|
unixtimestamp_created = i[0]
|
||||||
|
if k % 2 == 0:
|
||||||
|
evenodd = "evenrow"
|
||||||
|
else:
|
||||||
|
evenodd = "oddrow"
|
||||||
|
tv.insert('', 1, text=k, values=(unixtimestamp_created_c, barcode, validated, hint, unixtimestamp_checked_c, skipped, unixtimestamp_created), tags=(evenodd, ))
|
||||||
|
k+=1
|
||||||
|
treeview_sort_column(tv, 6, False) #wir sortieren nach unixtimestamp_created, weil unixtimestamp_created_c einen Fehler bringt
|
||||||
|
global autoscroll
|
||||||
|
if autoscroll is True:
|
||||||
|
if len(tv.get_children()) > 0:
|
||||||
|
lastItem = tv.get_children()[-1]
|
||||||
|
tv.focus(lastItem)
|
||||||
|
tv.selection_set(lastItem)
|
||||||
|
tv.yview_moveto(1)
|
||||||
|
|
||||||
|
#Tickets
|
||||||
|
def getTickets(tv):
|
||||||
|
global lastSelectedTicket
|
||||||
|
lastSelectedTicket = tv.index(tv.focus())
|
||||||
|
|
||||||
|
#sql = "SELECT * FROM tickets;"
|
||||||
|
sql = "SELECT DISTINCT tickets.*, CASE WHEN RES.validated = 1 THEN 'Ja' ELSE 'Nein' END FROM tickets FULL JOIN (SELECT * FROM scans WHERE validated = 1 and skipped = 0) AS RES ON RES.barcode = tickets.BarcodeContent;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
res = cur.execute(sql)
|
||||||
|
rows = res.fetchall()
|
||||||
|
for i in tv.get_children():
|
||||||
|
tv.delete(i)
|
||||||
|
k=1
|
||||||
|
for i in rows:
|
||||||
|
if k % 2 == 0:
|
||||||
|
evenodd = "evenrow"
|
||||||
|
else:
|
||||||
|
evenodd = "oddrow"
|
||||||
|
tv.insert('', 1, text=k, values=(i), tags=(evenodd, ))
|
||||||
|
k+=1
|
||||||
|
treeview_sort_column(tv, 1, False)
|
||||||
|
if len(tvScans.get_children()) > 0:
|
||||||
|
global autoscroll
|
||||||
|
if autoscroll is True:
|
||||||
|
Barcode = tvScans.item(tvScans.focus())['values'][1] #get row von Scan, wo Barcode dem Barcode in Ticket entspr
|
||||||
|
col=0 #Barcode-Spalte
|
||||||
|
l = [(tvTickets.set(k, col), k) for k in tvTickets.get_children('')]
|
||||||
|
for index, (val, k) in enumerate(l):
|
||||||
|
if val == Barcode:
|
||||||
|
tv.focus(tv.get_children()[index])
|
||||||
|
tv.selection_set(tv.get_children()[index])
|
||||||
|
tv.yview_moveto(0)
|
||||||
|
tv.yview_scroll(index, "units")
|
||||||
|
else:
|
||||||
|
tv.focus(tv.get_children()[lastSelectedTicket])
|
||||||
|
tv.selection_set(tv.get_children()[lastSelectedTicket])
|
||||||
|
|
||||||
|
def validate():
|
||||||
|
if len(currentScan) > 0:
|
||||||
|
sql = "UPDATE scans set unixtimestamp_checked = ?, hint = ?, validated = 1 WHERE unixtimestamp_created = ? AND barcode = ? AND validated = 0 AND skipped = 0;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
ts = time.time_ns()
|
||||||
|
res = cur.execute(sql,(int(ts), hintTextInput.get(1.0, "end-1c"), currentScan[0],currentScan[1]))
|
||||||
|
print(res)
|
||||||
|
conn.commit()
|
||||||
|
hintTextInput.delete(1.0,tk.END) #reset hintTextInput
|
||||||
|
|
||||||
|
def skip():
|
||||||
|
if len(currentScan) > 0:
|
||||||
|
sql = "UPDATE scans set unixtimestamp_checked = ?, hint = ?, skipped = 1 WHERE unixtimestamp_created = ? AND barcode = ? AND validated = 0;"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
ts = time.time_ns()
|
||||||
|
res = cur.execute(sql,(int(ts), hintTextInput.get(1.0, "end-1c"), currentScan[0],currentScan[1]))
|
||||||
|
print(res)
|
||||||
|
conn.commit()
|
||||||
|
hintTextInput.delete(1.0,tk.END) #reset hintTextInput
|
||||||
|
|
||||||
|
toggleScrollbarButton_t1 = "Umschalten zu: Ticket manuell auswählen"
|
||||||
|
toggleScrollbarButton_t2 = "Umschalten zu: Ticket automatisch per Scan auswählen (Standard)"
|
||||||
|
|
||||||
|
def manual():
|
||||||
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t2:
|
||||||
|
global lastSelectedTicket
|
||||||
|
mBarcode = None
|
||||||
|
if len(tvScans.get_children()) > 0:
|
||||||
|
item = tvTickets.item(tvTickets.focus())
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
for (val, k) in enumerate(item.values()):
|
||||||
|
row += 1
|
||||||
|
if row == 3:
|
||||||
|
mBarcode = k[0]
|
||||||
|
print(mBarcode)
|
||||||
|
insertManualSelectionAsScan(mBarcode) #insert a new "lastScan"
|
||||||
|
|
||||||
|
def ScrollToggle():
|
||||||
|
global autoscroll
|
||||||
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t1:
|
||||||
|
toggleScrollbarButton.config(text=toggleScrollbarButton_t2)
|
||||||
|
autoscroll = False
|
||||||
|
tvTickets['selectmode'] = "browse"
|
||||||
|
else:
|
||||||
|
toggleScrollbarButton.config(text=toggleScrollbarButton_t1)
|
||||||
|
autoscroll = True
|
||||||
|
tvTickets['selectmode'] = "none"
|
||||||
|
|
||||||
|
toggleScrollbarButton = tk.Button(text=toggleScrollbarButton_t1, width=70, relief="raised", bg='pink', command=ScrollToggle)
|
||||||
|
toggleScrollbarButton.pack(padx=10, pady=5)
|
||||||
|
|
||||||
|
f1=tk.Frame(window)
|
||||||
|
f1.pack(expand=1)
|
||||||
|
|
||||||
|
tk.Label(f1, text="Die Anzeige des aktuellen Barcodes setzt sich automatisch nach {} Sekunden zurück".format(timeoffset), padx=10, pady=5).pack()
|
||||||
|
|
||||||
|
manualButton = tk.Button(f1, text="Manuelle Auswahl", command=manual)
|
||||||
|
manualButton.pack(side=tk.LEFT)
|
||||||
|
manualButton['font']=font.Font(size=35)
|
||||||
|
|
||||||
|
barcodeLabel = tk.Label(f1, text="<>", bg='#000000', fg='#00ff00', padx=10, pady=5)
|
||||||
|
barcodeLabel.config(font=(None, 35))
|
||||||
|
barcodeLabel.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
checkinButton = tk.Button(f1, text="Einchecken/entwerten", bg='red', fg='white', command=validate)
|
||||||
|
checkinButton.pack(side=tk.RIGHT)
|
||||||
|
checkinButton['font']=font.Font(size=35)
|
||||||
|
skipButton = tk.Button(f1, text="Überspringen", bg='red', fg='white', command=skip)
|
||||||
|
skipButton.pack(side=tk.RIGHT)
|
||||||
|
skipButton['font']=font.Font(size=35)
|
||||||
|
|
||||||
|
commentLabel = tk.Label(window, text="Kommentar", padx=10, pady=5)
|
||||||
|
commentLabel.config(font=(None, 15))
|
||||||
|
commentLabel.pack()
|
||||||
|
|
||||||
|
hintTextInput = tk.Text(window, height = 3, width = 100)
|
||||||
|
hintTextInput.pack()
|
||||||
|
totalValidatedLabel = tk.Label(window, text="/", fg='#008000', padx=10, pady=5, width=100)
|
||||||
|
totalValidatedLabel.config(font=(None, 35))
|
||||||
|
totalValidatedLabel.pack()
|
||||||
|
|
||||||
|
tabControl = ttk.Notebook(window)
|
||||||
|
tabScans = ttk.Frame(tabControl)
|
||||||
|
tabTickets = ttk.Frame(tabControl)
|
||||||
|
|
||||||
|
tabControl.add(tabTickets, text='Alle Tickets')
|
||||||
|
tabControl.add(tabScans, text='Alle Scans')
|
||||||
|
tabControl.pack(fill='both')
|
||||||
|
|
||||||
|
tk.Label(tabScans, text="Scans").pack()
|
||||||
|
totalDuplicateCodeScansLabel = tk.Label(tabScans, text="/", padx=10, pady=5)
|
||||||
|
totalDuplicateCodeScansLabel.pack()
|
||||||
|
tk.Label(tabTickets, text="Tickets").pack()
|
||||||
|
|
||||||
|
def updateBarcodeLabel(label, scan):
|
||||||
|
if len(scan) == 0:
|
||||||
|
labeltext="Bitte scannen!"
|
||||||
|
label.config(fg='#0000ff')
|
||||||
|
else:
|
||||||
|
barcode = scan[1]
|
||||||
|
existing = exists(barcode)
|
||||||
|
alreadyCheckedIn = isAlreadyCheckedIn(barcode)
|
||||||
|
if existing is True:
|
||||||
|
if alreadyCheckedIn is False:
|
||||||
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t2:
|
||||||
|
ScrollToggle() #toggle zurück zu Standardmodus, wenn ein normaler Scan gekommen ist
|
||||||
|
labeltext="{0} ready!".format(barcode)
|
||||||
|
label.config(fg='#00ff00')
|
||||||
|
skip = scan[4]
|
||||||
|
if skip is not None:
|
||||||
|
labeltext="{0} übersprungen!".format(barcode)
|
||||||
|
label.config(fg='#ffffff')
|
||||||
|
else:
|
||||||
|
labeltext="{0} entwertet!".format(barcode)
|
||||||
|
label.config(fg='#ff0000')
|
||||||
|
else:
|
||||||
|
labeltext="{0} nicht gefunden!".format(barcode)
|
||||||
|
label.config(fg='#7f00ff')
|
||||||
|
label.config(text=labeltext)
|
||||||
|
|
||||||
|
tvScans = ttk.Treeview(tabScans, height=50, selectmode="browse")
|
||||||
|
tvScans["columns"] = ("c1", "c2", "c3", "c4", "c5", "c6", "c7") #eine Spalte mehr (unixtimestamp_created, welche aber ausgeblendet ist und nur zum Sortieren genutzt wird)
|
||||||
|
tvScans["displaycolumns"]=("c1", "c2", "c3", "c6", "c4", "c5")
|
||||||
|
tvScans.column("c1", width=100)
|
||||||
|
tvScans.column("c2", anchor='center', width=100)
|
||||||
|
tvScans.column("c3", anchor='center', width=100)
|
||||||
|
tvScans.column("c4", anchor='center', width=100)
|
||||||
|
tvScans.column("c5", anchor='center', width=100)
|
||||||
|
tvScans.column("c6", anchor='center', width=100)
|
||||||
|
tvScans.heading("#0", text="Nr.")
|
||||||
|
tvScans.heading("c1", text="TS Gescannt", anchor='w')
|
||||||
|
tvScans.heading("c2", text="Barcode")
|
||||||
|
tvScans.heading("c3", text="Entwertet")
|
||||||
|
tvScans.heading("c4", text="Anmerkung/Hinweise")
|
||||||
|
tvScans.heading("c5", text="TS Bearbeitet")
|
||||||
|
tvScans.heading("c6", text="Übersprungen")
|
||||||
|
tvScans.tag_configure('oddrow', background='white', font=("monospace", 10))
|
||||||
|
tvScans.tag_configure('evenrow', background='lightblue', font=("monospace", 10))
|
||||||
|
tvScansBar = ttk.Scrollbar(tabScans, orient="vertical", command=tvScans.yview)
|
||||||
|
tvScansBar.pack(side='right', fill='x')
|
||||||
|
tvScans.configure(yscrollcommand = tvScansBar.set)
|
||||||
|
tvScans.pack(fill='both')
|
||||||
|
|
||||||
|
tvTickets = ttk.Treeview(tabTickets, height=50, selectmode="none")
|
||||||
|
tvTickets["columns"] = ("c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11")
|
||||||
|
tvTickets["displaycolumns"]=("c1", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11")
|
||||||
|
tvTickets.column("c1", width=100)
|
||||||
|
tvTickets.column("c2", anchor='center', width=100)
|
||||||
|
tvTickets.column("c3", anchor='center', width=100)
|
||||||
|
tvTickets.column("c4", anchor='center', width=100)
|
||||||
|
tvTickets.column("c5", anchor='center', width=100)
|
||||||
|
tvTickets.column("c6", anchor='center', width=100)
|
||||||
|
tvTickets.column("c7", anchor='center', width=100)
|
||||||
|
tvTickets.column("c8", anchor='center', width=100)
|
||||||
|
tvTickets.column("c9", anchor='center', width=100)
|
||||||
|
tvTickets.column("c10", anchor='center', width=100)
|
||||||
|
tvTickets.column("c11", anchor='center', width=100)
|
||||||
|
tvTickets.heading("#0", text="Nr.")
|
||||||
|
tvTickets.heading("c1", text="Barcode", anchor='w')
|
||||||
|
tvTickets.heading("c2", text="Nr")
|
||||||
|
tvTickets.heading("c3", text="VIP")
|
||||||
|
tvTickets.heading("c4", text="Medium")
|
||||||
|
tvTickets.heading("c5", text="Aushändigung")
|
||||||
|
tvTickets.heading("c6", text="Bestellnummer/Verwendungszweck")
|
||||||
|
tvTickets.heading("c7", text="Bezahlt am")
|
||||||
|
tvTickets.heading("c8", text="Hinweise")
|
||||||
|
tvTickets.heading("c9", text="Besteller")
|
||||||
|
tvTickets.heading("c10", text="Typ")
|
||||||
|
tvTickets.heading("c11", text="Entwertet")
|
||||||
|
tvTickets.tag_configure('oddrow', background='white', font=("monospace", 10))
|
||||||
|
tvTickets.tag_configure('evenrow', background='lightblue', font=("monospace", 10))
|
||||||
|
#tvTicketsBar = ttk.Scrollbar(tvTickets, orient="vertical", command=tvTickets.yview)
|
||||||
|
#tvTicketsBar.pack(side='right', fill='x')
|
||||||
|
#tvTickets.configure(yscrollcommand = tvTicketsBar.set)
|
||||||
|
tvTickets.pack(fill='both')
|
||||||
|
|
||||||
|
def update():
|
||||||
|
getScans(tvScans)
|
||||||
|
getTickets(tvTickets)
|
||||||
|
global currentScan
|
||||||
|
currentScan=getLastScan() #bekomme den letzten, unbearbeiteten Scan
|
||||||
|
updateBarcodeLabel(barcodeLabel, currentScan)
|
||||||
|
totalValidatedLabel.config(text=getValidatedCount())
|
||||||
|
totalDuplicateCodeScansLabel.config(text=getScannedDuplicates())
|
||||||
|
window.after(2000, update)
|
||||||
|
|
||||||
|
update() # run first time
|
||||||
|
window.mainloop()
|
162
usb-scanner.py
Normal file
162
usb-scanner.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
'''
|
||||||
|
Hilfe:
|
||||||
|
- Scanner wird nicht mehr erkannt? Abziehen vom USB-Slot für 1-2 Minuten hilft + Stoppen dieses Programms für die gleiche Zeit
|
||||||
|
- Scanner-Wert wird nicht übertragen? -> Knopf ca. 1 Sekunde gedrückt halten. Nicht bloß antippen!
|
||||||
|
'''
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import pytz
|
||||||
|
from systemd import journal
|
||||||
|
import sqlite3
|
||||||
|
from sqlite3 import Error
|
||||||
|
from contextlib import closing
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
idVendor=0x0581
|
||||||
|
idProduct=0x0115
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
db_file = os.path.join(BASE_DIR, "kugelstossmeeting-prod.db")
|
||||||
|
|
||||||
|
def printwrite(str): #write to log and to stdout
|
||||||
|
journal.write(str)
|
||||||
|
print(str)
|
||||||
|
|
||||||
|
def hid2ascii(lst):
|
||||||
|
#array('B', [0, 0, 0, 0, 0, 0, 0, 0]) # nothing, ignore
|
||||||
|
conv_table = {
|
||||||
|
0:['', ''],
|
||||||
|
4:['a', 'A'],
|
||||||
|
5:['b', 'B'],
|
||||||
|
6:['c', 'C'],
|
||||||
|
7:['d', 'D'],
|
||||||
|
8:['e', 'E'],
|
||||||
|
9:['f', 'F'],
|
||||||
|
10:['g', 'G'],
|
||||||
|
11:['h', 'H'],
|
||||||
|
12:['i', 'I'],
|
||||||
|
13:['j', 'J'],
|
||||||
|
14:['k', 'K'],
|
||||||
|
15:['l', 'L'],
|
||||||
|
16:['m', 'M'],
|
||||||
|
17:['n', 'N'],
|
||||||
|
18:['o', 'O'],
|
||||||
|
19:['p', 'P'],
|
||||||
|
20:['q', 'Q'],
|
||||||
|
21:['r', 'R'],
|
||||||
|
22:['s', 'S'],
|
||||||
|
23:['t', 'T'],
|
||||||
|
24:['u', 'U'],
|
||||||
|
25:['v', 'V'],
|
||||||
|
26:['w', 'W'],
|
||||||
|
27:['x', 'X'],
|
||||||
|
28:['z', 'Z'],
|
||||||
|
29:['y', 'Y'],
|
||||||
|
30:['1', '!'],
|
||||||
|
31:['2', '@'],
|
||||||
|
32:['3', '#'],
|
||||||
|
33:['4', '$'],
|
||||||
|
34:['5', '%'],
|
||||||
|
35:['6', '^'],
|
||||||
|
36:['7' ,'&'],
|
||||||
|
37:['8', '*'],
|
||||||
|
38:['9', '('],
|
||||||
|
39:['0', ')'],
|
||||||
|
40:['\n', '\n'],
|
||||||
|
41:['\x1b', '\x1b'],
|
||||||
|
42:['\b', '\b'],
|
||||||
|
43:['\t', '\t'],
|
||||||
|
44:[' ', ' '],
|
||||||
|
45:['_', '_'],
|
||||||
|
46:['=', '+'],
|
||||||
|
47:['[', '{'],
|
||||||
|
48:[']', '}'],
|
||||||
|
49:['\\', '|'],
|
||||||
|
50:['#', '~'],
|
||||||
|
51:[';', ':'],
|
||||||
|
52:["'", '"'],
|
||||||
|
53:['`', '~'],
|
||||||
|
54:[',', '<'],
|
||||||
|
55:['.', '>'],
|
||||||
|
56:['/', '?'],
|
||||||
|
100:['\\', '|'],
|
||||||
|
103:['=', '='],
|
||||||
|
}
|
||||||
|
|
||||||
|
line = ''
|
||||||
|
char = ''
|
||||||
|
shift = 0
|
||||||
|
|
||||||
|
#print(lst)
|
||||||
|
|
||||||
|
for byte in lst: #raw byte array
|
||||||
|
if byte == 2:
|
||||||
|
shift = 1
|
||||||
|
if byte != 0:
|
||||||
|
if byte not in conv_table and byte != 2:
|
||||||
|
printwrite("Warning: byte {0} not in conversion table".format(byte))
|
||||||
|
char = ''
|
||||||
|
line += char
|
||||||
|
elif byte != 40 and byte != 2: #skip newline character
|
||||||
|
char = conv_table[byte][shift]
|
||||||
|
line += char
|
||||||
|
shift = 0 #reset shift
|
||||||
|
return line
|
||||||
|
|
||||||
|
sleepTime = 5 #secs
|
||||||
|
def mainloop():
|
||||||
|
while True:
|
||||||
|
dev = usb.core.find(idVendor=idVendor, idProduct=idProduct)
|
||||||
|
time.sleep(sleepTime) #in jedem Fall etwas warten
|
||||||
|
if dev is None:
|
||||||
|
printwrite("No USB Scanner. Restarting mainloop {0}".format(sleepTime))
|
||||||
|
mainloop()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
detached = dev.is_kernel_driver_active(0)
|
||||||
|
if detached is False:
|
||||||
|
printwrite("USB Scanner found! Device is already detached")
|
||||||
|
if detached is True:
|
||||||
|
dev.detach_kernel_driver(0)
|
||||||
|
#journal.write("Detached USB device from kernel driver")
|
||||||
|
printwrite("USB Scanner found! Detached from kernel driver")
|
||||||
|
|
||||||
|
cfg = dev.get_active_configuration()
|
||||||
|
intf = cfg[(0,0)]
|
||||||
|
ep = usb.util.find_descriptor(intf, custom_match = lambda e: \
|
||||||
|
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN)
|
||||||
|
assert ep is not None, "Endpoint for USB device not found. Something is wrong."
|
||||||
|
while cfg is not None:
|
||||||
|
print("... in the mainloop")
|
||||||
|
# Wait up to 1 seconds for data
|
||||||
|
data = ep.read(1000)
|
||||||
|
line = hid2ascii(data)
|
||||||
|
if len(line) == 5: #wir akzepieren nur Codes, die 5-stellig sind (so, wie sie auf dem Ticket sind)
|
||||||
|
#printwrite(line)
|
||||||
|
sql = "INSERT INTO scans(unixtimestamp_created,barcode,validated,hint,unixtimestamp_checked,skipped) VALUES(?,?,?,?,?,?)"
|
||||||
|
with closing(sqlite3.connect(db_file)) as conn:
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
#standardmäßig validated=0 und leerer Hinweis. Das wird vom GUI befüllt!
|
||||||
|
ts = time.time_ns()
|
||||||
|
res = cur.execute(sql,(int(ts),line,0,None,None,0))
|
||||||
|
printwrite("{0}: Inserting {1} into database".format(
|
||||||
|
datetime.fromtimestamp(int(str(ts)[0:10]), pytz.timezone("Europe/Berlin")).strftime('%d.%m.%Y %H:%M:%S'),
|
||||||
|
line))
|
||||||
|
conn.commit()
|
||||||
|
time.sleep(0.1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
printwrite("Stopping program")
|
||||||
|
break
|
||||||
|
except usb.core.USBError as e:
|
||||||
|
printwrite("Device disappeared. Restarting mainloop | Error: {}".format(e))
|
||||||
|
#Error: [Errno None] Configuration not set
|
||||||
|
#Error: [Errno 5] Input/Output Error
|
||||||
|
#Error: [Errno 16] Resource busy
|
||||||
|
#Error: [Errno 13] Access denied (insufficient permissions)
|
||||||
|
#Error: [Errno 19] No such device (it may have been disconnected)
|
||||||
|
#Error: [Errno 110] Operation timed out
|
||||||
|
time.sleep(sleepTime)
|
||||||
|
#continue
|
||||||
|
mainloop()
|
||||||
|
mainloop()
|
16
usb-scanner.service
Normal file
16
usb-scanner.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=USB Scanner Eyoyo Kugelstossmeeting Rochlitz
|
||||||
|
After=network.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
#RestartSec=1
|
||||||
|
ExecStartPre=/usr/bin/sleep 1
|
||||||
|
ExecStart=/opt/kugelstossmeeting-ticketing/venv/bin/python3 "/opt/kugelstossmeeting-ticketing/usb-scanner.py"
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
Loading…
x
Reference in New Issue
Block a user