ต่อเครื่องชั่ง RS-232 Toledo ผ่าน Bluetooth อ่านด้วย PowerShell
เครื่องชั่งการค้าส่วนใหญ่ยังพูดภาษา RS-232 อยู่ แต่พีซีสมัยนี้ไม่มีพอร์ต DB9 มาให้แล้ว ทางที่ผมใช้คือเอา Bluetooth adapter มาคั่นกลาง ให้ Windows มองเป็น COM port ธรรมดา เหลือแค่งานเดียวคือแกะ frame ที่มันส่งมาให้ถูก
พอดีมีงานต้องเอาเครื่องชั่งดิจิทัลตัวหนึ่งมาต่อเข้าระบบ POS บนพีซี ปัญหาคือเครื่องชั่งรุ่นที่ใช้ (TWT-01 พิกัด 30kg) มีทางออกข้อมูลเป็นพอร์ต RS-232 DB9 อย่างเดียว ส่วนพีซีเดี๋ยวนี้ก็ไม่มีพอร์ต serial มาให้แล้ว ผมเลยเอาตัวแปลง Bluetooth มาคั่นกลาง ให้มันกลายเป็น COM port ที่ Windows อ่านได้เหมือน serial ปกติ แล้วเขียน PowerShell ดูดค่าน้ำหนักออกมาเรียลไทม์ บทความนี้ผมเก็บทั้งเส้นทางตั้งแต่ต่อสายจนถึงโค้ดที่ใช้อ่านค่าจริง
MAC, หมายเลข COM และชื่ออุปกรณ์ในบทความเป็นค่าตัวอย่าง ส่วนโค้ดกับรูปแบบ frame คือของจริงที่แกะออกมาจากเครื่อง
ต่อสายจริง: เครื่องชั่ง ไป BT578 ไป พีซี
หัวใจของการต่อคือตัวกลาง — ผมใช้ IRXON BT578 v3 มันเป็น serial-to-Bluetooth bridge แบบ transparent คือรับ RS-232 เข้าทางหนึ่ง แล้วส่งออกเป็น Bluetooth SPP อีกทางหนึ่ง ไม่ยุ่งกับเนื้อข้อมูลเลย เครื่องชั่งส่ง byte อะไรมา มันก็ส่งต่อ byte นั้นให้พีซีตรงๆ
จุดที่คนพลาดกันบ่อยคือสวิตช์ M/S ข้างตัว BT578 ต้องตั้งเป็น S (Slave) เท่านั้น เพราะเราต้องการให้พีซีเป็นฝ่ายค้นหาและเปิดการเชื่อม ไม่ใช่ให้ BT578 ไปวิ่งหาใคร ถ้าตั้งผิดเป็น Master พีซีจะมองไม่เห็นมันตอน scan เสียบ DB9 male ของ BT578 เข้าพอร์ตเครื่องชั่ง เปิดไฟทั้งสองตัว ถ้า LED สถานะ BT บน BT578 กะพริบ แปลว่าพร้อมจับคู่แล้ว ถ้า pin ไม่ตรงจนไม่มีสัญญาณเข้าเลย ค่อยไปหา gender changer กับ null modem มาลอง แต่ส่วนใหญ่ต่อตรงก็ติด
จับคู่ Bluetooth แล้วได้ COM port มาสองตัว
ฝั่ง Windows จับคู่เหมือนอุปกรณ์ Bluetooth ทั่วไป แต่มีจุดที่ต้องรู้ล่วงหน้าคือ PIN กับเรื่อง COM port ที่จะได้มาเป็นคู่
- เปิด Settings -> Bluetooth & devices -> Add device -> Bluetooth
- เลือกอุปกรณ์ที่ชื่อขึ้นต้นว่า BT578_SPP_XXXX ที่โผล่ขึ้นมา
- ใส่ PIN 1234 ซึ่งเป็นค่ามาตรฐานของ BT578 ทุกตัว
- รอจนสถานะขึ้น Paired แล้ว Windows จะสร้าง virtual COM ให้สองพอร์ต
ที่มันสับสนคือหลัง pair เสร็จจะได้ COM มา สองพอร์ต ไม่ใช่พอร์ตเดียว ตัวหนึ่งเป็น outgoing อีกตัวเป็น incoming ตัวที่เราต้องเปิดคือ outgoing เพราะการเปิดพอร์ตนี้จะไป trigger ให้ BT เชื่อมจริง ส่วน incoming เป็นตัวสำรอง วิธีเช็คที่ชัวร์สุดคือเปิดทีละตัวแล้วดูว่าอันไหนมี byte เข้ามา อันที่รับข้อมูลได้คือตัวที่ใช้
ถ้าใส่ 1234 แล้วไม่ผ่าน แปลว่าเครื่องนั้นเคยถูกเปลี่ยน PIN ไว้ ลอง 0000 ก่อน ถ้ายังไม่ได้ต้อง reset factory ด้วย AT command (AT+RESET ตอนยังไม่เชื่อม BT) แล้วค่อยจับคู่ใหม่
พารามิเตอร์ที่ต้องตั้งให้ตรง: 9600 8N1
ทั้ง TWT-01 และ BT578 มาจากโรงงานด้วยค่า serial เดียวกัน จำสั้นๆ ว่า 9600 8N1 — baud 9600, data 8 bit, ไม่มี parity, stop bit 1 ตัว ตรงนี้ถ้าตั้งผิดจะได้ byte เข้ามาแต่เป็นขยะอ่านไม่ออก (garbage) ไม่ใช่ ASCII สวยๆ ถ้าเจอขยะให้ย้อนมาดู baud ก่อนเลย
อีกจุดที่ผมเสียเวลาไปพอควรตอนแรกคือ DTR กับ RTS บางระบบ BT578 จะไม่ยอมส่งข้อมูลออกมาจนกว่าฝั่งพีซีจะ assert สองเส้นนี้ก่อน เพราะฉะนั้นเวลาสร้าง SerialPort object ต้องเปิด DtrEnable กับ RtsEnable ไว้ด้วย ไม่งั้นเปิดพอร์ตติดแต่รอทั้งวันก็ไม่มี byte เข้า
$sp = New-Object System.IO.Ports.SerialPort 'COM5', 9600, 'None', 8, 'One'
$sp.ReadTimeout = 200
$sp.DtrEnable = $true # บาง BT578 ไม่ส่งข้อมูลจนกว่าจะ assert DTR
$sp.RtsEnable = $true
$sp.NewLine = "`r`n"
$sp.Open()
หน้าตาของ frame — Toledo Continuous 18 ไบต์
TWT-01 ส่งข้อมูลต่อเนื่องราวๆ 3-4 ครั้งต่อวินาที (ทุก 250-330 ms) ด้วยรูปแบบที่เรียกว่า Toledo Continuous Protocol ซึ่งเป็นมาตรฐานที่เครื่องชั่งจีนส่วนใหญ่ใช้กัน หนึ่ง frame ยาว 18 ไบต์ เป็น ASCII ล้วน จบด้วย \r\n เอา dumper จับ byte ดิบออกมาดูจะเห็นแบบนี้:
53 54 2C 47 53 2C 2B 20 20 20 30 2E 30 30 6B 67 0D 0A
S T , G S , + SP SP SP 0 . 0 0 k g CR LF
แกะทีละช่องคือ: สองตัวแรก ST เป็น สถานะ ตามด้วย GS เป็น ชนิดน้ำหนัก ตามด้วยเครื่องหมาย + แล้วก็ค่าน้ำหนัก 0.00 ปิดท้ายด้วยหน่วย kg แล้ว CR LF สองช่องที่ต้องเอาไปตัดสินใจในโค้ด POS คือสถานะกับชนิด:
น้ำหนักนิ่งแล้ว
ค่านิ่งพร้อมบันทึกลงบิล — โค้ด POS ควรหยิบค่าไปตัดเงินตอนเจอ ST เท่านั้น
ยังกระดิกอยู่
ของเพิ่งวาง เข็มยังไม่นิ่ง ค่ายังวิ่ง — โชว์ขึ้นจอให้ลูกค้าเห็นได้ แต่ห้ามเอาไปคิดเงิน
ส่วนชนิดน้ำหนักมี GS (Gross น้ำหนักรวมยังไม่หัก tare) กับ NT (Net น้ำหนักสุทธิหลังหัก tare แล้ว) และในโหมด NT เครื่องหมายเป็น - ได้ ถ้ายกของออกจนต่ำกว่าค่าที่ tare ไว้ เพราะฉะนั้น parser ต้องรับเลขติดลบเป็นด้วย ไม่ใช่สมมติว่าน้ำหนักบวกเสมอ
อ่านค่าจริงด้วย PowerShell
ทีนี้มาถึงตัวจริง วิธีที่ผมใช้คือไม่อ่านทีละ frame ตายตัว 18 ไบต์ เพราะการ Read() ครั้งเดียวมันอาจได้มาไม่ครบ frame (ได้ 12 ไบต์บ้าง 20 ไบต์บ้าง คาบเกี่ยวสอง frame) ผมเลย buffer ต่อเข้าไปเรื่อยๆ แล้วค่อยตัด frame ด้วย \r\n เอา แล้วโยนแต่ละบรรทัดเข้า regex ตัวเดียว ถ้าบรรทัดไหน match ไม่ผ่านก็ทิ้งบรรทัดนั้นทิ้งไป (frame มันขาด)
# ST|US , GS|NT , (+|-) ช่องว่าง + ตัวเลข + หน่วย
$framePattern = '^(ST|US),(GS|NT),([+-])\s*(\d+(?:\.\d+)?)(\w+)$'
$buffer = ''
while ($true) {
$n = $sp.BytesToRead
if ($n -gt 0) {
$bytes = New-Object byte[] $n
$read = $sp.Read($bytes, 0, $n)
# กรอง null byte ที่โผล่มาก่อน handshake เสร็จ (สำคัญมาก อ่านหัวข้อถัดไป)
$text = -join ($bytes[0..($read-1)] |
Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
$buffer += $text
# ตัด frame ด้วย \r\n ไม่ใช่ตัดตามความยาวคงที่
while ($buffer -match "(.+?)`r`n") {
$line = $Matches[1]
$buffer = $buffer.Substring($Matches[0].Length)
if ($line -match $framePattern) {
$stable = $Matches[1] -eq 'ST' # ST = นิ่ง, US = ยังกระดิก
$type = $Matches[2] # GS | NT
$value = [double]$Matches[4]
if ($Matches[3] -eq '-') { $value = -$value }
$unit = $Matches[5]
Write-Host ("{0,10:N2} {1} stable={2} {3}" -f $value, $unit, $stable, $type)
}
}
}
Start-Sleep -Milliseconds 30
}
regex ตัวเดียวนี้ทำงานสองหน้าที่พร้อมกัน — แกะค่าออกมา และกรอง frame เสียทิ้งไปในตัว ถ้า byte แปลกปลอมแทรกเข้ามากลางบรรทัด (เช่นได้ 1.3O แทน 1.30) มันจะ match ไม่ผ่านแล้ว drop เอง ไม่มีทางเผลอเอาค่าเพี้ยนไปคิดเงิน ส่วนตัวผมชอบวิธี fail-and-drop แบบนี้กว่าการพยายามซ่อม frame ที่ขาด เพราะเครื่องมันส่งมา 3-4 frame ต่อวินาทีอยู่แล้ว ทิ้ง frame เสียไปหนึ่งอันไม่มีใครสังเกตเห็น
อย่าฮาร์ดโค้ด COM5 — หา port จาก MAC เอา
จุดที่จะเจ็บทีหลังคือหมายเลข COM port มันไม่คงที่ ถ้าวันไหน unpair แล้ว pair ใหม่ Windows อาจให้เลขใหม่ (COM5 กลายเป็น COM9) ถ้าฮาร์ดโค้ด COM5 ไว้ในแอป มันจะพังทันทีที่เลขเปลี่ยน ของที่ไม่เปลี่ยนคือ MAC address ของ BT578 ผมเลยเก็บ MAC ไว้ใน config แล้วให้แอปวิ่งหา COM port ปัจจุบันจาก MAC ตอน startup เอาเอง
MAC หาได้จาก InstanceId ของ PnpDevice (มันฝังอยู่หลังคำว่า DEV_) ส่วน COM port ปัจจุบันอยู่ใน registry ใต้ BTHENUM:
function Get-BT578Mac {
$dev = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue |
Where-Object { $_.FriendlyName -match 'BT578' } | Select-Object -First 1
if (-not $dev) { return $null }
if ($dev.InstanceId -match 'DEV_([0-9A-F]{12})') {
$raw = $Matches[1] # เช่น AABBCC112233
return ($raw -split '(..)' | Where-Object { $_ }) -join ':' # AA:BB:CC:11:22:33
}
}
function Get-BT578ComPort {
param([string]$MacRaw) # AABBCC112233 (ไม่มี :)
$mac = $MacRaw.ToUpper()
$enumBase = 'HKLM:\SYSTEM\CurrentControlSet\Enum\BTHENUM'
if (-not (Test-Path $enumBase)) { return $null }
$ports = @()
Get-ChildItem $enumBase -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match $mac } | ForEach-Object {
$paramPath = Join-Path $_.PSPath 'Device Parameters'
if (Test-Path $paramPath) {
$p = (Get-ItemProperty $paramPath -ErrorAction SilentlyContinue).PortName
if ($p) { $ports += $p }
}
}
return ($ports | Select-Object -Unique)
}
# ใช้งาน: เก็บ MAC ไว้ใน config แล้วเรียกหา port ตอนเปิดแอป
Get-BT578ComPort -MacRaw 'AABBCC112233' # -> COM5 (หรือเลขปัจจุบัน)
แบบนี้ user เปลี่ยนเครื่อง หรือ unpair แล้ว pair ใหม่ ก็แค่รัน setup wizard เก็บ MAC ใหม่รอบเดียว โค้ดเดิมใช้ต่อได้หมด ไม่ต้องมานั่งแก้เลข COM ในไฟล์ตั้งค่าทุกครั้ง
กับดักที่เจอจริงตอนใช้งาน
สามเรื่องนี้ผมเจอมากับตัว เขียนไว้กันคนอื่นเสียเวลาซ้ำ
1. Null byte ต้นสตรีม พอเปิด COM port ครั้งแรก จะมี 0x00 ไหลมาก่อน data จริงราวๆ 200-400 ไบต์ เป็นเพราะ BT handshake ของ BT578 ส่ง framing null ก่อน bridge จะเปิดใช้เต็มตัว ถ้าไม่กรองทิ้ง buffer รอบแรกจะเละหมด นี่คือเหตุผลที่ในลูปข้างบนผมใส่ Where-Object { $_ -ne 0 } ไว้ก่อนทุกครั้ง
2. เครื่องชั่ง auto-sleep ถ้าไม่มีของวางหรือไม่มีการขยับนานประมาณ 1-2 นาที เครื่องจะเข้า sleep แล้วหยุดส่ง byte ทั้งที่ BT ยังไม่หลุด อาการคือพอร์ตเปิดอยู่แต่เงียบ อย่าไปตั้ง timeout สั้นแล้วเด้ง error ให้ผู้ใช้ตกใจ ผมให้ UI ขึ้นว่า "รอวางของ..." แทน พอมีคนวางของเครื่องก็ตื่นส่ง data กลับมาเอง
3. BT หลุดกลางคัน ถ้าอยู่ไกลเกินหรือมีสัญญาณกวน BT อาจหลุด เวลาอ่าน BytesToRead แล้วเจอ exception ให้ปิดพอร์ตแล้วเปิดใหม่ ตัว dashboard ที่ผมทำใช้ auto-reconnect แบบ backoff 1 วิ 2 วิ 4 วิ แต่ cap ไว้ที่ 10 วิ จะได้ไม่วิ่งรัวจนกินซีพียู
ตอนของเพิ่งวางลงตาชั่ง เครื่องจะส่ง US (Unstable) พร้อมค่าที่ยังวิ่งอยู่หลาย frame ก่อนจะนิ่งเป็น ST — ถ้าโค้ดหยิบค่าแรกที่เข้ามาไปคิดเงินเลยจะได้น้ำหนักมั่ว ลูกค้าจ่ายเกินหรือร้านขาดทุนได้ทั้งคู่ กติกาเดียวคือรอ ST ก่อนเสมอ แล้วค่อยล็อกค่า
ส่วนเคส over-range (วางของเกินพิกัด) เครื่องน่าจะส่ง OL มาแทนตัวเลข อันนี้ผมยังไม่ได้ทดสอบจริงจัง แต่ด้วยวิธี fail-and-drop ข้างบน มันจะ match regex ไม่ผ่านแล้ว drop เอง ค่อยไปเติม logic แจ้งเตือนทีหลังได้ ไม่ทำให้ค่าเพี้ยนหลุดเข้าระบบอยู่แล้ว
ใครจะเอาแนวนี้ไปต่อเครื่องชั่งยี่ห้ออื่นก็ได้ตามสะดวก เพราะ Toledo Continuous เป็นมาตรฐานที่เครื่องจีนใช้กันเยอะ ส่วนใหญ่ปรับแค่ regex กับหน่วยก็พอ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย