Subsistema de impresión¶
La impresora es el punto donde más cosas pueden salir mal en una terminal desatendida: se queda sin papel, se abre la tapa, se desconecta el Bluetooth, se cae el USB. Por eso la impresión en TiprePOS es un subsistema completo —no un print()—, con cuatro tipos de conexión, monitoreo de estado, reconexión automática con backoff e impresión en lote resiliente. Todo orquestado por PrinterBloc (lib/viewmodels/printer/printer_bloc.dart, ~2000 líneas).
El código es la fuente de verdad
Reproducimos estados, eventos y el algoritmo de reconexión tal como están a la fecha. Los detalles finos viven en PrinterBloc y lib/services/printer/.
Los cuatro tipos de conexión¶
Un PrinterServiceFactory crea la implementación según PrinterConnectionType:
| Tipo | Implementación | Notas |
|---|---|---|
usb |
UsbPrinterService |
Puerto USB/COM. |
bluetooth |
BluetoothPrinterService |
Búsqueda por MAC, no soporta monitoreo de estado por comando. |
network |
NetworkPrinterService |
TCP IP:puerto. |
serial |
SerialPrinterService |
Puerto serie tradicional. |
Todas implementan la misma interfaz PrinterService: getAvailablePrinters(), connect(config), disconnect(), checkStatus(), sendRawData(bytes), dispose(), isBluetoothEnabled(). El resto del sistema no sabe qué tipo de conexión hay debajo.
Estados¶
enum PrinterConnectionStatus { notInitialized, disconnected, connecting, connected, error }
enum PrintStatus { idle, printing, success, error }
enum PrinterErrorType { paperOut, coverOpen, offline, communicationError, deviceNotFound, unknown }
El PrinterBlocState lleva, entre otros: la lista de impresoras, la seleccionada, el PrinterConnectionStatus, el PrintStatus, el tipo de conexión, usePrinter (SI/NO de config), isMonitoring, la MAC Bluetooth persistida, y el progreso de impresión en lote (currentVoucherIndex/totalVouchers).
Ciclo de vida de la conexión¶
stateDiagram-v2
[*] --> disconnected: LoadPrinters
disconnected --> connecting: ConnectPrinter / autoReconnect
connecting --> connected: connect() ok
connecting --> error: agota reintentos
connected --> disconnected: desconexión detectada
disconnected --> connecting: backoff exponencial
connected --> connected: StartMonitoring (status cada 3s)
- Carga (
LoadPrintersEvent): siusePrinter != 'SI', no hace nada. Para Bluetooth, recupera la MAC guardada e inicia reconexión; para USB/Serial/Network, busca dispositivos y auto-selecciona el primero. - Conexión (
ConnectPrinterEvent): arma la config, llamaconnect(), y si tiene éxito, hacecheckStatus(), persiste la MAC (Bluetooth) y arranca el monitoreo si la conexión lo soporta.
Monitoreo de estado (timer adaptativo)¶
Si la conexión soporta status (USB/Serial/Network), un Timer.periodic(3s) dispara CheckStatusEvent, que pregunta a la impresora su estado ESC/POS (sin papel, tapa abierta, etc.). Cada ~15 s, además, chequea el estado del radio Bluetooth.
Bluetooth no se puede monitorear por comando
BluetoothPrinterService no soporta el chequeo de estado ESC/POS, así que para Bluetooth el monitoreo se reduce a detectar desconexión del radio. Es una limitación del transporte, no un bug.
Reconexión automática con backoff exponencial¶
Cuando se detecta una desconexión o falla una conexión, el bloc reintenta solo, con delay creciente para no martillar:
- USB/Serial/Network: busca dispositivos, auto-selecciona, reintenta con el delay exponencial.
- Bluetooth: busca el dispositivo por la MAC guardada; reintenta hasta 3 veces (
bluetoothReconnectMaxAttempts); si no aparece, pide reconfiguración (requiresPrinterConfiguration).
La MAC del Bluetooth se persiste localmente (callback onBluetoothMacChanged) para poder reconectar sin que el cliente vuelva a parear.
Impresión de un recibo¶
PrintReceiptEvent { type, data, currentVoucherIndex } — data es el ticket ya formateado en Base64. El handler es deliberadamente cuidadoso:
graph TD
A["¿impresión en curso? → ignora"] --> B["¿datos vacíos / usePrinter=NO? → éxito silencioso"]
B --> C["¿impresora desconectada? → error"]
C --> D["pausar monitoreo<br/>(evita colisión de comandos)"]
D --> E["printStatus = printing"]
E --> F["validar conexión post-async"]
F --> G["decodificar Base64 → sendRawData"]
G --> H{ok?}
H -->|sí| OK["printStatus = success"]
H -->|no| ERR["printStatus = error"]
OK --> I["limpiar chip tras 3s + reanudar monitoreo"]
ERR --> I
El detalle clave: se pausa el monitoreo antes de imprimir (esperando confirmación, hasta 500 ms) para que un checkStatus no se cruce con el envío del recibo en el mismo canal, y se reanuda después con un segundo de estabilización.
ReceiptType distingue ticket, voucher, vale, factura.
Impresión en lote (StartPrintingVouchers)¶
Cuando hay varios comprobantes (ticket + vouchers de pago + vale), StartPrintingVouchers { vouchers, stopOnError } los imprime en secuencia:
- Antes de cada voucher chequea que la impresora siga conectada (si se perdió, corta y reporta).
- Espera ~300 ms entre comprobantes.
- Acumula fallos: si
stopOnError, corta en el primero; si no, sigue e informa al final cuántos salieron y cuáles fallaron ("Parcial: 2 exitosos…").
El resultado parcial importa
En autoservicio, que salgan 2 de 3 comprobantes no es lo mismo que fallar todo. El subsistema distingue éxito total, fallo total y parcial — y lo comunica. No asumas binario.
Test y diagnóstico¶
PrintTestPageEvent→ patrón de prueba ESC/POS (buildTestTicketBytes).- "Solo QR" → imprime únicamente el QR (
buildQrTicketBytes). - Ambos pasan por
_runManualPrintJob: mismas guardas (no imprimir si hay un job en curso, pausar/reanudar monitoreo).
La pantalla management_printer (lib/views/management_printer/) expone todo esto al operador: elegir tipo de conexión, listar/seleccionar dispositivo, conectar/desconectar, ver estado en tiempo real, test page y gestión de la MAC Bluetooth.
Issue gemelo en el backend
Del lado del backend, VoucherService tiene un problema de thread-safety conocido (plan 007) en la config de impresora compartida entre requests. Son dos capas distintas (formateo en el backend, envío físico en la terminal); si depurás un voucher mal impreso, fijate en cuál de las dos está el problema.