initial commit

This commit is contained in:
Mario Voigt 2025-02-02 11:49:45 +01:00
parent bd4e69d0c3
commit 05f06f7817
19 changed files with 817 additions and 2 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
venv

Binary file not shown.

Binary file not shown.

5
LICENSE.md Normal file
View 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
View File

@ -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:
![QR Code Config](qrcodes.png)
**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
```
![DB Browser for SQLite](db1.png)
![DB Browser for SQLite](db2.png)
![DB Browser for SQLite](db3.png)
**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
![GUI](gui.png)
## 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

BIN
db1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
db2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
db3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

BIN
gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

9
hooks/pre-commit Executable file
View 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!"

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

44
icon.svg Normal file
View 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

Binary file not shown.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pyusb
systemd
pytz

388
sqlite.py Executable file
View 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
View 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
View 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