bluetooth_pos_printer_utils 1.0.3
bluetooth_pos_printer_utils: ^1.0.3 copied to clipboard
This package is a fork of flutter_esc_pos_utils customized for bluetooth_pos_printer.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:bluetooth_pos_printer_utils/bluetooth_pos_printer_utils.dart';
import 'package:bluetooth_pos_printer/bluetooth_pos_printer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'POS Utils Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const PrinterScreen(),
);
}
}
class PrinterScreen extends StatefulWidget {
const PrinterScreen({super.key});
@override
State<PrinterScreen> createState() => _PrinterScreenState();
}
class _PrinterScreenState extends State<PrinterScreen> {
List<BluetoothPrinterDevice> _devices = [];
bool _isScanning = false;
BluetoothPrinterDevice? _connectedDevice;
final _printerPlugin = BluetoothPosPrinter.instance;
PrinterStatus _connectionStatus = PrinterStatus.disconnected;
bool _is80mm = true;
late StreamSubscription<PrinterStatus> _statusSubscription;
@override
void initState() {
super.initState();
_statusSubscription = _printerPlugin.statusStream.listen((status) {
if (mounted) {
setState(() {
_connectionStatus = status;
});
}
});
_startScan();
}
@override
void dispose() {
_statusSubscription.cancel();
super.dispose();
}
void _startScan() async {
final isBluetoothOn = await _printerPlugin.isBluetoothEnabled();
if (!isBluetoothOn && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ative o Bluetooth para escanear.')),
);
return;
}
setState(() {
_isScanning = true;
_devices.clear();
});
try {
final devices = await _printerPlugin.scan(mode: BluetoothScanMode.active);
if (mounted) {
setState(() {
_devices = devices;
});
}
} catch (e) {
debugPrint("Error scanning: \$e");
} finally {
if (mounted) {
setState(() {
_isScanning = false;
});
}
}
}
void _connect(BluetoothPrinterDevice device) async {
final success = await _printerPlugin.connect(device.address);
if (success && mounted) {
setState(() {
_connectedDevice = device;
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Conectado a \${device.name}')));
}
}
void _disconnect() async {
await _printerPlugin.disconnect();
if (mounted) {
setState(() {
_connectedDevice = null;
});
}
}
Future<void> _printAllFeatures() async {
if (_connectedDevice == null) return;
try {
final profile = await CapabilityProfile.load();
final generator = Generator(
_is80mm ? PaperSize.mm80 : PaperSize.mm58,
profile,
);
final bytes = <int>[];
// O comando reset injeta a tabela CP1252 por padrão para suportar o Euro
bytes.addAll(generator.reset());
// Teste 0: Imagem com Halftone
bytes.addAll(
generator.text(
'0. TESTE DE IMAGEM',
styles: const PosStyles(bold: true),
),
);
try {
final data = await rootBundle.load('assets/image1.jpg');
final Uint8List imageBytes = data.buffer.asUint8List();
bytes.addAll(generator.image(imageBytes, align: PosAlign.center));
bytes.addAll(
generator.image(imageBytes, align: PosAlign.left, width: 150),
);
bytes.addAll(
generator.image(imageBytes, align: PosAlign.right, width: 50),
);
bytes.addAll(generator.feed(2));
} catch (e) {
debugPrint('Erro ao carregar a imagem do asset: \$e');
}
// Cabeçalho
bytes.addAll(
generator.text(
'TESTE COMPLETO DE FEATURES',
styles: const PosStyles(
align: PosAlign.center,
bold: true,
height: PosTextSize.size2,
width: PosTextSize.size2,
),
),
);
bytes.addAll(
generator.text(
'================================',
styles: const PosStyles(align: PosAlign.center),
),
);
// Teste 1: Acentuação e Euro (Valida o remove_diacritic)
bytes.addAll(
generator.text(
'1. ACENTUACAO E EURO',
styles: const PosStyles(bold: true),
),
);
bytes.addAll(generator.text('Símbolo do Euro: €'));
bytes.addAll(generator.text('Acentuação: Ação, Coração, Avó'));
bytes.addAll(generator.text('Moedas: \$ £ ¥ ¢'));
bytes.addAll(generator.feed(1));
// Teste 2: Alinhamentos
bytes.addAll(
generator.text('2. ALINHAMENTO', styles: const PosStyles(bold: true)),
);
bytes.addAll(
generator.text(
'Alinhado à Esquerda',
styles: const PosStyles(align: PosAlign.left),
),
);
bytes.addAll(
generator.text(
'Centralizado',
styles: const PosStyles(align: PosAlign.center),
),
);
bytes.addAll(
generator.text(
'Alinhado à Direita',
styles: const PosStyles(align: PosAlign.right),
),
);
bytes.addAll(generator.feed(1));
// Teste 3: Estilos (Negrito, Sublinhado, Reverso, Fontes)
bytes.addAll(
generator.text(
'3. ESTILOS E FONTES',
styles: const PosStyles(bold: true),
),
);
bytes.addAll(
generator.text('Texto em Negrito', styles: const PosStyles(bold: true)),
);
bytes.addAll(
generator.text(
'Texto Sublinhado',
styles: const PosStyles(underline: true),
),
);
bytes.addAll(
generator.text('Texto Reverso', styles: const PosStyles(reverse: true)),
);
bytes.addAll(
generator.text(
'Fonte A (Padrão)',
styles: const PosStyles(fontType: PosFontType.fontA),
),
);
bytes.addAll(
generator.text(
'Fonte B (Menor)',
styles: const PosStyles(fontType: PosFontType.fontB),
),
);
bytes.addAll(
generator.text(
'Fonte C (Opcional)',
styles: const PosStyles(fontType: PosFontType.fontC),
),
);
// Restaura para Fonte A
bytes.addAll(
generator.text(
'De volta para Fonte A',
styles: const PosStyles(fontType: PosFontType.fontA),
),
);
bytes.addAll(generator.feed(1));
// Teste 4: Códigos de Barras
bytes.addAll(
generator.text(
'4. CÓDIGOS DE BARRAS',
styles: const PosStyles(bold: true),
),
);
final List<int> barData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2];
bytes.addAll(generator.barcode(Barcode.upcA(barData)));
bytes.addAll(generator.feed(1));
// Teste 5: QR Code
bytes.addAll(
generator.text('5. QR CODE', styles: const PosStyles(bold: true)),
);
bytes.addAll(generator.qrcode('https://github.com/senuntech'));
bytes.addAll(generator.feed(2));
// Teste 6: Colunas
bytes.addAll(
generator.text('6. COLUNAS', styles: const PosStyles(bold: true)),
);
bytes.addAll(
generator.row([
PosColumn(text: 'Produto', width: 6),
PosColumn(
text: 'Qtd',
width: 2,
styles: const PosStyles(align: PosAlign.center),
),
PosColumn(
text: 'Valor',
width: 4,
styles: const PosStyles(align: PosAlign.right),
),
]),
);
bytes.addAll(
generator.row([
PosColumn(text: 'Coca Cola', width: 6),
PosColumn(
text: '2',
width: 2,
styles: const PosStyles(align: PosAlign.center),
),
PosColumn(
text: '€10.00',
width: 4,
styles: const PosStyles(align: PosAlign.right),
),
]),
);
bytes.addAll(generator.feed(2));
// Corte final
bytes.addAll(generator.cut());
final success = await _printerPlugin.printRawBytes(bytes);
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Falha ao enviar impressão!')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro: \$e')));
}
}
}
Future<void> _printLongImage() async {
if (_connectedDevice == null) return;
try {
final profile = await CapabilityProfile.load();
final generator = Generator(
_is80mm ? PaperSize.mm80 : PaperSize.mm58,
profile,
);
final bytes = <int>[];
bytes.addAll(generator.reset());
final data = await rootBundle.load('assets/image2.png');
final Uint8List imageBytes = data.buffer.asUint8List();
bytes.addAll(
generator.text(
'IMPRESSAO COMPRIDA',
styles: const PosStyles(align: PosAlign.center, bold: true),
),
);
// Definimos halftone: false para textos ficarem nítidos (threshold puro)
// Passar width 576 garante que ele preencha toda a folha,
// pois o Generator fará o clamp automático para 384 se a impressora for 58mm!
bytes.addAll(
generator.image(
imageBytes,
align: PosAlign.center,
halftone: false,
width: 576,
),
);
bytes.addAll(generator.feed(2));
final success = await _printerPlugin.printRawBytes(bytes);
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Falha ao enviar impressão longa!')),
);
}
} catch (e) {
debugPrint('Erro ao imprimir a imagem comprida: \$e');
}
}
Future<void> _printAllFeaturesAsImage() async {
if (_connectedDevice == null) return;
try {
final profile = await CapabilityProfile.load();
final generator = ImageGenerator(
_is80mm ? PaperSize.mm80 : PaperSize.mm58,
profile,
);
// Teste 0: Imagem com Halftone
generator.text('0. TESTE DE IMAGEM', styles: const PosStyles(bold: true));
try {
final data = await rootBundle.load('assets/image1.jpg');
final Uint8List imageBytes = data.buffer.asUint8List();
generator.image(imageBytes, align: PosAlign.center);
generator.image(imageBytes, align: PosAlign.left, width: 150);
generator.image(imageBytes, align: PosAlign.right, width: 50);
generator.feed(2);
} catch (e) {
debugPrint('Erro ao carregar a imagem do asset: \$e');
}
// Cabeçalho
generator.text(
'TESTE COMPLETO DE FEATURES',
styles: const PosStyles(
align: PosAlign.center,
bold: true,
height: PosTextSize.size2,
width: PosTextSize.size2,
),
);
generator.text(
'================================',
styles: const PosStyles(align: PosAlign.center),
);
// Teste 1: Acentuação e Euro
generator.text(
'1. ACENTUACAO E EURO',
styles: const PosStyles(bold: true),
);
generator.text('Símbolo do Euro: €', styles: const PosStyles(bold: true));
generator.text(
'Acentuação: Ação, Coração, Avó',
styles: const PosStyles(bold: true),
);
generator.text('Moedas: \$ £ ¥ ¢', styles: const PosStyles(bold: true));
generator.feed(1);
// Teste 2: Alinhamentos
generator.text('2. ALINHAMENTO', styles: const PosStyles(bold: true));
generator.text(
'Alinhado à Esquerda',
styles: const PosStyles(align: PosAlign.left, bold: true),
);
generator.text(
'Centralizado',
styles: const PosStyles(align: PosAlign.center, bold: true),
);
generator.text(
'Alinhado à Direita',
styles: const PosStyles(align: PosAlign.right, bold: true),
);
generator.feed(1);
// Teste 3: Estilos (Negrito, Sublinhado, Reverso, Fontes)
generator.text(
'3. ESTILOS E FONTES',
styles: const PosStyles(bold: true),
);
generator.text('Texto em Negrito', styles: const PosStyles(bold: true));
generator.text(
'Texto Sublinhado',
styles: const PosStyles(underline: true, bold: true),
);
generator.text(
'Texto Reverso',
styles: const PosStyles(reverse: true, bold: true),
);
generator.text(
'Fonte A (Padrão)',
styles: const PosStyles(fontType: PosFontType.fontA, bold: true),
);
generator.text(
'Fonte B (Menor)',
styles: const PosStyles(fontType: PosFontType.fontB, bold: true),
);
generator.text(
'Fonte C (Opcional)',
styles: const PosStyles(fontType: PosFontType.fontC, bold: true),
);
// Restaura para Fonte A
generator.text(
'De volta para Fonte A',
styles: const PosStyles(fontType: PosFontType.fontA, bold: true),
);
generator.feed(1);
// Teste 4: Códigos de Barras
generator.text(
'4. CÓDIGOS DE BARRAS',
styles: const PosStyles(bold: true),
);
final List<int> barData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2];
generator.barcode(Barcode.upcA(barData));
generator.feed(1);
// Teste 5: QR Code
generator.text('5. QR CODE', styles: const PosStyles(bold: true));
generator.qrcode('https://github.com/senuntech');
generator.feed(2);
// Teste 6: Colunas
generator.text('6. COLUNAS', styles: const PosStyles(bold: true));
generator.row([
PosColumn(
text: 'Produto',
width: 6,
styles: const PosStyles(bold: true),
),
PosColumn(
text: 'Qtd',
width: 2,
styles: const PosStyles(align: PosAlign.center, bold: true),
),
PosColumn(
text: 'Valor',
width: 4,
styles: const PosStyles(align: PosAlign.right, bold: true),
),
]);
generator.row([
PosColumn(
text: 'Coca Cola',
width: 6,
styles: const PosStyles(bold: true),
),
PosColumn(
text: '2',
width: 2,
styles: const PosStyles(align: PosAlign.center, bold: true),
),
PosColumn(
text: '€10.00',
width: 4,
styles: const PosStyles(align: PosAlign.right, bold: true),
),
]);
generator.feed(2);
// O parâmetro threshold (padrão 127) controla o contraste.
// Valores menores que 127 deixam mais claro, maiores deixam mais escuro (0-255).
final bytes = await generator.buildImageBytes(threshold: 90);
final success = await _printerPlugin.printRawBytes(bytes);
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Falha ao enviar impressão!')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro: \$e')));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Utils POS Printer Demo'),
actions: [
Row(
children: [
const Text('58mm', style: TextStyle(fontSize: 12)),
Switch(
value: _is80mm,
onChanged: (val) {
setState(() => _is80mm = val);
},
activeTrackColor: Colors.grey,
activeThumbColor: Colors.white,
),
const Text('80mm', style: TextStyle(fontSize: 12)),
const SizedBox(width: 10),
],
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isScanning ? null : _startScan,
),
],
),
body: Column(
children: [
if (_isScanning) const LinearProgressIndicator(),
if (_connectionStatus == PrinterStatus.reconnecting)
const Text(
'Reconectando impressora...',
style: TextStyle(color: Colors.orange),
),
Expanded(
child: ListView.builder(
itemCount: _devices.length,
itemBuilder: (context, index) {
final device = _devices[index];
final isConnected = _connectedDevice?.address == device.address;
return ListTile(
leading: const Icon(Icons.print),
title: Text(
device.name.isNotEmpty ? device.name : 'Unknown Device',
),
subtitle: Text(device.address),
trailing: ElevatedButton(
onPressed: isConnected
? _disconnect
: () => _connect(device),
child: Text(isConnected ? 'Desconectar' : 'Conectar'),
),
);
},
),
),
],
),
floatingActionButton: _connectedDevice != null
? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
heroTag: 'btn1',
onPressed: _printLongImage,
icon: const Icon(Icons.receipt_long),
label: const Text('Imprimir Print Comprido'),
),
const SizedBox(height: 10),
FloatingActionButton.extended(
heroTag: 'btn2',
onPressed: _printAllFeatures,
icon: const Icon(Icons.print),
label: const Text('Imprimir Funcionalidades'),
),
const SizedBox(height: 10),
FloatingActionButton.extended(
heroTag: 'btn3',
onPressed: _printAllFeaturesAsImage,
icon: const Icon(Icons.image),
label: const Text('Imprimir como Imagem (€)'),
),
],
)
: null,
);
}
}