码禾数字
← 返回码禾快打

/quickprint · 产品手册

码禾快打 · 产品手册

这份手册面向把码禾快打接入业务系统的开发者:怎么连、发什么、怎么渲染页面、怎么处理任务与错误,一份文档讲完。终端用户只需看第 1 节。

01快速上手(终端用户)

  1. 01

    下载运行

    从下载页取回单文件 exe(约 1.3MB),放到固定目录双击运行;托盘出现「码禾快打」图标即代表服务已就绪。程序无主窗口,左键托盘看服务状态,右键是完整菜单。

  2. 02

    预览打印

    业务系统发起打印时弹出原生预览窗口:可翻页缩放、切换打印机、选纸张、改份数与边距;确认后点「打印」立即出纸。

  3. 03

    退出

    右键托盘图标 →「停止服务并退出」(有确认框)。同一时刻只允许一个实例。

服务状态窗口
服务状态窗口
托盘退出确认
托盘退出确认

02架构与工作原理

业务系统(宿主程序)负责决定「打印什么」:把单据、标签或小票排版成整页点阵图(PNG),尺寸按目标打印机的 DPI 精确计算。

码禾快打负责「精确送到打印机」:同一份点阵,预览窗口直接显示,打印时按设备点 1:1 输出,中间不做任何二次缩放——预览看到什么样,打出来就是什么样。

两者之间走 Windows 命名管道(本机内部通道),不经过网络、不开放端口,只有当前登录用户的程序能连上。浏览器网页不能也不需要直接连接——Web 系统由其后端或本地客户端(如 Electron 主进程)充当管道客户端。

03接入总览:五步接通

  1. 01

    确认服务在线

    连接命名管道并发送 system.hello,拿到 serverVersion 即代表服务在。

  2. 02

    枚举打印机

    printers.list 拿到打印机与队列状态,选定目标 printerId。

  3. 03

    查询能力

    printers.capabilities 拿到 dpiX / dpiY、纸型列表(微米)、可打印区域与 capabilityHash。

  4. 04

    渲染整页 PNG

    按目标 DPI 把页面排成整页 PNG:点数 = 毫米 × DPI ÷ 25.4(四舍五入)。

  5. 05

    提交预览或打印

    jobs.preview 弹出原生预览窗口,jobs.print 静默直打;之后用 jobs.status / jobs.cancel 跟踪或取消。

04通信通道与限制

管道名固定为 \\.\pipe\LOCAL\MhQuickPrint.v1,整数一律无符号小端,文本为 UTF-8 JSON。管道访问边界(DACL)只允许:当前交互用户、管理员组、SYSTEM;网络登录一律拒绝。每个请求还携带 callerId 与时间戳。

限制项数值
控制文档(JSON)≤ 1 MiB
单个页面 blob≤ 32 MiB
每任务页数≤ 200
任务 blob 总量≤ 256 MiB
并发请求≤ 4
时间戳偏差≤ 5 分钟(本地 UTC)

05帧协议

每个请求和响应都是一帧:JSON 控制文档在前,页面二进制(PNG)按 blob 跟在后面,长度都由帧头声明,不做 Base64。

帧布局(小端)
uint32  jsonLength        // 控制文档长度
byte[]  json              // UTF-8 JSON
uint32  blobCount         // 附带二进制页数量
repeat blobCount 次:
  uint64  blobLength      // 该页 PNG 字节数
  byte[]  blob            // 整页 PNG 点阵
请求公共字段(envelope)
{
  "protocol": 1,
  "requestId": "123e4567-e89b-12d3-a456-426614174000",
  "callerId": "LabelTrunk",
  "timestampUtcMs": 1700000000000,
  "command": "system.health"
}
字段类型说明
protocolint协议版本,当前为 1
requestIduuid请求关联号,响应原样带回
callerIdstring调用方标识(你的系统名)
timestampUtcMslong本地 UTC 毫秒时间戳(±5 分钟)
commandstring命令名,见下一节

06命令参考

命令一览(携带 blob 数与用途):

jobs.print 完整请求示例
{
  "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": "页面与二进制数据不匹配"
  }
}
命令blob用途
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 暂未开放)
job 字段类型说明
clientJobIdstring业务任务唯一号:同号同内容返回原任务;同号不同内容返回 MQP-4002,绝不重复打印
namestring任务名称,便于识别
printerIdstring目标打印机稳定 ID(来自 printers.list)
copiesint份数,1–999
paper.widthMm / heightMmnumber纸张宽/高(毫米,权威单位)
device.dpiX / dpiYint渲染所用 DPI,须与打印机能力一致
device.capabilityHashstring能力哈希(来自 printers.capabilities);变化则任务被拒并要求重渲染
pages[].widthDots / heightDotsint页面点阵尺寸 = 毫米 × DPI ÷ 25.4 四舍五入
pages[].formatstring页面格式,当前仅接受 "png"
pages[].blobIndexint对应帧内 blob 下标:须唯一且完整覆盖所有 blob

07页面渲染规范

排版权在调用方:条码、二维码、表格、文字都由你的引擎画进整页 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 主进程或本地服务:

qp-client.mjs
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);

11接入排查 FAQ

连不上管道(ECONNREFUSED / ENOENT)?
按顺序检查:①托盘里「码禾快打」图标在不在(服务未运行是最常见原因,双击 exe 启动);②客户端与服务是否同一用户会话(管道只允许当前交互用户 / 管理员 / SYSTEM);③是否误用了 HTTP 客户端——本服务没有端口,只有命名管道。
收到 MQP-2007 能力哈希不匹配?
打印机的 DPI / 纸型 / 驱动设置发生了变化。重新调用 printers.capabilities,用新的 dpiX / dpiY / capabilityHash 按新尺寸重渲染页面,再提交任务。
收到 MQP-3002 页面与数据不匹配?
检查三件事:blobIndex 是否唯一且覆盖全部 blob;提交的字节是否真的是 PNG(PNG 魔数);widthDots / heightDots 是否严格等于 毫米 × DPI ÷ 25.4 四舍五入。
收到 MQP-4002?
同一个 clientJobId 提交了不同的内容。这是防重打在工作:要么沿用原任务的结果,要么这是新的打印业务,换一个新的 clientJobId。
收到 MQP-5001 未授权?
免授权体验期已结束。托盘右键 → 授权管理,复制 32 位设备码发给我们签发授权码,粘贴或导入授权文件即恢复。
浏览器网页能直接连吗?
不能,也不需要:命名管道仅本机进程可连。Web 系统的正确姿势是后端或本地客户端(Electron 主进程、本地常驻服务)充当管道客户端,页面通过它与码禾快打通信。
下载最新版 产品详情