/quickprint · คู่มือ
MhQuickPrint · คู่มือ
คู่มือนี้สำหรับนักพัฒนาที่เชื่อมต่อ MhQuickPrint เข้ากับระบบธุรกิจ: เชื่อมอย่างไร ส่งอะไร เรนเดอร์หน้าอย่างไร จัดการงานและข้อผิดพลาดอย่างไร ครบในฉบับเดียว ผู้ใช้ทั่วไปอ่านหัวข้อ 1 ก็พอ
01เริ่มใช้เร็ว (ผู้ใช้ทั่วไป)
- 01
ดาวน์โหลดแล้วรัน
รับไฟล์ exe เดียว (ราว 1.3 MB) จากหน้าดาวน์โหลด วางในโฟลเดอร์ถาวรแล้วดับเบิลคลิก ไอคอนในถาดระบบแปลว่าบริการพร้อมแล้ว โปรแกรมไม่มีหน้าต่างหลัก คลิกซ้ายดูสถานะ คลิกขวาเปิดเมนูเต็ม
- 02
พรีวิวและพิมพ์
เมื่อระบบสั่งพิมพ์ หน้าต่างพรีวิวเนทีฟจะเปิดขึ้น: เปลี่ยนหน้า ซูม เปลี่ยนเครื่องพิมพ์ เลือกกระดาษ ตั้งจำนวนสำเนาและขอบ กดพิมพ์แล้วออกทันที
- 03
ออก
คลิกขวาไอคอนถาดระบบ → "หยุดบริการและออก" (มีหน้าต่างยืนยัน) รันพร้อมกันได้ครั้งละหนึ่งอินสแตนซ์


02สถาปัตยกรรมและหลักการ
โปรแกรมโฮสต์เป็นผู้ตัดสินว่า "จะพิมพ์อะไร": จัดหน้าเอกสาร ป้าย หรือสลิปเป็นภาพหน้าเต็ม (PNG) ขนาดคำนวณตาม DPI ของเครื่องพิมพ์เป้าหมาย
MhQuickPrint ทำหน้าที่ "ส่งถึงเครื่องพิมพ์อย่างแม่นยำ": ภาพเดียวกันใช้ทั้งแสดงพรีวิวและสั่งพิมพ์แบบ 1:1 ไม่มีการย่อขยายซ้ำ พรีวิวเป็นอย่างไร ผลลัพธ์เป็นอย่างนั้น
ทั้งสองฝั่งคุยกันผ่าน named pipe ของ Windows ซึ่งเป็นช่องทางภายในเครื่อง ไม่ผ่านเครือข่าย ไม่เปิดพอร์ต มีเฉพาะโปรแกรมของผู้ใช้ปัจจุบันที่เชื่อมได้ หน้าเว็บเชื่อมตรงไม่ได้และไม่จำเป็น — ระบบเว็บให้แบ็กเอนด์หรือไคลเอนต์ท้องถิ่น (เช่น main process ของ Electron) เป็นไคลเอนต์ของ pipe แทน
03ภาพรวมการเชื่อมต่อ: 5 ขั้น
- 01
เช็กบริการ
เชื่อม named pipe แล้วส่ง system.hello ถ้าได้ serverVersion ตอบมาแปลว่าบริการออนไลน์
- 02
รวบรวมเครื่องพิมพ์
printers.list คืนรายการเครื่องพิมพ์และสถานะคิว เลือก printerId เป้าหมาย
- 03
สอบถามความสามารถ
printers.capabilities คืน dpiX / dpiY ขนาดกระดาษ (ไมโครเมตร) พื้นที่พิมพ์ได้ และ capabilityHash
- 04
เรนเดอร์หน้า
เรนเดอร์หน้าเป็น PNG เต็มหน้าที่ DPI เป้าหมาย: จุด = มม. × DPI ÷ 25.4 (ปัดเศษ)
- 05
พรีวิวหรือพิมพ์
jobs.preview เปิดหน้าต่างพรีวิวเนทีฟ jobs.print ปรินต์เงียบ ติดตามด้วย jobs.status และยกเลิกด้วย jobs.cancel
04ช่องทางสื่อสารและขีดจำกัด
ชื่อ pipe ตายตัว: \\.\pipe\LOCAL\MhQuickPrint.v1 จำนวนเต็มเป็น unsigned little-endian ข้อความเป็น UTF-8 JSON สิทธิ์ pipe (DACL) อนุญาตเฉพาะผู้ใช้อินเทอร์แอกทีฟปัจจุบัน กลุ่มผู้ดูแล และ SYSTEM ปฏิเสธ network logon ทุกกรณี ทุกคำขอมี callerId และ timestamp กำกับ
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หน้าละหนึ่งเปิดพรีวิวเนทีฟด้วยข้อมูลหน้าjobs.printหน้าละหนึ่งตรวจแล้วส่งงานพิมพ์เงียบjobs.status0อ่านสถานะงานปัจจุบันjobs.cancel0ขอยกเลิกและคืนสถานะที่แท้จริงsettings.getCalibration0อ่านค่าสอบเทียบ (ยังไม่เปิดใน 0.1.0 คืน MQP-1005)settings.openCalibration0เปิดหน้าจอสอบเทียบ (ยังไม่เปิดใน 0.1.0)clientJobIdstringรหัสงานเฉพาะของธุรกิจ: เหมือนกันทั้งหมดคืนงานเดิม เหมือน id ต่างเนื้อหาคืน MQP-4002 ไม่พิมพ์ซ้ำnamestringชื่องานเพื่อสะดวกจำprinterIdstringรหัสเครื่องพิมพ์เป้าหมาย (จาก printers.list)copiesintจำนวนสำเนา 1–999paper.widthMm / heightMmnumberกว้าง/สูงของกระดาษเป็นมิลลิเมตร (หน่วยหลัก)device.dpiX / dpiYintDPI ที่ใช้เรนเดอร์ ต้องตรงกับความสามารถเครื่องพิมพ์device.capabilityHashstringแฮชความสามารถ ถ้าเปลี่ยนงานจะถูกปฏิเสธเพื่อเรนเดอร์ใหม่pages[].widthDots / heightDotsintขนาดหน้าเป็นจุด = มม. × DPI ÷ 25.4 ปัดเศษpages[].formatstringรูปแบบหน้า ปัจจุบันรับเฉพาะ "png"pages[].blobIndexintดัชนี blob ในเฟรม ต้องไม่ซ้ำและครบทุก blob07กฎการเรนเดอร์หน้า
การจัดหน้าเป็นของผู้เรียก: บาร์โค้ด QR ตาราง ข้อความ วาดลง PNG เต็มหน้าโดยเอนจินของคุณ MhQuickPrint ทำสองอย่าง — แสดงพรีวิวและพิมพ์ 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: เฟรมเสีย เกินขีด เวลาคลาด คำสั่งยังไม่ implement (MQP-1005)MQP-2xxxเครื่องพิมพ์ / ไดรเวอร์: ไม่มีเครื่อง ออฟไลน์ แฮชความสามารถเปลี่ยน (MQP-2007)MQP-3xxxหน้า / ภาพ: ไม่ใช่ PNG ขนาดไม่ตรง (MQP-3002)MQP-4xxxงาน / คิว: clientJobId ขัดกัน (MQP-4002) ฯลฯMQP-5xxxสิทธิ์: ยังไม่ได้รับสิทธิ์ (MQP-5001)MQP-6xxxการติดตั้ง / อัปเดต10ไคลเอนต์ตัวอย่าง Node.js
ไคลเอนต์ pipe ตัวอย่างที่รันได้ (ชุดเดียวกับเครื่องมือ debug ทางการ) เอาไปใช้ใน main process ของ 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);