kkprotocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kkprotocol/__init__.py +105 -0
- kkprotocol/builder.py +1005 -0
- kkprotocol/codec.py +804 -0
- kkprotocol/compiler.py +182 -0
- kkprotocol/contrib/__init__.py +8 -0
- kkprotocol/contrib/artnet.py +55 -0
- kkprotocol/contrib/bacnet_mstp.py +110 -0
- kkprotocol/contrib/cctalk.py +53 -0
- kkprotocol/contrib/cjt188.py +57 -0
- kkprotocol/contrib/dlt645.py +81 -0
- kkprotocol/contrib/hdlc.py +47 -0
- kkprotocol/contrib/mbus.py +61 -0
- kkprotocol/contrib/modbus_ascii.py +59 -0
- kkprotocol/contrib/nmea.py +59 -0
- kkprotocol/contrib/slip.py +38 -0
- kkprotocol/contrib/xmodem.py +74 -0
- kkprotocol/crc.py +103 -0
- kkprotocol/errors.py +48 -0
- kkprotocol/example_device.py +186 -0
- kkprotocol/example_modbus_rtu.py +176 -0
- kkprotocol/example_modbus_rtu_multi.py +391 -0
- kkprotocol/fields.py +167 -0
- kkprotocol/frame.py +114 -0
- kkprotocol/frame_codec.py +50 -0
- kkprotocol/framing.py +190 -0
- kkprotocol/length.py +151 -0
- kkprotocol/pack.py +222 -0
- kkprotocol/py.typed +0 -0
- kkprotocol/spec.py +494 -0
- kkprotocol-0.1.0.dist-info/METADATA +1015 -0
- kkprotocol-0.1.0.dist-info/RECORD +34 -0
- kkprotocol-0.1.0.dist-info/WHEEL +5 -0
- kkprotocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- kkprotocol-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""示例磁致伸缩位移传感器(A)(RS485 / Modbus RTU)。
|
|
3
|
+
|
|
4
|
+
协议来自某设备通讯规格书第 6 章,逐字节核对过:
|
|
5
|
+
|
|
6
|
+
读保持寄存器(03H)—— 请求固定 8 字节::
|
|
7
|
+
|
|
8
|
+
Addr | 03h | RegH RegL | CntH CntL | CRCL CRCH
|
|
9
|
+
01 03 00 04 00 03 44 0A ← 读 0004H 起 3 个寄存器
|
|
10
|
+
|
|
11
|
+
读保持寄存器(03H)—— 响应变长,第 3 字节是后续数据字节数::
|
|
12
|
+
|
|
13
|
+
Addr | 03h | N | Data(N) | CRCL CRCH
|
|
14
|
+
01 03 06 00 77 99 45 01 AC EB DA
|
|
15
|
+
|
|
16
|
+
写单寄存器(06H)—— 请求与响应同格式(响应是原样回显)::
|
|
17
|
+
|
|
18
|
+
Addr | 06h | RegH RegL | ValH ValL | CRCL CRCH
|
|
19
|
+
01 06 00 2C 00 00 48 03
|
|
20
|
+
|
|
21
|
+
* CRC 是 Modbus 标准的 CRC16(多项式 0xA001,初值 0xFFFF),**低字节在前**
|
|
22
|
+
* 寄存器都是 16 位、**高字节在前**
|
|
23
|
+
* 03H 响应里的 "N" 是**字节数**(读 3 个寄存器 = 6),不是寄存器个数
|
|
24
|
+
|
|
25
|
+
本模块用 ``kkprotocol`` 的声明式描述把上述三种帧写清楚:请求帧全定长所以不需要
|
|
26
|
+
长度字段,响应帧靠 ``N`` 切帧;收发结构不同,所以是**两份 spec**。
|
|
27
|
+
|
|
28
|
+
.. note::
|
|
29
|
+
规格书的"返回"示例有两个笔误,用 CRC 能反推出来(详见 :func:`decode_read_result`
|
|
30
|
+
与 README):示例写 ``00 77 CE 95``,但按文档自己的公式 0xCE95/0xFFFF = 0.807,
|
|
31
|
+
与文档写的 0.598 对不上;把小数换成 ``99 45``(=0.598)后 CRC 才等于报文里的
|
|
32
|
+
``EB DA``。所以那 6 字节应为 ``00 77 99 45 01 AC``,文档笔误。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Any, Dict, Tuple
|
|
38
|
+
|
|
39
|
+
from .builder import FrameBuilder, Protocol
|
|
40
|
+
from .crc import crc16_modbus
|
|
41
|
+
from .fields import Field
|
|
42
|
+
from .spec import ChecksumRule, FrameSpec
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"FUNC_READ",
|
|
46
|
+
"FUNC_WRITE",
|
|
47
|
+
"REGISTERS",
|
|
48
|
+
"crc_bytes_le",
|
|
49
|
+
"RY_REQ",
|
|
50
|
+
"RY_RSP",
|
|
51
|
+
"protocol",
|
|
52
|
+
"build_read",
|
|
53
|
+
"build_write",
|
|
54
|
+
"decode_registers",
|
|
55
|
+
"decode_read_result",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
FUNC_READ = 0x03
|
|
59
|
+
FUNC_WRITE = 0x06
|
|
60
|
+
|
|
61
|
+
# 规格书表一:保持寄存器地址(16 位)
|
|
62
|
+
REGISTERS: Dict[str, int] = {
|
|
63
|
+
"POS1": 0x0000, # 位置1:0000H 整数 / 0001H 小数
|
|
64
|
+
"POS2": 0x0002, # 位置2:0002H 整数 / 0003H 小数
|
|
65
|
+
"POS3": 0x0004, # 位置3:0004H 整数 / 0005H 小数
|
|
66
|
+
"TEMP1": 0x0006,
|
|
67
|
+
"TEMP2": 0x0007,
|
|
68
|
+
"TEMP3": 0x0008,
|
|
69
|
+
"TEMP4": 0x0009,
|
|
70
|
+
"TEMP5": 0x000A,
|
|
71
|
+
"TEMP6": 0x000B,
|
|
72
|
+
"TEMP7": 0x000C,
|
|
73
|
+
"TEMP8": 0x000D,
|
|
74
|
+
"ADDR": 0x000E, # 从站地址
|
|
75
|
+
"FULL_SCALE": 0x000F, # 满量程:000FH 整数 / 0010H 小数
|
|
76
|
+
"ZERO": 0x0011, # 零点:0011H 整数 / 0012H 小数
|
|
77
|
+
"SPEED": 0x0013, # 速度:0013H 整数 / 0014H 小数
|
|
78
|
+
"POS_TEMP": 0x0015, # 位置温度系数
|
|
79
|
+
"SET_ADDR": 0x0020, # 修改从站地址
|
|
80
|
+
"DIR": 0x002C, # 正反向:00H 反向 / FFH 正向
|
|
81
|
+
"FACTORY": 0x002E, # 恢复工厂设置(写 FFH)
|
|
82
|
+
"ZERO_SHIFT": 0x0034, # 零点迁移值(0034H/0035H/0036H)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def crc_bytes_le(data: bytes) -> bytes:
|
|
87
|
+
"""Modbus 线上 CRC:低字节在前,与报文字节直接对照(不走 int 解包)。"""
|
|
88
|
+
return crc16_modbus(data).to_bytes(2, "little")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------- 请求帧:8 字节定长
|
|
92
|
+
# 03H 读请求与 06H 写请求结构完全相同(只是第 5/6 字节语义不同),共用一份 spec。
|
|
93
|
+
RY_REQ = FrameSpec(
|
|
94
|
+
fields=[
|
|
95
|
+
Field.u8("addr", desc="从站地址"),
|
|
96
|
+
Field.u8("func", desc="03h 读 / 06h 写"),
|
|
97
|
+
Field.u16("reg", desc="寄存器地址"),
|
|
98
|
+
Field.u16("value", desc="读=寄存器数量;写=写入值"),
|
|
99
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
100
|
+
],
|
|
101
|
+
cmd="func",
|
|
102
|
+
device="addr",
|
|
103
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
104
|
+
max_frame_size=16,
|
|
105
|
+
desc="Modbus RTU 请求(03H/06H),固定 8 字节",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------- 响应帧:靠 N 切帧
|
|
109
|
+
RY_RSP = FrameSpec(
|
|
110
|
+
fields=[
|
|
111
|
+
Field.u8("addr", desc="从站地址"),
|
|
112
|
+
Field.u8("func", fixed=FUNC_READ, desc="03h"),
|
|
113
|
+
Field.u8("n", desc="后续数据字节数(读 3 个寄存器 = 6)"),
|
|
114
|
+
Field.raw("data", "{n.val}", desc="寄存器数据,高字节在前"),
|
|
115
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
116
|
+
],
|
|
117
|
+
payload="data",
|
|
118
|
+
device="addr",
|
|
119
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
120
|
+
max_frame_size=256,
|
|
121
|
+
desc="Modbus RTU 读保持寄存器响应",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def protocol(device_id: Any = None, enforce_device_id: bool = False) -> Protocol:
|
|
126
|
+
"""一台设备一个实例:发 8 字节请求、收变长 03 响应。"""
|
|
127
|
+
return Protocol(
|
|
128
|
+
tx=RY_REQ, rx=RY_RSP, device_id=device_id, enforce_device_id=enforce_device_id
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_REQ_BUILDER = FrameBuilder(RY_REQ)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_read(addr: int, reg: int = REGISTERS["POS3"], count: int = 3) -> bytes:
|
|
136
|
+
"""组一条 03H 读请求(默认读位置3 的整数/小数 + 温度,共 3 个寄存器)。"""
|
|
137
|
+
return _REQ_BUILDER.build(FUNC_READ, addr=addr, reg=reg, value=count)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_write(addr: int, reg: int, value: int) -> bytes:
|
|
141
|
+
"""组一条 06H 写单寄存器请求(响应是同字节回显)。"""
|
|
142
|
+
return _REQ_BUILDER.build(FUNC_WRITE, addr=addr, reg=reg, value=value)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------- 数据解析
|
|
146
|
+
def decode_registers(data: bytes) -> Tuple[int, ...]:
|
|
147
|
+
"""把 Data 区按 16 位大端切成寄存器值。"""
|
|
148
|
+
if len(data) % 2:
|
|
149
|
+
raise ValueError("寄存器数据字节数必须是偶数,实际 {}".format(len(data)))
|
|
150
|
+
return tuple(
|
|
151
|
+
int.from_bytes(data[i : i + 2], "big") for i in range(0, len(data), 2)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def decode_read_result(data: bytes) -> Dict[str, Any]:
|
|
156
|
+
"""按规格书公式解出物理量。
|
|
157
|
+
|
|
158
|
+
* 位置 = 整数寄存器 + 小数寄存器 / 0xFFFF(单位 mm)
|
|
159
|
+
* 温度 = 温度寄存器 / 16(单位 ℃)
|
|
160
|
+
|
|
161
|
+
读 3 个寄存器(默认)时三者都在;读 2 个只解位置;读 1 个只回原始值。
|
|
162
|
+
"""
|
|
163
|
+
regs = decode_registers(data)
|
|
164
|
+
if not regs:
|
|
165
|
+
raise ValueError("读响应里没有数据")
|
|
166
|
+
if len(regs) == 1:
|
|
167
|
+
return {"value": regs[0], "registers": regs}
|
|
168
|
+
result: Dict[str, Any] = {
|
|
169
|
+
"position_mm": regs[0] + regs[1] / 0xFFFF,
|
|
170
|
+
"registers": regs,
|
|
171
|
+
}
|
|
172
|
+
if len(regs) >= 3:
|
|
173
|
+
result["temperature_c"] = regs[2] / 16.0
|
|
174
|
+
if len(regs) > 3:
|
|
175
|
+
result["extra"] = regs[3:]
|
|
176
|
+
return result
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""示例磁致伸缩位移传感器(B,RS485 / Modbus RTU,V8.0 协议)。
|
|
3
|
+
|
|
4
|
+
协议取自某设备 Modbus 通讯协议 V8.0第 2~4 章,逐字节核对过:
|
|
5
|
+
|
|
6
|
+
读输入寄存器(04H)/ 读保持寄存器(03H)—— 请求固定 8 字节,响应带"数据字节数 N"::
|
|
7
|
+
|
|
8
|
+
Addr | 04h | RegH RegL | CntH CntL | CRCL CRCH ← 请求
|
|
9
|
+
Addr | 04h | N | Data(N) | CRCL CRCH ← 响应(N = 字节数)
|
|
10
|
+
|
|
11
|
+
写单寄存器(06H)—— 请求/响应同结构(8 字节)::
|
|
12
|
+
|
|
13
|
+
Addr | 06h | RegH RegL | ValH ValL | CRCL CRCH
|
|
14
|
+
|
|
15
|
+
写多寄存器(10H)—— 请求多一个"字节数"字段,响应不带数据::
|
|
16
|
+
|
|
17
|
+
Addr | 10h | RegH RegL | CntH CntL | Bcnt | Data | CRCL CRCH ← 请求
|
|
18
|
+
Addr | 10h | RegH RegL | CntH CntL | CRCL CRCH ← 响应
|
|
19
|
+
|
|
20
|
+
异常响应:Addr | (func|0x80) | ErrCode(1) | CRCL CRCH
|
|
21
|
+
|
|
22
|
+
* CRC 是 CRC16/MODBUS,低字节在前(与示例 A 一致)
|
|
23
|
+
* 寄存器 16 位,高字节在前
|
|
24
|
+
* 位移整数方式:4 字节 = 高 2 字节 int16(整数部分)+ 低 2 字节 uint16(小数部分 / 65535)
|
|
25
|
+
* 位移浮点方式:4 字节单精度浮点,但字节序是 **CDAB**(要重排成 ABCD 才是标准大端)
|
|
26
|
+
* 温度:2 字节 int16,/100 = ℃
|
|
27
|
+
|
|
28
|
+
这份协议比示例 A 麻烦的地方在于:读响应带 N、写响应不带 N,结构不同。解析引擎
|
|
29
|
+
``to_codec()`` 是把整份字段平铺成一条固定结构,没法在一个 spec 里同时表达两种
|
|
30
|
+
响应。所以这里把每类响应写成独立 ``FrameSpec``,再用 :class:`MultiFuncProtocol`
|
|
31
|
+
按功能码自动分派——同一段粘在一起的字节流里读/写/异常帧混着来也能正确切分。
|
|
32
|
+
|
|
33
|
+
.. note::
|
|
34
|
+
文档里浮点位移的字节序是 CDAB(小端字序 + 字节交换),不是常见的 ABCD。
|
|
35
|
+
反例:响应数据 ``02 24 42 9D`` 要重排成 ``42 9D 02 24`` 再按大端单精度解,
|
|
36
|
+
才得到 78.5042 mm。
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import struct
|
|
42
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
43
|
+
|
|
44
|
+
from .builder import FrameBuilder, Protocol
|
|
45
|
+
from .crc import crc16_modbus
|
|
46
|
+
from .fields import Field
|
|
47
|
+
from .spec import ChecksumRule, FrameSpec
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"FUNC_READ_INPUT",
|
|
51
|
+
"FUNC_READ_HOLDING",
|
|
52
|
+
"FUNC_WRITE_SINGLE",
|
|
53
|
+
"FUNC_WRITE_MULTI",
|
|
54
|
+
"REGISTERS",
|
|
55
|
+
"crc_bytes_le",
|
|
56
|
+
"XW_READ_TX",
|
|
57
|
+
"XW_MULTI_TX",
|
|
58
|
+
"XW_READ_RSP",
|
|
59
|
+
"XW_WRITE_SINGLE_RSP",
|
|
60
|
+
"XW_WRITE_MULTI_RSP",
|
|
61
|
+
"XW_ERROR_RSP",
|
|
62
|
+
"build_read",
|
|
63
|
+
"build_read_holding",
|
|
64
|
+
"build_write_single",
|
|
65
|
+
"build_write_multiple",
|
|
66
|
+
"split_registers",
|
|
67
|
+
"decode_displacement_int",
|
|
68
|
+
"decode_displacement_float",
|
|
69
|
+
"decode_temperature",
|
|
70
|
+
"MultiFuncProtocol",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
FUNC_READ_INPUT = 0x04
|
|
75
|
+
FUNC_READ_HOLDING = 0x03
|
|
76
|
+
FUNC_WRITE_SINGLE = 0x06
|
|
77
|
+
FUNC_WRITE_MULTI = 0x10
|
|
78
|
+
|
|
79
|
+
REGISTERS: Dict[str, int] = {
|
|
80
|
+
"POS1_INT": 0x0000, # 位移1 整数方式(0000H 整数 + 0001H 小数)
|
|
81
|
+
"POS1_FLOAT": 0x0006, # 位移1 浮点方式(0006H + 0007H)
|
|
82
|
+
"TEMP1": 0x000C, # 温度1
|
|
83
|
+
"ADDR": 0x0030, # 从机地址
|
|
84
|
+
"BAUD": 0x0031, # 波特率
|
|
85
|
+
"PARITY": 0x0032, # 奇偶校验
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
BAUD_CODES: Dict[int, int] = {
|
|
89
|
+
1200: 0x00, 2400: 0x01, 4800: 0x02, 9600: 0x03, 19200: 0x04,
|
|
90
|
+
38400: 0x05, 57600: 0x06, 115200: 0x07, 256000: 0x08, 460800: 0x09,
|
|
91
|
+
}
|
|
92
|
+
PARITY_CODES: Dict[str, int] = {"odd": 0x01, "even": 0x02, "none": 0x03}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def crc_bytes_le(data: bytes) -> bytes:
|
|
96
|
+
"""Modbus 线上 CRC:低字节在前,与报文字节直接对照。"""
|
|
97
|
+
return crc16_modbus(data).to_bytes(2, "little")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------- 发送:03/04/06 三类同结构(8 字节定长)
|
|
101
|
+
XW_READ_TX = FrameSpec(
|
|
102
|
+
fields=[
|
|
103
|
+
Field.u8("addr", desc="从站地址"),
|
|
104
|
+
Field.u8("func", desc="03h 读保持 / 04h 读输入 / 06h 写单"),
|
|
105
|
+
Field.u16("reg", desc="寄存器地址"),
|
|
106
|
+
Field.u16("value", desc="读=寄存器个数;写=写入值"),
|
|
107
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
108
|
+
],
|
|
109
|
+
cmd="func",
|
|
110
|
+
device="addr",
|
|
111
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
112
|
+
max_frame_size=16,
|
|
113
|
+
desc="Modbus 读/写单寄存器请求(03H/04H/06H),固定 8 字节",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------- 发送:10H 写多寄存器(变长)
|
|
117
|
+
XW_MULTI_TX = FrameSpec(
|
|
118
|
+
fields=[
|
|
119
|
+
Field.u8("addr", desc="从站地址"),
|
|
120
|
+
Field.u8("func", fixed=FUNC_WRITE_MULTI, desc="10h"),
|
|
121
|
+
Field.u16("reg", desc="起始寄存器地址"),
|
|
122
|
+
Field.u16("count", desc="寄存器个数"),
|
|
123
|
+
Field.u8("bytecount", desc="数据字节数 = count * 2"),
|
|
124
|
+
Field.raw("data", "{bytecount.val}", desc="各寄存器大端拼接"),
|
|
125
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
126
|
+
],
|
|
127
|
+
cmd="func",
|
|
128
|
+
payload="data",
|
|
129
|
+
device="addr",
|
|
130
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
131
|
+
max_frame_size=256,
|
|
132
|
+
desc="Modbus 写多寄存器请求(10H)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------- 接收:读响应(03H/04H,带字节数 N)
|
|
136
|
+
XW_READ_RSP = FrameSpec(
|
|
137
|
+
fields=[
|
|
138
|
+
Field.u8("addr", desc="从站地址"),
|
|
139
|
+
Field.u8("func", desc="03h / 04h"),
|
|
140
|
+
Field.u8("n", desc="后续数据字节数"),
|
|
141
|
+
Field.raw("data", "{n.val}", desc="寄存器数据,高字节在前"),
|
|
142
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
143
|
+
],
|
|
144
|
+
payload="data",
|
|
145
|
+
device="addr",
|
|
146
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
147
|
+
max_frame_size=256,
|
|
148
|
+
desc="Modbus 读响应(03H/04H)",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------- 接收:06H 写单寄存器响应(8 字节定长)
|
|
152
|
+
XW_WRITE_SINGLE_RSP = FrameSpec(
|
|
153
|
+
fields=[
|
|
154
|
+
Field.u8("addr", desc="从站地址"),
|
|
155
|
+
Field.u8("func", fixed=FUNC_WRITE_SINGLE, desc="06h"),
|
|
156
|
+
Field.u16("reg", desc="寄存器地址"),
|
|
157
|
+
Field.u16("value", desc="写入值"),
|
|
158
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
159
|
+
],
|
|
160
|
+
device="addr",
|
|
161
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
162
|
+
max_frame_size=16,
|
|
163
|
+
desc="Modbus 写单寄存器响应(06H)",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------- 接收:10H 写多寄存器响应(8 字节定长)
|
|
167
|
+
XW_WRITE_MULTI_RSP = FrameSpec(
|
|
168
|
+
fields=[
|
|
169
|
+
Field.u8("addr", desc="从站地址"),
|
|
170
|
+
Field.u8("func", fixed=FUNC_WRITE_MULTI, desc="10h"),
|
|
171
|
+
Field.u16("reg", desc="起始寄存器地址"),
|
|
172
|
+
Field.u16("count", desc="寄存器个数"),
|
|
173
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
174
|
+
],
|
|
175
|
+
device="addr",
|
|
176
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
177
|
+
max_frame_size=16,
|
|
178
|
+
desc="Modbus 写多寄存器响应(10H)",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------- 接收:异常响应(func | 0x80)
|
|
182
|
+
XW_ERROR_RSP = FrameSpec(
|
|
183
|
+
fields=[
|
|
184
|
+
Field.u8("addr", desc="从站地址"),
|
|
185
|
+
Field.u8("func", desc="原功能码 | 0x80"),
|
|
186
|
+
Field.u8("err", desc="异常码(01~04)"),
|
|
187
|
+
Field(key="crc", length=2, crc=True, desc="CRC16/MODBUS,低字节在前"),
|
|
188
|
+
],
|
|
189
|
+
device="addr",
|
|
190
|
+
checksum=ChecksumRule("crc", crc_bytes_le),
|
|
191
|
+
max_frame_size=16,
|
|
192
|
+
desc="Modbus 异常响应",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------- 组指令
|
|
197
|
+
_READ_BUILDER = FrameBuilder(XW_READ_TX)
|
|
198
|
+
_MULTI_BUILDER = FrameBuilder(XW_MULTI_TX)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_read(addr: int, reg: int = REGISTERS["POS1_INT"], count: int = 2) -> bytes:
|
|
202
|
+
"""组一条 04H 读输入寄存器请求(默认读位置1 整数方式 2 个寄存器)。"""
|
|
203
|
+
return _READ_BUILDER.build(FUNC_READ_INPUT, addr=addr, reg=reg, value=count)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_read_holding(addr: int, reg: int = REGISTERS["ADDR"], count: int = 1) -> bytes:
|
|
207
|
+
"""组一条 03H 读保持寄存器请求(例如读从机地址 0x0030)。"""
|
|
208
|
+
return _READ_BUILDER.build(FUNC_READ_HOLDING, addr=addr, reg=reg, value=count)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_write_single(addr: int, reg: int, value: int) -> bytes:
|
|
212
|
+
"""组一条 06H 写单寄存器请求。"""
|
|
213
|
+
return _READ_BUILDER.build(FUNC_WRITE_SINGLE, addr=addr, reg=reg, value=value)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def build_write_multiple(addr: int, reg: int, values: List[int]) -> bytes:
|
|
217
|
+
"""组一条 10H 写多寄存器请求(同时改地址/波特率/校验时用)。"""
|
|
218
|
+
data = b"".join(v.to_bytes(2, "big") for v in values)
|
|
219
|
+
return _MULTI_BUILDER.build_raw(
|
|
220
|
+
FUNC_WRITE_MULTI, data, addr=addr, reg=reg,
|
|
221
|
+
count=len(values), bytecount=len(data),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------- 数据解析
|
|
226
|
+
def split_registers(data: bytes) -> Tuple[int, ...]:
|
|
227
|
+
"""把寄存器区按 16 位大端切成整数(无符号)。"""
|
|
228
|
+
if len(data) % 2:
|
|
229
|
+
raise ValueError("寄存器数据字节数必须是偶数,实际 {}".format(len(data)))
|
|
230
|
+
return tuple(int.from_bytes(data[i : i + 2], "big") for i in range(0, len(data), 2))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def decode_displacement_int(data: bytes) -> float:
|
|
234
|
+
"""整数方式:高 2 字节 int16(整数)+ 低 2 字节 uint16(小数 / 65535)。"""
|
|
235
|
+
if len(data) != 4:
|
|
236
|
+
raise ValueError("整数位移必须是 4 字节,实际 {}".format(len(data)))
|
|
237
|
+
integer = int.from_bytes(data[0:2], "big", signed=True)
|
|
238
|
+
fraction = int.from_bytes(data[2:4], "big")
|
|
239
|
+
return integer + fraction / 65535.0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def decode_displacement_float(data: bytes) -> List[float]:
|
|
243
|
+
"""浮点方式:每 4 字节一个单精度浮点,字节序 CDAB(重排成 ABCD 再解)。"""
|
|
244
|
+
if len(data) % 4:
|
|
245
|
+
raise ValueError("浮点位移必须 4 字节对齐,实际 {}".format(len(data)))
|
|
246
|
+
out: List[float] = []
|
|
247
|
+
for i in range(0, len(data), 4):
|
|
248
|
+
chunk = data[i : i + 4]
|
|
249
|
+
# CDAB → ABCD:交换前后两个 16 位字
|
|
250
|
+
reordered = chunk[2:4] + chunk[0:2]
|
|
251
|
+
out.append(struct.unpack(">f", reordered)[0])
|
|
252
|
+
return out
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def decode_temperature(data: bytes) -> float:
|
|
256
|
+
"""温度:2 字节 int16,/100 = ℃。"""
|
|
257
|
+
if len(data) != 2:
|
|
258
|
+
raise ValueError("温度数据必须是 2 字节,实际 {}".format(len(data)))
|
|
259
|
+
return int.from_bytes(data, "big", signed=True) / 100.0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------- 组合收发:按功能码分派
|
|
263
|
+
class MultiFuncProtocol:
|
|
264
|
+
"""一台示例设备一个实例:能组指令、能解析响应、能处理黏包。
|
|
265
|
+
|
|
266
|
+
响应有四类结构(读/写单/写多/异常),没法用一份解析规则表达,所以内部
|
|
267
|
+
持有四份解析器,``feed`` 时按"哪份能凑出整帧且 CRC 过"来分派。字节流里
|
|
268
|
+
读/写/异常帧混着来、或粘连/分片,都能正确切出。
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, device_id: Any = None, enforce_device_id: bool = False) -> None:
|
|
272
|
+
self._device_id = device_id
|
|
273
|
+
self.enforce_device_id = enforce_device_id
|
|
274
|
+
self._tx = FrameBuilder(XW_READ_TX)
|
|
275
|
+
self._tx_multi = _MULTI_BUILDER
|
|
276
|
+
self._response_codecs = self._make_codecs()
|
|
277
|
+
self._buf = bytearray()
|
|
278
|
+
for codec in self._response_codecs:
|
|
279
|
+
codec.compile()
|
|
280
|
+
if device_id is not None:
|
|
281
|
+
codec.bind_device(device_id)
|
|
282
|
+
codec.enforce_device_id = enforce_device_id
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------ 组指令
|
|
285
|
+
def build_read(self, addr: int, reg: int = REGISTERS["POS1_INT"], count: int = 2) -> bytes:
|
|
286
|
+
return self._tx.build(FUNC_READ_INPUT, addr=addr, reg=reg, value=count)
|
|
287
|
+
|
|
288
|
+
def build_read_holding(self, addr: int, reg: int = REGISTERS["ADDR"], count: int = 1) -> bytes:
|
|
289
|
+
return self._tx.build(FUNC_READ_HOLDING, addr=addr, reg=reg, value=count)
|
|
290
|
+
|
|
291
|
+
def build_write_single(self, addr: int, reg: int, value: int) -> bytes:
|
|
292
|
+
return self._tx.build(FUNC_WRITE_SINGLE, addr=addr, reg=reg, value=value)
|
|
293
|
+
|
|
294
|
+
def build_write_multiple(self, addr: int, reg: int, values: List[int]) -> bytes:
|
|
295
|
+
data = b"".join(v.to_bytes(2, "big") for v in values)
|
|
296
|
+
return self._tx_multi.build_raw(
|
|
297
|
+
FUNC_WRITE_MULTI, data, addr=addr, reg=reg,
|
|
298
|
+
count=len(values), bytecount=len(data),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# ------------------------------------------------------------------ 解析
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _make_codecs():
|
|
304
|
+
from .codec import AbstractFrameCodec
|
|
305
|
+
|
|
306
|
+
specs = [
|
|
307
|
+
XW_READ_RSP,
|
|
308
|
+
XW_WRITE_SINGLE_RSP,
|
|
309
|
+
XW_WRITE_MULTI_RSP,
|
|
310
|
+
XW_ERROR_RSP,
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
codecs = []
|
|
314
|
+
for spec in specs:
|
|
315
|
+
impl = spec.checksum.impl
|
|
316
|
+
# 把 spec/impl 通过默认参数"钉死",避免闭包捕获到循环最后一个值
|
|
317
|
+
def _make(spec=spec, impl=impl):
|
|
318
|
+
class _Rsp(AbstractFrameCodec):
|
|
319
|
+
framing = spec.framing
|
|
320
|
+
device_field = spec.device
|
|
321
|
+
max_frame_size = spec.max_frame_size
|
|
322
|
+
|
|
323
|
+
def rules(self):
|
|
324
|
+
return spec.fields
|
|
325
|
+
|
|
326
|
+
def crc_imp(self, d: bytes):
|
|
327
|
+
return impl(d)
|
|
328
|
+
|
|
329
|
+
return _Rsp()
|
|
330
|
+
|
|
331
|
+
codecs.append(_make())
|
|
332
|
+
return codecs
|
|
333
|
+
|
|
334
|
+
def feed(self, chunk: Union[bytes, bytearray, memoryview]) -> List["Any"]:
|
|
335
|
+
"""喂入收到的字节,返回解析出的帧(可能为空)。"""
|
|
336
|
+
if isinstance(chunk, memoryview):
|
|
337
|
+
chunk = chunk.tobytes()
|
|
338
|
+
self._buf += chunk
|
|
339
|
+
out: List[Any] = []
|
|
340
|
+
pos = 0
|
|
341
|
+
size = len(self._buf)
|
|
342
|
+
while pos < size:
|
|
343
|
+
matched = False
|
|
344
|
+
for codec in self._response_codecs:
|
|
345
|
+
snapshot = codec._buf
|
|
346
|
+
codec._buf = bytearray(self._buf[pos:])
|
|
347
|
+
try:
|
|
348
|
+
parsed = codec._parse_at(0)
|
|
349
|
+
except Exception: # 任意解析异常都视为"这份规则不匹配"
|
|
350
|
+
parsed = None
|
|
351
|
+
finally:
|
|
352
|
+
codec._buf = snapshot
|
|
353
|
+
if parsed is not None:
|
|
354
|
+
frame, consumed = parsed
|
|
355
|
+
if frame is not None:
|
|
356
|
+
out.append(frame)
|
|
357
|
+
pos += consumed
|
|
358
|
+
matched = True
|
|
359
|
+
break
|
|
360
|
+
if not matched:
|
|
361
|
+
break
|
|
362
|
+
if pos:
|
|
363
|
+
del self._buf[:pos]
|
|
364
|
+
return out
|
|
365
|
+
|
|
366
|
+
def reset(self) -> None:
|
|
367
|
+
self._buf = bytearray()
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def pending_bytes(self) -> bytes:
|
|
371
|
+
return bytes(self._buf)
|
|
372
|
+
|
|
373
|
+
# ------------------------------------------------------------------ 设备号
|
|
374
|
+
@property
|
|
375
|
+
def device_id(self) -> Any:
|
|
376
|
+
return self._device_id
|
|
377
|
+
|
|
378
|
+
@device_id.setter
|
|
379
|
+
def device_id(self, value: Any) -> None:
|
|
380
|
+
self._device_id = value
|
|
381
|
+
for codec in self._response_codecs:
|
|
382
|
+
if value is None:
|
|
383
|
+
codec.forget_device()
|
|
384
|
+
else:
|
|
385
|
+
codec.bind_device(value)
|
|
386
|
+
|
|
387
|
+
def feed_through_protocol(self, chunk) -> List["Any"]:
|
|
388
|
+
"""用通用 ``Protocol`` 单独走读路径,验证读响应也能被标准 Protocol 解析。"""
|
|
389
|
+
proto = Protocol(XW_READ_TX, rx=XW_READ_RSP, device_id=self._device_id,
|
|
390
|
+
enforce_device_id=self.enforce_device_id)
|
|
391
|
+
return proto.feed(chunk)
|