Aplicativo e biblioteca de referência para impressão térmica em Android usando React Native. O projeto oferece uma API TypeScript única para impressoras ESC/POS Bluetooth, impressoras de rede e impressoras internas de terminais POS.
O pacote Android gerado é com.reactposprinter.
- Bluetooth clássico SPP por
BluetoothSocket; - impressão TCP RAW por IP, usando a porta ESC/POS 9100;
- descoberta de impressoras na sub-rede IPv4 local;
- impressora interna SUNMI pela API
sendRAWData; - impressora interna PAX pelos SDKs IDAL/Neptune;
- impressora interna Gertec/Wiseasy pelo SDK GEDI;
- impressora interna POSITIVO L300/L400/L500 por AIDL;
- escolha automática do transporte conforme o fabricante do terminal;
- papel térmico de 58 mm e 80 mm;
- texto, alinhamento, negrito, escala, QR Code e Code 128;
- QR Code e código de barras rasterizados para maior compatibilidade;
- persistência da configuração e da última impressora utilizada;
- interface pronta para descoberta, conexão e testes de impressão;
- API reutilizável por outras telas e fluxos do aplicativo.
| Componente | Versão ou requisito |
|---|---|
| React Native | 0.75.5 |
| React | 18.3.1 |
| Android mínimo | Android 6.0, API 23 |
| Android de compilação | API 34 |
| Android alvo | API 34 |
| Node.js | 18 ou superior |
| Java | JDK 17 ou 21 |
| Arquitetura React Native | Legada, newArchEnabled=false |
O React Native 0.75 foi escolhido porque é a última linha com suporte ao Android 6/API 23, ainda comum em terminais POS. O JDK 23 não deve ser usado com o Gradle 8.8 deste projeto.
| Método | Identificador | Descoberta | Formato enviado | Observação |
|---|---|---|---|---|
| Automático | auto |
Conforme o método detectado | Conforme o terminal | Recomendado como padrão |
| Bluetooth SPP | bluetooth |
Dispositivos já pareados | ESC/POS bruto | Funciona com impressoras térmicas Bluetooth clássicas |
| Rede TCP | network |
Varredura /24 na porta 9100 |
ESC/POS bruto | Aceita também endereço manual |
| SUNMI interna | sunmiInternal |
Dispositivo virtual interno | ESC/POS bruto | Usa com.sunmi:printerlibrary |
| PAX interna | paxInternal |
Dispositivo virtual interno | Interpretado para IDAL | Pode variar conforme modelo e firmware |
| Gertec interna | gertecInternal |
Dispositivo virtual interno | Interpretado para GEDI | SDK incluído para GPOS700 |
| POSITIVO interna | positivoInternal |
Dispositivo virtual interno | ESC/POS bruto | Usa o serviço com.xcheng.printerservice |
No modo automático:
- SUNMI seleciona
sunmiInternal; - Gertec ou Wiseasy seleciona
gertecInternal; - POSITIVO e modelos L300/L400/L500 selecionam
positivoInternal; - PAX e aparelhos genéricos usam
bluetoothpor segurança.
O método PAX interno permanece disponível manualmente. Bluetooth é usado automaticamente em PAX porque a disponibilidade da API interna depende da combinação entre SDK, modelo e firmware.
App.tsx
|
+-- PrinterConnection (TypeScript)
| |
| +-- permissões Bluetooth
| +-- resolução automática por fabricante
| +-- persistência com AsyncStorage
| +-- NativeModules.POSPrinter
|
+-- escpos.ts
|
+-- texto e formatação ESC/POS
+-- QR Code rasterizado
+-- Code 128 rasterizado
+-- cupom de exemplo
NativeModules.POSPrinter
|
+-- BluetoothPrinterManager -> BluetoothSocket/SPP
+-- NetworkPrinterManager -> Socket TCP/9100
+-- SunmiPrinterManager -> SUNMI sendRAWData
+-- PaxPrinterManager -> PAX IDAL/Neptune
+-- GertecPrinterManager -> Gertec GEDI
+-- PositivoPrinterManager -> AIDL IPrinterService
Os arquivos io/flutter/plugin/common/MethodCall.java e MethodChannel.java são apenas uma pequena camada de compatibilidade para reaproveitar os drivers Java já validados no projeto Flutter. O APK React Native não incorpora nem inicializa o runtime Flutter.
ReactPOSPrinter/
├── App.tsx
├── src/
│ ├── index.ts
│ ├── types.ts
│ ├── nativePrinter.ts
│ ├── printerService.ts
│ └── escpos.ts
├── __tests__/
├── android/
│ └── app/
│ ├── libs/
│ └── src/main/
│ ├── aidl/
│ ├── AndroidManifest.xml
│ └── java/
└── ios/
Arquivos principais:
App.tsx: interface de configuração, conexão e testes;src/types.ts: tipos públicos de impressora, papel, configuração e cupom;src/printerService.ts: API de alto nível usada pelo aplicativo;src/nativePrinter.ts: contrato entre TypeScript e Android;src/escpos.ts: criação dos bytes de impressão;PrinterModule.java: módulo nativo exposto comoNativeModules.POSPrinter;PrinterPackage.java: registro do módulo no React Native;android/app/libs: SDKs proprietários necessários para PAX e Gertec;android/app/src/main/aidl: contratos usados nos terminais POSITIVO.
Instale:
- Node.js 18 ou superior;
- Android Studio e Android SDK 34;
- Android SDK Platform Tools, para acesso ao
adb; - JDK 17 ou JDK 21.
No Windows, o Java que acompanha o Android Studio pode ser utilizado:
$env:JAVA_HOME='C:\Program Files\Android\Android Studio1\jbr'A localização pode ser diferente conforme a instalação do Android Studio.
git clone https://github.com/vasarely-ops/UniversalPrinterReactNative.git
cd UniversalPrinterReactNative
npm installO repositório é privado. A conta usada no clone precisa ter acesso à organização vasarely-ops.
Terminal 1:
npm startTerminal 2:
$env:JAVA_HOME='C:\Program Files\Android\Android Studio1\jbr'
npm run androidVerifique antes se o aparelho está autorizado:
adb devices -l- Abra o aplicativo no terminal Android.
- Selecione
Automáticoou um transporte específico. - Escolha papel de 58 mm ou 80 mm.
- Para Bluetooth, pareie a impressora nas configurações do Android.
- Toque em
Atualizar lista. - Toque na impressora encontrada para conectar.
- Para rede, informe
IP:portaou useBuscar na rede. - Execute um dos testes disponíveis.
- Use
Desconectarantes de trocar de impressora quando necessário.
Impressoras internas aparecem como Impressora interna do terminal, com endereço virtual internal.
Os componentes públicos são exportados por src/index.ts.
type PrinterMethod =
| 'auto'
| 'bluetooth'
| 'network'
| 'sunmiInternal'
| 'paxInternal'
| 'gertecInternal'
| 'positivoInternal';
type EffectivePrinterMethod = Exclude<PrinterMethod, 'auto'>;
type PaperSize = '58mm' | '80mm';
interface PrinterDevice {
name: string;
address: string;
}auto precisa ser resolvido antes de criar uma conexão. Para isso, use resolveMethod.
import {PrinterConnection, feed, textLine} from './src';
async function imprimirBluetooth() {
const printer = new PrinterConnection('bluetooth');
const devices = await printer.listDevices();
if (devices.length === 0) {
throw new Error('Nenhuma impressora Bluetooth pareada.');
}
const connected = await printer.connect(devices[0]);
if (!connected) {
throw new Error('Não foi possível conectar à impressora.');
}
await printer.printBytes([
...textLine('PEDIDO #123', {align: 1, scale: 1, bold: true}),
...textLine('Pagamento aprovado'),
...feed(3),
]);
await printer.disconnect();
}Em Android 12 ou superior, listDevices e connect solicitam automaticamente a permissão BLUETOOTH_CONNECT.
import {PrinterConnection, buildTestReceipt} from './src';
async function imprimirNaRede() {
const printer = new PrinterConnection('network');
await printer.connect({
name: 'Impressora da cozinha',
address: '192.168.0.50:9100',
});
await printer.printBytes(buildTestReceipt('80mm'));
await printer.disconnect();
}Quando a porta é omitida na interface, o aplicativo acrescenta 9100. Na API, prefira sempre fornecer host:porta explicitamente.
import {PrinterConnection, resolveMethod} from './src';
const method = await resolveMethod('auto');
const printer = new PrinterConnection(method);
const devices = await printer.listDevices();resolveMethod consulta Build.MANUFACTURER, Build.BRAND e Build.MODEL pelo módulo Android.
import {PrinterConnection, buildTestReceipt} from './src';
const printer = new PrinterConnection('sunmiInternal');
const [internalPrinter] = await printer.listDevices();
await printer.connect(internalPrinter);
await printer.printBytes(buildTestReceipt('58mm'));Substitua o método por paxInternal, gertecInternal ou positivoInternal conforme o terminal.
A impressão é independente da origem dos dados. Um pedido recebido de uma API pode ser convertido para ReceiptData e impresso normalmente:
import {PrinterConnection, ReceiptData, buildReceipt} from './src';
async function imprimirPedido(id: string) {
const response = await fetch(`https://api.exemplo.com/pedidos/${id}`);
if (!response.ok) {
throw new Error(`Falha ao consultar pedido: HTTP ${response.status}`);
}
const payload = await response.json();
const receipt: ReceiptData = {
storeName: payload.loja.nome,
storeSubtitle: payload.loja.cnpj,
dateTime: new Date(payload.criadoEm),
items: payload.itens.map((item: any) => ({
name: item.descricao,
quantity: item.quantidade,
unitPrice: item.valorUnitario,
})),
qrPayload: payload.urlFiscal,
barcodePayload: payload.codigoBarras,
};
const printer = new PrinterConnection('network');
await printer.connect({name: 'Caixa', address: '192.168.0.50:9100'});
await printer.printBytes(buildReceipt(receipt, '80mm'));
}Em produção, valide o formato da resposta antes de montar o cupom e use try/finally para desconectar quando a conexão não for reutilizada.
Cria uma conexão para um método efetivo. Não passe auto diretamente.
- Bluetooth: retorna dispositivos já pareados;
- rede: procura hosts com a porta 9100 aberta;
- interna: retorna um dispositivo virtual.
Abre o socket ou conecta ao serviço interno. O retorno true confirma que o transporte foi aberto, mas não garante que papel e estado mecânico estejam corretos.
Reconecta ao dispositivo informado ou ao último dispositivo usado pela instância.
Consulta o estado do socket ou serviço nativo.
Envia bytes de 0 a 255. Rejeita uma lista vazia. A Promise pode ser rejeitada por desconexão, permissão, serviço ausente ou erro de escrita.
Fecha a conexão e libera o dispositivo mantido pela instância.
textLine('Produto', {
align: 0, // 0 esquerda, 1 centro, 2 direita
scale: 0, // 0 normal, 1 duplo, 2 triplo
bold: false,
maxScale: 1, // limita a escala para impressoras conservadoras
});Funções disponíveis:
reset()— inicializa o estado ESC/POS;align(0 | 1 | 2)— define alinhamento;bold(boolean)— ativa ou desativa negrito;text(string)— converte texto para bytes;textLine(string, options)— gera uma linha formatada;feed(lines)— avança o papel;cut()— solicita corte parcial;qrCode(data, sizeDots?, stripeHeight?)— gera QR raster;barcodeCode128(data, widthDots?, height?)— gera Code 128 raster;buildTestReceipt(paperSize)— gera o teste completo;buildReceipt(receipt, paperSize)— gera um cupom comercial.
O texto é normalizado para caracteres compatíveis com Latin-1 básico. Caracteres que não podem ser representados são substituídos. Para layouts que dependam de fontes ou Unicode exato, renderize o conteúdo como bitmap antes de enviá-lo.
import {ReceiptData, buildReceipt} from './src';
const receipt: ReceiptData = {
storeName: 'MINHA LOJA',
storeSubtitle: 'CNPJ 12.345.678/0001-90',
dateTime: new Date(),
items: [
{name: 'CAFE 500G', quantity: 2, unitPrice: 18.9},
{name: 'LEITE 1L', quantity: 3, unitPrice: 5.49},
],
qrPayload: 'https://exemplo.com/documento/123',
barcodePayload: '7891234567895',
};
const bytes = buildReceipt(receipt, '58mm');
await printer.printBytes(bytes);Impressoras ESC/POS de fabricantes diferentes não implementam de maneira uniforme os comandos nativos de QR e barcode. Alguns terminais PAX ignoram esses comandos. O projeto gera a matriz em TypeScript e a envia em blocos raster GS v 0.
As imagens são divididas em faixas para não ultrapassar buffers pequenos. Essa abordagem aumenta a compatibilidade entre Bluetooth, rede e APIs internas.
loadSettings e saveSettings utilizam AsyncStorage com a chave @react-pos-printer/settings-v1.
import {loadSettings, saveSettings} from './src';
const settings = await loadSettings();
await saveSettings({
...settings,
method: 'bluetooth',
paperSize: '58mm',
});As configurações armazenam:
- método escolhido pelo usuário;
- largura do papel;
- última impressora utilizada em cada transporte.
O manifesto declara:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.pax.permission.PRINTER" />No Android 12/API 31 ou superior, BLUETOOTH_CONNECT é uma permissão solicitada durante a execução. Em versões anteriores, BLUETOOTH e BLUETOOTH_ADMIN são concedidas na instalação.
O manifesto também declara consultas aos serviços SUNMI e POSITIVO, necessárias devido às regras de visibilidade de pacotes do Android 11 ou superior.
Os seguintes binários estão versionados em android/app/libs:
neptune-lite-api-v3.26.00-20210903.jar;pax-sdk.jar;libgedi-1.16.8-gpos700-release.aar;gertec-T1_V2_20230804.jar.
A biblioteca SUNMI é obtida do Maven:
implementation "com.sunmi:printerlibrary:1.0.24"Ao atualizar qualquer SDK, teste em hardware real. APIs proprietárias podem mudar entre modelos e versões de firmware mesmo quando o código compila.
cd android
$env:JAVA_HOME='C:\Program Files\Android\Android Studio1\jbr'
.\gradlew.bat assembleDebugSaída:
android/app/build/outputs/apk/debug/app-debug.apk
cd android
$env:JAVA_HOME='C:\Program Files\Android\Android Studio1\jbr'
.\gradlew.bat assembleReleaseSaída:
android/app/build/outputs/apk/release/app-release.apk
A configuração atual assina o release com a chave de debug para facilitar a homologação. Não publique esse APK em uma loja.
- Gere um keystore de upload seguro.
- Guarde o arquivo fora do repositório.
- Configure as propriedades do keystore localmente ou no cofre de secrets do CI.
- Crie
signingConfigs.releaseemandroid/app/build.gradle. - Substitua
signingConfig signingConfigs.debugporsigningConfigs.release. - Gere o AAB com
gradlew bundleRelease.
Arquivos *.keystore, *.apk e *.aab estão ignorados pelo Git.
adb devices -l
adb install -r android\app\build\outputs\apk\release\app-release.apk
adb shell monkey -p com.reactposprinter -c android.intent.category.LAUNCHER 1Para remover o aplicativo:
adb uninstall com.reactposprinternpx tsc --noEmit
npm run lint
npm test -- --runInBandOs testes atuais verificam a renderização da interface e a geração dos principais comandos ESC/POS, incluindo os blocos raster de QR e Code 128.
Limpe o log, reproduza o problema e consulte as exceções:
adb logcat -c
adb shell monkey -p com.reactposprinter -c android.intent.category.LAUNCHER 1
adb logcat -v threadtime AndroidRuntime:E ReactNativeJS:E ReactNative:E '*:S'Para confirmar se o processo continua ativo:
adb shell pidof com.reactposprinter- confirme que a impressora foi pareada nas configurações do Android;
- confirme que o Bluetooth está ligado;
- aceite a permissão solicitada no Android 12 ou superior;
- alguns dispositivos BLE não oferecem o perfil clássico SPP e não são compatíveis com este transporte;
- reinicie a impressora e atualize a lista.
- confira se outro aplicativo está conectado à impressora;
- desligue e ligue novamente o Bluetooth;
- remova o pareamento e pareie novamente;
- confirme que o equipamento oferece o UUID SPP
00001101-0000-1000-8000-00805F9B34FB.
- aparelho e impressora devem estar na mesma rede local;
- confirme que a impressora aceita RAW/JetDirect na porta 9100;
- redes com isolamento de clientes impedem a descoberta;
- VPNs podem fazer o app escolher outra interface IPv4;
- use o endereço manual se a descoberta estiver bloqueada;
- teste a porta a partir de outro equipamento da mesma rede.
- confirme fabricante e modelo do terminal;
- confira se o serviço de impressão do fabricante está instalado e ativo;
- valide a versão do firmware contra a versão do SDK incluído;
- em PAX, tente
bluetoothantes de concluir que a impressora está indisponível; - consulte o
adb logcatprocurando porSunmiPrinterManager,PaxPrinterManager,GertecPrinterManagerouPositivoPrinterManager.
- diminua o conteúdo do QR;
- reduza
sizeDots; - use faixas menores em
stripeHeight; - confirme que o transporte não foi interrompido durante o envio;
- em buffers pequenos, evite enviar vários trabalhos grandes em sequência sem intervalo.
- Bluetooth trabalha com dispositivos previamente pareados; o app não realiza o pareamento.
- Bluetooth Low Energy sem perfil SPP não é suportado.
- A descoberta de rede considera uma sub-rede IPv4
/24e a porta 9100. - Não existe retorno padronizado de papel ausente ou tampa aberta em todos os transportes.
- O comando de corte é ignorado por impressoras sem guilhotina.
- Recursos avançados variam conforme a implementação ESC/POS do fabricante.
- A integração iOS do template não possui os drivers de impressão deste projeto; o foco é Android.
- Impressão em PAX e Gertec depende dos SDKs binários incluídos e deve ser homologada em cada modelo.
- Adicione o identificador em
PrinterMethodeEffectivePrinterMethod. - Implemente o transporte Android com
connect,disconnect,isConnectedeprintBytes. - Registre o gerenciador em
PrinterModule.java. - Atualize
methodLabelseresolveMethod, se necessário. - Reutilize os bytes criados em
escpos.tssempre que o hardware aceitar ESC/POS bruto. - Se a API for de alto nível, implemente um interpretador como os drivers PAX e Gertec.
- Adicione testes e valide em equipamento real.
- Nunca versione tokens, senhas, certificados ou keystores de produção.
- Não confie diretamente em dados recebidos de APIs para montar trabalhos de impressão.
- Limite tamanho de textos, imagens e payloads de QR antes de gerar os bytes.
- Use HTTPS e autenticação adequada ao buscar pedidos em serviços externos.
- Trate dados fiscais e pessoais conforme as regras aplicáveis ao produto.
O repositório é privado. Antes de redistribuir o aplicativo ou os SDKs binários, confirme os termos de licença de cada fabricante e as permissões da organização responsável pelo projeto.