Revisión de TiprePOS20260716 + ti_printer_plugin-main (v1.1.4) contra el
fix de estabilización W1–W11 del 02 Jul. Cada afirmación de este doc está verificada leyendo el código y
corriendo la suite: 155/155 tests en verde en el árbol nuevo.
O_NONBLOCK, escrituras acotadas, drain) está intacto — cero deleciones; construyeron encima (+265 líneas).printer_bloc.dart:2135), supresión de errores transitorios con impresión en vuelo (:2057), interpretConfirmation (base + T20IIIL), guards isClosed, guards anti doble-dispatch por estado del bloc (management_ticket:1106,1154), keys de log separadas.HexIdParser (recortaron docs, comportamiento e isValid intactos), validación pre-vuelo, badges "⚠ INVÁLIDO", serial "NO SOPORTADO" — todo presente.asb_status_test.dart. Total 155/155 en verde.Para TM-T88V y TM-T20IIIL, la impresora ahora empuja frames de estado de 4 bytes ante cada cambio (papel, tapa, error) — un loop de lectura cada 60ms los captura. RPT008/006B quedan en DLE EOT con razón documentada (los adaptadores USB-serial no generan tráfico ASB). Detalles que muestran criterio:
ESC @ — el init resetea el modo GS a; lo pensaron.read_status_usb_detailed, retry ×3): distingue respuesta DLE coherente (1 byte) de frame ASB (patrón de 4 bytes) y lo descarta en vez de leerlo como respuesta equivocada — la extensión natural del drain del fix anterior, con schema idéntico a Windows (success / timeout / discarded_asb / discarded_ambiguous / transport_error).paperEnd priorizado sobre coverOpen en el mapeo ASB, con comentario explicando el porqué (al agotarse el papel algunas impresoras reportan ambos).ti_printer_plugin-main/linux/ti_printer_plugin.cc (+265 sobre 1.0.17) · lib/models/printer/asb_status.dart · usb_printer_service.dart:664–762 (loop + pausa) · status_interpreter_factory.dart:33 (supportsAsb)
El fix de la Causa D estableció: "éxito" = releer la impresora DESPUÉS de enviar. Con ASB activo, la confirmación ahora devuelve un snapshot cacheado — que puede ser de antes del envío. En el escenario exacto del incidente de junio (ticket largo + fin de rollo a mitad de la alimentación), el sistema puede volver a decir "impreso correctamente" sin que salga papel. Es el peor modo de falla para un POS de autoservicio: el cliente se va sin su comprobante y nadie se entera.
_readAndInterpretStatus(confirmation: true) devuelve el cache ASB sin consultar el dispositivo:
primero el "fresco" (≤12s) y, si no hay, el último conocido sin límite de edad — el log lo dice literal:
"Confirmacion usando estado ASB fresco, sin consultar DLE". El fallback a lectura DLE real existe
(:474) pero solo corre cuando no hay cache.
sendRawData(ticket) — pausa el loop ASB, escribe, reanuda. Bytes aceptados ✓_confirmPrintedAfterSend() intento 1 → cache ASB con timestamp PRE-envío ("con papel") → isReadyToPrint == truePrintStatus.success → UI: "Comprobante impreso"* la confirmación corre inmediatamente después de que el send retorna — antes de que el loop (60ms + cola del
port-lock) alcance a leer el frame pendiente.
usb_printer_service.dart:416–432 (cache primero, "sin consultar DLE") · :424–432 (acepta "último conocido" sin límite de edad)
· :474 (fallback DLE solo si NO hay cache) · no existe tracking de _lastSendCompletedAt (verificado por grep).
_lastSendCompletedAt al terminar cada
sendRawBytes, y en el path de confirmación aceptar el cache ASB solo si su timestamp es posterior al
último envío; si no, caer a la lectura DLE que ya existe. Alternativa equivalente: para confirmation: true
invertir el orden — DLE primero, cache como fallback (el clasificador nativo ya maneja la colisión DLE/ASB). El monitoreo
puede seguir usando el cache tal como está. Test de regresión: fake service con cache "listo" pre-envío + frame
"sin papel" post-envío → la confirmación debe reportar error, no éxito.Dentro del mismo path de N1: si el cache "fresco" (≤12s) expiró, la confirmación acepta _getLatestAsbStatus()
— un estado sin cota de antigüedad (podría ser de minutos atrás si el loop se detuvo). Cualquier solución de N1
debería eliminar también este segundo escalón para el path de confirmación.
usb_printer_service.dart:424–432
Entre conectar y el primer frame ASB, checkStatus devuelve "Esperando estado de la impresora"
(hasError: true) → una impresión disparada en esa ventana falla el pre-vuelo con "Impresora no lista".
En la práctica es corta (GS a transmite el estado actual al habilitarse) y el watchdog de 15s la acota — pero
conviene observarlo en banco: si aparece "Impresora no lista" justo después de reconectar, es esto.
usb_printer_service.dart:434–461
_portLockTail serializa
igual todas las llamadas al plugin → sin corrupción posible. OK.b3 & 0x0C fin de rollo, b3 & 0x03 near-end) correcto según spec ESC/POS. OK.| # | Acción | Costo | Riesgo si no se hace |
|---|---|---|---|
| 1 | Cerrar N1/N2: confirmación solo acepta cache ASB post-envío (o DLE primero en confirmación) + test de regresión — código propuesto completo en §06 | ~1 archivo, <30 líneas + test | "Impreso correctamente" falso con fin de rollo a mitad de ticket — el modo de falla del incidente de junio, reaparecido |
| 2 | Banco: escenario específico — ticket largo + quitar papel DURANTE la alimentación, con TM-T20IIIL/T88V (modelos ASB) | 1 sesión de banco | N1 no se detecta con los escenarios de la matriz anterior (que validaban el path DLE) |
| 3 | Banco: observar warm-up post-reconexión (N3) | incluido en #2 | "Impresora no lista" esporádico tras reconectar, sin explicación |
TiprePOS20260716/lib/services/printer/usb_printer_service.dart (los bloques "antes" son copia literal).
Aplicar con TDD: primero el test del Cambio 6 (debe fallar en rojo), después los cambios 1–5 (verde).Idea del fix en una línea: un estado ASB solo puede confirmar una impresión si fue recibido después de que el envío terminó. Se registra el instante del envío y la confirmación exige cache post-envío; si no hay, cae a la lectura DLE real que ya existe. El monitoreo no cambia.
PrinterStatus? _lastAsbStatus; DateTime? _lastAsbStatusAt; // FIX N1: instante en que terminó el último envío de datos. Un estado // ASB solo puede CONFIRMAR una impresión si es posterior a este momento. DateTime? _lastSendCompletedAt;
sendRawBytes — sellar el instante del envío (~línea 575)final success = await _withPausedAsbReads(() { return _pluginCall( () => _plugin.sendCommandToUsb(payload), timeout: _sendTimeout, ); }); if (success) { // FIX N1: a partir de acá, solo un frame ASB más nuevo que este // instante puede dar fe del estado POST-impresión. _lastSendCompletedAt = DateTime.now(); } if (success && _payloadResetsPrinter(payload)) { await Future.delayed(_postInitAsbDelay); await _attemptEnableAsbIfSupported(); } return success;
_getFreshAsbStatus() (~línea 115) /// Cache ASB válido para CONFIRMAR una impresión (Causa D): además de
/// fresco, su timestamp debe ser POSTERIOR al último envío completado.
/// Un snapshot pre-envío diría "con papel" aunque el rollo se haya
/// agotado a mitad del ticket.
PrinterStatus? _getPostSendAsbStatus() {
final status = _getFreshAsbStatus();
final at = _lastAsbStatusAt;
if (status == null || at == null) return null;
final sentAt = _lastSendCompletedAt;
if (sentAt != null && !at.isAfter(sentAt)) return null;
return status;
}_readAndInterpretStatus — el bloque de cache (líneas 416–432)// ── ANTES (el problema N1/N2) ────────────────────────────────────────── final freshAsbStatus = _getFreshAsbStatus(); if (freshAsbStatus != null) { if (confirmation) { _log('Confirmacion usando estado ASB fresco, sin consultar DLE'); } return freshAsbStatus; } final lastAsbStatus = _getLatestAsbStatus(); if (lastAsbStatus != null) { if (confirmation) { _log('Confirmacion usando ultimo estado ASB conocido, sin consultar DLE'); } else { _log('Monitoreo ASB activo: sin novedades en la impresora'); } return lastAsbStatus; } // ── DESPUÉS ──────────────────────────────────────────────────────────── if (confirmation) { // FIX N1/N2: para confirmar una impresión solo sirve un estado ASB // recibido DESPUÉS del envío. Sin cache post-envío se sigue de // largo a la lectura DLE real de más abajo (el clasificador nativo // ya descarta frames ASB que colisionen con la respuesta DLE). final postSendStatus = _getPostSendAsbStatus(); if (postSendStatus != null) { _log('Confirmacion usando estado ASB posterior al envio'); return postSendStatus; } } else { final freshAsbStatus = _getFreshAsbStatus(); if (freshAsbStatus != null) { return freshAsbStatus; } final lastAsbStatus = _getLatestAsbStatus(); if (lastAsbStatus != null) { _log('Monitoreo ASB activo: sin novedades en la impresora'); return lastAsbStatus; } }
// El mismo cambio en ambos sitios: final fallbackAsbStatus = confirmation ? _getLatestAsbStatus() // ← sin límite de edad (N2) : _getFreshAsbStatus(); final fallbackAsbStatus = confirmation ? _getPostSendAsbStatus() // ← solo post-envío (N1/N2) : _getFreshAsbStatus();
test/services/printer/usb_printer_service_confirm_test.dart (archivo NUEVO)// Esqueleto orientativo: la escena exacta de N1. El servicio acepta un // TiPrinterPlugin inyectado (constructor `plugin:`), así que se fakea el // canal completo sin tocar nativo. Verificar los bytes ASB contra // AsbStatus.tryParse antes de dar por bueno el fake. import 'dart:typed_data'; import 'package:auto_compra/services/printer/usb_printer_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ti_printer_plugin/ti_printer_plugin.dart'; /// Frame ASB "con papel" : b1=0x10 (patrón estándar), b3 sin bits de papel. final asbPaperOk = Uint8List.fromList([0x10, 0x00, 0x00, 0x00]); /// Frame ASB "sin papel" : b3 & 0x0C == 0x0C (fin de rollo). final asbPaperOut = Uint8List.fromList([0x10, 0x00, 0x0C, 0x00]); class FakeAsbPlugin implements TiPrinterPlugin { bool sent = false; @override Future<bool> sendCommandToUsb(Uint8List command) async { sent = true; // el papel "se acaba" durante el envío return true; // bytes aceptados igual (Causa D) } @override Future<Uint8List> readFromUsb() async { // Antes del envío el loop ASB cachea "con papel"; después del envío // la impresora emite "sin papel". return sent ? asbPaperOut : asbPaperOk; } // openUsbPort/enableAsb => true · getUsbPrinters => device con el // VID/PID configurado · readStatusUsbDetailed => DLE "sin papel" // (0x72) cuando sent==true · resto: stubs. // ... } void main() { test('confirmación NO acepta cache ASB pre-envío (regresión N1)', () async { final plugin = FakeAsbPlugin(); final service = UsbPrinterService( vendorId: 0x04B8, productId: 0x0E28, plugin: plugin, ); await service.connect(/* config USB del fake */); await Future<void>.delayed(const Duration(milliseconds: 200)); // precondición: el loop ASB ya cacheó "con papel" (pre-envío) final ok = await service.sendRawData(Uint8List.fromList([0x1B, 0x40])); expect(ok, isTrue); // bytes aceptados — eso NUNCA fue "impreso" final confirm = await service.checkStatusForConfirmation(); // SIN el fix: devuelve el cache pre-envío ("con papel") => ready => // falso "impreso correctamente". CON el fix: exige cache post-envío // o lee DLE real => reporta el sin-papel. expect(confirm.isReadyToPrint, isFalse, reason: 'la confirmación debe reflejar el estado POST-envío'); }); }
confirmation: true se endurece.ESC @): la impresora no emite frame post-envío → la confirmación cae a la lectura DLE real — el comportamiento verificado del fix 20260702. Cero regresión.ESC @: el re-enable de ASB dispara la transmisión inmediata del estado actual → el cache post-envío aparece en ~100ms y la confirmación lo aprovecha (más barato que 3 lecturas DLE)._lastAsbStatus == null, y acá el cache existe (es pre-envío) → se sigue de largo al DLE.| Checklist de aplicación (TDD) |
|---|
1. Crear el test del Cambio 6 → flutter test → debe FALLAR (rojo) reproduciendo N1. |
2. Aplicar cambios 1–5 en usb_printer_service.dart → el test pasa (verde). |
3. Regresión completa: flutter test test/models/printer test/viewmodels/printer test/core/helpers test/services — hoy 155/155, deben seguir verdes. |
| 4. Banco: ticket largo + quitar papel DURANTE la alimentación en TM-T20IIIL/T88V → debe reportar error con "Verifique si el ticket salió impreso", nunca "Comprobante impreso". |