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 chamaConfiguration.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 deandroid/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.
- Por que este projeto existe
- Arquitetura
- Métodos de conexão
- Formatação universal (ESC/POS)
- Cupom como imagem
- Detecção de marca e modo automático
- Telas do app
- Estrutura do projeto
- Como rodar
- Como adicionar um novo backend de impressora
- Limitações conhecidas por marca
- Créditos e referências
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:
- Quando dá para falar direto com o SDK nativo do fabricante, fala. Ver Métodos de conexão.
- 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.
┌─────────────────────────────────────────────────────────────┐
│ 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.
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 | — | — |
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 viaPaint.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 usaPaint.setTextSize; PAX usadoubleWidth()/doubleHeight()reais da API (limitado a 2x).GS v 0(imagem raster) → ambos reconstroem umBitmapa partir dos bits recebidos e chamamDrawPictureExt/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.
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 ( kpara QR,GS kpara 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 pacotebarcode(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, verPrinterBrandProfile) — nem toda impressora aceita a magnificação 3x doGS ! nde forma confiável. - Nunca chame
reset()depois dealign()—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 mandavaalign()e, logo em seguida,reset().
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).
lib/services/device_brand_detector.dart lê Build.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.flutterPackageAo 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.
| 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.
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
- Flutter 3.24.5 (canal stable) — obrigatório, não é sugestão. Versões mais novas do Flutter elevaram o piso oficial de
minSdkpara 24 e o engine passou a chamarConfiguration.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
jlinkcom JDK 21). Configurar emandroid/gradle.properties→org.gradle.java.home. - Android SDK com
platform-tools,build-tools, e as plataformas necessárias.
flutter pub get
flutter build apk --debug
adb install -r build/app/outputs/flutter-apk/app-debug.apkandroid/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 33Para um método baseado em bytes ESC/POS crus (como SUNMI/POSITIVO):
- Implemente
PrinterConnectionem Dart (lib/services/), chamando umMethodChannelpróprio. - Implemente o
MethodCallHandlerem Java (android/app/src/main/java/.../) comconnect/disconnect/isConnected/printBytes—printBytessó precisa escrever os bytes recebidos no transporte (socket, AIDL, o que for). - Registre o canal em
MainActivity.java. - Adicione o valor no enum
PrinterConnectionMethode noswitchde_buildConnectiondas 3 telas. - 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):
- Siga os passos acima, mas em vez de escrever os bytes direto, implemente um interpretador que percorre os bytes gerados por
EscPosFormate traduz cada comando reconhecido para chamadas da API nativa (verPaxPrinterManager.EscPosToPaxInterpretercomo 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.
- 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
.aarvendorizado (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
sendRAWDatae foram validados imprimindo com sucesso.
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.