เปิดลิ้นชักแคชเชียร์ด้วย Browser — สร้าง POS Drawer Web App ด้วย Web Serial, WebUSB และ WebSocket
ทดสอบและควบคุมลิ้นชักเงินสดของ POS ผ่านหน้าเว็บเพียว ๆ โดยไม่ต้องติดตั้งโปรแกรมอะไรเพิ่ม — แค่เปิด Chrome แล้วคลิก

ทดสอบและควบคุมลิ้นชักเงินสดของ POS ผ่านหน้าเว็บเพียว ๆ โดยไม่ต้องติดตั้งโปรแกรมอะไรเพิ่ม — แค่เปิด Chrome แล้วคลิก

งานนี้เกิดจากความรำคาญล้วน ๆ ครับ
ตอนที่ผมต้องทดสอบลิ้นชักเงินสดในระบบ POS ทุกครั้ง ก็ต้องเปิด application เฉพาะ หรือไม่ก็เขียน script Python ขึ้นมาใหม่ มันดูยุ่งยากเกินไปสำหรับแค่อยากรู้ว่า "ลิ้นชักมันเปิดได้ไหม" เลยตัดสินใจสร้าง web app เล็ก ๆ ที่เปิดผ่าน browser แล้วกดปุ่มเดียวจบ — ลองเล่นได้เลยที่ pos-drawer.thana.in.th
ก่อนอื่นต้องเข้าใจก่อนว่าลิ้นชักแคชเชียร์ (cash drawer) ในระบบ POS นั้นไม่ได้ต่อตรงกับคอมพิวเตอร์ครับ มันต่อผ่านสายสัญญาณ RJ11 (ปลั๊กเดียวกับโทรศัพท์บ้านสมัยก่อน) เข้ากับ เครื่องพิมพ์ใบเสร็จ แล้วเครื่องพิมพ์นั้นแหละที่จะส่งสัญญาณไฟฟ้าไปดีด pin เปิดลิ้นชักออกมา
คำสั่งที่ใช้เปิดลิ้นชักคือ ESC/POS command ซึ่งเป็น protocol มาตรฐานของเครื่องพิมพ์ความร้อน POS ทั่วโลก รูปแบบคำสั่งคือ:
ESC p m t1 t2
แปลงเป็น hex bytes ได้:
| Pin | Hex Bytes |
|---|---|
| Pin 2 (ค่า default) | 1B 70 00 19 FA |
| Pin 5 | 1B 70 01 19 FA |
ใน JavaScript แปลงได้แบบนี้:
function getDrawerCmd(pin) {
return new Uint8Array([0x1B, 0x70, pin === 2 ? 0x00 : 0x01, 0x19, 0xFA]);
}
แค่ 5 bytes นี้แหละครับที่เป็นหัวใจทั้งหมด ส่งไปให้เครื่องพิมพ์ แล้วลิ้นชักก็เปิด
ตรงนี้แหละที่มันน่าสนใจครับ Browser ถูก sandbox ไว้ ไม่สามารถเปิด raw TCP socket ไปหาเครื่องพิมพ์ในเครือข่ายได้โดยตรง และแน่นอนว่าไม่สามารถคุยกับ Serial port หรือ USB device ได้เองตามใจ
แต่ปัจจุบัน Web Platform มี API ใหม่ ๆ ที่น่าสนใจมากครับ ผมจึงรองรับการเชื่อมต่อถึง 3 วิธี:
สำหรับเครื่องพิมพ์ที่ต่อผ่าน USB-to-Serial adapter หรือ COM port โดยตรง ใช้ Web Serial API ซึ่งเป็น API ใหม่ที่ Chrome/Edge รองรับ:
serialPort = await navigator.serial.requestPort();
await serialPort.open({ baudRate: parseInt(document.getElementById('serialBaud').value) });
const writer = serialPort.writable.getWriter();
await writer.write(cmd);
writer.releaseLock();
Flow คือ browser จะ popup ให้ผู้ใช้เลือก port เอง (เป็น security feature ที่ browser บังคับ) แล้วค่อยเปิดและส่งข้อมูล ง่ายมากครับ แต่ใช้ได้แค่ Chrome และ Edge เท่านั้น
สำหรับเครื่องพิมพ์ที่ต่อ USB ตรง ๆ ใช้ WebUSB API ซึ่งทำงานในระดับ USB protocol:
usbDevice = await navigator.usb.requestDevice({ filters: [] });
await usbDevice.open();
await usbDevice.selectConfiguration(1);
await usbDevice.claimInterface(i);
await usbDevice.transferOut(epOut.endpointNumber, cmd);
ส่วนที่น่าสนใจคือต้อง enumerate interface และหา endpoint ที่เป็น bulk-out ก่อนครับ เพราะ USB printer แต่ละตัวอาจมี configuration ต่างกัน แล้วถึงจะ transferOut ส่งคำสั่งได้
นี่คือวิธีที่ซับซ้อนที่สุด แต่ก็ flexible ที่สุดครับ สำหรับเครื่องพิมพ์ที่อยู่ในระบบ LAN/WiFi
ปัญหาคือ browser ส่ง raw TCP ไม่ได้ ผมเลยสร้าง WebSocket-to-TCP proxy ด้วย Node.js ขึ้นมา:
ฝั่ง Browser:
wsConn = new WebSocket(url);
wsConn.send(JSON.stringify({
ip,
port: parseInt(port),
data: Array.from(cmd)
}));
ฝั่ง proxy.js (Node.js): proxy รับ WebSocket connection แล้วเปิด TCP socket ไปหาเครื่องพิมพ์ ส่ง bytes ตามที่ browser ส่งมา แล้วปิด connection ทิ้ง
npm install ws
node proxy.js
proxy ฟังอยู่ที่ port 8765 โดย default ครับ
ตัว web app นี้เป็น static HTML + JS ล้วน ๆ ครับ ไม่มี build step เลย เลยเหมาะมากกับ Cloudflare Pages ที่ deploy ได้ง่ายมาก แค่ push ขึ้น GitHub แล้วเชื่อม repo กับ Pages project เดียวก็เสร็จ
ใช้ Wrangler CLI deploy ได้เลย:
npx wrangler pages deploy .
แล้วผูก custom domain pos-drawer.thana.in.th ใน dashboard โดยใช้ CNAME ชี้ไปที่ pos-drawer.pages.dev
| วิธีเชื่อมต่อ | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| Serial/COM Port | ✅ | ✅ | ❌ | ❌ |
| USB Direct | ✅ | ✅ | ❌ | ❌ |
| Network (IP) | ✅ | ✅ | ✅ | ✅ |
Web Serial และ WebUSB ยังเป็น Chrome-only feature ครับ ถ้าใช้ Firefox หรือ Safari ทางเดียวคือวิธี Network
1. ESC/POS ง่ายกว่าที่คิด คิดว่าจะต้องศึกษา protocol ยาวมาก แต่จริง ๆ แล้วสำหรับแค่การเปิดลิ้นชัก มันแค่ 5 bytes เท่านั้นเอง ไม่มีอะไรซับซ้อน
2. Web Serial / WebUSB เป็น API ที่น่าสนใจมาก ไม่รู้ว่ามีคนใช้ API พวกนี้เยอะแค่ไหน แต่มันเปิดโอกาสให้เว็บคุยกับ hardware ได้โดยตรง ใช้แทน native app ในบางงานได้เลย
3. WebSocket proxy เป็น pattern ที่ใช้ได้จริง สำหรับงานที่ browser ต้องคุยกับ TCP device ใน local network การทำ proxy แบบนี้ถือว่า practical มาก ๆ แม้ว่าจะต้องรัน Node.js เพิ่มบนเครื่อง
4. Static site + Cloudflare Pages คือ combo ที่ดีมาก ไม่มี server ให้ดูแล ไม่มี maintenance ให้กังวล แค่ push code แล้วได้ URL ใช้ได้เลย
ถ้ามีเครื่องพิมพ์ POS หรือลิ้นชักอยู่ที่บ้านหรือที่ทำงาน ลองเอาไปเล่นดูได้ครับ — เปิดใน Chrome แล้วกดปุ่มได้เลย ไม่ต้องติดตั้งอะไรเพิ่ม