Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FlutterPrinter

Impressão térmica universal para terminais POS Android (PAX, SUNMI, Gertec/Wiseasy, POSITIVO) e impressoras Bluetooth/rede genéricas — pensado para dar a mesma aparência de cupom independente do fabricante do hardware, algo que normalmente diverge muito entre terminais POS.

Compatível com Android 6.0 (API 23) até Android 13 (API 33).

⚠️ Versão do Flutter é parte do requisito de compatibilidade, não um detalhe de ambiente. Este projeto usa Flutter 3.24.5 (canal stable) fixado propositalmente — não é a versão mais recente disponível. A partir de versões mais novas do Flutter, o próprio engine chama Configuration.getLocales() sem guarda de versão e trava no boot em qualquer aparelho Android 6 (API 23). Isso não é um bug deste app: é um piso de compatibilidade que o Flutter em si abandonou. Se o SDK/toolchain for atualizado no futuro, API 23 deixa de funcionar até que essa mesma investigação seja refeita para a nova versão. Ver Requisitos e a limitação documentada em código nos comentários de android/app/build.gradle (minSdk).

Este README documenta as decisões técnicas do projeto para servir de referência a quem for implementar ou estender a impressão. Se você só quer rodar o app, veja Como rodar.


Índice


Por que este projeto existe

Terminais POS Android (maquininhas de pagamento com impressora térmica embutida) não são impressoras ESC/POS genéricas. Cada fabricante expõe a impressora interna de um jeito diferente:

Fabricante Como a impressora interna é exposta
PAX SDK proprietário IDAL/Neptune (com.pax.dal.IPrinter)
SUNMI Serviço AIDL oficial (woyou.aidlservice.jiuiv5)
Gertec/Wiseasy SDK proprietário GEDI (br.com.gertec.gedi.interfaces.IPRNTR)
POSITIVO (L300/L400/L500) Serviço AIDL (com.xcheng.printerservice)

Muitos apps (incluindo versões anteriores deste) tentam contornar isso tratando a impressora interna como se fosse um dispositivo Bluetooth externo, através de uma "ponte" ESC/POS que cada fabricante disponibiliza para compatibilidade. Na prática, essas pontes costumam implementar só um subconjunto do ESC/POS — em testes reais neste projeto, terminais PAX simplesmente não imprimiam QR Code/código de barras nem aplicavam alinhamento por essa via.

Este projeto resolve isso em duas frentes:

  1. Quando dá para falar direto com o SDK nativo do fabricante, fala. Ver Métodos de conexão.
  2. Para o conteúdo em si (texto, QR, código de barras, layout de cupom), renderiza tudo como uma única imagem e imprime via raster — assim a aparência não depende da fonte/alinhamento/comandos que cada fabricante decidiu implementar. Ver Cupom como imagem.

Arquitetura

┌─────────────────────────────────────────────────────────────┐
│                          Dart / Flutter                       │
│                                                                 │
│  EscPosFormat            ReceiptImageRenderer                 │
│  (bytes ESC/POS puros,   (cupom inteiro → bitmap via dart:ui, │
│   texto/estilo/QR/       texto+QR+barcode desenhados juntos)  │
│   barcode/raster)                                              │
│         │                          │                           │
│         └──────────┬───────────────┘                           │
│                     ▼                                          │
│         PrinterConnection (interface)                          │
│         hasPermission / connect / printBytes / disconnect      │
│                     │                                          │
│   ┌────────┬────────┼────────┬─────────┬─────────┬─────────┐  │
│   ▼        ▼        ▼        ▼         ▼         ▼         │  │
│ Flutter  Java     Network  SUNMI      PAX      Gertec  POSITIVO│
│ Package Bluetooth (socket) (AIDL)   (IDAL/    (GEDI)   (AIDL) │
│ (plugin) (Bluetooth         nativo   Neptune   nativo   nativo│
│           Socket)           MethodCh. nativo   MethodCh MethodCh
└─────────────────────────────────────────────────────────────┘
                     │ MethodChannel (para os 4 nativos)
                     ▼
┌─────────────────────────────────────────────────────────────┐
│                      Android / Java                            │
│  BluetoothPrinterManager · SunmiPrinterManager ·               │
│  PaxPrinterManager · GertecPrinterManager · PositivoPrinterManager│
└─────────────────────────────────────────────────────────────┘

Peça central: lib/services/printer_connection.dart — uma interface pequena e comum às 7 estratégias:

abstract class PrinterConnection {
  Future<bool> hasPermission();
  Future<bool> requestPermission();
  Future<bool> isBluetoothEnabled();
  Future<List<BluetoothPrinterDevice>> listPairedDevices();
  Future<bool> connect(BluetoothPrinterDevice device);
  Future<void> disconnect();
  Future<bool> isConnected();
  Future<bool> printBytes(List<int> bytes);
}

Cada implementação só sabe transportar bytes. Nenhuma delas formata texto, desenha QR ou decide alinhamento — isso é sempre responsabilidade de EscPosFormat ou ReceiptImageRenderer, o que garante que a mesma chamada produza a mesma impressão em qualquer backend.


Métodos de conexão

Enum: PrinterConnectionMethod.

Método Transporte Canal nativo Aceita bytes ESC/POS crus?
flutterPackage Bluetooth clássico (SPP), via print_bluetooth_thermal — (plugin) Sim (writeBytes)
nativeJava Bluetooth clássico (SPP), BluetoothSocket implementado neste app bluetooth_printer_java Sim
network Socket TCP raw, porta 9100 (padrão "JetDirect" de impressoras de rede) — (dart:io Socket) Sim
sunmiInternal Impressora interna SUNMI, SDK oficial com.sunmi:printerlibrary sunmi_printer Sim (sendRAWData)
paxInternal Impressora interna PAX, SDK IDAL/Neptune pax_printer Não — precisa de um interpretador (ver abaixo)
gertecInternal Impressora interna Gertec/Wiseasy, SDK GEDI gertec_printer Não — idem
positivoInternal Impressora interna POSITIVO L300/L400/L500, AIDL com.xcheng.printerservice positivo_printer Sim (sendRAWData)
auto Detecta a marca do terminal e resolve para um dos métodos acima

Por que PAX e Gertec precisam de um interpretador

As APIs nativas do PAX (IPrinter) e do Gertec (IPRNTR) não aceitam bytes ESC/POS crus — são APIs de alto nível orientadas a chamadas de método (printStr, DrawBarCode, DrawPictureExt...). Só SUNMI e POSITIVO expõem sendRAWData.

Para que o mesmo EscPosFormat.buildTestReceipt(...) funcione em qualquer backend, PaxPrinterManager.java e GertecPrinterManager.java implementam um interpretador ESC/POS → chamadas nativas: percorrem os bytes recebidos reconhecendo os comandos que EscPosFormat gera (ESC @, ESC a n, ESC E n, GS ! n, GS v 0, etc.) e os traduzem:

  • ESC a n (alinhamento) → Gertec tem alinhamento real via Paint.setTextAlign; PAX não tem um primitivo de alinhamento na API (confirmado tanto na SDK quanto em wrappers de terceiros) — o interpretador do PAX simula alinhando com preenchimento de espaços.
  • GS ! n (escala de fonte) → Gertec usa Paint.setTextSize; PAX usa doubleWidth()/doubleHeight() reais da API (limitado a 2x).
  • GS v 0 (imagem raster) → ambos reconstroem um Bitmap a partir dos bits recebidos e chamam DrawPictureExt/printBitmap.
  • Negrito/itálico/sublinhado/riscado → Gertec tem tudo via Paint (setFakeBoldText, setTextSkewX, setUnderlineText, setStrikeThruText — riscado de verdade, não aproximação); PAX não tem primitivo equivalente, então são ignorados silenciosamente nesse backend.

Formatação universal (ESC/POS)

lib/services/esc_pos_format.dart é o único lugar que gera bytes ESC/POS. Pontos importantes de design (motivados por bugs reais encontrados em teste com hardware real):

  • QR Code e código de barras são rasterizados como imagem (GS v 0), em vez de usar os comandos nativos de símbolo (GS ( k para QR, GS k para barcode). Em teste real, terminais PAX simplesmente ignoravam os comandos nativos — nem imprimiam, nem aplicavam alinhamento. Raster funciona de forma consistente em qualquer impressora porque não depende do firmware ter um gerador de símbolos embutido. A geração da matriz de pontos usa o pacote barcode (puro Dart, sem dependência nativa).
  • Imagens grandes são enviadas em tiras horizontais (stripeHeight, por padrão 48–64 pontos dependendo da marca), replicando a prática usada por BluetoothUniversalPrinter para evitar estourar o buffer de impressoras mais simples.
  • Fonte explícita no início do trabalho (ESC M 0, Fonte A) — sem isso, cada fabricante usa sua fonte padrão de fábrica, que difere em tamanho.
  • Escala de fonte com teto por marca (maxScale, ver PrinterBrandProfile) — nem toda impressora aceita a magnificação 3x do GS ! n de forma confiável.
  • Nunca chame reset() depois de align()ESC @ reinicializa o estado da impressora, inclusive o alinhamento. Esse foi um bug real neste projeto: o QR Code não aplicava alinhamento porque o código mandava align() e, logo em seguida, reset().

Cupom como imagem

lib/services/receipt_image_renderer.dart é a solução mais robusta para divergência de aparência entre marcas: em vez de mandar comandos de texto/QR/código de barras separados (que dependem de cada impressora interpretar igual), o cupom inteiro é desenhado como uma única imagem usando dart:ui (PictureRecorder + Canvas + TextPainter, sem precisar montar uma árvore de widgets), depois convertido para uma matriz preto/branco e impresso via raster (EscPosFormat.rasterImage).

final pixels = await ReceiptImageRenderer.render(
  ReceiptData.sample(),
  widthDots: paperSize.dotsWidth, // 384 (58mm) ou 576 (80mm)
  includeQr: true,
  includeBarcode: true,
);
final bytes = EscPosFormat.rasterImage(pixels, alignValue: PrintAlign.left);
await connection.printBytes(bytes);

Como o hardware só recebe pontos prontos, a impressão fica pixel-idêntica em qualquer backend — inclusive nos que exigem interpretador (PAX/Gertec), já que ambos reconstroem o mesmo bitmap a partir do bloco GS v 0. Veja a tela Teste de Impressão de Imagem (4 botões: Cupom / +QRCode / +Code128 / +ambos).


Detecção de marca e modo automático

lib/services/device_brand_detector.dartBuild.MANUFACTURER/Build.BRAND/Build.MODEL via o canal nativo Java (getDeviceInfo) e classifica em PrinterBrand (pax, sunmi, gertec, positivo, generic).

lib/services/brand_method_resolver.dart mapeia marca → método recomendado:

PrinterBrand.pax      → PrinterConnectionMethod.nativeJava      // SDK nativo não conectou em testes reais
PrinterBrand.sunmi    → PrinterConnectionMethod.sunmiInternal   // validado
PrinterBrand.gertec   → PrinterConnectionMethod.gertecInternal
PrinterBrand.positivo → PrinterConnectionMethod.positivoInternal
PrinterBrand.generic  → PrinterConnectionMethod.flutterPackage

Ao selecionar "Automático (detectar pela marca)" em Configurações, o app roda essa detecção e mostra "Detectado: X → Y" — mas a escolha final continua alterável manualmente a qualquer momento pelo dropdown.


Telas do app

Tela Arquivo O que faz
Início home_screen.dart 3 botões: Configurar impressora / Testes de impressão / Teste de Impressão de Imagem
Configuração de Impressora printer_settings_screen.dart Escolhe método de conexão (dropdown), tamanho de papel, dispositivo/IP, conecta e salva em SharedPreferences
Testes de Impressão print_tests_screen.dart Um botão por recurso ESC/POS: alinhamento, tamanho de fonte, negrito/itálico/sublinhado/riscado, QR, código de barras, imagem
Teste de Impressão de Imagem image_print_test_screen.dart 4 botões de cupom de supermercado renderizado como imagem (ver Cupom como imagem)

A impressora escolhida é persistida por PrinterSettingsStore (shared_preferences), com uma chave de "último dispositivo" por método — trocar de método não perde o dispositivo salvo dos outros.


Estrutura do projeto

lib/
├── models/
│   ├── bluetooth_printer_device.dart   # {name, address} genérico (MAC, "ip:porta" ou "internal")
│   ├── printer_connection_method.dart  # enum dos 8 métodos + labels/descrições
│   ├── printer_paper_size.dart         # mm58 (384pt) / mm80 (576pt)
│   ├── printer_brand_profile.dart      # PrinterBrand + limites por marca
│   └── receipt_data.dart               # modelo de cupom (itens, QR, barcode)
├── services/
│   ├── printer_connection.dart         # interface comum
│   ├── flutter_package_printer_connection.dart
│   ├── native_java_printer_connection.dart
│   ├── network_printer_connection.dart
│   ├── sunmi_printer_connection.dart
│   ├── pax_printer_connection.dart
│   ├── gertec_printer_connection.dart
│   ├── positivo_printer_connection.dart
│   ├── esc_pos_format.dart             # composer ESC/POS universal
│   ├── receipt_image_renderer.dart     # cupom → bitmap (dart:ui)
│   ├── device_brand_detector.dart
│   ├── brand_method_resolver.dart
│   └── printer_settings_store.dart
└── screens/
    ├── home_screen.dart
    ├── printer_settings_screen.dart
    ├── print_tests_screen.dart
    └── image_print_test_screen.dart

android/app/src/main/
├── java/com/gertec/flutterprinter/flutter_printer/
│   ├── MainActivity.java               # registra os 5 MethodChannel
│   ├── BluetoothPrinterManager.java    # Bluetooth SPP + getDeviceInfo (detecção de marca)
│   ├── SunmiPrinterManager.java        # bindService + sendRAWData
│   ├── PaxPrinterManager.java          # IDAL/Neptune + interpretador ESC/POS
│   ├── GertecPrinterManager.java       # GEDI + interpretador ESC/POS
│   └── PositivoPrinterManager.java     # bindService AIDL + sendRAWData
├── aidl/com/xcheng/printerservice/     # interface AIDL da POSITIVO (sem binário)
│   ├── IPrinterService.aidl
│   └── IPrinterCallback.aidl
└── libs/                               # SDKs proprietários vendorizados (PAX, Gertec)
    ├── neptune-lite-api-v3.26.00-20210903.jar
    ├── pax-sdk.jar
    ├── libgedi-1.16.8-gpos700-release.aar
    └── gertec-T1_V2_20230804.jar

Como rodar

Requisitos

  • Flutter 3.24.5 (canal stable) — obrigatório, não é sugestão. Versões mais novas do Flutter elevaram o piso oficial de minSdk para 24 e o engine passou a chamar Configuration.getLocales() sem guarda de versão, o que trava no boot em qualquer aparelho Android 6 (API 23) — sem exceção, sem flag de configuração para contornar. Compilar este projeto com uma versão diferente do Flutter quebra o suporte a API 23 imediatamente, mesmo que o build feche sem erro. Se for necessário atualizar o Flutter, refaça a investigação de compatibilidade com API 23 antes de considerar a atualização segura.
  • JDK 17 para o Gradle (AGP 8.1.0, usado pelo Flutter 3.24.5, tem um bug de jlink com JDK 21). Configurar em android/gradle.propertiesorg.gradle.java.home.
  • Android SDK com platform-tools, build-tools, e as plataformas necessárias.

Compilar e instalar

flutter pub get
flutter build apk --debug
adb install -r build/app/outputs/flutter-apk/app-debug.apk

Faixa de SDK Android

android/app/build.gradle:

minSdk 23     // Android 6.0 — abaixo do piso oficial do Flutter atual, usa um
              // workaround (variável em vez de literal) para escapar da
              // migração automática do Flutter que reverteria isso para 24.
targetSdk 33
maxSdk 33

Como adicionar um novo backend de impressora

Para um método baseado em bytes ESC/POS crus (como SUNMI/POSITIVO):

  1. Implemente PrinterConnection em Dart (lib/services/), chamando um MethodChannel próprio.
  2. Implemente o MethodCallHandler em Java (android/app/src/main/java/.../) com connect/disconnect/isConnected/printBytesprintBytes só precisa escrever os bytes recebidos no transporte (socket, AIDL, o que for).
  3. Registre o canal em MainActivity.java.
  4. Adicione o valor no enum PrinterConnectionMethod e no switch de _buildConnection das 3 telas.
  5. Se o fabricante tiver marca própria detectável, adicione em PrinterBrand + DeviceBrandDetector + BrandMethodResolver.

Para um método com API proprietária de alto nível (sem passthrough de bytes crus, como PAX/Gertec):

  1. Siga os passos acima, mas em vez de escrever os bytes direto, implemente um interpretador que percorre os bytes gerados por EscPosFormat e traduz cada comando reconhecido para chamadas da API nativa (ver PaxPrinterManager.EscPosToPaxInterpreter como referência de estrutura). Documente no código quais comandos não têm equivalente naquela API (e portanto são ignorados) — não simule um recurso que a API não suporta de verdade.

Limitações conhecidas por marca

  • PAX: sem alinhamento nativo (confirmado na API oficial); o interpretador simula com espaços. Nesta versão do SDK vendorizado (neptune-lite-api-v3.26.00), a conexão nativa não conectou em testes reais — usar Bluetooth (nativeJava) até validar uma versão de SDK compatível com o firmware do terminal.
  • Gertec: o .aar vendorizado (libgedi-1.16.8-gpos700-release) foi empacotado visando o modelo GPOS700; o parque de testes deste projeto usa GPOS780, um modelo diferente — validar em hardware real antes de depender disso em produção.
  • SUNMI / POSITIVO: sem ressalvas conhecidas até o momento — ambos aceitam bytes ESC/POS crus via sendRAWData e foram validados imprimindo com sucesso.

Créditos e referências

Este projeto foi construído estudando o código de bibliotecas de referência para cada fabricante (nenhuma delas é dependência direta — os padrões e, quando necessário, arquivos de interface/binários foram adaptados/vendorizados com atribuição):

  • BluetoothUniversalPrinter — driver ESC/POS de referência e interface AIDL da POSITIVO (com.xcheng.printerservice).
  • pax_sdk — wrapper Flutter do SDK IDAL/Neptune do PAX.
  • gertec_pos_printer — wrapper Flutter do SDK GEDI do Gertec.
  • SunmiPrinterDemo — demo oficial SUNMI do com.sunmi:printerlibrary.
  • ThermalPrinterESCPOS — biblioteca ESC/POS genérica (fork do padrão DantSu), usada para validar os comandos de QR/código de barras.

About

Diferentes metodos de impressao para varios equipamento pos existentes no mercado.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages