- Python 86.1%
- Shell 13.9%
Il README radice era ancora quello del driver SK1-only, con due ritocchi a mano rimasti a metà (titolo "SK1-211 e SK60", JSON di /health non valido e con un campo "stato carta" che l'endpoint non restituisce). README.md riscritto: modelli supportati con i valori misurati, cosa cambia nella 2.0.0 per chi aggiorna un player in campo, procedura di aggiornamento con backup e --ff-only, tabella completa del .env, collaudo, output reale di /health e /stats, diagnosi dei guasti (emulazione StarPRNT, carta finita che si presenta come write timeout, LPGETSTATUS, USBDEVFS_RESET, il falso indizio "usblp0: removed"), limiti noti e struttura del repository. printer_driver/README.md: il profilo sk1 era descritto come non verificato con head_to_cutter_dots=0, in contraddizione con la tabella in testa e col codice (verified=True, 576 dot, lama a 80 dot). Aggiunta la codifica di stato Sanei misurata, l'opposizione dei registri fra i due modelli, e una sezione sui limiti noti del driver. printer_driver/CHANGELOG.md: valori misurati per entrambi i modelli e note SK1 allineate al codice. |
||
|---|---|---|
| printer_driver | ||
| .env.example | ||
| .gitignore | ||
| env_loader.py | ||
| health_check.py | ||
| install_printer.sh | ||
| printer.py | ||
| qr_handler.py | ||
| README.md | ||
| redis_listener.py | ||
| requirements.txt | ||
| test_protocollo.py | ||
| test_redis_integration.py | ||
| test_stampante.py | ||
| ticket-printer.service | ||
| uninstall_printer.sh | ||
dlq_printer — servizio di stampa biglietti da chiosco
Servizio che resta in ascolto su Redis, filtra i biglietti destinati a questa macchina e li stampa sulla stampante termica collegata via USB.
Dalla versione 2.0.0 la stampa non è più legata a un solo modello: tutta la
parte hardware sta nel pacchetto printer_driver/,
che riconosce il modello dal bus USB e ne applica i parametri misurati.
Redis (ticket:created) → redis_listener.py → printer.py → printer_driver → USB
│ │
filtro site_id/IP modello, larghezza,
health-check :8080 taglio, stato carta
Modelli supportati
| Stampante | Chiave | Larghezza utile | USB | Stato |
|---|---|---|---|---|
| CUSTOM KX60 | kx60 |
448 dot / 56mm | 0dd4:f108 |
verificata su hardware, firmware 1.02 |
| Sanei SK1-311 | sk1 |
576 dot / 72mm | 10c5:0007 |
verificata su hardware, firmware V3.13 |
Il PID 0519:004d (SK1-311 in emulazione StarPRNT) è riconosciuto di
proposito: serve a segnalare l'emulazione sbagliata invece di lasciare la
stampante invisibile. Vedi
Emulazione ESC/POS vs StarPRNT.
Altri modelli — SK1-211 e qualunque altra termica ESC/POS — non hanno
ancora un profilo. Aggiungerne uno costa un file in
printer_driver/src/printer_driver/models/, senza toccare il codice di stampa
né questo servizio: la procedura è in
models/_template.py.
I numeri (larghezza testina, distanza lama) si misurano con
printer-cli calibrate e printer-cli cutcal; copiarli dal datasheet non
funziona — quello della KX60 dichiara 60mm mentre la testina stampa su 56mm.
Che cosa è cambiato nella 2.0.0
Il changelog completo del driver è in
printer_driver/CHANGELOG.md. In sintesi, per
chi aggiorna un player già in campo:
- Un solo pacchetto multi-modello. Prima esistevano due driver separati e
redis_listener.is_printer_available()chiamavafind_sk1_devices(): su una KX60 l'health-check restava{"status":"degraded","printer":false}per sempre, pur stampando. Ora il riconoscimento è per VID:PID del profilo, non per stringa USB. - Interlinea impostata prima delle bande immagine (
ESC 3 24/ESC 2). Senza, ogni banda da 24 dot veniva chiusa da unLFche avanzava di ~34 dot: l'immagine usciva rigata di bianco per tutta l'altezza. - Avanzamento di taglio con
ESC Jinvece diGS V 66 n: sul firmware KX60 1.02 quel comando taglia ma ignoran, quindi la lama cadeva dentro il contenuto e tranciava l'ultima riga. - Attesa dello svuotamento calcolata sulla carta accodata, non un
sleep(0.1)fisso: chiudendo il device troppo presto il kernel può abortire le URB pendenti e il taglio non arriva mai (difetto intermittente, proporzionale alla lunghezza del biglietto). - Stato carta decodificato per modello e misurato, con
paper_state_knownche distingue un dato letto da uno presunto. La versione precedente partiva ottimista e correggeva solo se la stampante rispondeva: a carta finita i registri tacciono, quindi dichiarava "pronta" una stampante a secco. - Trasporto preferito
/dev/usb/lp0(modulo kernelusblp), con libusb/PyUSB solo come ripiego: niente piùdetach_kernel_drivere nienteResource busyfra processi che si contendono la stampante. - Installer multi-modello che preserva il
.envesistente (Redis, password,CUSTOMER_SITE_ID,MY_IP) e ne fa il backup, e sostituisce la vecchia regola udev99-sk1.rulescon99-kiosk-printer.rulesper il VID:PID effettivamente presente. - Diagnostica richiamabile (
printer-cli calibrate|cutcal|align|watch): le pagine di misura stanno nel driver, non nella CLI, e sono usabili dall'installer e dal collaudo.
Installazione
Prima installazione
git clone https://git.okone.it/s.moretti/dlq_printer.git
cd dlq_printer
sudo bash install_printer.sh
Opzioni:
sudo bash install_printer.sh --model kx60 # forza il modello, non lo rileva
sudo bash install_printer.sh --list-models # elenca i profili e esce
sudo bash install_printer.sh --dry-run # mostra cosa farebbe, non tocca nulla
sudo bash install_printer.sh --no-apt # salta apt update/upgrade (player già provisionato)
sudo bash install_printer.sh --yes # non chiede conferme
Lo script procede in nove passi: aggiornamento del sistema (salvo --no-apt),
dipendenze di sistema, clone o aggiornamento del repository in
/var/www/kipanga/system/dlq_printer, virtualenv, dipendenze Python e
installazione del pacchetto printer_driver, scelta della stampante via
printer-cli detect --shell, scrittura del .env, permessi USB (regola udev
per il VID:PID trovato più i gruppi lp e dialout all'utente kipanga),
installazione e avvio del servizio systemd ticket-printer.
La lista dei VID:PID non è duplicata nello script: la fonte di verità è il pacchetto. Una lista in bash divergerebbe al primo modello aggiunto, e divergerebbe in silenzio.
Se il modello scelto non è verificato su hardware, l'installer lo dice, chiede conferma e ricorda i comandi di misura da eseguire.
Aggiornare un player già installato
I player in campo possono essere molto indietro (casi reali: 16 commit, con
il vecchio printer.py monolitico e senza il pacchetto printer_driver).
Procedura sicura:
# 1. backup dell'installazione corrente
sudo tar czf ~/dlq_printer_backup.tgz -C /var/www/kipanga/system dlq_printer
# 2. aggiornamento del codice
cd /var/www/kipanga/system/dlq_printer
sudo git fetch origin && sudo git pull --ff-only origin main
# 3. reinstallazione col modello giusto
sudo bash install_printer.sh --model kx60 --no-apt --yes
--ff-only è voluto: se il fast-forward non passa, sul player ci sono
modifiche locali da guardare prima di sovrascriverle.
Il .env esistente viene preservato e messo in backup. La regola udev
SK1-specifica viene rimossa e sostituita con quella del modello rilevato.
Con
--dry-runl'esecuzione si ferma allo step 6 perchéprinter-clinon esiste finché non gira ilpip install: è normale, non è un errore.
Configurazione (.env)
Scritto dall'installer in /var/www/kipanga/system/dlq_printer/.env
(640, utente kipanga). Si modifica a mano e si riavvia il servizio.
| Variabile | Default | |
|---|---|---|
REDIS_HOST |
10.0.11.4 |
server che distribuisce i biglietti |
REDIS_PORT |
6379 |
|
REDIS_PASSWORD |
— | va compilata se Redis richiede auth |
REDIS_DB |
0 |
|
REDIS_CHANNEL |
ticket:created |
canale pub/sub |
CUSTOMER_SITE_ID |
dal player-config.json |
primo filtro sui biglietti |
MY_IP |
IP locale | secondo filtro: deve combaciare con dlq_ip_printer |
PRINTER_MODEL |
rilevato | kx60, sk1, o auto |
PRINT_WIDTH_DOTS |
dal profilo | larghezza testina — misurare con calibrate |
HEAD_TO_CUTTER_DOTS |
dal profilo | distanza testina-lama — misurare con cutcal |
TAIL_MARGIN_DOTS |
24 |
margine bianco voluto dopo il footer (3mm) |
NEAR_END_SENSOR |
1 |
0 se il sensore di esaurimento è stato rimosso |
DOMAIN |
— | base URL per il logo |
TIMEOUT_LOGO |
10 |
secondi |
HEARTBEAT_INTERVAL |
30 |
secondi |
HEALTH_CHECK_PORT |
8080 |
Il driver ne legge altre, utili in diagnosi:
IGNORE_PAPER_SENSOR, CHECK_STATUS, CUT_FEED_DOTS, PRINTER_TRANSPORT,
PRINTER_USB_IDS — documentate nel
README del driver.
⚠️ MY_IP non è cosmetico. Un biglietto il cui dlq_ip_printer non
combacia viene scartato in silenzio, con il servizio sano e i log puliti.
Su alcuni siti gli IP sono gestiti da un MikroTik e MY_IP non coincide
con hostname -I: è normale, quello che conta è che combaci con il valore
mandato dal backend.
⚠️ IGNORE_PAPER_SENSOR=1 è un'arma carica: con quello attivo un biglietto
per una coda reale viene dato per stampato mentre non esce niente. Il driver
logga un warning ogni volta che lo usa.
Collaudo dopo l'installazione
Nell'ordine, dal venv del repository (printer-cli è installato lì):
printer-cli info # cosa si vede sul bus e perché ha matchato
printer-cli status # carta e firmware, non stampa
printer-cli ticket # biglietto di esempio, stesso codice del servizio
curl localhost:8080/health
Su una stampante appena aggiunta, o su un allestimento nuovo, prima di mandarla in produzione:
printer-cli calibrate # MISURA la larghezza reale della testina
printer-cli cutcal # MISURA la distanza testina-lama
printer-cli align # verifica allineamento e posizione di taglio
Sbagliare la larghezza per eccesso tronca il lato destro del biglietto senza alcun segnale software: il servizio riporta successo e i log sono puliti. È l'errore peggiore dei due, ed è il motivo per cui questi due comandi esistono.
E non si compensa nel layout: mettere spazio bianco in fondo al biglietto per allontanare la lama sembra funzionare finché il formato resta quello, e si rompe al primo cambio di disegno.
Gestione del servizio
sudo systemctl status ticket-printer
sudo systemctl restart ticket-printer
sudo systemctl stop ticket-printer
sudo systemctl start ticket-printer
sudo systemctl enable ticket-printer # avvio al boot
sudo systemctl disable ticket-printer
Log
journalctl -u ticket-printer -f # in tempo reale
journalctl -u ticket-printer -n 100 # ultimi 100
journalctl -u ticket-printer -b # sessione corrente
journalctl -u ticket-printer -p err # solo errori
Health check
curl http://localhost:8080/health
{
"status": "ok",
"redis": true,
"printer": true,
"timestamp": "2026-07-30T11:04:22.913140"
}
status è ok se Redis e stampante rispondono, degraded se solo uno dei
due, down se nessuno. Il codice HTTP è 200 solo per ok, altrimenti 503.
⚠️ "printer": true significa "una stampante supportata è sul bus USB", non
"può stampare". Non guarda la carta: a rotolo finito questo endpoint
risponde ok. Vedi Limiti noti.
curl http://localhost:8080/stats
{
"uptime": "3:14:07",
"started_at": "2026-07-30T07:50:15.221000",
"tickets_ok": 42,
"tickets_err": 0,
"last_ticket_at": "2026-07-30T11:02:58.004000",
"redis_connected": true
}
Emulazione ESC/POS vs StarPRNT
⚠️ La Sanei SK1-311 ha due emulazioni commutabili e questo software funziona
solo in ESC/POS (bit-image ESC * per l'immagine, GS V per il taglio).
In StarPRNT la stampante ignora entrambi: stampa caratteri illeggibili, srotola metri di carta e non taglia. Non è un difetto del software — è la stampante nell'emulazione sbagliata.
Riconoscerla
lsusb | grep -iE "10c5|0519"
| PID | Emulazione | |
|---|---|---|
10c5:0007 |
ESC/POS | corretto |
0519:004d |
StarPRNT | da cambiare |
Oppure, che dice anche perché ha matchato: printer-cli info.
Cambiarla (dai tasti, non da software)
- Carica la carta e spegni la stampante.
- Tieni premuti insieme FEED + SELECT e accendi, poi rilascia: viene stampata l'emulazione corrente con le opzioni di modifica.
- Premi SELECT per cambiare, fino a leggere ESC/POS.
- Premi FEED per salvare, poi SELECT per uscire.
L'impostazione è permanente. Conferma con un self-test.
Diagnosi dei guasti in campo
Lo stato carta non si legge sempre allo stesso modo
I due modelli rispondono in modo opposto, e per questo il decodificatore è un dato del profilo e non una funzione globale:
| registro | CUSTOM KX60 | Sanei SK1-311 |
|---|---|---|
DLE EOT 2 |
unico affidabile | muto |
GS r 1 |
0x00 carta ok |
0x00 carta ok, 0x0C assente |
ESC v |
muto | risponde |
| firmware | 1.02 |
V3.13 |
Applicare a una la mappatura dell'altra significa leggere lo stato dal registro
sbagliato. Con printer-cli watch si vede dal vivo quali bit cambiano togliendo
la carta o aprendo il coperchio: è così che si scrive un decodificatore nuovo.
Carta finita: come si presenta
Non si presenta come "carta finita". Si presenta come timeout di scrittura:
TransportError: Timeout in scrittura dopo 0/2 byte
A rotolo finito la stampante accetta i primi byte in buffer e poi si impianta —
una write da 4097 byte passa 4096/4097 in 8 secondi. Una write da 2 byte
invece passa: un test corto dà un falso positivo, serve un payload più
grande del buffer.
Diagnosi remota, senza nessuno sul posto. Quando le query ESC/POS sono mute
(bulk IN morto), lo stato si legge comunque sul control endpoint con il
GET_PORT_STATUS della USB Printer Class, che usblp espone come ioctl
LPGETSTATUS = 0x060b su /dev/usb/lp0. Non serve staccare usblp:
import fcntl
from array import array
buf = array('i', [0])
with open('/dev/usb/lp0', 'wb') as f:
fcntl.ioctl(f.fileno(), 0x060b, buf, True)
print(hex(buf[0])) # 0x20 PAPER_EMPTY | 0x10 SELECTED | 0x08 NO_ERROR
0x20 stabile = carta finita. Il driver non usa questa via (vedi
Limiti noti): oggi va fatta a mano.
Stampante impiantata: reset, non il cavo
Se le write vanno in timeout ma il bus è sano, non è il cavo. Prima di mandare un tecnico, verificalo dai log del kernel:
dmesg | grep -c 'USB disconnect'
dmesg | grep -c 'idProduct=0007' # quante volte si è enumerata
dmesg | grep -E '\-71|\-110|reset'
Se la stampante si è enumerata una volta sola dal boot e non ci sono
disconnect, reset o errori -71/-110, cavo e alimentazione non c'entrano.
⚠️ usblp0: removed in dmesg non è un disconnect USB: è usblp sbindato
via software, e l'unico codice che lo fa è UsbTransport.open() →
detach_kernel_driver() (ripristinato in close()). A coppie ogni ~40s vuol
dire che qualcuno stava girando test_stampante.py sul percorso libusb. Il
servizio usa il nodo carattere e non stacca mai nulla.
Quello che sblocca davvero è un reset del dispositivo
(USBDEVFS_RESET = 0x5514); dopo il reset il kernel riaggancia usblp da solo
e la write passa subito. Ferma il servizio prima e riavvialo dopo:
sudo systemctl stop ticket-printer
lsusb | grep -iE "10c5|0dd4" # -> Bus 001 Device 007
sudo python3 - <<'PY'
import fcntl
with open('/dev/bus/usb/001/007', 'wb') as f:
fcntl.ioctl(f.fileno(), 0x5514, 0)
PY
sudo systemctl start ticket-printer
Gli endpoint in halt, invece, non sono la causa: GET_STATUS su 0x81 e 0x02
ha dato [0,0] nei casi osservati, quindi clear_halt non serve.
Il biglietto non esce e nei log non c'è niente
Nell'ordine:
curl localhost:8080/health— Redis connesso? stampante vista?journalctl -u ticket-printer -n 200— cercaTicket ignorato (dlq_ip_printer ... != MY_IP ...): è il filtro, non la stampante.CUSTOMER_SITE_IDnel.envcombacia con il sito che genera i biglietti?printer-cli ticket— se questo stampa, il problema è a monte del driver.
Debug hardware
lsusb # la stampante è sul bus?
cat /etc/udev/rules.d/99-kiosk-printer.rules # regola generata dall'installer
sudo udevadm control --reload-rules && sudo udevadm trigger
ls -l /dev/usb/lp0 # crw-rw---- root:lp
id -nG kipanga | grep -w lp # l'utente è nel gruppo lp?
Limiti noti
Cose vere del codice attuale, da sapere prima di andare in campo — e da sistemare quando si torna sul driver.
LPGETSTATUSnon è usato da nessuna parte. Il driver si affida solo alle query ESC/POS. Con quelle muteis_ready()ritornaTrueper scelta progettuale (una stampante che non espone lo stato non va dichiarata guasta), quindi l'health-check dice"printer": truea carta finita e ogni biglietto brucia ~30s in timeout invece di fallire subito con "carta finita". Aggiungerlo come ripiego risolverebbe sia la diagnosi sia l'health-check.- Nessun auto-recupero dalla stampante impiantata. Non esistono
clear_haltné reset nel driver:UsbTransport.close()fa solo release/attach/dispose. Il reset va dato a mano come sopra. /healthnon riporta lo stato della carta. L'informazione esiste (printer-cli status,paper_state_known) ma non è esposta sull'endpoint.- Coperchio aperto ed errore hardware non sono decodificati su nessuno dei due modelli: non è stato possibile osservarli, e mappare bit mai visti accendersi rischia di bloccare la stampa su una stampante sana.
- Il logo è disattivato per default.
scarica_logoeDOMAINfunzionano ma il logo non è mai stato disegnato sul biglietto; si abilita conTICKET_SHOW_LOGO=1, che cambia l'aspetto della stampa. uninstall_printer.shrimuove solo la vecchia regola99-sk1.rules, non99-kiosk-printer.rulesscritta dall'installer nuovo, e laDescriptionditicket-printer.servicenomina ancora la sola SK1-311.
Struttura del repository
redis_listener.py |
pub/sub Redis, filtri CUSTOMER_SITE_ID/MY_IP, statistiche, punto d'ingresso del servizio |
printer.py |
ponte fra listener e driver: sceglie la stampante, chiede il biglietto alla larghezza giusta, lo stampa. Aggiungere un modello non tocca questo file |
qr_handler.py |
generazione del QR |
health_check.py |
server HTTP :8080 con /health e /stats |
env_loader.py |
caricamento del .env |
printer_driver/ |
pacchetto driver multi-modello (README, CHANGELOG) |
install_printer.sh |
installazione e aggiornamento |
uninstall_printer.sh |
rimozione |
ticket-printer.service |
unit systemd |
test_stampante.py |
prova di stampa end-to-end |
test_protocollo.py |
prove dei comandi ESC/POS |
test_redis_integration.py |
prove del listener |
Rimozione
sudo bash uninstall_printer.sh
Ferma e disabilita il servizio, rimuove la unit e la vecchia regola udev. Vedi Limiti noti per quello che non ripulisce.