La versión nueva (TiprePOS20260702) corrige las 4 causas raíz del incidente de junio —
verificado línea por línea. Este documento cubre lo que todavía puede fallar: 3 auditorías adversariales
(BLoC nuevo, seam Dart↔nativo del plugin, cadena de provisioning VID/PID), con cada hallazgo verificado contra
el código fuente antes de entrar acá.
connect() busca la impresora por vendor/product ID, no por ruta — sobrevive la re-enumeración lp0→lp1 que dejaba el equipo "fuera de servicio" hasta reiniciar._safeCloseUsbPort() ahora llama closeUsbPort() (antes estaba comentado).paperOut incluso online._confirmPrintedAfterSend() relee el estado después de enviar; "éxito" ya no es "bytes aceptados". El loop de vouchers NO reintenta un voucher sin confirmar (evita re-bufferizar duplicados)._portLockTail serializa monitoreo e impresión.La confirmación post-envío — el fix más importante — se apoya en una capa nativa que puede bloquear el thread principal de la UI y devolver bytes de estado contaminados. Peor: la confirmación hace 3 escrituras de estado justo después de llenar el buffer de la impresora con un ticket completo — el momento exacto de máxima exposición al bloqueo. La feature nueva amplifica la debilidad vieja. Por eso el plan endurece el plugin PRIMERO.
open() con O_NONBLOCK comentado; send_command_to_usb es un loop de
write() bloqueante; los method calls se atienden directo en el main loop (sin g_task ni thread).
Si la impresora asserta BUSY (buffer lleno, tapa abierta, offline), write() bloquea → UI congelada.
El .timeout() de Dart NO cancela el código nativo: tras el timeout, Dart sigue pero el thread nativo queda
trabado y toda llamada posterior al plugin se encola detrás. Congelamiento total hasta que la impresora se recupere.
printer_plugin/linux/ti_printer_plugin.cc:189 (O_NONBLOCK comentado) · :220–261 (write loop) · :524–529 (main loop, sin offload)
O_NONBLOCK + poll(POLLOUT) con deadline (~5s) → false en vez de bloquear para siempre.read_status_usb: envía DLE EOT → select() 500ms → UN read(). Si la respuesta llega
tarde (>500ms), queda en el buffer del kernel y se devuelve como respuesta del siguiente comando
(ej: la respuesta de papel EOT 4 leída como causa-offline EOT 2). No hay drain/flush del input antes de enviar.
Esto corrompe exactamente las lecturas de las que dependen validateBeforeCriticalPoint y la confirmación post-envío.
printer_plugin/linux/ti_printer_plugin.cc:270–316
_confirmPrintedAfterSend() hace UNA lectura de estado, sin ventana de gracia. Dos modos de falla verificados:
Resultado: el operador ve error, reimprime → ticket duplicado. Es el modo de falla del incidente de junio, ahora vía el loop humano.
printer_bloc.dart:2042–2051 (single-shot) · :1503–1527 (recibo) · ~:1739 (voucher) · tmt20iiil_status_interpreter.dart:31–32,122–123 (debounce ×6)
_confirmPrintedAfterSend y validateBeforeCriticalPoint hacen add(StatusUpdatedEvent(status)).
Un estado transitorio con error rutea a _markConnectionLost → disconnected + reconexión, con una impresión en vuelo.
En el loop de vouchers eso aborta el lote sin registro de cuáles salieron de verdad.
printer_bloc.dart:2045 · :2060 · handler _onStatusUpdated
printStatus == printing, no marcar conexión perdida por errores transitorios (diferir o exigir N consecutivos), o no emitir StatusUpdatedEvent desde el path de confirmación.El connect nuevo exige VID/PID válido (correcto), pero: _parseHexId('UNKNOWN') devuelve null sin loguear;
el mensaje al operador es "Impresora fuera de servicio…" — suena a impresora rota, no a config; y la pantalla de settings
muestra idVendor/idProducto crudos sin indicador de validez. Un técnico va a cambiar la impresora en vez de corregir la config.
Gate de rollout: si la flota no está provisionada con VID/PID hex reales, esta versión pasa de "imprime (a veces mal)" a "no conecta".
apply_terminal_configuration.dart:843–848 (_parseHexId silencioso) · :694–708 (aborta y reporta, mensaje genérico) · tab_printer.dart:71–78 (sin badge)
PrinterDeviceInfo.fromMap usa casts as int/as String: un tipo inesperado del lado nativo
crashea fuera del try-catch de getUsbPrinters (que solo atrapa PlatformException).
printer_plugin/lib/printer_device_info.dart:27–34
Sin tests: _parseHexId, rechazo de connect sin VID/PID, lógica de confirmación, byte de papel del T20IIIL,
contaminación/timeout de estado, mapeo de errores del canal. El comportamiento que arregló el incidente no tiene red de regresión.
_logInfoOnce/_logWarnOnce comparten UNA key (_lastOperationalLogKey) en bloc y servicio USB — keys intercaladas suprimen logs que se necesitan para debuggear en campo. Separar keys.isClosed tras awaits largos en los handlers de impresión; el guard anti doble-dispatch de management_ticket es un field del widget, no estado del bloc.fsync() sobre char device devuelve EINVAL ignorado — cosmético, remover.read() se limita al tamaño del buffer; es truncamiento inofensivo y las respuestas DLE EOT son de 1 byte.deviceInstanceId, Uint8List crudo); errores nativos → PlatformException → false (patrón correcto); timers del bloc se cancelan en close()._portLockTail en Dart.O_NONBLOCK; loop de escritura con poll(POLLOUT) + deadline (~5s) → false en timeout. Manejar EAGAIN.fromMap defensivo en printer_device_info.dart._confirmPrintedAfterSend reintenta el estado hasta ~3 veces / 2–3s; éxito apenas isReadyToPrint; error solo al agotar la ventana.printStatus == printing, no ejecutar _markConnectionLost por errores transitorios (o no emitir StatusUpdatedEvent desde la confirmación)._parseHexId: warn-log cuando un valor no vacío no parsea ('UNKNOWN' incluido)._parseHexId; rechazo de connect; gracia de confirmación (servicio fake busy→ready); byte de papel + debounce del T20IIIL; fromMap malformado; mapeo de errores del canal.isClosed; guard de dispatch por estado del bloc en management_ticket; serial marcado "no soportado en Linux".| # | Escenario | Resultado esperado |
|---|---|---|
| 1 | Ticket largo (QR pesado) | Confirmación exitosa sin falso "no se confirmó"; la UI nunca se congela. |
| 2 | Sin papel a mitad de un lote de 3 vouchers → reponer papel | Voucher fallido reportado con "verifique antes de reimprimir"; sin ráfaga de duplicados bufferizados al recuperar. |
| 3 | Abrir/cerrar tapa (oscilación) e imprimir inmediatamente | Sin falso error por la ventana ciega del debounce ×6. |
| 4 | Desenchufar USB en medio de un print → re-enchufar | App responsiva (sin freeze del main thread); reconexión por VID/PID sin reiniciar la app. |
| 5 | Terminal con idVendor='UNKNOWN' | Error de configuración accionable y visible en UI/settings — no un genérico "fuera de servicio". |
| 6 | Soak 1h: monitoreo continuo + impresiones repetidas | Sin starvation del port-lock; sin lecturas de estado contaminadas en los logs. |
Unit: flutter test test/viewmodels/printer test/models/printer (app) · flutter test en printer_plugin/ (canal + parsing).
Las cuatro fases del plan se implementaron con TDD estricto (test rojo primero,
después el fix). Evidencia: 290/290 tests de la app + 17/17 tests del plugin en verde,
flutter analyze limpio en todos los archivos tocados.
| Fase | Qué se arregló | Archivos clave | Tests nuevos |
|---|---|---|---|
| A plugin 1.0.16→1.0.17 |
W1: todo el I/O USB corre en un thread dedicado (pool FIFO de 1 + respuesta async vía g_idle_add) — una impresora en BUSY ya no congela la UI.W1: open() con O_NONBLOCK | O_NOCTTY (antes comentado); escrituras acotadas con poll(POLLOUT) + deadline (5s datos / 1s estado) → error en vez de colgar.W2: drain de input antes de cada comando DLE EOT — la causa raíz que 1.0.15 parchaba en Dart con status.last y gaps de 80ms.W6: fromMap defensivo + filtrado de entradas malformadas en getUsbPrinters.W11: fsync() sobre char device eliminado.
|
linux/ti_printer_plugin.cc lib/printer_device_info.dart lib/ti_printer_plugin_method_channel.dart CHANGELOG + pubspec |
6 fromMap +1 canal (17/17 total) |
| B app |
W3: ventana de gracia en _confirmPrintedAfterSend (3 intentos × 900ms, inyectable por constructor); nuevo checkStatusForConfirmation() en PrinterService y los 4 servicios; interpretConfirmation() en el T20IIIL = lectura fresca sin la ventana ciega del debounce ×6 (sigue alimentando el contador del monitoreo).W4: la confirmación ya no emite StatusUpdatedEvent; con una impresión en vuelo, _onStatusUpdated difiere los errores transitorios (offline/comunicación) — deviceNotFound sigue cortando.UX anti-duplicado: "No se confirmó la impresión (…). Verifique si el ticket salió impreso antes de reimprimir" en recibo único y vouchers. |
printer_bloc.dart status_interpreter.dart tmt20iiil_status_interpreter.dart services/printer/*.dart |
4 bloc-confirm 6 interpreter |
| C config |
W5: HexIdParser público y testeable — un valor no parseable ('UNKNOWN') ahora loguea warning en vez de fallar en silencio; validación pre-vuelo (PASO 2.5) en apply_terminal_configuration con mensaje accionable: «Configuración de impresora inválida: idVendor="UNKNOWN"… Corrija la configuración del terminal en el backend (ej: 0x04B8)» — ya no el genérico "fuera de servicio" que hacía cambiar impresoras sanas; badge "⚠ INVÁLIDO" junto a Vendor/Product id en settings.
|
core/helpers/hex_id_parser.dart (nuevo) routing/apply_terminal_configuration.dart views/settings/widgets/tab_printer.dart |
10 parser |
| D higiene |
W8: keys de log-once separadas por nivel (info/warn) en bloc y servicio USB — ya no se suprimen logs entre sí. W9: 7 guards isClosed tras awaits largos en los handlers de impresión + guard en _scheduleClearPrintChip; los guards anti doble-dispatch de management_ticket ahora consultan también printerBloc.state.printStatus (no solo el flag del widget).W10: serial marcado "⚠ NO SOPORTADO EN ESTE EQUIPO" en settings. W7: resuelto por la suite completa de esta implementación. |
printer_bloc.dart usb_printer_service.dart management_ticket_page.dart tab_printer.dart |
cubierto por suites A–C |
.cc nuevo NO se compiló acá (máquina Windows, target Linux POS) — el build de ustedes es el gate de compilación. Después: publicar ti_printer_plugin 1.0.17 y subir el pubspec de la app (hoy consume ^1.0.14 de pub.dev).