ti_printer_plugin 1.1.0 copy "ti_printer_plugin: ^1.1.0" to clipboard
ti_printer_plugin: ^1.1.0 copied to clipboard

Plugin Flutter para imprimir y consultar estado en impresoras térmicas ESC/POS por USB, ideal para POS y self-checkout en Linux/Windows.

ti_printer_plugin #

Plugin de Flutter para impresión en impresoras térmicas ESC/POS por USB con soporte para:

  • 🟦 Windows
  • 🐧 Linux

Incluye:

  • Capa nativa en C/C++ (Windows y Linux) usando MethodChannel.
  • Una aplicación de ejemplo en la carpeta example/ que muestra cómo consumir el plugin:
    • Validación de ASB (Automatic Status Back) con lectura continua desde el endpoint IN.
    • Reconexión automática ante desconexión física.
    • Monitoreo de estado en tiempo real (online/offline, papel, tapa).
    • Construcción de ticket con logo, encabezado, detalle, totales y código QR.
    • Generación de imágenes ESC/POS mediante bitmap 1-bit manual.
    • Consola de logs en vivo.
  • El plugin incluye la librería ESC/POS (esc_pos_utils_platform dentro de lib/) para generar comandos de ticket.

⚠️ Importante:
Todo lo que está dentro de la carpeta example/ no forma parte del plugin publicado.
Es solo una app de referencia para mostrar una posible integración.


Índice #


Características #

  • 🔌 Detección de impresoras USB (Windows y Linux) con información de VID, PID y nombre descriptivo.
  • 📋 PrinterDeviceInfo expone instanceId, displayName, vid, pid para que puedas identificar y seleccionar impresoras específicas.
  • 🏷️ resolvedDisplayName asigna nombres legibles según VID/PID usando una base de datos de +30 modelos conocidos (Epson, Star, Bixolon, Zebra, genéricas POS58, etc.).
  • 🖨️ Apertura y cierre de puerto USB.
  • 🧰 Soporte de puerto serial disponible actualmente solo en Windows.
  • 🧾 Envío de comandos ESC/POS "raw" a la impresora.
  • ↔️ Las lecturas de estado USB/serial devuelven todos los bytes recibidos; no se truncan al primer byte.
  • 📡 Lectura de estado de impresora usando comandos DLE EOT:
    • DLE EOT 1 – estado online.
    • DLE EOT 4 – sensor de papel.
    • DLE EOT 2 – causas de offline (tapa abierta, error, etc.).
  • 🔄 Soporte ASB (Automatic Status Back): activá enableAsb() para que la impresora envíe estado automáticamente sin polling, y recibí los paquetes con readFromUsb().
  • 🧠 readStatusUsbDetailed() clasifica la respuesta nativa en success, timeout, discardedAsb, discardedAmbiguousPayload o transportError mediante el modelo UsbStatusReadResult.
  • sendAndReadStatusUsb() envía un comando y lee la respuesta en una sola operación atómica.
  • API simple en Dart basada en MethodChannel.
  • Opcional (en example/):
    • AsbValidationPage, TicketBuilder, PrinterState y helpers para:
      • Interpretar estados ESC/POS.
      • Generar tickets completos.
      • Monitorear estado en tiempo real con ASB.
      • Reconexión automática ante desconexión USB.

Plataformas soportadas #

  • Windows:

    • Plugin nativo en C++ (archivos en windows/).
    • getUsbPrinters() devuelve List<PrinterDeviceInfo> con instanceId, displayName, vid y pid.
    • Dos pasadas de enumeración: primera por servicio usbprint o winusb (drivers genéricos), segunda por GUID_DEVCLASS_PRINTER (drivers propietarios Epson, Star, Bixolon, etc.).
    • Soporte para impresoras USB raw y puertos seriales (COMx).
    • USB abierto con FILE_FLAG_OVERLAPPED para lecturas/escrituras con timeout.
    • Todo el I/O USB corre en un thread dedicado (worker thread FIFO) — una impresora BUSY nunca congela la UI de la app.
    • SendCommandToUsb() reintenta escrituras parciales hasta enviar todo el buffer o fallar.
    • ReadStatusUsb() drena bytes viejos del buffer del kernel antes de enviar cada comando DLE EOT, eliminando la contaminación de respuestas entre lecturas consecutivas.
    • CloseUsbPort() cancela I/O overlapped pendiente (CancelIoEx) antes de cerrar el handle.
    • Lectura de estado ESC/POS por USB y serial, devolviendo todos los bytes recibidos.
  • Linux:

    • Plugin nativo en C++ (archivo principal: linux/ti_printer_plugin.cc).
    • getUsbPrinters() devuelve List<PrinterDeviceInfo> con instanceId, displayName, vid y pid reales.
    • Enumeración de dispositivos candidatos:
      • /dev/usb/lp*
      • /dev/ttyUSB*
      • /dev/ttyACM*
    • Para cada dispositivo, resuelve VID/PID real desde sysfs recorriendo /sys/dev/char/<major>:<minor> y caminando hacia arriba hasta encontrar idVendor/idProduct.
    • Si no encuentra VID/PID (falla el acceso a sysfs), vid=0, pid=0 y displayName usa el nombre base del dispositivo (ej: "lp0").
    • Todo el I/O USB corre en un pool de un solo thread (GThreadPool) con respuestas asíncronas vía g_idle_add — la UI nunca se congela aunque la impresora esté ocupada.
    • Apertura con O_NONBLOCK | O_NOCTTY: ni open() ni write() pueden bloquear indefinidamente.
    • Escrituras acotadas con poll(POLLOUT) + deadline (5s datos / 1s comandos de estado): si la impresora no acepta datos, se responde error en lugar de colgar.
    • ReadStatusUsb() drena bytes viejos del buffer del kernel antes de enviar cada comando DLE EOT, eliminando la contaminación de respuestas entre lecturas consecutivas (la causa raíz que v1.0.15 mitigaba con status.last y gaps de 80ms).
    • Si write falla con ENODEV, EIO o EBADF, el descriptor se cierra y el plugin considera el dispositivo desconectado.
    • fsync() eliminado: sobre character device (/dev/usb/lp*) devuelve EINVAL sin aportar nada.
    • La API serial de Dart existe, pero actualmente en Linux responde false o Uint8List vacio segun el metodo.

Nota: Android, iOS y Web no están soportados por este plugin.


Requisitos #

  • Flutter (canal stable, versión reciente).
  • Para Linux:
    • Toolchain de C++ (g++, clang, etc.).
    • Paquetes de desarrollo de GTK3 (requeridos por flutter_linux).
    • Permisos de acceso a dispositivos /dev/usb/lp* / /dev/ttyUSB* / /dev/ttyACM*.

Instalación #

En pubspec.yaml de tu aplicación:

dependencies:
  # Si está publicado en pub.dev:
  # ti_printer_plugin: ^1.1.0

  # O como dependencia de Git:
  ti_printer_plugin:
    git:
      url: https://github.com/jsalvini/ti_printer_plugin.git

Luego:

flutter pub get

En Linux/Windows, Flutter generará automáticamente los enlaces del plugin al compilar.


Arquitectura #

La arquitectura se divide en dos capas principales:

  1. Plugin (lib/, windows/, linux/):
    Es lo que se publica y lo que tu aplicación va a consumir.
  2. Aplicación de ejemplo (example/):
    Solo sirve como referencia de cómo usar el plugin. No es parte de la API pública.

Plugin (paquete ti_printer_plugin) #

Capa Dart

Dentro de lib/ (raíz del plugin):

  • ti_printer_plugin.dart
    API de alto nivel que tu app importa, por ejemplo:

    import 'package:ti_printer_plugin/ti_printer_plugin.dart';
    
  • ti_printer_plugin_method_channel.dart
    Implementación concreta usando:

    const MethodChannel('ti_printer_plugin');
    
  • ti_printer_plugin_platform_interface.dart
    Define la interfaz abstracta que MethodChannelTiPrinterPlugin implementa.
    Permite:

    • Crear otras implementaciones (ej: mocks para tests).
    • Mantener tipado y contrato de la API.

La API expone métodos como:

  • Future<String> getPlatformVersion()
  • Future<List<PrinterDeviceInfo>> getUsbPrinters()
  • Future<bool> openUsbPort(String deviceInstanceId)
  • Future<bool> closeUsbPort()
  • Future<bool> sendCommandToUsb(Uint8List data)
  • Future<Uint8List> readStatusUsb(Uint8List command)
  • Future<UsbStatusReadResult> readStatusUsbDetailed(Uint8List command)
  • Future<Uint8List> readFromUsb()
  • Future<bool> enableAsb()
  • Future<bool> disableAsb()
  • Future<Uint8List> sendAndReadStatusUsb(Uint8List command)
  • Future<bool> openSerialPort(String port, int baudRate)
  • Future<bool> closeSerialPort()
  • Future<bool> sendCommandToSerial(Uint8List data)
  • Future<Uint8List> readStatusSerial(Uint8List command)

Los métodos readStatusUsb, readStatusUsbDetailed y readStatusSerial aceptan Uint8List directamente como argumento (no un Map), coincidiendo con el resto de la API de envío de comandos.

Modelo PrinterDeviceInfo #

class PrinterDeviceInfo {
  final String instanceId;   // Identificador único del dispositivo (ruta /dev/... en Linux)
  final String displayName;  // Nombre original desde el sistema operativo
  final int vid;             // Vendor ID (0 si no se pudo resolver desde sysfs)
  final int pid;             // Product ID (0 si no se pudo resolver desde sysfs)

  // Nombre legible: busca (vid,pid) en la base de datos de impresoras
  // conocidas; si no encuentra, genera "USB Printer (VID:0xPPPP, PID:0xPPPP)".
  String get resolvedDisplayName;
}

Modelo UsbStatusReadResult (v1.1.0+) #

enum UsbStatusReadKind {
  success,                     // Respuesta válida
  timeoutOrNoResponse,         // Sin respuesta en ventana de lectura
  discardedAsb,                // Se recibió un paquete ASB en lugar de DLE EOT
  discardedAmbiguousPayload,   // Payload no clasificable
  transportError,              // Error de transporte (puerto cerrado, etc.)
}

class UsbStatusReadResult {
  final UsbStatusReadKind kind;  // Clasificación del resultado
  final Uint8List data;          // Bytes de respuesta (vacíos si no es success)
}

Los métodos readStatusUsbDetailed() y sendAndReadStatusUsb() devuelven este modelo, permitiendo a la aplicación distinguir entre los distintos modos de fallo sin depender de heurísticas.

Base de datos de impresoras (database_printer.dart) #

El plugin incluye una base de datos de +30 modelos de impresoras térmicas con su VID/PID:

Marca Modelos
Epson TM-T88, TM-T70, TM-T20, TM-m30, y USB Controller genérico
Star TSP100ECO, TSP100II
Bixolon SRP-350II
Citizen CT-E351, PPU-700
Zebra LP2844, GK420d, GK420t, GX420d, ZP 450, GC420d, ZD500, ZD410, ZD620, ZT411
TSC TTP-245C
DYMO LabelWriter 450, 550
Genéricas POS58 / Zjiang / GD32, HaoYin CX588, POS58/POS80 chinas, Gprinter
Rongta RP Series

Para agregar nuevas impresoras:

import 'package:ti_printer_plugin/ti_printer_plugin.dart';

knownThermalUsbPrinters.add(
  KnownUsbPrinter(
    vid: 0xXXXX,
    pid: 0xYYYY,
    displayName: 'Mi Modelo',
    protocol: 'escpos',
  ),
);

O usar lookupPrinterInfo(vid, pid) para búsqueda programática.

Contrato de respuestas #

  • getUsbPrinters() devuelve List<PrinterDeviceInfo> con instanceId listo para reutilizar en openUsbPort() (DeviceInstanceId en Windows, rutas /dev/... en Linux), más displayName (original de Windows), vid y pid. Usar resolvedDisplayName para obtener un nombre legible según base de datos de VID/PID conocidos.
  • Los métodos booleanos (openUsbPort, closeUsbPort, sendCommandToUsb, openSerialPort, closeSerialPort, sendCommandToSerial, etc.) devuelven true en éxito y false en fallo o si la capacidad no está soportada en la plataforma actual.
  • Los métodos readStatusUsb y readStatusSerial devuelven un Uint8List con todos los bytes recibidos cuando la impresora responde.
  • Si no hay respuesta antes del timeout, ocurre un error nativo o la capacidad no está soportada, las lecturas de estado devuelven un Uint8List vacío.

Capa nativa Windows

Carpeta windows/ (plugin, no example):

  • ti_printer_plugin.cpp
  • ti_printer_plugin_c_api.cpp
  • ti_printer_plugin.h
  • CMakeLists.txt

Responsabilidades:

  • Implementar las funciones que el MethodChannel invoca:

    • getPlatformVersion
    • getUsbPrinters
    • openUsbPort
    • closeUsbPort
    • sendCommandToUsb
    • readStatusUsb
    • openSerialPort
    • readStatusSerial
    • etc.
  • Gestionar en C++:

    • Apertura/cierre de puerto USB/serie.
    • Envío de bytes ESC/POS.
    • Escritura USB overlapped con timeout y reintento en caso de escritura parcial.
    • Lectura de estado desde la impresora y conversión a bytes completos que se devuelven a Dart.

Capa nativa Linux

Carpeta linux/ (plugin, no example):

  • CMakeLists.txt
  • ti_printer_plugin.cc
  • include/ti_printer_plugin/ti_printer_plugin.h
  • ti_printer_plugin_private.h

Responsabilidades principales de ti_printer_plugin.cc:

  • Enumerar posibles impresoras y resolver VID/PID real desde sysfs:

    static std::vector<PrinterDeviceInfo> list_usb_printers() {
      std::vector<PrinterDeviceInfo> printers;
      auto add_entries = [&](const std::string &dir, const std::string &pref) {
        // ... enumerar /dev/usb/lp*, /dev/ttyUSB*, /dev/ttyACM*
        for (auto &path : paths) {
          // Resolver VID/PID desde sysfs
          std::string sysfs = resolve_sysfs_path(path);
          if (!sysfs.empty()) {
            auto vp = read_vid_pid_from_sysfs(sysfs);
            info.vid = vp.first;
            info.pid = vp.second;
          }
          printers.push_back(info);
        }
      };
      add_entries("/dev/usb", "lp");
      add_entries("/dev", "ttyUSB");
      add_entries("/dev", "ttyACM");
      return printers;
    }
    

    resolve_sysfs_path() sigue el symlink /sys/dev/char/<major>:<minor> y read_vid_pid_from_sysfs() camina hacia arriba en el árbol sysfs buscando idVendor / idProduct. Sin dependencias externas (solo POSIX).

  • Abrir/cerrar puerto:

    static bool open_usb_port(TiPrinterPlugin* self,
                              const std::string& device_path);
    static bool close_usb_port(TiPrinterPlugin* self);
    
  • Enviar datos:

    static bool send_command_to_usb(TiPrinterPlugin* self,
                                    const uint8_t* data,
                                    size_t length);
    
    • Usa write_all_with_deadline() con O_NONBLOCK + poll(POLLOUT) + deadline de 5s.
    • En caso de errores como ENODEV, EIO o EBADF, cierra el descriptor y lo marca en -1 para indicar que el dispositivo ya no está disponible.
  • Leer estado ESC/POS:

    static std::vector<uint8_t> read_status_usb(
        TiPrinterPlugin* self,
        const std::vector<uint8_t>& command);
    
    • Drena los bytes viejos del buffer del kernel antes de enviar el comando.
    • Envía el comando ESC/POS (por ejemplo DLE EOT n) con deadline de 1s.
    • Espera hasta 500 ms con poll(POLLIN).
    • Si hay datos, devuelve todos los bytes leidos.
    • Si no hay respuesta o hay error, devuelve un vector vacío.
  • Integrarse con Flutter por medio de FlMethodChannel:

    • getPlatformVersion
    • getUsbPrinters
    • openUsbPort
    • closeUsbPort
    • sendCommandToUsb
    • readStatusUsb

Aplicación de ejemplo (example/) #

La carpeta example/ contiene una app Flutter separada que:

  • Declara dependencia al plugin ti_printer_plugin.
  • Implementa su propia lógica de dominio.
  • Muestra cómo:
    • Listar impresoras.
    • Conectar y habilitar ASB (Automatic Status Back).
    • Leer paquetes ASB en un loop continuo.
    • Monitorear desconexión física y reconectar automáticamente.
    • Enviar comandos ESC/POS (init, corte, avance) con manejo de pausas.
    • Construir y enviar un ticket de prueba.

La app de ejemplo actual es AsbValidationApp (en lugar de la antigua PrinterStatusView), diseñada específicamente para validar el comportamiento ASB y la reconexión USB en entornos de producción.

Algunos archivos relevantes (solo ejemplo, no forman parte del plugin):

  • example/lib/ui/asb_validation_page.dart
    Página principal con lógica de conexión, reconexión, lectura ASB en vivo y envío de comandos.

  • example/lib/logic/ticket_builder.dart
    Construye secuencias de bytes ESC/POS usando la librería esc_pos_utils_platform (incluida en el plugin, pero el builder es puro ejemplo).

  • example/lib/models/printer_state.dart
    Modelo de estado inmutable (con Equatable).

  • example/lib/utils/printer_status_interpreter.dart
    Traduce el byte de respuesta ESC/POS (estado) a flags booleanos; incluye describeAsbPacket() e interpretAsbPacket() para paquetes ASB de 4 bytes.

Puedes tomar el código del example como referencia para tu propia app, pero no es parte de la API pública del plugin.


API Dart #

La API del plugin se expone a través de TiPrinterPlugin (en lib/ti_printer_plugin.dart).

Detección y conexión USB #

import 'package:ti_printer_plugin/ti_printer_plugin.dart';

final plugin = TiPrinterPlugin();

// Obtener versión de la plataforma
final version = await plugin.getPlatformVersion();

// Listar impresoras USB disponibles
final printers = await plugin.getUsbPrinters(); // List<PrinterDeviceInfo>
// Cada elemento tiene:
//   .instanceId  → DeviceInstanceId (Windows) o ruta /dev/... (Linux)
//   .displayName → nombre original desde Windows
//   .vid         → Vendor ID (0 si no disponible)
//   .pid         → Product ID (0 si no disponible)

// Nombre legible según base de datos de VID/PID conocidos
print(printers.first.resolvedDisplayName);
// Ejemplo: "EPSON TM-T20", "POS58 / Zjiang / GD32 USB Printer"

// Abrir un puerto USB con el .instanceId
final ok = await plugin.openUsbPort(printers.first.instanceId);

// Cerrar el puerto USB
final closed = await plugin.closeUsbPort();

Conexión serial (solo Windows) #

final plugin = TiPrinterPlugin();

final opened = await plugin.openSerialPort('COM3', 9600);
if (!opened) {
  throw Exception('No se pudo abrir el puerto serial');
}

final sent = await plugin.sendCommandToSerial(Uint8List.fromList([0x1B, 0x40]));
if (!sent) {
  throw Exception('No se pudo enviar comando por serial');
}

await plugin.closeSerialPort();

En Linux estos metodos existen en Dart para mantener el contrato cruzado, pero hoy responden como capacidad no soportada.

Lectura de estado ESC-POS #

La API expone métodos para leer el estado enviando comandos ESC/POS (DLE EOT).
Desde tu app podés usar cualquier librería para generar esos comandos ESC/POS.
En el ejemplo se usa la API pública del barrel package:ti_printer_plugin/esc_pos_utils_platform/esc_pos_utils_platform.dart:

final profile = await CapabilityProfile.load();
final generator = Generator(PaperSize.mm80, profile);

// DLE EOT 1 – online status
final cmdOnline = Uint8List.fromList(generator.status());
final rspOnline = await plugin.readStatusUsb(cmdOnline);

// DLE EOT 4 – sensor de papel
final cmdPaper = Uint8List.fromList(generator.paperSensorStatus());
final rspPaper = await plugin.readStatusUsb(cmdPaper);

// DLE EOT 2 – offline cause
final cmdOffline = Uint8List.fromList(generator.offLineStatus());
final rspOffline = await plugin.readStatusUsb(cmdOffline);

if (rspOnline.isEmpty) {
  // Sin respuesta dentro del timeout, error nativo o capacidad no soportada.
}

// Si la impresora devuelve mas de un byte, el plugin entrega la respuesta completa.

CapabilityProfile.load() resuelve capabilities.json desde los assets del propio paquete ti_printer_plugin, asi que tu app consumidora no necesita declarar ni copiar ese archivo en su propio pubspec.yaml.

Lo que hagas con esos bytes de respuesta (interpretar flags, actualizar UI, etc.) ya es responsabilidad de tu aplicación.
El código de ejemplo (PrinterStatusInterpreter) muestra una posible forma de hacerlo.

Lectura de estado con diagnóstico (readStatusUsbDetailed) #

Devuelve un UsbStatusReadResult que clasifica el resultado de la operación:

import 'package:ti_printer_plugin/usb_status_read_result.dart';

final result = await plugin.readStatusUsbDetailed(
  Uint8List.fromList([0x10, 0x04, 0x01]),
);

switch (result.kind) {
  case UsbStatusReadKind.success:
    print('Respuesta válida: ${result.data}'); // 1 byte coherente
    break;
  case UsbStatusReadKind.discardedAsb:
    print('Se recibió un paquete ASB en lugar de respuesta DLE EOT');
    break;
  case UsbStatusReadKind.discardedAmbiguousPayload:
    print('Payload no clasificable descartado');
    break;
  case UsbStatusReadKind.timeoutOrNoResponse:
    print('La impresora no respondió');
    break;
  case UsbStatusReadKind.transportError:
    print('Error de transporte (puerto cerrado, etc.)');
    break;
}

Soporte ASB (Automatic Status Back) #

Las impresoras ESC/POS compatibles pueden enviar su estado automáticamente sin necesidad de polling:

// Activar ASB: la impresora comenzará a enviar paquetes de 4 bytes
await plugin.enableAsb();

// Leer paquetes ASB directamente del endpoint IN (sin comando previo)
final asbPacket = await plugin.readFromUsb();
// asbPacket tiene 4 bytes (Epson) con información de online, papel, tapa, errores

// Desactivar ASB cuando ya no sea necesario
await plugin.disableAsb();

Envío y lectura atómica (sendAndReadStatusUsb) #

Combina escritura de comando + lectura de respuesta en una sola llamada:

final response = await plugin.sendAndReadStatusUsb(
  Uint8List.fromList([0x10, 0x04, 0x01]),
);
if (response.isNotEmpty) {
  print('Respuesta: ${response.length} bytes');
}

Impresión de datos crudos #

Si ya tienes tus bytes ESC/POS construidos (por ejemplo un ticket):

final Uint8List data = Uint8List.fromList([...]);
final bool ok = await plugin.sendCommandToUsb(data);
if (ok != true) {
  throw Exception('Error enviando datos a la impresora USB');
}

Construcción de tickets ESC/POS (ejemplo) #

El plugin no obliga a usar una forma particular de construir tickets. En la app de ejemplo se incluye TicketBuilder para mostrar una posible implementación.

Ejemplo (adaptado a tu app):

final profile = await CapabilityProfile.load();
final builder = TicketBuilder(profile);

final bytes = await builder.buildTicket(
  items: items,                // List<Item> (modelo propio de tu app)
  nroReferencia: '000123',
  efectivo: efectivo,          // monto recibido (se imprime en el ticket)
  cambio: cambio,              // vuelto (se imprime en el ticket)
  qrData: 'https://mi-app/…',  // contenido del QR
);

// Enviar a la impresora
await plugin.sendCommandToUsb(Uint8List.fromList(bytes));

El TicketBuilder de la app de ejemplo:

  1. Añade un logo.
  2. Imprime encabezado de comercio (nombre, CUIT, dirección, etc.).
  3. Imprime líneas de detalle (items).
  4. Imprime totales con formato destacado.
  5. Genera un código QR y lo renderiza como imagen ESC/POS.
  6. Añade un pie de ticket y un corte de papel.

Puedes copiar ese enfoque y adaptarlo a las necesidades de tu negocio,
o construir tus propios bytes ESC/POS desde cero.


Monitor de estado en tiempo real (ejemplo) #

El plugin ofrece los bloques básicos (lectura de estado vía readStatusUsb y soporte ASB).
En la app de ejemplo (AsbValidationPage) se implementan dos estrategias de monitoreo:

Con ASB habilitado (recomendado) #

  1. Se activa ASB con enableAsb() que envía GS a 0x0F.
  2. Un loop continuo llama a readFromUsb() cada ~60ms para capturar los paquetes automáticos de 4 bytes que la impresora envía.
  3. Cada paquete ASB se interpreta con AsbStatus.tryParse() que extrae: online, tapa, papel, errores recuperables/irrecuperables/cutter.
  4. Sin polling: la impresora notifica los cambios inmediatamente.

Sin ASB (fallback) #

  1. Envía DLE EOT 1 con sendAndReadStatusUsb() para consultar online.
  2. Usa readStatusUsbDetailed() para clasificar la respuesta nativa.
  3. El poll se dispara cada 1200ms como plan B.

Reconexión automática #

  • Un timer de 1500ms verifica que el dispositivo siga presente en la enumeración USB.
  • Si desaparece, se marca como lost y se inicia un loop de reconexión cada 3 segundos (hasta 30 intentos).
  • Al reaparecer el dispositivo, se reabre el puerto y se rehabilita ASB automáticamente.

Ese monitor está implementado en AsbValidationPage dentro de example/lib/ui/asb_validation_page.dart.
En tu aplicación podés reutilizar el enfoque o implementar tu propia lógica de monitoreo.


Ejemplo de uso en Flutter #

Un patrón típico con ASB (similar al example) sería:

class MyPrinterScreen extends StatefulWidget {
  const MyPrinterScreen({super.key});

  @override
  State<MyPrinterScreen> createState() => _MyPrinterScreenState();
}

class _MyPrinterScreenState extends State<MyPrinterScreen> {
  final _plugin = TiPrinterPluginPlatform.instance;
  List<PrinterDeviceInfo> _printers = [];
  PrinterDeviceInfo? _selected;
  bool _connected = false;

  @override
  void initState() {
    super.initState();
    _refresh();
  }

  Future<void> _refresh() async {
    final list = await _plugin.getUsbPrinters();
    setState(() => _printers = list);
  }

  Future<void> _connect() async {
    if (_selected == null) return;
    final ok = await _plugin.openUsbPort(_selected!.instanceId);
    if (ok) await _plugin.enableAsb();
    setState(() => _connected = ok);
  }

  Future<void> _disconnect() async {
    await _plugin.disableAsb();
    await _plugin.closeUsbPort();
    setState(() => _connected = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Estado impresora')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            DropdownButton<PrinterDeviceInfo>(
              value: _selected,
              hint: const Text('Seleccione impresora USB'),
              items: _printers
                  .map((p) => DropdownMenuItem(
                        value: p,
                        child: Text(p.resolvedDisplayName),
                      ))
                  .toList(),
              onChanged: (v) => setState(() => _selected = v),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                if (!_connected)
                  FilledButton(
                    onPressed: _connect,
                    child: const Text('Conectar + ASB'),
                  )
                else
                  OutlinedButton(
                    onPressed: _disconnect,
                    child: const Text('Desconectar'),
                  ),
              ],
            ),
            if (_connected) ...[
              const SizedBox(height: 12),
              Row(
                children: [
                  Icon(Icons.power,
                      color: Colors.green),
                  const SizedBox(width: 8),
                  const Text('ASB activo — estado en tiempo real'),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}

De nuevo: este patrón viene del example y es solo una guía.
En tu app podés usar Riverpod, BLoC, MobX, etc.


Permisos en Linux #

En Linux es habitual que el acceso a /dev/usb/lp* (y similares) requiera permisos adicionales.

Opciones típicas:

  • Ejecutar la app con permisos elevados (no recomendado en producción).
  • Agregar tu usuario al grupo que tiene acceso al dispositivo (por ejemplo lp).
  • Crear una regla udev que ajuste los permisos del dispositivo.

Si ves errores como:

No se pudo abrir /dev/usb/lp1: Permission denied

revisá los permisos con:

ls -l /dev/usb/lp*

y ajustalos según la política de tu sistema.


Desarrollo y estructura de carpetas #

Estructura típica del repo:

ti_printer_plugin/
├── lib/
│   ├── ti_printer_plugin.dart
│   ├── ti_printer_plugin_method_channel.dart
│   ├── ti_printer_plugin_platform_interface.dart
│   ├── printer_device_info.dart          # Modelo PrinterDeviceInfo
│   ├── database_printer.dart             # Mapeo VID/PID → nombre conocido
│   ├── usb_status_read_result.dart       # Modelo UsbStatusReadResult (v1.1.0)
│   └── esc_pos_utils_platform/           # Librería ESC/POS para generar comandos
│       ├── esc_pos_utils_platform.dart
│       └── src/
│           ├── barcode.dart
│           ├── capability_profile.dart
│           ├── commands.dart
│           ├── enums.dart
│           ├── generator.dart
│           ├── pos_column.dart
│           ├── pos_styles.dart
│           └── qrcode.dart
├── assets/
│   └── resources/
│       └── capabilities.json
├── linux/
│   ├── CMakeLists.txt
│   ├── ti_printer_plugin.cc
│   ├── ti_printer_plugin_private.h
│   └── include/
│       └── ti_printer_plugin/
│           └── ti_printer_plugin.h
├── windows/
│   ├── CMakeLists.txt
│   ├── ti_printer_plugin.cpp
│   ├── ti_printer_plugin.h
│   ├── ti_printer_plugin_c_api.cpp
│   └── test/
│       └── ti_printer_plugin_test.cpp
└── example/
    ├── lib/
    │   ├── main.dart
    │   ├── logic/
    │   │   ├── item.dart
    │   │   └── ticket_builder.dart
    │   ├── models/
    │   │   └── printer_state.dart
    │   ├── ui/
    │   │   ├── asb_validation_app.dart
    │   │   ├── asb_validation_page.dart
    │   │   ├── asb_status.dart
    │   │   ├── action_chip.dart
    │   │   ├── badge.dart
    │   │   ├── conn_state.dart
    │   │   ├── esc_pos_commands.dart
    │   │   ├── status_badges_row.dart
    │   │   └── status_banner.dart
    │   └── utils/
    │       ├── image_utils.dart
    │       └── printer_status_interpreter.dart
    ├── assets/
    │   └── logo.png
    └── linux/
        └── ...

Todo lo que está dentro de example/ es una APP de demostración, no parte del plugin.


Problemas conocidos / troubleshooting #

  • El logo se imprime como un rectángulo negro:

    • Ocurría cuando la imagen del logo tiene fondo transparente (alpha=0 con R=G=B=0).
    • La librería imageRaster() invierte los píxeles antes de empaquetarlos, combinado con píxeles transparentes en negro, producía un fondo completamente negro.
    • Solución aplicada en v1.0.12: TicketBuilder._buildLogoBytes() construye un bitmap 1-bit manualmente, ignorando píxeles transparentes y usando el comando raw GS v 0 en lugar de imageRaster().
  • Error al escribir en USB: No existe el dispositivo:

    • La impresora se apagó / desconectó.
    • En Linux, si write falla con ENODEV, EIO o EBADF, el plugin cierra el descriptor y readStatusUsb empieza a devolver vacío.
    • El monitor de la app de ejemplo lo interpreta como offline y se detiene.
    • Para reconectar: encender impresora, getUsbPrinters(), openUsbPort(), reanudar tu lógica de monitoreo.
  • Permisos en Linux:

  • Las lecturas de estado devuelven vacío pero la app no se cuelga:

    • Es el comportamiento esperado ante timeout, falta de respuesta o capacidad no soportada.
    • En Windows, ReadStatusUsb() cancela la lectura pendiente tras ~500 ms para evitar bloqueos indefinidos, y previamente drena bytes viejos del buffer para evitar contaminación entre lecturas.
    • En Linux, poll(POLLIN) espera hasta 500 ms y luego devuelve vacío si no hubo respuesta. Todas las operaciones corren en un thread de I/O dedicado, por lo que la UI nunca se congela.
  • Error de CMake: Compatibility with CMake < 3.5 has been removed (Windows):

    • Al usar CMake >= 4.0, el cmake_minimum_required de GoogleTest (release-1.11.0) falla porque CMake 4.x eliminó la compatibilidad con versiones < 3.5.
    • Solución aplicada en v1.0.12: Se agregó set(CMAKE_POLICY_VERSION_MINIMUM 3.5) antes de FetchContent_MakeAvailable(googletest) en windows/CMakeLists.txt.
  • No se ven logs nativos en la app:

    • Los logs nativos (g_printerr, printf, OutputDebugStringA) se ven en la consola o debugger donde lanzás flutter run.
    • La consola de la app de ejemplo (UI) muestra solo los logs que se agregan desde Dart.

Licencia #

Agregá aquí la licencia que corresponda, por ejemplo MIT, Apache 2.0, etc.

Copyright (c) 2025 José Salvini

Se concede permiso por la presente, libre de cargos, a cualquier persona que obtenga
una copia de este software y de los archivos de documentación asociados (el "Software"),
para utilizar el Software sin restricción, incluyendo sin limitación los derechos
a usar, copiar, modificar, fusionar, publicar, distribuir, sublicenciar y/o vender
copias del Software, y a permitir a las personas a las que se les proporcione el
Software que lo hagan.

0
likes
0
points
820
downloads

Publisher

unverified uploader

Weekly Downloads

Plugin Flutter para imprimir y consultar estado en impresoras térmicas ESC/POS por USB, ideal para POS y self-checkout en Linux/Windows.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, gbk_codec, hex, image, plugin_platform_interface

More

Packages that depend on ti_printer_plugin

Packages that implement ti_printer_plugin