npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shuofang-t50-sdk

v0.1.3

Published

Cross-runtime JavaScript SDK for SUPVAN T50 label printers

Readme

shuofang-t50-sdk

用于在 JavaScript/TypeScript 应用中连接硕方 T50 标签打印机、准备打印数据、渲染标签页面并执行 BLE 或 USB HID 打印。

SDK 接收两类输入:

  • RasterPage:已经按打印点生成的灰度或 RGBA 输入图像;发送前会转换成黑白点阵。
  • DrawPage / DrawJob:使用毫米坐标描述文字、二维码、条码、图片、矩形和直线,由 SDK 渲染成打印点阵。

安装

npm install shuofang-t50-sdk

按运行环境选择入口:

| 入口 | 适用场景 | | --- | --- | | shuofang-t50-sdk | 通用打印、绘制、状态、栅格和自定义 transport API;不包含运行时设备选择 | | shuofang-t50-sdk/browser | 浏览器 Web Bluetooth、WebHID、Canvas 预览,以及通用打印 API | | shuofang-t50-sdk/protocol | BLE/USB 协议帧、状态解析、型号配置和协议常量 | | shuofang-t50-sdk/internal | LZMA、栅格和字节工具;用于实现自定义集成,不建议业务代码直接依赖 |

浏览器的设备选择必须在 HTTPS 或 localhost 中,并且通常需要由用户点击事件直接触发。

最短打印示例

下面的示例打印一张 40 x 30 mm 的空白标签。T50 默认是 8 点/mm,所以图像尺寸为 320 x 240 点。

import {
  PaperType,
  SupvanPrinter,
  WebBluetoothTransport,
} from "shuofang-t50-sdk/browser";

const data = new Uint8Array(320 * 240);
data.fill(255); // 灰度值:0 是黑色,255 是白色

const transport = await WebBluetoothTransport.request("T0");
const printer = new SupvanPrinter(transport);

await printer.connect();
try {
  await printer.print({
    pages: [{ width: 320, height: 240, data }],
    settings: {
      materialWidth: 40,
      materialHeight: 30,
      paperType: PaperType.Gap,
      gap: 3,
      density: 4,
      speed: 40,
      copies: 1,
    },
  });
} finally {
  await printer.disconnect();
}

WebBluetoothTransport.request("T0") 中的 T0 是设备名称前缀,不是固定设备地址。需要 USB HID 时,将 transport 换成:

import { SupvanPrinter, WebHidTransport } from "shuofang-t50-sdk/browser";

const printer = new SupvanPrinter(await WebHidTransport.request());
await printer.connect();

打印数据

RasterPage

interface RasterPage {
  width: number;
  height: number;
  data: Uint8Array | Uint8ClampedArray;
  repeat?: number;
}

| 字段 | 单位/格式 | 说明 | | --- | --- | --- | | width | 点 | 图像宽度,必须是正整数;不能超过当前页面的可用打印宽度 | | height | 点 | 图像高度,必须是正整数 | | data | 灰度或 RGBA | 灰度长度必须是 width * height;RGBA 长度必须是 width * height * 4 | | repeat | 次数 | 当前页面连续打印次数,默认 1,必须是正整数 |

灰度数据使用 0 到 255 表示黑到白。RGBA 数据会先按亮度和透明度转换为灰度,再按热敏阈值转换为黑白。T50 不支持连续灰度打印,灰度值只用于阈值判断;density 调整热敏头打印浓度,也不会产生灰度级别。SDK 不会因为图像太宽而自动缩放;超出 maxWidthDots 会抛出 ValidationError。未指定 maxWidthDots 时,页面可用宽度是 Math.round(materialWidth * dotsPerMm) 点。

PrintJob

interface PrintJob {
  pages: RasterPage[];
  settings?: PrintSettings;
}

pages 不能为空。每个页面先应用自己的 repeat,再根据 settings.copies 展开:

  • oneByOne: true(默认):第 1 份的全部页面 -> 第 2 份的全部页面。
  • oneByOne: false:第 1 页的全部份数 -> 第 2 页的全部份数。

例如两页、copies: 2 时,默认顺序是 A, B, A, B;关闭 oneByOne 后是 A, A, B, B。

打印和绘制任务共用以下页面任务模型;expandPageJob(job, { taskName?, validatePage? }) 负责展开顺序,expandPrintPages() 和 renderDrawJob() 分别补充栅格校验和页面渲染:

interface PageJob<TPage> {
  pages: TPage[];
  settings?: {
    copies?: number;
    oneByOne?: boolean;
  };
}

打印参数

PrintSettings

所有尺寸字段 materialWidth、materialHeight、gap 使用毫米。图像 RasterPage.width 和 height 使用打印点,两者不是同一个单位。

| 字段 | 默认值 | 有效范围 | 作用 | | --- | ---: | --- | --- | | materialWidth | 48 | 1 到 50 mm | 耗材宽度。影响协议耗材参数和居中区域;省略时优先使用已读取耗材信息 | | materialHeight | 30 | 1 到 120 mm | 单张标签高度。BLE 会用它建立打印工作区高度;省略时优先使用已读取耗材信息 | | copies | 1 | 1 到 99 | 整个任务的份数;通常应传整数 | | direction | 0 | 0 到 3 | BLE 和 USB 共用的图像方向编码:0 = 0°、1 = 180°、2 = 270°、3 = 90°(顺时针) | | density | 4 | 0 到 9 | 热敏打印浓度,数值越大通常越深 | | horizontalOffset | 0 | -9 到 9 | 图像水平偏移,单位是打印点;正数向右 | | verticalOffset | 0 | -9 到 9 | 图像垂直偏移,单位是打印点;正数向下 | | paperType | PaperType.Gap | 1、2、5 | 纸张检测方式,见下表 | | gap | 3 | 0 到 8 mm | 相邻标签之间的间隙;省略时优先使用已读取耗材信息 | | oneByOne | true | 布尔值 | 控制多页任务的份数展开顺序 | | tailLength | 0 | 协议字节值 | BLE 参数块中的尾部长度字段;当前 SDK 不单独校验它,通常保持 0 | | speed | 40 | 20 到 60 | USB 传输时下发给打印机的速度参数;当前 BLE 打印路径不使用它控制传输速度 | | maxWidthDots | Math.round(materialWidth * dotsPerMm) | 1 到当前页面宽度 | 可选的图像工作区宽度,单位是点;不能超过当前页面宽度,也不会自动缩放 | | dotsPerMm | 8 | 0.1 到 32 | 协议使用的点密度,单位是点/mm;T50 为 8 点/mm |

PaperType

enum PaperType {
  Gap = 1,
  BlackMark = 2,
  BlackMarkCard = 5,
}

| 值 | 含义 | | ---: | --- | | PaperType.Gap | 使用标签间隙定位 | | PaperType.BlackMark | 使用黑标定位 | | PaperType.BlackMarkCard | 使用黑标卡片类耗材定位 |

对于 USB,设备耗材配置块会把小于 2 的 gap 按 2 写入;如果使用 USB,建议直接传打印机耗材上的实际值。

参数解析和耗材自动读取

resolvePrintSettings(settings?, labelBox?, profile?) 返回包含所有字段的 ResolvedPrintSettings:

const resolved = resolvePrintSettings(
  { density: 5, copies: 2 },
  labelBox,
  SUPVAN_T50_PROFILE,
);

字段的解析优先级如下:

  1. settings 中明确传入的值。
  2. materialWidth、materialHeight、gap 使用 labelBox 中的值。
  3. T50 默认值或传入 PrinterProfile 的 dotsPerMm。
  4. 未指定 maxWidthDots 时,根据解析后的 materialWidth 和 dotsPerMm 计算页面宽度;显式宽度只能小于或等于该值。

SupvanPrinter.print() 默认在这三个耗材字段任意一个缺失时调用 readLabelBox(),再解析设置。可以在构造选项中设置 autoReadLabelBox: false 禁用这次额外通信;禁用后缺失字段使用默认值或 profile 值。

expandPrintPages(job) 会校验页面尺寸、repeat 和像素长度,并返回按照 copies、repeat、oneByOne 展开后的页面数组。它适合在真正连接打印机前检查任务展开结果。

打印机对象

SupvanPrinter

SupvanPrinter 根据 transport.kind 自动选择 BLE 或 USB 后端。

interface SupvanPrinterOptions {
  autoReadLabelBox?: boolean;
  ble?: BlePrinterOptions;
  usb?: UsbPrinterOptions;
  profile?: PrinterProfile;
}

| 成员 | 返回值 | 说明 | | --- | --- | --- | | connected | boolean | 当前连接状态 | | connect() | Promise<void> | 建立连接并初始化 transport | | disconnect() | Promise<void> | 关闭连接并清理接收缓冲区 | | getStatus() | Promise<PrinterStatus> | 读取当前状态 | | readLabelBox() | Promise<LabelBoxInfo> | 读取耗材盒信息 | | print(job) | Promise<void> | 校验、转换并打印整个任务 | | stop() | Promise<boolean> | 请求停止打印并返回设备是否确认停止;BLE 收到停止确认后返回 true |

构造函数会检查 transport 类型:BLE transport 只能配 BLE 后端,USB transport 只能配 USB 后端。没有连接就调用 getStatus()、readLabelBox() 或 print() 会抛出 CommunicationError。

直接使用 BlePrinter / UsbPrinter

通常使用 SupvanPrinter 即可。需要访问后端独有的超时配置时,可以直接构造:

interface BlePrinterOptions {
  timeouts?: {
    commandTimeoutMs?: number;
    printTimeoutMs?: number;
  };
  autoReadLabelBox?: boolean;
  profile?: PrinterProfile;
}

interface UsbPrinterOptions {
  timeouts?: {
    readTimeoutMs?: number;
    printTimeoutMs?: number;
  };
  autoReadLabelBox?: boolean;
  pollIntervalMs?: number;  // 默认 50 ms
  profile?: PrinterProfile;
}

两者都提供 connected、connect()、disconnect()、getStatus()、readLabelBox()、print(job) 和返回 Promise<boolean> 的 stop()。命令和打印超时通过 timeouts 配置;BlePrinter.getStatus(timeoutMs?) 和 BlePrinter.readLabelBox(timeoutMs?) 可单独覆盖本次命令超时。

传输层

Web Bluetooth

interface WebBluetoothOptions {
  chunkSize?: number;    // 默认 20
  chunkDelayMs?: number; // 默认 10 ms
}
const transport = await WebBluetoothTransport.request("T0", {
  chunkSize: 20,
  chunkDelayMs: 10,
});

chunkSize 是每次写入 BLE characteristic 的字节数,chunkDelayMs 是连续写入之间的等待时间。设备名称以 namePrefix 开头才会出现在选择框中。类实例还提供:

| 成员 | 说明 | | --- | --- | | name | 设备名称 | | kind | 固定为 "ble" | | connected | GATT 连接状态 | | connect() / disconnect() | 建立或断开 GATT 连接 | | write(data) | 按 BLE 分片写入一段逻辑数据 | | read(size = 512, timeoutMs = 2000) | 等待通知并读取最多 size 字节 |

WebHID

const transport = await WebHidTransport.request();

WebHidTransport.request() 只筛选 SDK 中列出的硕方 T50 USB VID/PID,不接受额外筛选参数。实例的 write() 会按 64 字节 HID report 拆分;T50 输入报告还带一个协议前导字节,transport 会去掉它并消费每份报告的剩余填充。read(size = 512, timeoutMs = 2000) 从 input report 缓冲区读取逻辑数据。

DrawObject 页面渲染

页面和任务

DrawPage 的坐标和尺寸全部使用毫米;渲染时按 profile.dotsPerMm 转换为点。对象数组的顺序就是绘制顺序,后绘制的对象会覆盖前面的对象。

interface DrawPage {
  width: number;        // mm
  height: number;       // mm
  objects: DrawObject[];
  repeat?: number;      // 默认 1
}

interface DrawJob {
  pages: DrawPage[];
  settings?: {
    copies?: number;    // 默认 1,1-99 的整数
    oneByOne?: boolean; // 默认 true
  };
}

DrawObject

核心对象是按 format 区分的判别联合。所有对象都必须提供 x、y、width、height,单位是毫米;x、y 不能为负,宽高必须大于 0。

| 字段 | 默认值 | 说明 | | --- | --- | --- | | x, y | 无 | 对象左上角坐标,单位 mm | | width, height | 无 | 对象绘制区域,单位 mm | | format | 无 | 必须是 DrawObjectFormat 中的类型值 | | content | 依类型而定 | 文字、二维码或条码内容;二维码和条码必须提供 | | fontFamily | "sans-serif" | TEXT 对象的 Canvas 字体名 | | fontWeight | 普通 | 只有值为 "bold" 时使用粗体 | | fontSize | 3 | TEXT 字号,按 mm 转换为打印点 | | fontStyle | DrawFontStyle.Normal | TEXT 位标志组合,见下表 | | align | "left" | TEXT 对齐方式:left、center 或 right | | antiColor | false | 反色;文字变为白字黑底,二维码/条码变为白色图案黑底 | | autoReturn | false | 按对象宽度自动换行;显式 \n 始终换行 | | lineHeight | 1.25 | 文本行高倍率,最小按 0.5 处理 | | rotation | 0 | 绕对象中心旋转,单位为度;负数会归一化 | | fill | 无 | 矩形填充;transparent 表示不填充,热敏输出只保留黑/白 | | stroke | "#000000" | 矩形或直线颜色;非白色值会按黑色处理 | | strokeWidth | 0.35 | 矩形边框或直线宽度,单位 mm | | image | 无 | IMAGE 对象必须提供图片源;浏览器中通常传 CanvasImageSource |

对象类型

enum DrawObjectFormat {
  Text = "TEXT",
  Ean13 = "EAN_13",
  Code128 = "CODE_128",
  QrCode = "QR_CODE",
  Image = "IMAGE",
  Rectangle = "RECTANGLE",
  Line = "LINE",
}

| 类型 | 内容和行为 | | --- | --- | | Text | 使用 content 绘制文字;默认字体 sans-serif、字号 3、不自动换行 | | QrCode | 使用 content 绘制二维码,内容不能为空;二维码带四个模块的空白区 | | Code128 | 使用 content 绘制 Code 128;内容只能包含 ASCII 0x20 到 0x7f | | Ean13 | 使用 content 绘制 EAN-13;内容必须是 12 位数字,或带正确校验位的 13 位数字 | | Image | 把图片缩放到对象区域;字符串 URL 等资源需通过 imageResolver 转成 CanvasImageSource | | Rectangle | 使用 fill 填充并使用 stroke/strokeWidth 描边 | | Line | 在对象区域垂直居中绘制一条水平线,使用 stroke/strokeWidth |

enum DrawFontStyle {
  Normal = 0,
  Bold = 1,
  Underline = 4,
  Strikeout = 8,
}

例如同时加粗和下划线可以使用 DrawFontStyle.Bold | DrawFontStyle.Underline。

渲染函数

interface DrawRenderOptions {
  imageResolver?: (object: DrawObject) => CanvasImageSource | undefined;
}

interface DrawRenderTarget {
  context: DrawCanvasContext;
  width: number;
  height: number;
}

| API | 参数和返回值 | | --- | --- | | drawPageSize(page, profile?) | 将页面毫米尺寸按 profile.dotsPerMm 转换为点,返回 { width, height };页面宽度由页面尺寸决定,不会自动缩放 | | drawObject(context, object, profile?, options?) | 在给定 Canvas 2D context 中绘制一个对象 | | normalizeDrawObject(input) | 将输入对象归一化为规范 DrawObject;对象类型不匹配时抛出 ValidationError | | renderDrawPage(context, page, profile?, options?) | 清空并填充白色背景,然后按顺序绘制页面对象;返回页面点尺寸 | | renderDrawJob(job, targetFactory, profile?, options?) | 按 copies/repeat/oneByOne 展开页面;targetFactory(size, page, index) 为每张物理页创建目标,返回目标数组 | | normalizeAngle(value) | 将角度归一化到 0 到 <360;非有限数返回 0 |

DrawCanvasContext 是浏览器 Canvas 2D context 的最小接口。只要运行时提供 save、restore、变换、路径、文字和图片绘制方法,就可以把 renderDrawPage 接到其他 Canvas 实现。

浏览器预览

以下 API 从 shuofang-t50-sdk/browser 导出,结果是实际打印点尺寸的 HTMLCanvasElement:

interface BrowserPreviewOptions extends DrawRenderOptions {
  profile?: PrinterProfile;
  canvas?: HTMLCanvasElement;
  canvasFactory?: (width: number, height: number) => HTMLCanvasElement;
  monochrome?: boolean; // 默认 true
}

interface BrowserObjectPreviewOptions extends BrowserPreviewOptions {
  pageWidth?: number;
  pageHeight?: number;
}

| API | 说明 | | --- | --- | | previewDrawPage(page, options?) | 将一个 DrawPage 渲染到 Canvas;可复用 options.canvas 或通过 canvasFactory 创建 | | previewDrawObject(object, options?) | 将一个对象渲染到页面;省略 pageWidth/pageHeight 时使用对象边界 | | previewDrawJob(job, options?) | 渲染任务展开后的所有物理页;每页由 canvasFactory 创建 | | rasterFromPreviewCanvas(canvas, monochrome = true) | 读取 Canvas 像素并转换为 RasterPage;monochrome 为真时输出黑白 RGBA |

示例:

import {
  DrawFontStyle,
  DrawObjectFormat,
  previewDrawJob,
  rasterFromPreviewCanvas,
} from "shuofang-t50-sdk/browser";

const canvases = previewDrawJob({
  pages: [{
    width: 40,
    height: 30,
    objects: [
      {
        x: 3,
        y: 3,
        width: 34,
        height: 8,
        content: "咖啡豆",
        format: DrawObjectFormat.Text,
        fontSize: 4,
        fontStyle: DrawFontStyle.Bold,
        align: "center",
      },
      {
        x: 14,
        y: 13,
        width: 12,
        height: 12,
        content: "https://example.com",
        format: DrawObjectFormat.QrCode,
      },
    ],
  }],
});

const pages = canvases.map((canvas) => rasterFromPreviewCanvas(canvas));

预览结果可以直接放入 PrintJob:

await printer.print({ pages });

栅格和热敏处理

这些函数从通用入口导出,适合在调用打印机前准备图像:

| API | 参数和行为 | | --- | --- | | toGrayscale(page) | 将灰度或 RGBA RasterPage 转为 { width, height, data: Uint8Array };RGBA 使用亮度和 alpha 合成到白底 | | createGrayRaster(width, height, fill = 255) | 创建灰度图;fill 是初始灰度值 | | resizeRaster(source, width, height) | 使用双线性插值缩放灰度图 | | rotateRaster(source, clockwiseQuarterTurns) | 按顺时针 90 度倍数旋转;负数和大于 4 的值会按模 4 处理 | | pasteRaster(target, source, left, top) | 将源图复制到目标图;超出目标边界的部分会裁剪 | | mirrorRaster(source) | 水平镜像并返回新灰度图 | | rasterFromImageData(image) | 将 { width, height, data } 包装为 RasterPage,不复制像素 | | toThermalPixels(data, threshold = 190) | 将 RGBA 转为不透明黑白 RGBA;透明度小于 16 或亮度大于等于阈值的像素变为白色 |

GrayRaster 的 data 是每像素一个字节,0 为黑,255 为白。RgbaImageData 的 data 可以是 Uint8Array 或 Uint8ClampedArray。

型号配置

interface PrinterProfile {
  id: string;
  name: string;
  dotsPerMm: number;
  physicalDpi: number;
}

SDK 自带的 SUPVAN_T50_PROFILE:

| 字段 | 值 | 含义 | | --- | --- | --- | | id | "t50" | 型号标识 | | name | "T50 · 203 DPI" | 显示名称 | | dotsPerMm | 8 | 协议点密度,8 点/mm | | physicalDpi | 203 | 人类可读的物理 DPI |

dotsForMm(value, profile?) 将正的毫米尺寸转换为点并四舍五入,最小返回 1。自定义 profile 可传给 SupvanPrinterOptions.profile、drawPageSize、renderDrawPage、renderDrawJob 和预览选项。

normalizePrinterProfile(profile?) 校验 profile 并返回包含 dotsPerMm 的规范 profile。

打印状态和耗材信息

PrinterState

| 枚举 | 值 | 含义 | | --- | ---: | --- | | Ready | 0 | 准备就绪 | | HeadOverheat | 1 | 打印头温度过高 | | CoverOpen | 2 | 上盖未关好 | | MediaNotInstalled | 3 | 耗材未装好 | | MediaLow | 4 | 耗材余量不足 | | MediaNotDetected | 5 | 未检测到耗材 | | MediaUnrecognized | 6 | 未识别到耗材 | | MediaEmpty | 7 | 耗材已用完 | | BatteryLow | 8 | 电池电压低 | | CommunicationError | 9 | 通信异常 |

PrinterStatus

状态的稳定结构是状态码、标志集合、指标集合和原始响应:

interface PrinterStatus {
  state: PrinterState;
  flags: PrinterStatusFlags;
  metrics: PrinterMetrics;
  raw: Uint8Array;
  rawFlags: Uint8Array;
}

interface PrinterStatusFlags {
  bufferFull: boolean;
  headOverheat: boolean;
  labelReadWriteError: boolean;
  mediaNotDetected: boolean;
  mediaLow: boolean;
  mediaEmpty: boolean;
  mediaUnrecognized: boolean;
  mediaNotInstalled: boolean;
  batteryLow: boolean;
  busy: boolean;
  coverOpen: boolean;
  usbInserted: boolean;
  printing: boolean;
  secondDeviceBusy: boolean;
  labelNotInstalled: boolean;
  charging: boolean;
}

interface PrinterMetrics {
  printedPages: number;
  totalPages: number;
  temperatureC?: number;
  voltageV?: number;
}

| 字段 | 说明 | | --- | --- | | state | PrinterState 枚举值 | | flags | 设备布尔标志;耗材、上盖、忙碌、打印、电池和 USB 状态都在这里 | | metrics | 页计数、打印头温度和电压;设备不提供的指标为 undefined | | raw | 原始状态响应 | | rawFlags | 原始状态标志字节 |

LabelBoxInfo

| 字段 | 说明 | | --- | --- | | uuidHex | 耗材 UUID 的大写十六进制字符串 | | codeHex | 耗材编码的大写十六进制字符串 | | serialNumber | 耗材序列号 | | typeCode | 耗材类型编码 | | rawHeight | 响应中的原始高度值 | | height | SDK 使用的高度值 | | width | 耗材宽度,单位 mm | | rawGap | 响应中的原始间隙值 | | gap | SDK 使用的间隙值,单位 mm | | remaining | 剩余数量 | | template5mm / template40mm | 耗材响应中的模板字段 | | timestampDigits | 响应中的时间数字串 | | raw | 原始耗材响应 |

运行时能力

interface RuntimeCapabilities {
  secureContext: boolean;
  webBluetooth: boolean;
  webHid: boolean;
}

const capabilities = detectCapabilities();

webBluetooth 和 webHid 只有在安全上下文且浏览器提供对应 API 时为真。能力检测不会请求权限,也不会连接设备。

LabelDocument、模板和 SVG

LabelDocument 是标签的跨模块主文档,坐标和尺寸使用毫米;DrawPage 仍作为低复杂度绘制和打印 API 保留。模板只需保存一份 LabelDocument,载入时调用 cloneTemplateDocument(),因此编辑不会修改预制内容。

跨端复制使用 SUPVAN1:<base64url-json> 文本协议:浏览器调用 encodeLabelTransfer() 写入剪贴板,小程序通过 wx.getClipboardData() 读取后调用 decodeLabelTransfer(),即可恢复可编辑的 LabelDocument。协议包包含 magic: "SUPVAN_LABEL"、version: 1 和 document,不依赖图片剪贴板。

import {
  cloneTemplateDocument,
  createLabelDocument,
  createLabelTemplate,
  labelDocumentToSvgString,
  packMonochromeBitmap,
} from "shuofang-t50-sdk";

const template = createLabelTemplate(
  "storage",
  "收纳模板",
  createLabelDocument(40, 30, []),
);
const editable = cloneTemplateDocument(template);
const svg = labelDocumentToSvgString(editable);

黑白图片不需要保存原始大图。可以使用 packMonochromeBitmap(widthDots, heightDots, pixels) 保存为 LabelBitmapResource,再让 image.resourceId 指向 resources.bitmaps。每个点只占 1 bit,导出 SVG 时会按黑点行段生成 <rect>,大图尺寸不会改变标签的毫米映射。

高级协议 API

下面的函数从 shuofang-t50-sdk/protocol 导出。普通应用应使用 SupvanPrinter;只有需要自定义 transport、抓取协议帧或实现其他运行时适配时才需要直接调用。

BLE 协议

| API | 参数和返回值 | | --- | --- | | buildR1(command, value = 0) | 构造单值 BLE 控制帧;command 是 8 位命令,value 是 16 位值 | | buildR2(command, transferSize, packetCount) | 构造批量传输控制帧;传输大小和分包数都是 16 位值 | | buildDataQuery(command, data) | 构造带数据的 BLE 请求帧 | | parseBleResponse(frame, expectedCommand?) | 校验帧头、声明长度、校验和和可选命令号;返回原始帧 | | parseBleStatus(frame) | 校验 0x11 状态响应并返回 PrinterStatus | | parseLabelBoxData(data) | 从耗材响应数据解析 LabelBoxInfo | | parseBleLabelBox(frame) | 校验 0x30 BLE 响应并解析耗材信息 | | makeParameterBlock(settings) | 根据完整设置生成 BLE 耗材参数块 | | prepareBleRaster(page, settings) | 将输入转换为中间灰度图,按公共 direction 方向旋转、居中和偏移;后续会按阈值打包为黑白 BLE 点阵 | | bleImageFrames(page, settings, jobLastPage = false) | 生成固定 4096 字节的 BLE 图像帧;最后一页标志由 jobLastPage 控制 | | compressedBleBatches(frames, maxFrames = 4) | 将图像帧按批次使用 T50 LZMA 压缩;返回 frameCount、data 和估算 speed | | buildPrintBulkPacket(packetIndex, packetCount, data) | 生成 506 字节内层分包;data 最多 500 字节 | | buildBulkOuter512(inner) | 将 506 字节内层分包包装成 512 字节外层分包 | | bulkPackets(data) | 按 500 字节拆分压缩数据并生成 512 字节外层分包;分包总数不能超过 255 |

USB 协议

| API | 参数和返回值 | | --- | --- | | buildVendorRequest(command, value1 = 0, value2?) | 构造 USB vendor request;command 为 8 位,数值参数为 16 位 | | buildMediaConfig(settings) | 根据耗材设置生成 USB 媒介配置块 | | parseUsbStatus(data, totalPages = 0) | 解析至少 8 字节的 USB 状态,并填充总页数 | | prepareUsbRaster(page, settings) | 将输入转换为中间灰度图,按 direction 旋转、居中、偏移并水平镜像;后续会按阈值打包为黑白 USB 点阵 | | usbImageFrames(page, settings, lastJobPage = false, threshold = 190) | 按行打包 USB 图像帧;threshold 控制灰度转黑点的阈值 | | selectUsbTransferBlock(frames, maxFrames = 8, maxSize = 4096) | 从帧列表前部选择一个压缩后不超过 maxSize 的传输块 | | hidReports(payload) | 按 64 字节切分 HID payload,并追加驱动需要的尾部空 report |

LZMA 和底层工具

LZMA 函数和栅格/字节工具从 shuofang-t50-sdk/internal 导出。它们属于实现工具,不是普通业务打印入口。

SUPVAN_LZMA_OPTIONS 是只读配置:

| 参数 | 值 | 作用 | | --- | --- | --- | | a | 2 | 压缩模式 | | d | 13 | 字典参数 | | fb | 128 | nice length | | mf | "bt4" | 匹配查找器 | | lc | 3 | literal context bits | | lp | 0 | literal position bits | | pb | 2 | position bits | | eos | true | 写入 end marker |

| API | 说明 | | --- | --- | | lzmaCompress(source) | 压缩一段数据,并校验输出为 T50 兼容的 LZMA-Alone 格式 | | lzmaCompressFrames(frames) | 先拼接多帧,再调用 lzmaCompress | | inspectLzmaHeader(data) | 读取 properties、字典大小和未压缩大小 | | assertSupvanLzmaHeader(data) | 要求 properties 为 0x5D、字典为 8192 字节,否则抛出 ValidationError |

通用 transport 接口

如果要接入 SDK 没有内置的运行时,实现下面的接口即可交给 SupvanPrinter:

type TransportKind = "ble" | "usb";

interface PrinterTransport {
  readonly kind: TransportKind;
  readonly name: string;
  readonly connected: boolean;
  readonly capabilities?: {
    bulkAck: "required" | "optional" | "none";
    pageSubmission: "separate" | "batched";
    completion: "device-confirmed" | "submit-confirmed";
  };
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  write(data: Uint8Array): Promise<void>;
  read(size?: number, timeoutMs?: number): Promise<Uint8Array>;
}

优先设置结构化的 capabilities:

  • bulkAck:批量 BLE 分包确认方式;required 表示必须等待,optional 表示设备可能发送,none 表示不等待。
  • pageSubmission:多页任务是逐物理页提交还是批量提交。
  • completion:以设备状态确认实际完成,还是提交成功即可结束。

writeChunked(write, data, chunkSize, delayMs = 0) 可把任意数据按指定大小分片写入。AsyncByteQueue 提供 push(data)、clear() 和 read(size = 512, timeoutMs = 2000),用于把异步通知缓冲成 transport 的读取接口。

常量

| 常量 | 值/含义 | | --- | --- | | FRAME_SIZE | 4096,逻辑图像帧大小 | | FRAME_HEADER_SIZE | 14 | | USB_FRAME_DATA_SIZE | 4074 | | HID_REPORT_SIZE | 64 | | SUPVAN_VENDOR_ID | 0x1820 | | T50_PRODUCT_IDS | 0x2072、0x2073、0x2074、0x2076、0x2077、0x207d、0x207f、0x2170 | | LZMA_ALONE_HEADER | [0x5d, 0x00, 0x20, 0x00, 0x00] | | LZMA_DICTIONARY_SIZE | 8192 |

BLE UUID:

| 名称 | UUID | | --- | --- | | BLE_UUIDS.service | 0000e0ff-3c17-d293-8e48-14fe2e4da212 | | BLE_UUIDS.write | 0000ffe9-0000-1000-8000-00805f9b34fb | | BLE_UUIDS.notify | 0000ffe1-0000-1000-8000-00805f9b34fb | | BLE_UUIDS.bulkNotify | 0000ffea-0000-1000-8000-00805f9b34fb |

USB 命令常量:

| 名称 | 值 | | --- | ---: | | USB_COMMANDS.bufferFull | 0x10 | | USB_COMMANDS.inquiryStatus | 0x11 | | USB_COMMANDS.checkDevice | 0x12 | | USB_COMMANDS.startPrint | 0x13 | | USB_COMMANDS.stopPrint | 0x14 | | USB_COMMANDS.returnMaterial | 0x30 | | USB_COMMANDS.transferData | 0x5c | | USB_COMMANDS.setMedia | 0x5d |

错误类型

| 类型 | 常见原因 | | --- | --- | | SupvanError | 所有 SDK 错误的基类 | | ValidationError | 参数范围、页面尺寸、像素长度、帧格式不符合要求 | | CapabilityError | 当前环境没有 Web Bluetooth 或 WebHID 能力,或没有选择设备 | | CommunicationError | 未连接、读写失败、响应长度/校验和错误、打印超时 | | TimeoutError | CommunicationError 的子类,等待设备数据超时 | | DeviceError | 打印机报告上盖、耗材、电池、温度或其他设备状态错误 |

建议在一次打印任务外层统一处理这些错误,并在 DeviceError 时先读取 getStatus();不要把 ValidationError 当成设备故障重试。

已知行为

  • T50 默认 profile 使用 8 点/mm、物理约 203 DPI;页面宽度根据 materialWidth * dotsPerMm 计算。
  • 页面和图像不会自动缩放。需要缩放时先使用 resizeRaster,或在生成 DrawPage 时调整毫米尺寸。
  • PrintSettings.dotsPerMm 表示协议点密度,即点/mm;它不是浏览器屏幕分辨率,也不是 physicalDpi。
  • BLE 和 USB 共用 PrintSettings.direction;底层栅格布局、镜像和帧格式仍分别遵循各自协议。
  • DrawObject 的文字、二维码、条码、矩形和直线最终都会变成黑白热敏输出;彩色 fill/stroke 不会保留彩色。
  • 浏览器 transport 依赖安全上下文,并且设备选择必须由用户操作触发。

开发验证

在 SDK 目录执行:

npm run typecheck
npm test
npm run build
npm run pack:check

npm run build 会先执行类型检查和测试,再生成发布文件。发布前确认版本号和 npm pack --dry-run 内容,然后执行:

npm publish --access public

协议帧和图像处理的补充说明见 docs/PROTOCOL.md。