Skip to content

Latest commit

Β 

History

History
101 lines (77 loc) Β· 4.94 KB

File metadata and controls

101 lines (77 loc) Β· 4.94 KB

Architecture

What is thermoprint?

@thermoprint/core is a TypeScript library for printing on Bluetooth Low Energy thermal printers (Marklife P15, P12, and compatible models). It handles image processing, protocol encoding, and BLE communication with credit-based flow control.

The library does not bundle a BLE stack. Consumers provide a BleTransport implementation for their runtime (Noble for Node.js, Web Bluetooth for browsers, react-native-ble-plx for React Native, etc.).

Monorepo structure

thermoprint/
  packages/
    core/              # @thermoprint/core β€” the library
      src/
        transport/     # BleTransport interface + FlowController
        protocol/      # PrinterProtocol interface + L11 implementation
        device/        # DeviceProfile type + registry + built-in profiles
        image/         # RGBA β†’ 1bpp image pipeline
        printer.ts     # Printer orchestrator (top-level API)
        discovery.ts   # discover() / discoverAll()
        errors.ts      # ThermoprintError + ErrorCode enum
        index.ts       # Public API surface
  docs/

Layer diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Printer orchestrator        β”‚  printer.ts β€” connects, prints, events
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Image   β”‚ Protocol β”‚   Device      β”‚
β”‚ pipeline β”‚ (L11)    β”‚  registry     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚           FlowController            β”‚  Credit-based BLE chunking
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       BleTransport (injected)       β”‚  Consumer-provided adapter
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data flows down when printing. Responses flow up from BLE notifications through parseResponse().

Dependency injection

The library defines a BleTransport interface (in transport/types.ts) and never imports a concrete BLE library. The consumer creates a transport and passes it to Printer.connect() and discover():

import { Printer, discoverAll } from "@thermoprint/core";

const transport = createNobleTransport(); // you build this
const [found] = await discoverAll(transport, { timeoutMs: 5000 });
const printer = await Printer.connect(transport, found);

This keeps the core library platform-agnostic and dependency-free.

Data flow: image to BLE

RawImageData (RGBA Uint8Array, any size)
  β”‚
  β–Ό  toGrayscale()       β€” luminance: 0.299R + 0.587G + 0.114B, alpha blended to white
Float32Array
  β”‚
  β–Ό  floydSteinbergDither() or threshold()
Uint8Array (0=white, 1=black per pixel)
  β”‚
  β–Ό  packBits()          β€” MSB-first, rows padded to byte boundary
ImageBitmap1bpp { data, width, height, bytesPerRow }
  β”‚
  β–Ό  protocol.buildPrintSequence()
PrintCommand[] β€” labeled byte arrays (set-density, enable, print-bitmap, position-to-gap, stop)
  β”‚
  β–Ό  FlowController.send()
BLE chunks (≀ packetSize bytes each, credit-gated)

Package boundaries

Module Depends on Knows about
transport/ nothing BLE primitives only
image/ nothing pixel data only
protocol/ image/ types (ImageBitmap1bpp) command encoding, response parsing
device/ nothing profile data (UUIDs, settings)
printer.ts everything above orchestration

The image/ module is independently importable via the @thermoprint/core/image subpath export, so consumers can use the image pipeline without any BLE or protocol code.

Design decisions

Protocols are pure data transforms. PrinterProtocol.buildPrintSequence() takes an image and returns PrintCommand[] β€” plain byte arrays with labels. No I/O, no side effects, no async. This makes protocols trivially testable.

DeviceProfile is data, not a class. A profile is a plain object literal ({ modelId, protocolId, serviceUuid, ... }). Adding a new printer model is just defining an object and calling registerDevice(). No inheritance, no methods.

No singletons. The device registry and protocol registry are module-level arrays/maps, but every Printer instance holds its own FlowController, connection, and subscriptions. Multiple printers can operate concurrently.

Factory pattern for protocols. registerProtocol("l11", () => new L11Protocol()) β€” the registry stores factories, not instances. Each Printer.connect() call gets a fresh protocol instance.

Errors are typed. All library errors are ThermoprintError with an ErrorCode enum, making it straightforward to handle specific failure modes (e.g., FLOW_CONTROL_TIMEOUT, UNKNOWN_DEVICE, OUT_OF_PAPER).