No description
  • Python 86.1%
  • Shell 13.9%
Find a file
Max e821cb0a5e docs: riscrive il README con le modifiche 2.0.0 e le raccomandazioni di campo
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.
2026-07-30 15:19:38 +02:00
printer_driver docs: riscrive il README con le modifiche 2.0.0 e le raccomandazioni di campo 2026-07-30 15:19:38 +02:00
.env.example update driver 2026-05-08 14:39:59 +02:00
.gitignore Driver unico multi-modello: CUSTOM KX60 + Sanei SK1-311 2026-07-23 14:59:08 +02:00
env_loader.py update driver 2026-05-08 14:39:59 +02:00
health_check.py update driver 2026-05-08 14:39:59 +02:00
install_printer.sh Installer: fix NEW_IP unbound e riepilogo .env 2026-07-23 16:03:03 +02:00
printer.py Driver unico multi-modello: CUSTOM KX60 + Sanei SK1-311 2026-07-23 14:59:08 +02:00
qr_handler.py update driver 2026-05-08 14:39:59 +02:00
README.md docs: riscrive il README con le modifiche 2.0.0 e le raccomandazioni di campo 2026-07-30 15:19:38 +02:00
redis_listener.py Driver unico multi-modello: CUSTOM KX60 + Sanei SK1-311 2026-07-23 14:59:08 +02:00
requirements.txt Initial commit 2026-05-08 12:57:16 +02:00
test_protocollo.py Test adattati al pacchetto unico 2026-07-23 15:01:12 +02:00
test_redis_integration.py update driver 2026-05-08 14:39:59 +02:00
test_stampante.py Test adattati al pacchetto unico 2026-07-23 15:01:12 +02:00
ticket-printer.service aggiunte a .gitignore più correzione path installazione ticket-printer.service 2026-05-18 14:09:25 +02:00
uninstall_printer.sh add uninstall script 2026-06-26 10:37:27 +02:00

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() chiamava find_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 un LF che avanzava di ~34 dot: l'immagine usciva rigata di bianco per tutta l'altezza.
  • Avanzamento di taglio con ESC J invece di GS V 66 n: sul firmware KX60 1.02 quel comando taglia ma ignora n, 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_known che 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 kernel usblp), con libusb/PyUSB solo come ripiego: niente più detach_kernel_driver e niente Resource busy fra processi che si contendono la stampante.
  • Installer multi-modello che preserva il .env esistente (Redis, password, CUSTOMER_SITE_ID, MY_IP) e ne fa il backup, e sostituisce la vecchia regola udev 99-sk1.rules con 99-kiosk-printer.rules per 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-run l'esecuzione si ferma allo step 6 perché printer-cli non esiste finché non gira il pip 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)

  1. Carica la carta e spegni la stampante.
  2. Tieni premuti insieme FEED + SELECT e accendi, poi rilascia: viene stampata l'emulazione corrente con le opzioni di modifica.
  3. Premi SELECT per cambiare, fino a leggere ESC/POS.
  4. 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:

  1. curl localhost:8080/health — Redis connesso? stampante vista?
  2. journalctl -u ticket-printer -n 200 — cerca Ticket ignorato (dlq_ip_printer ... != MY_IP ...): è il filtro, non la stampante.
  3. CUSTOMER_SITE_ID nel .env combacia con il sito che genera i biglietti?
  4. 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.

  • LPGETSTATUS non è usato da nessuna parte. Il driver si affida solo alle query ESC/POS. Con quelle mute is_ready() ritorna True per scelta progettuale (una stampante che non espone lo stato non va dichiarata guasta), quindi l'health-check dice "printer": true a 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_halt né reset nel driver: UsbTransport.close() fa solo release/attach/dispose. Il reset va dato a mano come sopra.
  • /health non 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_logo e DOMAIN funzionano ma il logo non è mai stato disegnato sul biglietto; si abilita con TICKET_SHOW_LOGO=1, che cambia l'aspetto della stampa.
  • uninstall_printer.sh rimuove solo la vecchia regola 99-sk1.rules, non 99-kiosk-printer.rules scritta dall'installer nuovo, e la Description di ticket-printer.service nomina 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.