printRasterPages method

Future<void> printRasterPages(
  1. BleCharacteristic characteristic,
  2. List<MonochromeRaster> pages, {
  3. int density = 3,
  4. int labelType = 1,
  5. int copiesPerPage = 1,
  6. Duration interWriteDelay = const Duration(milliseconds: 5),
  7. Duration pageOpenDelay = const Duration(milliseconds: 30),
  8. Duration statusPollDelay = const Duration(milliseconds: 40),
  9. Duration responseTimeout = const Duration(seconds: 2),
  10. int maxStatusPollsPerPage = 125,
  11. bool coalesceWrites = true,
  12. D11hPrintProgressCallback? onProgress,
})

Prints pages as a single D11H print job.

The job handshake (0x2C/0x23/0x21/0x01) and the closing 0xF3 run once for the whole batch; only the page block (0xA3/0x13/raster/0xE3) repeats. Reopening a job per label makes the printer re-run its start-up and gap calibration between every label, which is the main reason a label-per-call loop is much slower than the official app's continuous printing.

Raster frames are coalesced into ATT-sized packets (at most mtu - 3 bytes) without ever splitting a frame, matching the captured official traffic in capturedTestPrintWrites, where one write carries five frames. For a 96-dot-wide label that is roughly nine row frames per BLE write instead of one. Pass coalesceWrites: false to go back to one write per frame while keeping the single-job structure.

onProgress reports the printer's own cumulative printed-label count as it advances, so callers keep per-label progress while still printing the batch as one job. It is driven by the 0xB3 status poll, which means it reflects labels the printer has actually ejected rather than labels merely sent. It is called synchronously from the print loop: do not block in it, and queue any slow work (network calls, disk writes) instead of awaiting it, or the printer will stall between labels. A callback that throws is logged and ignored — the job is already committed and keeps going.

Implementation

Future<void> printRasterPages(
  BleCharacteristic characteristic,
  List<MonochromeRaster> pages, {
  int density = 3,
  int labelType = 1,
  int copiesPerPage = 1,
  Duration interWriteDelay = const Duration(milliseconds: 5),
  Duration pageOpenDelay = const Duration(milliseconds: 30),
  Duration statusPollDelay = const Duration(milliseconds: 40),
  Duration responseTimeout = const Duration(seconds: 2),
  // Completion polls allowed per *copy* of a page, so the budget scales with
  // copiesPerPage instead of timing out partway through a long run.
  int maxStatusPollsPerPage = 125,
  bool coalesceWrites = true,
  D11hPrintProgressCallback? onProgress,
}) async {
  _ensureConnected('print a raster label');
  if (_printingCapturedTestLabel) {
    throw StateError('A print is already in progress.');
  }
  if (!characteristic.canNotify ||
      !characteristic.properties.contains(
        BleCharacteristicProperty.writeWithoutResponse,
      )) {
    throw StateError(
      'Raster printing requires notify and writeWithoutResponse.',
    );
  }
  if (pages.isEmpty) {
    throw ArgumentError.value(pages, 'pages', 'Must not be empty.');
  }
  if (density < 1 || density > 5) {
    throw RangeError.range(density, 1, 5, 'density');
  }
  if (labelType < 1 || labelType > 3) {
    throw RangeError.range(labelType, 1, 3, 'labelType');
  }
  if (copiesPerPage < 1 || copiesPerPage > 0xFFFF) {
    throw RangeError.range(copiesPerPage, 1, 0xFFFF, 'copiesPerPage');
  }
  final totalCopies = copiesPerPage * pages.length;
  if (totalCopies > 0xFFFF) {
    throw RangeError.range(
      totalCopies,
      1,
      0xFFFF,
      'copiesPerPage * pages.length',
    );
  }

  final encodedPages = pages
      .map(encodeD11hRasterRows)
      .toList(growable: false);
  final largestWrite = encodedPages
      .expand((rows) => rows)
      .fold<int>(
        0,
        (largest, row) => row.length > largest ? row.length : largest,
      );
  final mtu = _mtu;
  if (mtu == null || mtu < largestWrite + 3) {
    throw StateError(
      'Raster printing requires MTU ${largestWrite + 3} or larger; '
      'negotiated MTU is ${mtu ?? 'unknown'}.',
    );
  }
  // Concatenating frames in one write is inferred from a single official-app
  // capture. `coalesceWrites: false` falls back to one frame per write so a
  // firmware that rejects concatenation can still use the single-job path.
  final maxPacketBytes = coalesceWrites ? mtu - 3 : 0;

  _printingCapturedTestLabel = true;
  try {
    await subscribe(characteristic);
    final deviceId = _connectedDevice!;
    final setup = <(Uint8List, int)>[
      (buildD11hCommand(0x2C, const <int>[1]), 0x00),
      (buildD11hCommand(0x23, <int>[labelType]), 0x33),
      (buildD11hCommand(0x21, <int>[density]), 0x31),
      (
        buildD11hCommand(0x01, <int>[
          totalCopies >> 8,
          totalCopies & 0xFF,
          0,
          0,
          0,
          0,
          0,
          1,
          0,
        ]),
        0x02,
      ),
    ];
    for (final (command, expectedResponse) in setup) {
      await _writeAndWaitForCommand(
        deviceId,
        characteristic,
        command,
        expectedResponse,
        responseTimeout,
      );
    }

    var expectedPageCount = 0;
    var reportedPageCount = 0;
    for (var index = 0; index < pages.length; index++) {
      final raster = pages[index];
      expectedPageCount += copiesPerPage;

      await _writeWithoutResponse(
        deviceId,
        characteristic,
        buildD11hCommand(0xA3, const <int>[1]),
      );
      if (pageOpenDelay > Duration.zero) {
        await Future<void>.delayed(pageOpenDelay);
      }

      await _writeAndWaitForCommand(
        deviceId,
        characteristic,
        buildD11hCommand(0x13, <int>[
          raster.height >> 8,
          raster.height & 0xFF,
          raster.width >> 8,
          raster.width & 0xFF,
          copiesPerPage >> 8,
          copiesPerPage & 0xFF,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
        ]),
        0x14,
        responseTimeout,
      );

      final packets = _coalesceFrames(encodedPages[index], maxPacketBytes);
      for (final packet in packets) {
        await _writeRasterPacket(deviceId, characteristic, packet);
        if (interWriteDelay > Duration.zero) {
          await Future<void>.delayed(interWriteDelay);
        }
      }

      await _writeAndWaitForCommand(
        deviceId,
        characteristic,
        buildD11hCommand(0xE3, const <int>[1]),
        0xE4,
        responseTimeout,
      );

      // The poll budget covers every copy of the page: one page with N copies
      // is N labels of physical printing, so a fixed budget would time out on
      // a long run even though the printer is working fine.
      final maxPolls = maxStatusPollsPerPage * copiesPerPage;
      var completed = false;
      for (var poll = 0; poll < maxPolls; poll++) {
        final status = await _writeAndWaitForCommand(
          deviceId,
          characteristic,
          buildD11hCommand(0xA3, const <int>[1]),
          0xB3,
          responseTimeout,
        );
        final printedPageCount = _printedPageCount(status);
        if (printedPageCount > reportedPageCount) {
          reportedPageCount = printedPageCount;
          _reportPrintProgress(onProgress, printedPageCount);
        }
        if (printedPageCount >= expectedPageCount) {
          completed = true;
          break;
        }
        await Future<void>.delayed(statusPollDelay);
      }
      if (!completed) {
        throw TimeoutException(
          'Printer did not report print completion.',
          statusPollDelay * maxPolls,
        );
      }
    }

    await _writeAndWaitForCommand(
      deviceId,
      characteristic,
      buildD11hCommand(0xF3, const <int>[1]),
      0xF4,
      responseTimeout,
    );
  } catch (error) {
    _recordError('raster print failed', error);
    rethrow;
  } finally {
    _printingCapturedTestLabel = false;
  }
}