Skip to main content

sprint_flutter 0.1.2 copy "sprint_flutter: ^0.1.2" to clipboard
sprint_flutter: ^0.1.2 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),
                  ),
              ]),
            ),
          ]),
        ),
      );
}
0
likes
0
points
0
downloads

Publisher

unverified uploader

Weekly Downloads

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 Android, iOS, macOS, Windows and Linux.

Repository (GitHub)
View/report issues

Topics

#printing #label #thermal #zpl #escpos

License

unknown (license)

Dependencies

ffi, flutter, shared_preferences

More

Packages that depend on sprint_flutter

Packages that implement sprint_flutter