ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多品牌称重机兼容打印软件:串口协议解析与上位机开发实践

多品牌称重机兼容打印软件:串口协议解析与上位机开发实践

简介:这是一款面向工业自动化与物流称重场景的打印软件资源,主要解决称重设备数据采集与标签打印的对接问题。软件不仅兼容上海来贺称重机,还可通过串口、USB或网络等方式对接其他品牌称重设备,适合产线操作人员、设备维护工程师及自动化系统集成者使用,帮助减少人工记录、提升称重数据处理的精度与效率。资源包为rar格式,共16个文件,约84.69MB,包含5个dll动态库、2个exe可执行程序、2个wmv操作演示视频、2个btw标签模板,以及db数据库、pdb调试符号、txt说明、xml配置和manifest清单等,覆盖程序运行、模板编辑与数据存储等环节。目前已有104人学习下载。通过包内的操作与模板编辑录屏,读者可快速了解设备连接配置、打印格式设定及称重数据实时打印的完整流程,对搭建自动化称重打印方案具有直接参考价值。

1. 称重打印软件怎么做到“一软多机”:从上海来贺到多品牌兼容

车间里最常见的场景是:电子秤已经装好,打印小票的软件却只认一个牌子。换一台秤,软件就罢工,操作员只能手抄重量再录进系统,一天下来错个三五单是常事。标题里说的“可以连接上海来贺称重机的打印软件,也可以连接其他的称重”,本质就是解决这个问题的——用一套上位机软件,通过串口或网络把不同品牌、不同协议的称重仪表统一读进来,再按固定格式打印标签或小票。它适合两类人:一是产线、仓库、地磅房的一线维护人员,需要快速把新秤接进现有流程;二是做上位机开发的工程师,想搞清楚多品牌称重设备兼容到底难在哪、怎么落地。下面我按“先跑通一台、再抽象成多台”的思路,把协议解析、串口配置、打印模板和踩坑点讲清楚。

2. 称重仪表的数据是怎么出来的:串口协议与连续输出模式

2.1 先搞懂仪表往外吐的是什么

绝大多数称重仪表,包括上海来贺这类常见型号,对外输出的都是连续发送的 ASCII 字符串,而不是你问一句它答一句。典型的一帧数据长这样:

ST,GS,+ 0012.34kg

拆开看:ST表示稳定(Stable),US表示不稳定;GS是毛重(Gross),NT是净重(Net);后面是带符号的重量数值和单位。不同品牌差别主要在三个地方:帧头标识、稳定位的位置、单位写法(kg/KG/g)。有些仪表还支持“命令应答模式”,你发一条READ它才回一帧,但产线上为了实时性,基本都用连续输出。

这里有个反直觉的点:不是所有仪表都默认开启连续输出。很多新秤出厂时是关闭的,需要进菜单把“连续发送”或“串口输出”打开,否则你代码写得再对也读不到数据。我一般拿到一台陌生仪表,先用串口助手手动确认它到底吐不吐数据,再动代码。

2.2 用 Python 把第一帧数据读进来

下面这段代码是我常用的最小验证脚本,跑通它说明硬件链路没问题:

import serial import re # 串口参数:绝大多数称重仪表是 9600 8N1,无流控 ser = serial.Serial( port='COM3', # Windows 下是 COMx,Linux 下是 /dev/ttyUSB0 baudrate=9600, # 常见还有 4800、19200,必须和仪表菜单一致 bytesize=8, parity='N', stopbits=1, timeout=1 # 读超时 1 秒,避免卡死 ) # 匹配形如 ST,GS,+0012.34kg 的帧 pattern = re.compile(r'(ST|US),(GS|NT),([+-]?\s*\d+\.?\d*)\s*(kg|g|KG|G)') while True: line = ser.readline().decode('ascii', errors='ignore').strip() if not line: continue m = pattern.search(line) if m: stable, mode, value, unit = m.groups() weight = float(value.replace(' ', '')) print(f'稳定:{stable} 模式:{mode} 重量:{weight}{unit}')

逻辑说明:readline()按换行读一帧,decode时用errors='ignore'防止个别乱码字符让程序崩掉。正则里[+-]?\s*\d+是为了兼容“正号后面带空格”这种仪表常见写法。参数上,baudrate必须和仪表菜单里的波特率完全一致,差一个档位就是满屏乱码;timeout别设太大,否则拔线后程序会僵住。

2.3 稳定位为什么必须判断

产线上秤体在晃动时,仪表会持续输出US帧,重量是跳变的。如果你不加判断直接打印,小票上就会出现一个一直在变的数字,客户拿到手就是废纸。正确做法是:只在收到ST帧且连续 N 帧重量一致时才触发打印。我一般用“连续 3 帧稳定且差值小于 0.02kg”作为触发条件,这个阈值对大多数 1g~10g 分度值的秤都够用。

3. 从单品牌到多品牌:协议适配层怎么设计

3.1 把“读数据”和“解析数据”拆开

只接一台上海来贺,你可以在主循环里直接写正则。但要接其他品牌,就必须把解析逻辑抽出来。我的做法是定义一个解析器接口,每个品牌一个实现类:

class ScaleParser: def parse(self, raw_line): """返回 (stable, weight_kg) 或 None""" raise NotImplementedError class LaiheParser(ScaleParser): def parse(self, raw_line): # 来贺常见格式:ST,GS,+ 0012.34kg m = re.search(r'(ST|US),(GS|NT),([+-]?\s*\d+\.?\d*)\s*(kg|g)', raw_line) if not m: return None stable = (m.group(1) == 'ST') value = float(m.group(3).replace(' ', '')) unit = m.group(4).lower() weight_kg = value / 1000 if unit == 'g' else value return stable, weight_kg class OtherBrandParser(ScaleParser): def parse(self, raw_line): # 另一品牌常见格式:= 12.340kg (无稳定位,靠帧头判断) m = re.search(r'=\s*([+-]?\d+\.?\d*)\s*kg', raw_line) if not m: return None # 该品牌无稳定位,默认视为稳定,由上层做连续帧校验 return True, float(m.group(1))

逻辑说明:LaiheParser处理带稳定位的帧,OtherBrandParser处理不带稳定位的帧。注意单位统一换算成千克,这样上层打印逻辑不用关心原始单位。参数上,正则里的\s*是为了吃掉仪表在正负号和数字之间填充的空格,这个空格数量不同品牌不一样,写死会翻车。

3.2 用配置文件决定加载哪个解析器

硬编码if brand == 'laihe'是最容易后悔的写法,加一个品牌就要改一次代码。我一般用一个简单的 JSON 配置:

{ "scales": [ {"name": "产线1", "port": "COM3", "baudrate": 9600, "parser": "laihe"}, {"name": "地磅", "port": "COM5", "baudrate": 4800, "parser": "other"} ] }

加载时用字典映射:

PARSERS = { 'laihe': LaiheParser, 'other': OtherBrandParser, } def build_parser(name): cls = PARSERS.get(name) if cls is None: raise ValueError(f'未知解析器: {name}') return cls()

这样新增品牌只需要加一个解析器类和一个配置项,主流程不动。参数说明:port和baudrate必须和实际接线、仪表菜单一致;parser字段就是上面注册的键名。

3.3 多台秤同时读:线程还是轮询

如果一台电脑要同时接产线秤和地磅,串口是独占资源,不能两个线程读同一个口。常见做法是每台秤一个独立线程,各自读各自的口,读到的数据丢进一个线程安全的队列,打印线程从队列取。这里有个坑:Python 的serial在 Windows 下多线程读不同口是没问题的,但如果你用同一个Serial对象跨线程写,会出乱子。所以原则是“一个口一个线程,一个线程一个 Serial 对象”。

4. 打印环节:模板、触发与常见故障

4.1 打印模板怎么和重量字段绑定

称重小票的模板通常包含:品名、批次、毛重、净重、皮重、时间、操作员。重量字段来自上面解析出的weight_kg。我一般用字符串模板占位:

TEMPLATE = """ 品名: {product} 批次: {batch} 毛重: {gross:.3f} kg 净重: {net:.3f} kg 时间: {time} 操作员: {operator} """ def render(data): return TEMPLATE.format( product=data['product'], batch=data['batch'], gross=data['gross'], net=data['net'], time=data['time'], operator=data['operator'] )

参数说明::.3f表示保留三位小数,和大多数仪表的分度值匹配;如果秤是 0.1kg 分度,改成:.1f更干净。注意模板里的字段名要和传入字典的键一致,否则会抛KeyError,这个错误在产线上就是“打印不出来”。

4.2 触发打印的三种方式

第一种是稳定自动打印:收到稳定帧且重量超过某个阈值就打印,适合流水线。第二种是脚踏开关触发:操作员踩一下才打印,适合需要人工确认的场景。第三种是软件按钮触发:界面上点一下。我一般把触发逻辑做成可配置,默认用稳定自动打印,但保留手动按钮作为兜底。阈值参数很关键:如果设成 0,空秤时的稳定帧也会触发,打出一堆 0.000kg 的废票。

4.3 打印机指令:ESC/POS 还是驱动打印

小票打印机分两类:一类支持 ESC/POS 指令,你直接往串口或网口发字节;另一类走 Windows 驱动,你调用系统打印接口。ESC/POS 更可控,但需要自己拼指令;驱动打印更简单,但排版受驱动影响。我一般优先用 ESC/POS,因为产线上换电脑不用重装驱动。下面是一个最小 ESC/POS 打印示例:

import serial def print_receipt(port, text): printer = serial.Serial(port, 9600, timeout=1) # 初始化打印机 printer.write(b'\x1b\x40') # 打印文本,GBK 编码兼容大多数国产小票机 printer.write(text.encode('gbk')) # 走纸并切纸 printer.write(b'\n\n\n') printer.write(b'\x1d\x56\x00') printer.close()

逻辑说明:\x1b\x40是初始化指令,\x1d\x56\x00是切纸指令。参数上,encode('gbk')是因为很多小票机默认字库是 GBK,用 UTF-8 会打出乱码。如果你的打印机是网口,把serial.Serial换成 socket 发送即可,指令字节不变。

5. 避坑与排查:称重打印软件最容易翻车的五个点

5.1 现象:串口能打开但读不到任何数据

原因:仪表连续输出没开,或者波特率、数据位、停止位不匹配。解决:先用串口助手以 9600 8N1 手动收,确认仪表在吐数据;如果收不到,进仪表菜单把“连续发送”打开。注意有些仪表需要短接某两个端子才进入连续发送模式,这个看仪表手册。

5.2 现象:读到的重量偶尔跳成 0 或超大值

原因:帧被截断或粘包。串口是字节流,readline()遇到半帧也会返回。解决:在解析前先做帧完整性校验,比如要求帧以ST或US开头、以单位结尾,不满足就丢弃。另外把timeout设小一点,让半帧尽快返回被丢弃,而不是等满超时。

5.3 现象:打印出来的中文是乱码

原因:编码不一致。仪表数据是 ASCII,但模板里的中文是 UTF-8,打印机字库是 GBK。解决:统一在发送前转成打印机支持的编码,通常是 GBK。如果打印机支持自定义字库,也可以加载字库文件,但产线上最省事的还是 GBK。

5.4 现象:多台秤同时读,其中一台偶尔丢数据

原因:多个线程共用了同一个串口对象,或者 USB 转串口线供电不足。解决:确保一个口一个线程一个Serial对象;如果是 USB 转串口,换带独立供电的转换器,或者把波特率降到 4800 减轻压力。

5.5 现象:稳定判断太灵敏,秤还没稳就打印

原因:稳定阈值设得太小,或者没做连续帧校验。解决:把“连续稳定帧数”从 1 改成 3,差值阈值从 0.01kg 放宽到 0.02kg~0.05kg。这个参数要根据秤的分度值和现场震动情况调,没有万能值。

6. 进阶:把称重数据接进现有系统的一个实用技巧

如果你已经跑通了单机打印,下一步大概率是想把重量数据接进 MES、ERP 或者一个简单的数据库。我的习惯是不直接在打印线程里写数据库,而是让打印线程只负责打印,另起一个消费者线程从队列取数据写库。这样打印机的卡顿不会拖慢数据入库,数据库的慢查询也不会堵住打印。

具体做法:解析线程把(时间, 重量, 稳定位, 秤编号)放进queue.Queue,打印线程和入库线程各自get。入库线程用批量插入,比如每 20 条或每 2 秒提交一次,减少数据库压力。下面是一个批量入库的骨架:

import queue import threading import time import sqlite3 data_queue = queue.Queue() def db_worker(db_path): conn = sqlite3.connect(db_path) conn.execute('CREATE TABLE IF NOT EXISTS weights ' '(ts TEXT, weight REAL, stable INTEGER, scale TEXT)') batch = [] while True: try: item = data_queue.get(timeout=2) batch.append(item) except queue.Empty: pass # 每 20 条或超时后批量提交 if len(batch) >= 20 or (batch and data_queue.empty()): conn.executemany('INSERT INTO weights VALUES (?,?,?,?)', batch) conn.commit() batch.clear() threading.Thread(target=db_worker, args=('weights.db',), daemon=True).start()

逻辑说明:timeout=2让线程在没数据时也能定期检查batch,避免最后几条一直不提交。daemon=True保证主程序退出时线程不阻塞。参数上,批量大小 20 和超时 2 秒是我在产线上比较常用的组合,数据量大就调大批量,实时性要求高就调小超时。

最后说一个我自己的教训:早期做多品牌兼容时,我总想写一个“万能正则”把所有仪表都匹配了,结果正则越来越长,最后连自己都看不懂,新来一台秤调半天。后来改成“一个品牌一个解析器,配置决定用哪个”,虽然代码文件多了几个,但排查问题时一眼就能定位到是哪个品牌的解析出了问题。这个习惯帮我省下的时间,远比省那几个文件多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进