factory/apps/cm/coder.py

263 lines
10 KiB
Python

"""
伟迪捷 1880 喷码机 ASCII 协议封装
帧格式: 纯 ASCII, 字段以 '|' 分隔, 整帧以 <CR>(0x0D) 结尾
- 写指令(SLA/JDA/CQI/JDI/SST/SNO) 通常无回包
- 查询指令(GST/GFT/GJD) 回包同样以 <CR> 结尾
"""
import socket
from rest_framework.exceptions import ParseError
CR = b"\x0d"
class CoderError(ParseError):
pass
class CoderClient:
def __init__(self, ip: str, port: int = 3100, timeout: float = 2.0):
if not ip:
raise CoderError("喷码IP未配置")
self.ip = ip
self.port = port
self.timeout = timeout
def _send(self, frame: bytes, expect_reply: bool = False) -> bytes:
try:
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
s.sendall(frame)
if expect_reply:
buf = b""
while CR not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
return buf
# 写指令: 短暂等待, 捕获机器拒绝(ERR/!); 超时则视为成功
s.settimeout(0.3)
try:
buf = b""
while CR not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
except socket.timeout:
buf = b""
if buf:
text = buf.rstrip(b"\r\n").decode("latin-1", errors="replace")
if text.startswith("ERR") or text.startswith("!"):
raise CoderError(f"喷码机拒绝指令 {frame!r}: {text}")
return b""
except CoderError:
raise
except (socket.timeout, OSError) as e:
raise CoderError(f"喷码通讯失败 {self.ip}:{self.port} - {e}")
@staticmethod
def _encode(text: str, label: str) -> bytes:
if "\r" in text or "\n" in text:
raise CoderError(f"{label}含 CR/LF, 会破坏帧结构")
if "|" in text:
raise CoderError(f"{label}'|', 与 1880 分隔符冲突")
try:
return text.encode("latin-1")
except UnicodeEncodeError as e:
raise CoderError(f"{label}含喷码机不支持的字符: {e}")
def send_command(self, command: str, expect_reply: bool = False) -> bytes:
"""直接下发一条已拼装好的 1880 指令(不含末尾 CR), 由本方法统一补 CR。"""
if "\r" in command or "\n" in command:
raise CoderError("指令含 CR/LF, 由 send_command 统一补 CR, 不要手动拼")
return self._send(command.encode("latin-1") + CR, expect_reply=expect_reply)
def select_job(self, job_name: str):
"""SLA|<jobname>|<CR> 选择信息模板"""
self._send(b"SLA|" + self._encode(job_name, "模板名") + b"|" + CR)
def update_fields(self, fields: dict):
"""JDA|<field>=<value>|...|<CR> 一帧更新多个用户区"""
if not fields:
return
parts = [b"JDA"]
for k, v in fields.items():
parts.append(self._encode(str(k), "用户区名") + b"=" + self._encode(str(v), "喷印内容"))
self._send(b"|".join(parts) + b"|" + CR)
def clear_queue(self):
"""CQI<CR> 清空喷码机内部缓存队列"""
self._send(b"CQI" + CR)
def push_queue(self, fields: dict):
"""JDI|1|<field>=<value>|...|<CR> 入队列数据"""
if not fields:
return
parts = [b"JDI", b"1"]
for k, v in fields.items():
parts.append(self._encode(str(k), "用户区名") + b"=" + self._encode(str(v), "喷印内容"))
self._send(b"|".join(parts) + b"|" + CR)
def set_state(self, state: int):
"""SST|<State>|<CR> 0关机 1开机 3运行 4离线"""
self._send(b"SST|" + str(int(state)).encode("ascii") + b"|" + CR)
def register_print_callback(self):
"""SNO|PRC|1|<CR> 注册打印完成反馈, 之后喷码机每打印一次会回 PRC<CR>"""
self._send(b"SNO|PRC|1|" + CR)
def get_status(self) -> dict:
"""GST<CR> -> STS|<overall>|<error>|<currentjob>|<batchcount>|<totalcount>|<CR>"""
resp = self._send(b"GST" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "STS" or len(parts) < 6:
raise CoderError(f"GST 响应不识别: {resp!r}")
try:
return {
"overall": int(parts[1]),
"error": int(parts[2]),
"current_job": parts[3],
"batch_count": int(parts[4]),
"total_count": int(parts[5]),
}
except ValueError as e:
raise CoderError(f"GST 响应解析失败: {resp!r} - {e}")
def get_faults(self) -> list:
"""GFT<CR> -> FLT|<count>|[<code>|<clearable>|<title>|]*<CR>"""
resp = self._send(b"GFT" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "FLT" or len(parts) < 2:
raise CoderError(f"GFT 响应不识别: {resp!r}")
try:
count = int(parts[1])
except ValueError as e:
raise CoderError(f"GFT 响应解析失败: {resp!r} - {e}")
faults = []
for i in range(count):
base = 2 + i * 3
if base + 2 >= len(parts):
break
faults.append({
"code": parts[base],
"clearable": parts[base + 1] == "1",
"title": parts[base + 2],
})
return faults
def get_current_job(self) -> dict:
"""GJD<CR> -> JDL|<count>|<field>=<value>|...|<CR>"""
resp = self._send(b"GJD" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "JDL" or len(parts) < 2:
raise CoderError(f"GJD 响应不识别: {resp!r}")
try:
count = int(parts[1])
except ValueError as e:
raise CoderError(f"GJD 响应解析失败: {resp!r} - {e}")
fields = {}
for i in range(count):
idx = 2 + i
if idx >= len(parts):
break
pair = parts[idx]
if "=" not in pair:
continue
k, v = pair.split("=", 1)
fields[k] = v
return fields
# ============================================================================
# Hanslaser(大族激光) 老款打标机 标准通讯协议(Winsocket/TCP)
# 帧格式: STX(0x02) + 内容 + ETX(0x03), 每条命令下位机都回一帧
# 单件打标流程: $Initialize_<文件> -> $Data_<v1,v2,...> -> $MarkStart_
# 该协议无队列/批量能力, 只能逐件: 一组 Data 对应一次 MarkStart
# ============================================================================
STX = b"\x02"
ETX = b"\x03"
class HanslaserClient:
encoding = "gbk" # 大族激光为国产机, 文档 code page 936
def __init__(self, ip: str, port: int, timeout: float = 5.0):
if not ip:
raise CoderError("打标机IP未配置")
if not port:
raise CoderError("打标机端口未配置")
self.ip = ip
self.port = port
self.timeout = timeout
def _encode(self, text: str, label: str) -> bytes:
if "\x02" in text or "\x03" in text:
raise CoderError(f"{label}含 STX/ETX, 会破坏帧结构")
if "," in text:
raise CoderError(f"{label}',', 与 Hanslaser $Data 分隔符冲突")
if "\r" in text or "\n" in text:
raise CoderError(f"{label}含 CR/LF")
try:
return text.encode(self.encoding)
except UnicodeEncodeError as e:
raise CoderError(f"{label}含打标机不支持的字符: {e}")
def _send_recv(self, body: bytes) -> str:
"""发 STX+body+ETX, 读回一整帧, 返回去掉 STX/ETX 后的内容字符串。"""
frame = STX + body + ETX
try:
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
s.sendall(frame)
buf = b""
while ETX not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
except (socket.timeout, OSError) as e:
raise CoderError(f"打标通讯失败 {self.ip}:{self.port} - {e}")
if ETX not in buf:
raise CoderError(f"打标机无完整回包(缺 ETX): {buf!r}")
start = buf.find(STX)
end = buf.find(ETX, start + 1 if start >= 0 else 0)
content = buf[(start + 1 if start >= 0 else 0):end]
return content.decode(self.encoding, errors="replace")
def initialize(self, filename: str):
"""$Initialize_<文件名> -> $Initialize_OK / $Initialize_FALSE"""
resp = self._send_recv(b"$Initialize_" + self._encode(filename, "打标文件名"))
if resp != "$Initialize_OK":
raise CoderError(f"打标文件初始化失败: {resp}")
def send_data(self, values: list):
"""$Data_<v1,v2,...> -> $Receive_OK / $Receive_ERROR / $SysNoReady"""
if not values:
raise CoderError("打标数据为空")
payload = b",".join(self._encode(str(v), "打标内容") for v in values)
resp = self._send_recv(b"$Data_" + payload)
if resp == "$Receive_OK":
return
if resp == "$SysNoReady":
raise CoderError("打标机未准备好($SysNoReady)")
raise CoderError(f"打标数据被拒绝: {resp}")
def mark_start(self):
"""$MarkStart_ -> $MarkStart_OK / $MarkStart_OK_<次数> / $MarkStart_ERROR
下位机开始打标即回 $MarkStart_OK; 打标完成会再回 $MarkStart_OK_<次数>。
单件场景只确认开始成功, 不阻塞等待打标完成。
"""
resp = self._send_recv(b"$MarkStart_")
if not resp.startswith("$MarkStart_OK"):
raise CoderError(f"启动打标失败: {resp}")
return resp
def mark_stop(self):
"""$MarkStop_ -> $MarkStop_OK / $MarkStop_ERROR"""
resp = self._send_recv(b"$MarkStop_")
if not resp.startswith("$MarkStop_OK"):
raise CoderError(f"停止打标失败: {resp}")
return resp