☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
iot 中級

Print Agent 與硬體整合

💡 比喻:Web 和印表機之間的翻譯官 瀏覽器(Web)不能直接跟印表機溝通, 就像你不能直接用中文跟一個只懂日文的人對話。 Print Agent 就是「翻譯官」—— Web 說「請幫我印這張收據」,Print Agent 翻譯成印表機懂的指令。

瀏覽器的限制:
─────────────────────────────────────────────
❌ 瀏覽器不能直接控制 USB 印表機
❌ 瀏覽器不能送 ESC/POS 指令
❌ 瀏覽器不能開錢箱
❌ 瀏覽器不能控制客顯螢幕

解決方案:Print Agent(本機代理程式)
─────────────────────────────────────────────
✅ Print Agent 是一個跑在本機的小程式
✅ 它監聽 WebSocket 連線
✅ Web 透過 WebSocket 傳送列印指令
✅ Print Agent 收到後轉給印表機

資料流程:
  Web (JS) → WebSocket → Print Agent → USB → 印表機
              ↑
         這就是 Print Agent 扮演「翻譯官」的地方

熱感應印表機原理(ESC/POS 指令)

💡 比喻:印表機的「摩斯密碼」 ESC/POS 是 Epson 發明的印表機指令集, 就像摩斯密碼一樣,每個指令代表不同動作。 例如 \x1b\x40 就是「初始化」, \x1d\x56\x00 就是「切紙」。

常用 ESC/POS 指令

指令                     功能             十六進位
──────────────────────────────────────────────────
ESC @                    初始化印表機     1B 40
LF                       換行             0A
ESC a 1                  置中對齊         1B 61 01
ESC a 0                  靠左對齊         1B 61 00
ESC E 1                  粗體開           1B 45 01
ESC E 0                  粗體關           1B 45 00
GS V 0                   全切紙           1D 56 00
GS V 1                   半切紙           1D 56 01
ESC p 0 100 100          開錢箱           1B 70 00 64 64
GS ! 0x11                放大字體 2x2     1D 21 11

用 C# 定義 ESC/POS 指令

// 定義 ESC/POS 指令的靜態類別 // 封裝常用印表機指令
public static class EscPos // ESC/POS 指令類別
{
    // 初始化印表機 // 重設所有設定
    public static readonly byte[] Init = new byte[] { 0x1B, 0x40 }; // ESC @ 指令 // 印表機重設

    // 換行 // 印一行空白
    public static readonly byte[] LineFeed = new byte[] { 0x0A }; // LF 指令 // 換行

    // 置中對齊 // 文字置中
    public static readonly byte[] AlignCenter = new byte[] { 0x1B, 0x61, 0x01 }; // ESC a 1 // 置中

    // 靠左對齊 // 文字靠左(預設)
    public static readonly byte[] AlignLeft = new byte[] { 0x1B, 0x61, 0x00 }; // ESC a 0 // 靠左

    // 靠右對齊 // 文字靠右
    public static readonly byte[] AlignRight = new byte[] { 0x1B, 0x61, 0x02 }; // ESC a 2 // 靠右

    // 粗體開 // 加粗文字
    public static readonly byte[] BoldOn = new byte[] { 0x1B, 0x45, 0x01 }; // ESC E 1 // 粗體

    // 粗體關 // 取消加粗
    public static readonly byte[] BoldOff = new byte[] { 0x1B, 0x45, 0x00 }; // ESC E 0 // 取消粗體

    // 放大字體(2倍寬2倍高)// 用於店名或總金額
    public static readonly byte[] DoubleSize = new byte[] { 0x1D, 0x21, 0x11 }; // GS ! 0x11 // 2x2 放大

    // 恢復正常字體 // 取消放大
    public static readonly byte[] NormalSize = new byte[] { 0x1D, 0x21, 0x00 }; // GS ! 0x00 // 正常大小

    // 切紙(全切)// 切斷收據紙
    public static readonly byte[] CutPaper = new byte[] { 0x1D, 0x56, 0x00 }; // GS V 0 // 全切

    // 開錢箱 // 送電子信號打開錢箱
    public static readonly byte[] OpenDrawer = new byte[] { 0x1B, 0x70, 0x00, 0x64, 0x64 }; // ESC p // 開錢箱

    // 組合指令的輔助方法 // 把多個 byte 陣列串在一起
    public static byte[] Combine(params byte[][] commands) // 組合指令方法
    {
        var result = new List<byte>(); // 建立 byte 清單 // 收集所有位元組
        foreach (var cmd in commands) // 逐一加入指令
        {
            result.AddRange(cmd); // 加入指令的位元組 // 串接
        }
        return result.ToArray(); // 轉換為陣列回傳 // 完整的指令序列
    }
}

Node.js Print Agent 實作

💡 比喻:一個隨時待命的翻譯官 Print Agent 就是一個 Node.js 程式, 它啟動後會打開一個 WebSocket 伺服器, 等著 Web 端送來列印指令, 收到後就翻譯成 ESC/POS 指令送給印表機。

Node.js Print Agent 程式碼

// Node.js Print Agent 主程式 // 監聽 WebSocket 並控制印表機
const WebSocket = require('ws'); // 引入 WebSocket 套件 // 用於與 Web 端通訊
const { SerialPort } = require('serialport'); // 引入串列埠套件 // USB 印表機用
const escpos = require('escpos'); // 引入 ESC/POS 套件 // 印表機指令

// Print Agent 設定 // 連接埠和印表機設定
const CONFIG = { // 設定物件
    wsPort: 9100,        // WebSocket 監聽埠 // Web 端連線用
    printerPath: '/dev/usb/lp0', // 印表機裝置路徑 // Linux USB 印表機
    encoding: 'Big5',    // 字元編碼 // 中文用 Big5 或 UTF-8
}; // 設定結束

// 建立 WebSocket 伺服器 // 等待 Web 端連線
const wss = new WebSocket.Server({ port: CONFIG.wsPort }); // 監聽指定埠號
console.log(`Print Agent 啟動,監聽 ws://localhost:${CONFIG.wsPort}`); // 顯示啟動訊息

// 處理 WebSocket 連線 // 每個連線代表一個 Web 端
wss.on('connection', (ws) => { // 監聽連線事件
    console.log('Web 端已連線'); // 顯示連線成功

    // 處理收到的訊息 // Web 端送來的列印指令
    ws.on('message', (message) => { // 監聽訊息事件
        try { // 嘗試處理訊息
            const data = JSON.parse(message); // 解析 JSON 訊息 // 取得列印資料
            console.log('收到列印指令:', data.type); // 顯示指令類型

            switch (data.type) { // 根據指令類型處理
                case 'print-receipt': // 列印收據指令
                    printReceipt(data.payload); // 呼叫列印收據方法
                    ws.send(JSON.stringify({ status: 'ok', message: '收據列印成功' })); // 回傳成功
                    break; // 結束 case
                case 'open-drawer': // 開錢箱指令
                    openCashDrawer(); // 呼叫開錢箱方法
                    ws.send(JSON.stringify({ status: 'ok', message: '錢箱已開啟' })); // 回傳成功
                    break; // 結束 case
                case 'test-print': // 測試列印指令
                    testPrint(); // 呼叫測試列印方法
                    ws.send(JSON.stringify({ status: 'ok', message: '測試列印成功' })); // 回傳成功
                    break; // 結束 case
                default: // 未知指令
                    ws.send(JSON.stringify({ status: 'error', message: '未知指令' })); // 回傳錯誤
            }
        } catch (err) { // 處理錯誤
            console.error('處理指令失敗:', err); // 顯示錯誤訊息
            ws.send(JSON.stringify({ status: 'error', message: err.message })); // 回傳錯誤
        }
    }); // message 事件結束

    ws.on('close', () => console.log('Web 端已斷線')); // 斷線事件 // 顯示斷線訊息
}); // connection 事件結束

// 列印收據的方法 // 核心列印功能
function printReceipt(receipt) { // 列印收據函式
    console.log('開始列印收據...'); // 顯示開始列印
    console.log('店名:', receipt.storeName); // 顯示店名
    console.log('商品數:', receipt.items.length); // 顯示商品數量
    console.log('總金額:', receipt.total); // 顯示總金額
    // 實際上這裡會用 escpos 套件送 ESC/POS 指令到印表機 // 這裡簡化為 console.log
}

// 開錢箱的方法 // 送 ESC/POS 開錢箱指令
function openCashDrawer() { // 開錢箱函式
    console.log('開啟錢箱...'); // 顯示開啟訊息
    // 送 ESC p 0 100 100 指令到印表機 // 透過印表機的 RJ-11 接口控制錢箱
}

// 測試列印的方法 // 確認印表機是否正常
function testPrint() { // 測試列印函式
    console.log('執行測試列印...'); // 顯示測試訊息
    // 印出一張測試頁 // 包含日期時間和虛線
}

JS 透過 WebSocket 呼叫 Print Agent

// Web 端的 Print Agent 連線管理 // 負責與 Print Agent 通訊
class PrintClient { // 列印客戶端類別
    constructor(url = 'ws://localhost:9100') { // 建構函式 // 預設連線到本機
        this.url = url; // WebSocket URL // Print Agent 的位址
        this.ws = null; // WebSocket 實例 // 初始為 null
        this.connected = false; // 連線狀態 // 初始未連線
        this.reconnectInterval = 3000; // 重連間隔(毫秒)// 斷線後 3 秒重連
    }

    // 建立連線的方法 // 連到 Print Agent
    connect() { // 連線方法
        this.ws = new WebSocket(this.url); // 建立 WebSocket 連線

        this.ws.onopen = () => { // 連線成功事件
            this.connected = true; // 設定狀態為已連線
            console.log('已連線到 Print Agent'); // 顯示連線成功
            document.getElementById('printerStatus').textContent = '🟢 印表機已連線'; // 更新狀態顯示
        }; // onopen 結束

        this.ws.onclose = () => { // 斷線事件
            this.connected = false; // 設定狀態為未連線
            console.log('與 Print Agent 斷線,準備重連...'); // 顯示斷線訊息
            document.getElementById('printerStatus').textContent = '🔴 印表機已斷線'; // 更新狀態顯示
            setTimeout(() => this.connect(), this.reconnectInterval); // 排程重連 // 3 秒後重試
        }; // onclose 結束

        this.ws.onmessage = (event) => { // 收到訊息事件
            const response = JSON.parse(event.data); // 解析回應 // JSON 格式
            console.log('Print Agent 回應:', response); // 顯示回應內容
        }; // onmessage 結束

        this.ws.onerror = (err) => { // 錯誤事件
            console.error('WebSocket 錯誤:', err); // 顯示錯誤
        }; // onerror 結束
    }

    // 列印收據的方法 // 送列印指令到 Print Agent
    printReceipt(order) { // 列印收據方法
        if (!this.connected) { // 檢查是否已連線
            console.error('未連線到 Print Agent'); // 未連線的錯誤
            return false; // 回傳失敗
        }

        const message = { // 建立訊息物件
            type: 'print-receipt', // 指令類型:列印收據
            payload: { // 列印資料
                storeName: '我的小店', // 店名 // 會印在收據最上方
                items: order.items.map(i => ({ // 商品清單 // 轉換格式
                    name: i.name,      // 商品名稱
                    qty: i.quantity,   // 數量
                    price: i.price,   // 單價
                    subtotal: i.subtotal // 小計
                })), // 商品清單結束
                total: order.total, // 總金額
                payment: order.payment, // 付款方式
                time: new Date().toLocaleString('zh-TW') // 交易時間 // 格式化為中文日期
            } // payload 結束
        }; // 訊息物件結束

        this.ws.send(JSON.stringify(message)); // 送出訊息 // 轉為 JSON 字串
        return true; // 回傳成功
    }

    // 開錢箱的方法 // 送開錢箱指令
    openDrawer() { // 開錢箱方法
        if (!this.connected) return false; // 檢查連線
        this.ws.send(JSON.stringify({ type: 'open-drawer' })); // 送開錢箱指令
        return true; // 回傳成功
    }
}

// 初始化列印客戶端 // 頁面載入時自動連線
const printer = new PrintClient(); // 建立列印客戶端實例
printer.connect(); // 開始連線到 Print Agent

收據模板設計(店名、商品、金額、條碼)

// 定義進階收據模板的類別 // 支援更多格式化選項
public class AdvancedReceiptTemplate // 進階收據模板
{
    // 產生 ESC/POS 收據指令的方法 // 回傳 byte 陣列
    public static byte[] BuildReceiptBytes(Order order, string storeName) // 建立收據方法
    {
        var commands = new List<byte[]>(); // 建立指令清單 // 收集所有 ESC/POS 指令

        // === 收據頭部 === // 店名和基本資訊
        commands.Add(EscPos.Init);         // 初始化印表機 // 重設所有設定
        commands.Add(EscPos.AlignCenter);  // 置中對齊 // 店名要置中
        commands.Add(EscPos.DoubleSize);   // 放大字體 // 店名要大一點
        commands.Add(TextToBytes(storeName)); // 印出店名 // 轉換為 byte 陣列
        commands.Add(EscPos.LineFeed);     // 換行

        commands.Add(EscPos.NormalSize);   // 恢復正常字體 // 其他內容用正常大小
        commands.Add(TextToBytes("統一編號:12345678")); // 印出統編
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(TextToBytes("TEL: 02-1234-5678")); // 印出電話
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(TextToBytes("================================")); // 分隔線 // 32 字元
        commands.Add(EscPos.LineFeed);     // 換行

        // === 交易資訊 === // 日期、單號
        commands.Add(EscPos.AlignLeft);    // 靠左對齊 // 交易資訊靠左
        commands.Add(TextToBytes($"日期:{order.OrderTime:yyyy/MM/dd HH:mm}")); // 交易日期
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(TextToBytes($"單號:{order.Id:D8}")); // 訂單編號 // 8 位補零
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(TextToBytes("--------------------------------")); // 分隔線
        commands.Add(EscPos.LineFeed);     // 換行

        // === 商品明細 === // 逐筆列出
        foreach (var item in order.Items) // 逐一列出商品
        {
            commands.Add(TextToBytes(item.ProductName)); // 商品名稱 // 獨立一行
            commands.Add(EscPos.LineFeed); // 換行
            var detail = $"  {item.Quantity} x ${item.UnitPrice,-8} ${item.Subtotal,8:F0}"; // 格式化明細
            commands.Add(TextToBytes(detail)); // 數量 x 單價 = 小計
            commands.Add(EscPos.LineFeed); // 換行
        }

        // === 總計 === // 金額加總
        commands.Add(TextToBytes("================================")); // 雙線分隔
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(EscPos.BoldOn);       // 粗體開 // 總計要醒目
        commands.Add(EscPos.DoubleSize);   // 放大字體 // 總計要大
        commands.Add(TextToBytes($"總計 ${order.TotalAmount:F0}")); // 總金額
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(EscPos.NormalSize);   // 恢復正常 // 其他內容正常大小
        commands.Add(EscPos.BoldOff);      // 粗體關

        // === 付款資訊 === // 付款方式和找零
        commands.Add(TextToBytes($"付款:{order.Payment}")); // 付款方式
        commands.Add(EscPos.LineFeed);     // 換行

        // === 尾部 === // 感謝語
        commands.Add(TextToBytes("--------------------------------")); // 分隔線
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(EscPos.AlignCenter);  // 置中對齊
        commands.Add(TextToBytes("謝謝光臨,歡迎再來!")); // 感謝語
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(EscPos.LineFeed);     // 多換幾行 // 留白方便撕紙
        commands.Add(EscPos.LineFeed);     // 換行
        commands.Add(EscPos.CutPaper);     // 切紙 // 自動切斷收據

        return EscPos.Combine(commands.ToArray()); // 組合所有指令 // 回傳完整的 byte 陣列
    }

    // 文字轉 byte 陣列的輔助方法 // 使用 UTF-8 編碼
    private static byte[] TextToBytes(string text) // 文字轉 bytes
    {
        return System.Text.Encoding.UTF8.GetBytes(text); // UTF-8 編碼 // 支援中文字
    }
}

錢箱控制(Cash Drawer)

💡 比喻:用電子信號開保險箱 錢箱(Cash Drawer)通常用 RJ-11 線連接到印表機, 印表機收到「開錢箱」的 ESC/POS 指令後, 會透過 RJ-11 接口送出電子脈衝打開錢箱的電磁鎖。 就像用遙控器開車門一樣。

// 定義錢箱控制器的類別 // 管理錢箱的開關
public class CashDrawerController // 錢箱控制器
{
    private bool _isOpen = false; // 錢箱狀態 // 初始為關閉
    private DateTime? _lastOpenTime; // 上次開啟時間 // 記錄操作時間

    // 開啟錢箱的方法 // 送 ESC/POS 指令
    public void Open() // 開啟錢箱方法
    {
        if (_isOpen) // 如果已經開著
        {
            Console.WriteLine("錢箱已經是開啟狀態"); // 顯示提示訊息
            return; // 不需要重複開啟
        }

        // 送 ESC p 0 100 100 到印表機 // 透過印表機控制錢箱
        Console.WriteLine("送出開錢箱指令:ESC p 0 100 100"); // 顯示指令
        _isOpen = true; // 更新狀態為開啟
        _lastOpenTime = DateTime.Now; // 記錄開啟時間
        Console.WriteLine($"錢箱已開啟 - {_lastOpenTime:HH:mm:ss}"); // 顯示開啟時間
    }

    // 錢箱關閉的回呼 // 通常由感測器觸發
    public void OnDrawerClosed() // 錢箱關閉回呼
    {
        _isOpen = false; // 更新狀態為關閉
        Console.WriteLine("錢箱已關閉"); // 顯示關閉訊息
    }
}

客顯(Customer Display)

💡 比喻:面對客人的小螢幕 客顯(Customer Display)就是面對客人的那個小螢幕, 顯示商品名稱、價格、總金額,讓客人知道自己要付多少錢。 有些是 VFD(螢光顯示器),有些是小型 LCD。

// 定義客顯控制器的類別 // 控制客戶面對的顯示器
public class CustomerDisplayController // 客顯控制器
{
    private const int MaxLineLength = 20; // 每行最多顯示字元數 // VFD 通常是 20 字元

    // 顯示歡迎訊息的方法 // 待機時顯示
    public void ShowWelcome() // 顯示歡迎訊息
    {
        var line1 = CenterText("歡迎光臨", MaxLineLength); // 第一行:歡迎光臨 // 置中顯示
        var line2 = CenterText("Welcome", MaxLineLength);  // 第二行:Welcome // 英文歡迎
        Console.WriteLine($"客顯:{line1}"); // 送到客顯第一行
        Console.WriteLine($"客顯:{line2}"); // 送到客顯第二行
    }

    // 顯示商品資訊的方法 // 掃描商品時顯示
    public void ShowItem(string name, decimal price) // 顯示商品方法
    {
        var line1 = name.Length > MaxLineLength // 檢查名稱是否超過顯示寬度
            ? name[..MaxLineLength]  // 太長就截斷 // 只顯示前 20 字
            : name; // 沒超過就完整顯示
        var line2 = $"NT$ {price:F0}".PadLeft(MaxLineLength); // 價格靠右顯示 // 格式化為整數
        Console.WriteLine($"客顯:{line1}"); // 送到客顯第一行
        Console.WriteLine($"客顯:{line2}"); // 送到客顯第二行
    }

    // 顯示總金額的方法 // 結帳時顯示
    public void ShowTotal(decimal total) // 顯示總計方法
    {
        var line1 = CenterText("總  計", MaxLineLength); // 第一行:總計 // 置中
        var line2 = $"NT$ {total:F0}".PadLeft(MaxLineLength); // 第二行:金額 // 靠右
        Console.WriteLine($"客顯:{line1}"); // 送到客顯第一行
        Console.WriteLine($"客顯:{line2}"); // 送到客顯第二行
    }

    // 文字置中的輔助方法 // 在指定寬度內置中
    private static string CenterText(string text, int width) // 置中方法
    {
        var padding = (width - text.Length) / 2; // 計算左邊空白 // 置中計算
        return text.PadLeft(padding + text.Length).PadRight(width); // 左右補空白 // 填滿寬度
    }
}

條碼掃描器整合(USB HID → Keyboard Input)

💡 比喻:掃描器就是一個「超快速打字員」 USB 條碼掃描器的運作原理其實很簡單: 它掃到條碼後,會「假裝自己是鍵盤」, 把條碼數字一個一個「打」出來,最後按 Enter。 所以 Web 端只要監聽鍵盤輸入就能收到條碼了!

條碼掃描器的 JavaScript 監聽

// 條碼掃描器監聽器 // 自動偵測快速鍵盤輸入(=掃描器)
class BarcodeScanner { // 掃描器類別
    constructor(onScan) { // 建構函式 // onScan 是掃到條碼後的回呼
        this.buffer = ''; // 輸入緩衝區 // 暫存掃描到的字元
        this.lastKeyTime = 0; // 上次按鍵時間 // 用於判斷是否為掃描器
        this.threshold = 50; // 按鍵間隔閾值(毫秒)// 人打字不可能這麼快
        this.onScan = onScan; // 掃描完成的回呼函式
        this.minLength = 4; // 最短條碼長度 // 避免誤判
        this.init(); // 初始化監聽 // 開始監聽鍵盤事件
    }

    // 初始化鍵盤監聽 // 綁定 keydown 事件
    init() { // 初始化方法
        document.addEventListener('keydown', (e) => { // 監聽鍵盤按下事件
            const now = Date.now(); // 取得當前時間 // 毫秒級時間戳

            if (e.key === 'Enter') { // 如果按了 Enter // 掃描器讀完會送 Enter
                if (this.buffer.length >= this.minLength) { // 如果緩衝區有足夠長度
                    this.onScan(this.buffer); // 觸發回呼 // 傳入條碼字串
                }
                this.buffer = ''; // 清空緩衝區 // 準備下次掃描
                return; // 結束處理
            }

            if (now - this.lastKeyTime > this.threshold * 3) { // 如果距離上次按鍵太久
                this.buffer = ''; // 清空緩衝區 // 這不是掃描器的輸入
            }

            if (e.key.length === 1) { // 如果是可打印字元 // 排除 Shift、Ctrl 等
                this.buffer += e.key; // 加入緩衝區 // 累積條碼字串
            }

            this.lastKeyTime = now; // 更新上次按鍵時間
        }); // keydown 事件結束
    }
}

// 使用方式 // 建立掃描器並處理掃到的條碼
const scanner = new BarcodeScanner(async (barcode) => { // 建立掃描器實例
    console.log('掃描到條碼:', barcode); // 顯示掃到的條碼
    const response = await fetch(`/api/products/barcode/${barcode}`); // 用條碼查詢商品 // 呼叫後端 API
    if (response.ok) { // 如果查詢成功
        const product = await response.json(); // 解析商品資料 // JSON 格式
        cart.addItem(product); // 自動加入購物車 // 掃一下就加入
        console.log(`已加入:${product.name} $${product.price}`); // 顯示加入的商品
    } else { // 查詢失敗
        alert(`找不到條碼 ${barcode} 對應的商品`); // 顯示找不到的提示
    }
}); // 掃描器建立完成

C# Print Agent 替代方案

💡 比喻:換一個說 C# 的翻譯官 如果你比較熟 C#,也可以用 C# 寫 Print Agent。 功能完全一樣,只是語言不同。 就像有些翻譯官說英文,有些說日文, 但都能把你的話翻譯給印表機聽。

// C# Print Agent 使用 WebSocket // ASP.NET Core 最小 API
// 需要 NuGet:dotnet add package System.IO.Ports // 串列埠套件

// 定義 C# Print Agent 的類別 // 替代 Node.js 方案
public class CSharpPrintAgent // C# Print Agent 類別
{
    private const string PrinterPort = "/dev/usb/lp0"; // 印表機裝置路徑 // Linux USB 印表機

    // 啟動 Print Agent 的方法 // 建立 WebSocket 伺服器
    public static void StartAgent() // 啟動方法
    {
        var builder = WebApplication.CreateBuilder(); // 建立 Web 應用程式建構器
        var app = builder.Build(); // 建構應用程式

        app.UseWebSockets(); // 啟用 WebSocket 支援 // 必須加這行

        // 設定 WebSocket 端點 // 監聽 /ws 路徑
        app.Map("/ws", async (HttpContext context) => // WebSocket 端點
        {
            if (!context.WebSockets.IsWebSocketRequest) // 檢查是否為 WebSocket 請求
            {
                context.Response.StatusCode = 400; // 不是 WebSocket 就回 400 // 錯誤的請求
                return; // 結束處理
            }

            var ws = await context.WebSockets.AcceptWebSocketAsync(); // 接受 WebSocket 連線
            Console.WriteLine("Web 端已透過 WebSocket 連線"); // 顯示連線訊息

            var buffer = new byte[1024 * 4]; // 接收緩衝區 // 4KB
            while (ws.State == System.Net.WebSockets.WebSocketState.Open) // 持續接收訊息
            {
                var result = await ws.ReceiveAsync(buffer, CancellationToken.None); // 接收訊息
                if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close) // 如果是關閉訊息
                    break; // 結束迴圈

                var message = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); // 解碼訊息
                Console.WriteLine($"收到指令:{message}"); // 顯示收到的指令

                // 處理列印指令 // 根據指令類型執行對應操作
                var response = "{\"status\":\"ok\"}"; // 回應訊息 // JSON 格式
                var responseBytes = System.Text.Encoding.UTF8.GetBytes(response); // 編碼回應
                await ws.SendAsync(responseBytes, // 送出回應
                    System.Net.WebSockets.WebSocketMessageType.Text, // 文字類型
                    true, CancellationToken.None); // 送出完成
            }
        }); // WebSocket 端點結束

        app.Run("http://0.0.0.0:9100"); // 啟動伺服器 // 監聽所有介面的 9100 埠
    }
}

🤔 我這樣寫為什麼會錯?

// ❌ 錯誤:WebSocket 斷線後就不管了 // 印表機離線就再也印不了
const ws = new WebSocket('ws://localhost:9100'); // 建立連線 // 只建一次
ws.onclose = () => console.log('斷線了'); // 只印訊息 // 沒有重連邏輯
// 如果 Print Agent 重啟或網路中斷,就再也連不上了 // 店員只能重開整個系統

// ✅ 正確:斷線後自動重連 // 確保列印服務持續可用
function connectPrintAgent() { // 封裝連線函式 // 可重複呼叫
    const ws = new WebSocket('ws://localhost:9100'); // 建立連線
    ws.onopen = () => console.log('已連線到 Print Agent'); // 連線成功
    ws.onclose = () => { // 斷線事件
        console.log('Print Agent 斷線,3 秒後重連...'); // 顯示重連訊息
        setTimeout(connectPrintAgent, 3000); // 3 秒後重新連線 // 遞迴呼叫
    }; // 斷線事件結束
    return ws; // 回傳 WebSocket 實例
}

❌ 錯誤 2:ESC/POS 中文編碼錯誤

// ❌ 錯誤:用 ASCII 編碼送中文 // 中文會變成亂碼
var bytes = System.Text.Encoding.ASCII.GetBytes("美式咖啡"); // ASCII 不支援中文 // 會變成 ????
// 印出來的收據全是問號或亂碼 // 因為 ASCII 只有英文字母

// ✅ 正確:依照印表機支援的編碼 // 通常是 Big5 或 UTF-8
// 如果印表機支援 UTF-8 // 先送 UTF-8 模式指令
var utf8Bytes = System.Text.Encoding.UTF8.GetBytes("美式咖啡"); // UTF-8 編碼 // 支援中文

// 如果印表機只支援 Big5 // 要用 Big5 編碼
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); // 註冊 Big5 編碼支援
var big5Bytes = System.Text.Encoding.GetEncoding("big5").GetBytes("美式咖啡"); // Big5 編碼 // 繁體中文

❌ 錯誤 3:條碼掃描器把輸入當成普通鍵盤

// ❌ 錯誤:沒有區分掃描器和鍵盤輸入 // 店員打字也會被當成條碼
document.addEventListener('keydown', (e) => { // 監聽所有鍵盤輸入
    buffer += e.key; // 全部加入緩衝區 // 不管是掃描器還是鍵盤
    if (e.key === 'Enter') { // 按 Enter 就查詢
        searchProduct(buffer); // 當成條碼查詢 // 如果店員按 Enter 也會觸發
        buffer = ''; // 清空
    }
}); // 這樣店員在搜尋框打字再按 Enter 也會被當成掃描

// ✅ 正確:用時間間隔區分 // 掃描器的按鍵速度遠比人快
document.addEventListener('keydown', (e) => { // 監聽鍵盤輸入
    const now = Date.now(); // 取得當前時間
    if (now - lastKeyTime > 150) buffer = ''; // 間隔超過 150ms 就清空 // 人打字速度較慢
    if (e.key === 'Enter' && buffer.length >= 4) { // 按 Enter 且長度足夠
        searchProduct(buffer); // 才當成條碼處理
        buffer = ''; // 清空
    } else if (e.key.length === 1) { // 可打印字元
        buffer += e.key; // 加入緩衝區
    }
    lastKeyTime = now; // 更新時間
}); // 這樣可以正確區分掃描器和人工輸入

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊