22 hallazgos en total. Cada uno con archivo, línea y un fragmento antes / después.
Los fragmentos están reducidos a lo mínimo necesario para mostrar la diferencia;
la versión completa de cada archivo parcheado va en la sección siguiente.
PLG-01
Crítico
handle USB · construcción
hUsb_ no se inicializa en el constructor
windows/ti_printer_plugin.cpp · línea 48
El constructor sólo inicializa hSerial_. hUsb_ queda con basura de memoria, por lo que el check if (hUsb_ != INVALID_HANDLE_VALUE) del destructor casi nunca da falso y termina ejecutando CloseHandle() sobre un valor indeterminado. Además, si alguien llama a sendCommandToUsb antes de openUsbPort, el check de "puerto no abierto" puede fallar y entrar al WriteFile sobre el handle basura.
Antes
TiPrinterPlugin()
: hSerial_(INVALID_HANDLE_VALUE) {}
Después
TiPrinterPlugin()
: hSerial_(INVALID_HANDLE_VALUE),
hUsb_(INVALID_HANDLE_VALUE) {}
PLG-02
Crítico
I/O · lectura de estado
ReadStatusUsb y ReadStatusSerial truncan a 1 byte
windows/ti_printer_plugin.cpp · líneas 412 y 602 · linux/ti_printer_plugin.cc · línea 218
El código devuelve sólo el primer byte del buffer leído, descartando el resto. Los comandos DLE EOT 1/2/4 retornan un byte, pero ESC u y los auto status back de varias Epson devuelven streams de 2+ bytes. La versión correcta estaba comentada justo arriba.
Antes
if (bytes_read > 0) {
return { static_cast<uint8_t>(response[0]) };
}
Después
std::vector<uint8_t> result;
if (bytes_read > 0) {
result.assign(response, response + bytes_read);
}
return result;
PLG-03
Crítico
I/O · timeout USB Windows
ReadFile USB sin timeout — bloquea indefinidamente
windows/ti_printer_plugin.cpp · ReadStatusUsb (línea ~593)
El handle USB se abría sin FILE_FLAG_OVERLAPPED y la lectura era síncrona y bloqueante. Si la impresora está apagada o desconectada, ReadFile espera para siempre. Combinado con un Timer.periodic(3s) de monitor de estado, una impresora apagada freeza toda la app o apila llamadas en cola.
En Linux esto está bien resuelto con select() + timeout 500 ms. En Windows hay que abrir overlapped y usar WaitForSingleObject + CancelIoEx.
Después · OpenUsbPort + ReadStatusUsb
hUsb_ = CreateFile(..., FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
OVERLAPPED overlapped = {};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
BOOL ok = ReadFile(hUsb_, response, sizeof(response), &bytes_read, &overlapped);
if (!ok && GetLastError() == ERROR_IO_PENDING) {
DWORD wait = WaitForSingleObject(overlapped.hEvent, 500);
if (wait != WAIT_OBJECT_0) {
CancelIoEx(hUsb_, &overlapped);
...
}
}
PLG-04
Crítico
QR · longitud
QR Code limitado silenciosamente a 252 bytes de payload
lib/esc_pos_utils_platform/src/qrcode.dart · línea 60
El comando GS ( k pL pH cn fn ... codifica el tamaño del payload en dos bytes (pL + pH×256). El código hardcodea pH = 0x00 y mete textBytes.length + 3 en pL. Cualquier QR con más de 252 bytes hace overflow silencioso y la impresora interpreta basura. URLs largas de ARCA con muchos parámetros caen acá.
Antes
bytes += cQrHeader.codeUnits +
[textBytes.length + 3, 0x00, 0x31, 0x50, 0x30];
Después
final int storeLen = textBytes.length + 3;
final int pL = storeLen & 0xFF;
final int pH = (storeLen >> 8) & 0xFF;
bytes += cQrHeader.codeUnits +
[pL, pH, 0x31, 0x50, 0x30];
PLG-05
Crítico
QR · encoding
latin1.encode para datos de QR — rompe con cualquier carácter fuera de 0x00–0xFF
lib/esc_pos_utils_platform/src/qrcode.dart · línea 57
Un QR carga binario y los lectores móviles decodifican UTF-8 por estándar. Con latin1.encode, cualquier emoji, kanji o símbolo fuera de Latin-1 (un € en ciertos casos, ideogramas) tira ArgumentError. Y si el dato sí pasa, el celular puede interpretarlo distinto al servidor.
Antes
List<int> textBytes = latin1.encode(text);
Después
final List<int> textBytes = utf8.encode(text);
PLG-06
Crítico
generator · beep
beep() recursivo descarta el retorno
lib/esc_pos_utils_platform/src/generator.dart · línea 481
El comando ESC/POS ESC B n t tiene n tope en 9, por eso se hace recursión para n > 9. Pero el resultado del llamado recursivo no se concatena a bytes, así que beep(n: 20) emite sólo UNA tanda de 9 beeps.
Antes
bytes += Uint8List.fromList(...);
beep(n: n - 9, duration: duration);
return bytes;
Después
bytes += Uint8List.fromList(...);
if (n > 9) {
bytes += beep(n: n - 9, duration: duration);
}
return bytes;
PLG-07
Crítico
generator · oldRrow
oldRrow() pierde el wrap de columnas que se desborda
lib/esc_pos_utils_platform/src/generator.dart · línea 600
Si una columna excede su ancho, la fila siguiente se calcula y se acumula en nextRow, pero el llamado a row(nextRow) descarta el retorno. Resultado: el texto wrapeado no se imprime nunca. El método nuevo row() (línea 736) ya lo hace bien, pero oldRrow sigue en el API público.
Antes
if (isNextRow) {
row(nextRow);
}
Después
if (isNextRow) {
bytes += row(nextRow);
}
Sugerencia adicional: deprecar oldRrow y dejar sólo row(). El sufijo old ya da la pista.
PLG-08
Crítico
encoding · codeTable
_encode() ignora el codeTable del PosStyles
lib/esc_pos_utils_platform/src/generator.dart · línea 99
setStyles manda ESC t n a la impresora para que ésta interprete los bytes según un code page (CP437, CP858, CP1252, etc.), pero los bytes se generan SIEMPRE con latin1.encode independientemente del codeTable elegido. Por suerte Latin-1 es compatible byte-a-byte con CP1252, CP850, CP858 e ISO_8859-15 para los acentos comunes del español. Pero con CP437 (default histórico):
- ñ en CP437 va en 0xA4 — acá se manda 0xF1 que en CP437 es ±
- ° en CP437 va en 0xF8 — acá se manda 0xB0 que en CP437 es ░
Footgun real: el dev tiene que acordarse de poner codeTable: 'CP1252' en CADA PosStyles con tildes. Si una se olvida, sale quilombo. El default de PosStyles.defaults es CP437, lo que es engañoso porque no concuerda con el encoding.
Fix bajo: documentar la limitación + cambiar el default a CP1252.
Fix correcto: reemplazar latin1.encode por una conversión basada en el codeTable activo (paquete charset_converter o equivalente). Refactor de medio día.
PLG-09
Crítico
generator · lexemes
_getLexemes("") tira RangeError
lib/esc_pos_utils_platform/src/generator.dart · línea 120
Accede a text[0] sin chequear si el string está vacío. Si llega un PosColumn(text: '', ...) o un wrap que deja restos vacíos, explota.
Antes
List _getLexemes(String text) {
...
bool curLexemeChinese = _isChinese(text[0]);
...
}
Después
List _getLexemes(String text) {
...
if (text.isEmpty) {
return <dynamic>[lexemes, isLexemeChinese];
}
bool curLexemeChinese = _isChinese(text[0]);
...
}
PLG-10
Crítico
generator · precedencia
_intLowHigh con paréntesis mal en la cota máxima
lib/esc_pos_utils_platform/src/generator.dart · línea 148
Por precedencia de operadores, la expresión se evalúa como 256 << ((bytesNb * 8) - 1), dando para bytesNb = 2 un máximo de 8.388.608 en lugar de 65.535. La validación es 128× más permisiva y el bucle posterior trunca silenciosamente los bytes excedentes. Imágenes muy grandes pueden generar headers con tamaño truncado.
Antes
final dynamic maxInput = 256 << (bytesNb * 8) - 1;
Después
final dynamic maxInput = (256 << (bytesNb * 8)) - 1;
PLG-11
Latente
generator · dead code
Condiciones imposibles dentro de row()
lib/esc_pos_utils_platform/src/generator.dart · líneas 661–687
Dentro de if (realCharactersNb > maxCharactersNb) aparecen ternarios del estilo realCharactersNb < maxCharactersNb ? ... : ... cuya rama "verdadero" es inalcanzable. Después una asignación isNextRow = true redundante. Funciona pero confunde y hace pensar que falta un caso.
Después · simplificado
if (realCharactersNb > maxCharactersNb) {
final Uint8List encodedToPrintNextRow =
encodedToPrint.sublist(maxCharactersNb);
encodedToPrint = encodedToPrint.sublist(0, maxCharactersNb);
isNextRow = true;
nextRow.add(PosColumn(
textEncoded: encodedToPrintNextRow,
width: cols[i].width,
styles: cols[i].styles));
bytes += _text(encodedToPrint, ...);
}
PLG-12
Latente
generator · round-trip
String.fromCharCodes(bytes).trim() para pasar el wrap a la fila siguiente
lib/esc_pos_utils_platform/src/generator.dart · línea 671
Toma los bytes ya encodeados, los pasa a String con fromCharCodes (que interpreta cada byte como codepoint Unicode), y la siguiente iteración los vuelve a encodear. Funciona porque Latin-1 ≡ codepoints 0–255, pero:
- Si
_encode deja de ser Latin-1 (ver #08), rompe en silencio.
- El
.trim() al final se come espacios intencionales en columnas con alineación a la derecha.
Mejor pasar el textEncoded directo y evitar el round-trip — el PosColumn ya soporta textEncoded como parámetro.
PLG-13
Latente
generator · drawImage
Asignación dentro del ternario en drawImage
lib/esc_pos_utils_platform/src/generator.dart · línea 1061
Funciona por casualidad pero es código ofuscado. La línea siguiente (dstH) ya es la versión limpia — esta no debería disonar.
Antes
dstW ??= (dst.width < src.width)
? dstW = dst.width
: src.width;
Después
dstW ??= (dst.width < src.width)
? dst.width
: src.width;
PLG-14
Latente
method channel · args
Inconsistencia en el shape de argumentos del MethodChannel
lib/ti_printer_plugin_method_channel.dart
Algunas llamadas pasan el comando como argumento posicional (Uint8List directo) y otras lo envuelven en un Map:
sendCommandToSerial · sendCommandToUsb → mandan command directo
readStatusSerial · readStatusUsb → mandan { 'command': command }
El nativo refleja esa inconsistencia. Cualquiera que extienda el API se va a equivocar. Unificar a Map con nombres explícitos da espacio para crecer sin breaking changes.
PLG-15
Latente
WriteFile · partial write
SendCommandToUsb no reintenta escritura parcial
windows/ti_printer_plugin.cpp · SendCommandToUsb
Detecta bytes_written != data_size pero sólo marca como error, sin re-enviar el remanente. Para drivers usbprint es raro, pero si sucede dejás media factura en el stream y la impresora queda en un estado feo. En Linux esto está bien hecho con un while (left > 0) (línea 139).
Después · Windows con loop
while (remaining > 0) {
...
BOOL ok = WriteFile(hUsb_, ptr, remaining, &bytes_written, &overlapped);
...
if (bytes_written == 0) return false;
ptr += bytes_written;
remaining -= bytes_written;
}
PLG-16
Latente
enumeración · Windows
ListUsbInstance filtra sólo service == "usbprint"
windows/ti_printer_plugin.cpp · ListUsbInstance
Excluye impresoras con driver propietario: Epson Advanced Printer Driver, EpsonNet, OPOS, POS for .NET. En entornos POS Argentina muchas TM-T20III usan el driver de Epson y no aparecen en la lista.
Camino sugerido: agregar fallback enumerando por Class GUID = {4d36e979-e325-11ce-bfc1-08002be10318} (printers) o devolver todos los USB y exponer VID/PID para que el caller filtre.
PLG-17
Latente
linux · serial stub
Linux serial devuelve false silencioso en lugar de NotImplemented
linux/ti_printer_plugin.cc · líneas 252–271
El caller no puede distinguir "no hay soporte en esta plataforma" de "falló". Devolver fl_method_not_implemented_response_new() o un PlatformException con código "UNSUPPORTED" es más honesto.
PLG-18
Latente
linux · enumeración
getUsbPrinters trae cualquier ttyUSB / ttyACM
linux/ti_printer_plugin.cc · list_usb_printers
Aparecen Arduinos, módems 4G, GPS, lectores RFID, conversores FT232/CH340. Para una UI termina siendo confuso. Filtrar por VID/PID conocidos (Epson 0x04B8, Star Micronics 0x0519, Bixolon 0x1504) o leer /sys/class/usbmisc/lp*/device/idVendor levanta la señal.
PLG-19
Latente
encoding · win32
OpenUsbPort hace string→wstring byte-a-byte
windows/ti_printer_plugin.cpp · OpenUsbPort
Sólo es correcto para ASCII puro. Los InstanceIDs de Windows son ASCII en la práctica, pero la función convertWStringToString hace UTF-8 correcto en el camino de vuelta — la inversa debería ser simétrica con MultiByteToWideChar(CP_UTF8, ...).
Antes
std::wstring target(
device_instance_id.begin(),
device_instance_id.end());
Después
int sizeNeeded = MultiByteToWideChar(
CP_UTF8, 0,
device_instance_id.c_str(), -1,
NULL, 0);
std::vector<wchar_t> buf(sizeNeeded);
MultiByteToWideChar(CP_UTF8, 0,
device_instance_id.c_str(), -1,
buf.data(), sizeNeeded);
std::wstring target(buf.data());
PLG-20
Smell
pubspec
capabilities.json declarado dos veces en assets
pubspec.yaml + lib/resources/ + assets/resources/
Hay dos copias idénticas (5.110 bytes c/u) y pubspec.yaml declara ambas paths en assets:. El código carga packages/ti_printer_plugin/resources/capabilities.json únicamente — la otra es muerta. Sobra el archivo o sobra la entrada del pubspec.
PLG-21
Smell
destructor
Destructor usa CloseHandle directo en USB pero el método CloseSerialPort en serial
windows/ti_printer_plugin.cpp · ~TiPrinterPlugin
Inconsistencia menor. Si el destructor se invoca dos veces (no debería pasar pero…), USB hace double-close porque CloseHandle directo no setea hUsb_ a INVALID_HANDLE_VALUE. Llamar a CloseUsbPort() simétrico al serial elimina ese filo.
PLG-22
Smell
código muerto
OVERLAPPED + CreateEvent + GetOverlappedResult en handles síncronos
windows/ti_printer_plugin.cpp · SendCommandToSerial, SendCommandToUsb (versión original)
Los handles se abrían sin FILE_FLAG_OVERLAPPED, así que toda la ceremonia OVERLAPPED + CreateEvent + check de ERROR_IO_PENDING + GetOverlappedResult es código muerto: nunca se entra a la rama PENDING porque la I/O es síncrona. Genera trabajo desperdiciado en cada write y oculta la intención del código (parece async cuando no lo es).
Después del fix de #03, el handle USB sí queda overlapped y el código de async cobra sentido. En serial se simplificó a write síncrono limpio con SetCommTimeouts.