sprint_flutter 0.1.1
sprint_flutter: ^0.1.1 copied to clipboard
sPrint for Flutter: Superroute's printing SDK. Find network, USB and Bluetooth printers, detect their language, print labels, PDF, HTML, images and raw bytes, and run a Superroute print node, on Andro [...]
example/lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sprint_flutter/sprint_flutter.dart';
/// The sPrint example: runs the SDK's checks on this device and shows the
/// results (version, label encoding against the SuperPrinter agent's golden
/// bytes, network / Bluetooth / BLE discovery, a print node running against a
/// local stand-in for Superroute).
void main() => runApp(const ExampleApp());
/// The agent's sha256 of core/testdata/golden "label.shipping.zpl" (15329 bytes).
const goldenShippingZpl = 'afbecab2bbc1cc441a4bb70b4e64eba8d60b6e79e9d0f200459ae99aa48b57b1';
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
final List<(String, String, bool?)> _lines = [];
bool _running = false;
List<DiscoveredPrinter> _nearby = const [];
String? _hmLanguage;
static String _describe(DiscoveredPrinter p) =>
'${p.name} [${p.transports.join('+')} → ${p.transport} ${p.host}]'
'${p.known != null ? ' known ${p.known!.model} (${p.known!.language}, ${p.known!.dpi} dpi)' : ''}';
// The user's two test printers: matched by exact name, nothing else is printed to.
static const hmName = 'HM-A300-43B6';
static const m832Name = 'M832';
DiscoveredPrinter? _found(String name) {
for (final p in _nearby) {
if (p.name == name) return p;
}
return null;
}
Future<void> _testPrinter(String title, Future<String> Function() f) async {
setState(() => _running = true);
await _step(title, f);
setState(() => _running = false);
}
/// Silent identification of the HM-A300: the core's status queries over
/// LE (nothing is printed), else the model table.
Future<String> _identifyHm() async {
final p = _found(hmName) ?? (throw '$hmName not found; run the checks first');
final target = TargetPrinter(name: p.name, host: p.hosts['ble'] ?? p.host, transport: Transports.ble);
final probe = await SPrint.probeLanguage(target);
_hmLanguage = probe?.language;
return probe == null ? 'no answer to the status queries' : '${probe.language} (${probe.evidence})';
}
/// One test label (60 × 40 mm: text, Code 128, QR) to the HM-A300 over LE.
Future<String> _labelHm() async {
final p = _found(hmName) ?? (throw '$hmName not found; run the checks first');
final lang = _hmLanguage ?? Languages.hprt;
final target = TargetPrinter(
name: p.name, host: p.hosts['ble'] ?? p.host, transport: Transports.ble, language: lang, dpi: 203, widthDots: 576);
final label = LabelJob.parse(jsonEncode({
'version': 1,
'width_mm': 60,
'height_mm': 40,
'elements': [
{'type': 'text', 'x': 3, 'y': 3, 'w': 54, 'h': 7, 'text': 'sPrint test label', 'size_pt': 14, 'bold': true},
{'type': 'text', 'x': 3, 'y': 11, 'w': 54, 'h': 5, 'text': '$hmName - $lang', 'size_pt': 9},
{'type': 'barcode', 'x': 3, 'y': 18, 'w': 34, 'h': 12, 'value': 'SPRINT-0001', 'format': 'code128', 'show_value': true},
{'type': 'qr', 'x': 40, 'y': 17, 'w': 17, 'h': 17, 'value': 'https://superroute.ca'},
],
}));
final r = await SPrint.print(target, PrintJob(type: JobTypes.label, label: label));
if (!r.ok) throw r.message ?? r.reason ?? "failed";
return 'printed in $lang';
}
/// One A4 test page (the agent's test document) to the M832 over LE in XPP,
/// no width set: the core draws an XPP printer over LE 832 dots wide, the
/// width it prints over Bluetooth (2304 / 2480 dots it ignores).
Future<String> _pageM832() async {
final p = _found(m832Name) ?? (throw '$m832Name not found; run the checks first');
final target = TargetPrinter(
name: p.name, host: p.hosts['ble'] ?? p.host, transport: Transports.ble, language: Languages.xpp);
final pdf = await SPrint.testPagePdf(subtitle: 'sPrint on ${Platform.operatingSystem}');
final r = await SPrint.print(target, PrintJob(type: JobTypes.pdf, data: pdf));
if (!r.ok) throw r.message ?? r.reason ?? "failed";
return 'A4 page printed (${pdf.length}-byte PDF, xpp, default width over LE)';
}
void _add(String title, String detail, [bool? ok]) {
// ignore: avoid_print
print('SPRINT-CHECK ${ok == null ? 'INFO' : ok ? 'PASS' : 'FAIL'} $title: $detail');
setState(() => _lines.add((title, detail, ok)));
}
Future<void> _step(String title, Future<String> Function() f, {bool infoOnly = false}) async {
try {
final detail = await f();
_add(title, detail, infoOnly ? null : true);
} catch (e) {
_add(title, '$e', infoOnly ? null : false);
}
}
Future<void> _runChecks() async {
setState(() {
_lines.clear();
_running = true;
});
await _step('Core', () async => 'sPrint ${await SPrint.version()}, ${(await SPrint.languages()).length} languages');
await _step('Label encoding (golden)', () async {
final label = LabelJob.parse(await rootBundle.loadString('assets/shipping.json'));
final bytes = await SPrint.encodeLabel(
const TargetPrinter(name: 'golden zpl', host: 'AA:BB:CC:DD:EE:FF', transport: Transports.bluetooth, language: Languages.zpl),
label);
final sum = sha256.convert(bytes).toString();
if (sum != goldenShippingZpl) throw 'sha256 $sum differs from the agent\'s $goldenShippingZpl';
return '${bytes.length} bytes, identical to the SuperPrinter agent';
});
await _step('Identify', () async => 'Zebra ZD421 → ${(await SPrint.identify(const PrinterKeys(names: ['Zebra ZD421'])))?.language}');
await _step('Network scan', () async {
final prefixes = await Discovery.localPrefixes();
final found = await Discovery.scanNetwork();
return '${prefixes.join(', ')}: ${found.length} printer(s)'
'${found.isEmpty ? '' : ' — ${found.map((p) => '${p.name} ${p.host}${p.raw ? ' 9100' : ''}${p.ipp ? ' ipp' : ''}').join('; ')}'}';
}, infoOnly: true);
await _step('Bluetooth permissions', () async {
if (!await Discovery.requestBluetoothPermissions()) throw 'not granted';
return 'granted';
});
await _step('Bluetooth nearby (classic + LE)', () async {
final found = await Discovery.nearbyBluetoothPrinters(scanMs: 12000);
_nearby = found;
final printers = found.where((p) => p.likelyPrinter).toList();
return '${found.length} device(s), ${printers.length} likely printer(s)'
'${printers.isEmpty ? '' : ': ${printers.map(_describe).join('; ')}'}';
}, infoOnly: true);
await _step('USB', () async => '${(await Discovery.usbPrinters()).length} USB printer(s)', infoOnly: true);
await _step('Print node', _nodeCheck);
setState(() => _running = false);
}
/// A print node against a local stand-in for Superroute: it provisions,
/// heartbeats with the registry's printers and polls jobs; its state events
/// reach the app.
Future<String> _nodeCheck() async {
final hits = <String>[];
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((req) async {
hits.add(req.uri.path);
await utf8.decodeStream(req);
final body = switch (req.uri.path) {
'/api/odep/provision' => '{"result":true,"node_id":77,"provision_token":"pt-test","status":"unbound"}',
'/api/odep/heartbeat' => '{"is_bound":false,"name":"Pixel test"}',
_ => '{"result":true,"jobs":[],"commands":[]}',
};
req.response
..headers.contentType = ContentType.json
..write(body);
await req.response.close();
});
final dir = Directory.systemTemp.createTempSync('sprint');
final reg = await PrinterRegistry.open('${dir.path}/printers.json');
await reg.save(const TargetPrinter(name: 'Desk', host: '192.0.2.10', language: Languages.zpl));
final node = await PrintNode.create(reg, state: NodeState.of(server: 'http://127.0.0.1:${server.port}'), deviceName: 'sPrint example');
final states = <NodeState>[];
final sub = node.events.listen((e) {
if (e is NodeStateEvent) states.add(e.state);
});
await node.start();
final deadline = DateTime.now().add(const Duration(seconds: 15));
while (DateTime.now().isBefore(deadline) && !hits.contains('/api/odep/heartbeat')) {
await Future<void>.delayed(const Duration(milliseconds: 200));
}
await node.stop();
final st = await node.state();
await sub.cancel();
await node.dispose();
await reg.close();
await server.close(force: true);
if (!hits.contains('/api/odep/heartbeat')) throw 'no heartbeat reached the stand-in server (${hits.join(', ')})';
if (states.isEmpty) throw 'no state event reached the app';
return 'provisioned (node ${st.nodeId}), heartbeat sent, ${states.length} state event(s); ready to pair '
'(paired: ${st.paired})';
}
@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('sPrint example')),
body: Column(children: [
Padding(
padding: const EdgeInsets.all(12),
child: FilledButton(
key: const Key('run'),
onPressed: _running ? null : _runChecks,
child: Text(_running ? 'Running…' : 'Run checks'),
),
),
Wrap(spacing: 8, children: [
OutlinedButton(
key: const Key('identify-hm'),
onPressed: _running ? null : () => _testPrinter('Identify $hmName', _identifyHm),
child: const Text('Identify HM-A300'),
),
OutlinedButton(
key: const Key('label-hm'),
onPressed: _running ? null : () => _testPrinter('Test label $hmName', _labelHm),
child: const Text('Label → HM-A300'),
),
OutlinedButton(
key: const Key('page-m832'),
onPressed: _running ? null : () => _testPrinter('A4 test page $m832Name', _pageM832),
child: const Text('A4 page → M832'),
),
]),
Expanded(
child: ListView(children: [
for (final (title, detail, ok) in _lines)
ListTile(
leading: Icon(ok == null ? Icons.notes : ok ? Icons.check_circle : Icons.error,
color: ok == null ? Colors.blueGrey : ok ? Colors.green : Colors.red),
title: Text(title),
subtitle: Text(detail),
),
]),
),
]),
),
);
}