shuofang-t50-sdk
v0.1.3
Published
Cross-runtime JavaScript SDK for SUPVAN T50 label printers
Maintainers
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,
);字段的解析优先级如下:
- settings 中明确传入的值。
- materialWidth、materialHeight、gap 使用 labelBox 中的值。
- T50 默认值或传入 PrinterProfile 的 dotsPerMm。
- 未指定 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:checknpm run build 会先执行类型检查和测试,再生成发布文件。发布前确认版本号和 npm pack --dry-run 内容,然后执行:
npm publish --access public协议帧和图像处理的补充说明见 docs/PROTOCOL.md。
