/quickprint · 产品手册
码禾快打 · 产品手册
这份手册面向把码禾快打接入业务系统的开发者:怎么连、发什么、怎么渲染页面、怎么处理任务与错误,一份文档讲完。终端用户只需看第 1 节。
01快速上手(终端用户)
- 01
下载运行
从下载页取回单文件 exe(约 1.3MB),放到固定目录双击运行;托盘出现「码禾快打」图标即代表服务已就绪。程序无主窗口,左键托盘看服务状态,右键是完整菜单。
- 02
预览打印
业务系统发起打印时弹出原生预览窗口:可翻页缩放、切换打印机、选纸张、改份数与边距;确认后点「打印」立即出纸。
- 03
退出
右键托盘图标 →「停止服务并退出」(有确认框)。同一时刻只允许一个实例。


02架构与工作原理
业务系统(宿主程序)负责决定「打印什么」:把单据、标签或小票排版成整页点阵图(PNG),尺寸按目标打印机的 DPI 精确计算。
码禾快打负责「精确送到打印机」:同一份点阵,预览窗口直接显示,打印时按设备点 1:1 输出,中间不做任何二次缩放——预览看到什么样,打出来就是什么样。
两者之间走 Windows 命名管道(本机内部通道),不经过网络、不开放端口,只有当前登录用户的程序能连上。浏览器网页不能也不需要直接连接——Web 系统由其后端或本地客户端(如 Electron 主进程)充当管道客户端。
03接入总览:五步接通
- 01
确认服务在线
连接命名管道并发送 system.hello,拿到 serverVersion 即代表服务在。
- 02
枚举打印机
printers.list 拿到打印机与队列状态,选定目标 printerId。
- 03
查询能力
printers.capabilities 拿到 dpiX / dpiY、纸型列表(微米)、可打印区域与 capabilityHash。
- 04
渲染整页 PNG
按目标 DPI 把页面排成整页 PNG:点数 = 毫米 × DPI ÷ 25.4(四舍五入)。
- 05
提交预览或打印
jobs.preview 弹出原生预览窗口,jobs.print 静默直打;之后用 jobs.status / jobs.cancel 跟踪或取消。
04通信通道与限制
管道名固定为 \\.\pipe\LOCAL\MhQuickPrint.v1,整数一律无符号小端,文本为 UTF-8 JSON。管道访问边界(DACL)只允许:当前交互用户、管理员组、SYSTEM;网络登录一律拒绝。每个请求还携带 callerId 与时间戳。
05帧协议
每个请求和响应都是一帧:JSON 控制文档在前,页面二进制(PNG)按 blob 跟在后面,长度都由帧头声明,不做 Base64。
uint32 jsonLength // 控制文档长度
byte[] json // UTF-8 JSON
uint32 blobCount // 附带二进制页数量
repeat blobCount 次:
uint64 blobLength // 该页 PNG 字节数
byte[] blob // 整页 PNG 点阵{
"protocol": 1,
"requestId": "123e4567-e89b-12d3-a456-426614174000",
"callerId": "LabelTrunk",
"timestampUtcMs": 1700000000000,
"command": "system.health"
}protocolint协议版本,当前为 1requestIduuid请求关联号,响应原样带回callerIdstring调用方标识(你的系统名)timestampUtcMslong本地 UTC 毫秒时间戳(±5 分钟)commandstring命令名,见下一节06命令参考
命令一览(携带 blob 数与用途):
{
"protocol": 1,
"requestId": "123e4567-e89b-12d3-a456-426614174000",
"callerId": "LabelTrunk",
"timestampUtcMs": 1700000000000,
"command": "jobs.print",
"job": {
"clientJobId": "123e4567-e89b-12d3-a456-426614174001",
"name": "ENTRY-TEMPLATE",
"printerId": "stable-printer-id",
"copies": 1,
"paper": { "widthMm": 100.0, "heightMm": 60.0 },
"device": { "dpiX": 203, "dpiY": 203, "capabilityHash": "sha256" },
"pages": [
{ "widthDots": 799, "heightDots": 480, "format": "png", "blobIndex": 0 }
]
}
}{
"protocol": 1,
"requestId": "123e4567-e89b-12d3-a456-426614174000",
"ok": false,
"error": {
"code": "MQP-3002",
"message": "页面与二进制数据不匹配"
}
}system.hello0握手:协商协议与版本system.health0运行、授权与更新健康状态printers.list0打印机列表与队列状态printers.capabilities0DPI、纸型、可打印区域、能力哈希jobs.preview每页 1 个以页面字节打开原生预览jobs.print每页 1 个校验并提交静默打印jobs.status0读取任务当前状态jobs.cancel0请求取消并返回真实结果状态settings.getCalibration0读取校准(0.1.0 暂未开放,返回 MQP-1005)settings.openCalibration0打开校准界面(0.1.0 暂未开放)clientJobIdstring业务任务唯一号:同号同内容返回原任务;同号不同内容返回 MQP-4002,绝不重复打印namestring任务名称,便于识别printerIdstring目标打印机稳定 ID(来自 printers.list)copiesint份数,1–999paper.widthMm / heightMmnumber纸张宽/高(毫米,权威单位)device.dpiX / dpiYint渲染所用 DPI,须与打印机能力一致device.capabilityHashstring能力哈希(来自 printers.capabilities);变化则任务被拒并要求重渲染pages[].widthDots / heightDotsint页面点阵尺寸 = 毫米 × DPI ÷ 25.4 四舍五入pages[].formatstring页面格式,当前仅接受 "png"pages[].blobIndexint对应帧内 blob 下标:须唯一且完整覆盖所有 blob07页面渲染规范
排版权在调用方:条码、二维码、表格、文字都由你的引擎画进整页 PNG。码禾快打只做两件事——预览显示与 1:1 设备点输出,绝不二次缩放。
尺寸公式(四舍五入取整):
widthDots = Math.round(widthMm * dpiX / 25.4)
heightDots = Math.round(heightMm * dpiY / 25.4)
// 例:100 × 60 mm 热敏纸 @203 DPI
// widthDots = Math.round(100 * 203 / 25.4) = 799
// heightDots = Math.round(60 * 203 / 25.4) = 480校验规则:页面点阵尺寸必须与纸张毫米 × DPI 完全一致,否则任务被拒(防错位打印);换打印机后 DPI 不同,必须按新能力重新渲染再提交。
08任务状态与幂等
任务状态机:Created → Previewing / Validating → Spooling → Queued → Printing → Completed / Failed / Cancelled。语义诚实:Queued 表示 Windows 已接收,Completed 表示无已知错误地离开了打印队列——不虚报“已出纸”。
幂等防重打:提交时带上业务唯一的 clientJobId。网络重试、用户连点都只会得到同一份任务结果;同一 ID 换内容提交会得到 MQP-4002 而不会打印两份。
09错误码
所有错误都有稳定的 MQP 编号与中文可读信息,按家族划分:
MQP-1xxx协议 / IPC:帧损坏、超限、时间戳偏差、命令未实现(MQP-1005)MQP-2xxx打印机 / 驱动:打印机不存在、离线、能力哈希变化(MQP-2007)MQP-3xxx页面 / 图像:不是 PNG、尺寸与纸张不符(MQP-3002)MQP-4xxx任务 / 队列:clientJobId 冲突(MQP-4002)等MQP-5xxx授权:未授权(MQP-5001)MQP-6xxx安装 / 升级10Node.js 参考客户端
下面是可运行的管道客户端参考实现(与官方联调工具同源),可直接拷贝进 Electron 主进程或本地服务:
import net from 'node:net';
import { randomUUID } from 'node:crypto';
const PIPE = '\\\\.\\pipe\\LOCAL\\MhQuickPrint.v1';
function frame(json, blobs = []) {
const control = Buffer.from(JSON.stringify(json));
const parts = [Buffer.alloc(4), control, Buffer.alloc(4)];
parts[0].writeUInt32LE(control.length);
parts[2].writeUInt32LE(blobs.length);
for (const blob of blobs) {
const len = Buffer.alloc(8);
len.writeBigUInt64LE(BigInt(blob.length));
parts.push(len, blob);
}
return Buffer.concat(parts);
}
export function send(command, fields = {}, blobs = []) {
const request = {
protocol: 1,
requestId: randomUUID(),
callerId: 'my-app', // 你的系统标识
timestampUtcMs: Date.now(),
command,
...fields,
};
return new Promise((resolve, reject) => {
const socket = net.createConnection(PIPE);
const chunks = [];
let expected = null;
socket.setTimeout(10000, () => socket.destroy(new Error('响应超时')));
socket.once('connect', () => socket.write(frame(request, blobs)));
socket.on('data', chunk => {
chunks.push(chunk);
const data = Buffer.concat(chunks);
if (expected === null && data.length >= 4) expected = data.readUInt32LE(0);
if (expected !== null && data.length >= expected + 8) {
socket.end();
resolve(JSON.parse(data.subarray(4, 4 + expected).toString('utf8')));
}
});
socket.once('error', reject);
});
}import { send } from './qp-client.mjs';
// 1) 确认服务在线
await send('system.hello');
// 2) 选择打印机并读取能力(printerId 来自 printers.list)
const caps = await send('printers.capabilities', { printer: { printerId } });
const device = caps.result; // dpiX / dpiY / capabilityHash / papers[]
// 3) 按目标 DPI 渲染整页 PNG(100 × 60 mm 热敏纸)
const widthDots = Math.round(100 * device.dpiX / 25.4);
const heightDots = Math.round(60 * device.dpiY / 25.4);
const png = await renderLabelPng(widthDots, heightDots); // 你的排版引擎
// 4) 静默打印(blobIndex 对应 blobs 数组下标)
const result = await send('jobs.print', {
job: {
clientJobId: orderNo, // 业务唯一号:防重复打印
name: 'ENTRY-TEMPLATE',
printerId,
copies: 1,
paper: { widthMm: 100, heightMm: 60 },
device: { dpiX: device.dpiX, dpiY: device.dpiY,
capabilityHash: device.capabilityHash },
pages: [{ widthDots, heightDots, format: 'png', blobIndex: 0 }],
},
}, [png]);
if (!result.ok) throw new Error(result.error.code + ' ' + result.error.message);