serialpro 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: serialpro
3
+ Version: 0.1.0
4
+ Summary: 串口封装
5
+ Author: LiuWei
6
+ Author-email: 183074632@qq.com
7
+ Requires-Python: >=3.11
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: pyserial (>=3.5,<4.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+
File without changes
@@ -0,0 +1,487 @@
1
+ import serial
2
+
3
+
4
+ class SerialPro:
5
+ """三菱FX3U PLC RS232串口通讯类 (Format 1 专用协议)"""
6
+
7
+ # 协议常量
8
+ ENQ = b'\x05' # 请求帧的起始标志, 上位机 → PLC 的所有读写命令都以 ENQ 开头, 告诉 PLC:"我有一条请求要发给你,请做好接收准备"
9
+ ACK = b'\x06' # PLC 返回的正面应答, "命令接收成功,写入操作已执行"
10
+ NAK = b'\x15' # PLC 返回的错误应答, "命令格式有误 / 校验码不匹配 / 地址非法"
11
+ STX = b'\x02' # PLC 响应数据的起始标志, BR/WR 读操作中,PLC 返回数据帧以 STX 开头
12
+ ETX = b'\x03' # PLC 响应数据域的结束标志, "数据部分到此结束,后面两位是校验码"
13
+
14
+ # 支持的元件类型
15
+ BIT_DEVICES = ('X', 'Y', 'M', 'S', 'T', 'C')
16
+ WORD_DEVICES = ('D', 'T', 'C', 'R')
17
+ """
18
+ 三菱 FX3U PLC 支持的元件类型:
19
+
20
+ 位元件 (ON/OFF 两态):
21
+ X — 输入继电器,物理输入端子状态(只读)
22
+ Y — 输出继电器,驱动物理输出端子(可读写)
23
+ M — 辅助继电器,内部逻辑标志位
24
+ S — 状态继电器,用于步进梯形图或通用标志
25
+ T — 定时器线圈状态,ON表示正在计时
26
+ C — 计数器线圈状态,ON表示计数达到设定值
27
+
28
+ 字元件 (16位数值 0–65535):
29
+ D — 数据寄存器,存储数值数据
30
+ T — 定时器当前计时值
31
+ C — 计数器当前计数值
32
+ R — 文件寄存器,扩展存储,断电保持
33
+ """
34
+
35
+ def __init__(self, port='COM1', baud_rate=9600, station='00', timeout=1.0):
36
+ """初始化SerialPro.
37
+
38
+ Args:
39
+ port: 串口号, 如 'COM1'.
40
+ baud_rate: 波特率, 默认9600.
41
+ station: 站号, 两位十六进制字符串, 默认'00'.
42
+ timeout: 读写超时时间(秒).
43
+ """
44
+ self.port = port
45
+ self.baud_rate = baud_rate
46
+ self.station = station.upper()
47
+ self.timeout = timeout
48
+ self._serial = None
49
+
50
+ # ==================== 连接管理 ====================
51
+
52
+ def open(self):
53
+ """打开串口连接,配置为 9600,7,E,1"""
54
+ if self._serial is not None and self._serial.is_open:
55
+ return
56
+
57
+ self._serial = serial.Serial(
58
+ port=self.port,
59
+ baudrate=self.baud_rate,
60
+ bytesize=serial.SEVENBITS,
61
+ parity=serial.PARITY_EVEN,
62
+ stopbits=serial.STOPBITS_ONE,
63
+ timeout=self.timeout
64
+ )
65
+ self._serial.reset_input_buffer()
66
+ self._serial.reset_output_buffer()
67
+
68
+ def close(self):
69
+ """关闭串口连接"""
70
+ if self._serial is not None and self._serial.is_open:
71
+ self._serial.close()
72
+ self._serial = None
73
+
74
+ def is_connected(self):
75
+ """检查是否已连接.
76
+
77
+ Returns:
78
+ bool: True表示已连接, False表示未连接.
79
+ """
80
+ return self._serial is not None and self._serial.is_open
81
+
82
+ # ==================== 内部方法 ====================
83
+
84
+ @staticmethod
85
+ def _calc_checksum(data_str):
86
+ """计算和校验码.
87
+
88
+ Args:
89
+ data_str: 待校验的ASCII字符串.
90
+
91
+ Returns:
92
+ str: 两位十六进制大写校验码.
93
+ """
94
+ total = sum(ord(c) for c in data_str)
95
+ return f"{total & 0xFF:02X}"
96
+
97
+ def _format_bit_address(self, device, addr):
98
+ """格式化位元件地址.
99
+
100
+ Args:
101
+ device: 位元件类型, 如 'X', 'Y', 'M'.
102
+ addr: 十进制地址值.
103
+
104
+ Returns:
105
+ str: 格式化后的地址字符串, 如 'X0001'.
106
+
107
+ Raises:
108
+ ValueError: 当元件类型不支持时抛出.
109
+ """
110
+ if device.upper() not in self.BIT_DEVICES:
111
+ raise ValueError(f"不支持的位元件类型: {device}")
112
+ return f"{device.upper()}{addr:04d}"
113
+
114
+ def _format_word_address(self, device, addr):
115
+ """格式化字元件地址.
116
+
117
+ Args:
118
+ device: 字元件类型, 如 'D', 'T', 'C'.
119
+ addr: 十进制地址值.
120
+
121
+ Returns:
122
+ str: 格式化后的地址字符串, 如 'D0123'.
123
+
124
+ Raises:
125
+ ValueError: 当元件类型不支持时抛出.
126
+ """
127
+ if device.upper() not in self.WORD_DEVICES:
128
+ raise ValueError(f"不支持的字元件类型: {device}")
129
+ return f"{device.upper()}{addr:04d}"
130
+
131
+ def _send_and_receive(self, cmd_body, expect_stx=False):
132
+ """发送命令并接收PLC响应.
133
+
134
+ Args:
135
+ cmd_body: ENQ之后、校验码之前的命令体字符串.
136
+ expect_stx: 是否期望STX开头的数据响应(读操作), False则期望ACK(写操作).
137
+
138
+ Returns:
139
+ str or bool: 读操作返回响应数据字符串, 写操作返回True/False.
140
+
141
+ Raises:
142
+ RuntimeError: 当串口未打开时抛出.
143
+ """
144
+ if not self.is_connected():
145
+ raise RuntimeError("串口未打开, 请先调用 open()")
146
+
147
+ checksum = self._calc_checksum(cmd_body)
148
+ full_cmd = self.ENQ + cmd_body.encode('ascii') + checksum.encode('ascii')
149
+
150
+ self._serial.reset_input_buffer()
151
+ self._serial.write(full_cmd)
152
+
153
+ if expect_stx:
154
+ return self._read_stx_response()
155
+ else:
156
+ return self._read_ack_response()
157
+
158
+ def _read_stx_response(self):
159
+ """读取STX开头的多字节响应帧.
160
+
161
+ Returns:
162
+ str or None: 解码后的响应数据字符串, 收到NAK或解析失败返回None.
163
+ """
164
+ stx = self._serial.read(1)
165
+ if stx == self.NAK:
166
+ _ = self._serial.read(4)
167
+ return None
168
+ if stx != self.STX:
169
+ return None
170
+
171
+ buffer = bytearray()
172
+ while True:
173
+ ch = self._serial.read(1)
174
+ if not ch or ch == self.ETX:
175
+ break
176
+ buffer.extend(ch)
177
+
178
+ _ = self._serial.read(2) # 丢弃校验码
179
+ return buffer.decode('ascii')
180
+
181
+ def _read_ack_response(self):
182
+ """读取ACK/NAK响应.
183
+
184
+ Returns:
185
+ bool: ACK返回True, NAK或超时返回False.
186
+ """
187
+ ack = self._serial.read(1)
188
+ if ack == self.ACK:
189
+ _ = self._serial.read(4)
190
+ return True
191
+ elif ack == self.NAK:
192
+ _ = self._serial.read(4)
193
+ return False
194
+ return False
195
+
196
+ # ==================== 位元件读写 ====================
197
+
198
+ def read_bits(self, device, start_addr, count, delay='A'):
199
+ """批量读取位元件 (BR指令), BR: bit read.
200
+
201
+ Args:
202
+ device: 位元件类型, 'X'/'Y'/'M'/'S'/'T'/'C'.
203
+ start_addr: 起始地址(十进制).
204
+ count: 读取数量.
205
+ delay: 消息等待时间, '0'-'F' 对应 0-150ms, 默认'A'(100ms).
206
+
207
+ Returns:
208
+ str or None: 成功返回由'0'和'1'组成的位状态字符串, 失败返回None.
209
+ """
210
+ addr_str = self._format_bit_address(device, start_addr)
211
+ cmd_body = f"{self.station}FFBR{delay}{addr_str}{count:02X}"
212
+
213
+ response = self._send_and_receive(cmd_body, expect_stx=True)
214
+ if response is None or not isinstance(response, str):
215
+ return None
216
+ return response[4:] # 去掉 station(2) + PLC_No(2)
217
+
218
+ def write_bits(self, device, start_addr, values, delay='A'):
219
+ """批量写入位元件 (BW指令).
220
+
221
+ Args:
222
+ device: 位元件类型, 'Y'/'M'/'S'/'T'/'C'.
223
+ start_addr: 起始地址(十进制).
224
+ values: 写入值, 字符串(如'10101')或列表(如[1,0,1,0,1]).
225
+ delay: 消息等待时间, '0'-'F' 对应 0-150ms.
226
+
227
+ Returns:
228
+ bool: True表示写入成功, False表示失败.
229
+ """
230
+ if isinstance(values, (list, tuple)):
231
+ values = ''.join('1' if v else '0' for v in values)
232
+
233
+ count = len(values)
234
+ addr_str = self._format_bit_address(device, start_addr)
235
+ cmd_body = f"{self.station}FFBW{delay}{addr_str}{count:02X}{values}"
236
+
237
+ return self._send_and_receive(cmd_body, expect_stx=False)
238
+
239
+ def read_bit(self, device, addr, delay='A'):
240
+ """读取单个位元件状态.
241
+
242
+ Args:
243
+ device: 位元件类型, 'X'/'Y'/'M'/'S'/'T'/'C'.
244
+ addr: 元件地址(十进制).
245
+ delay: 消息等待时间, '0'-'F'.
246
+
247
+ Returns:
248
+ bool or None: True(ON) / False(OFF), 失败返回None.
249
+ """
250
+ result = self.read_bits(device, addr, 1, delay)
251
+ if result is None or len(result) == 0:
252
+ return None
253
+ return result[0] == '1'
254
+
255
+ def write_bit(self, device, addr, value, delay='A'):
256
+ """写入单个位元件.
257
+
258
+ Args:
259
+ device: 位元件类型, 'Y'/'M'/'S'/'T'/'C'.
260
+ addr: 元件地址(十进制).
261
+ value: True/1 为ON, False/0 为OFF.
262
+ delay: 消息等待时间, '0'-'F'.
263
+
264
+ Returns:
265
+ bool: True表示写入成功, False表示失败.
266
+ """
267
+ val_str = '1' if value else '0'
268
+ return self.write_bits(device, addr, val_str, delay)
269
+
270
+ def force_on(self, device, addr, delay='A'):
271
+ """强制置位 (等同于write_bit ON).
272
+
273
+ Args:
274
+ device: 位元件类型, 'Y'/'M'/'S'/'T'/'C'.
275
+ addr: 元件地址(十进制).
276
+ delay: 消息等待时间, '0'-'F'.
277
+
278
+ Returns:
279
+ bool: True表示操作成功, False表示失败.
280
+ """
281
+ return self.write_bit(device, addr, True, delay)
282
+
283
+ def force_off(self, device, addr, delay='A'):
284
+ """强制复位 (等同于write_bit OFF).
285
+
286
+ Args:
287
+ device: 位元件类型, 'Y'/'M'/'S'/'T'/'C'.
288
+ addr: 元件地址(十进制).
289
+ delay: 消息等待时间, '0'-'F'.
290
+
291
+ Returns:
292
+ bool: True表示操作成功, False表示失败.
293
+ """
294
+ return self.write_bit(device, addr, False, delay)
295
+
296
+ # ==================== 字元件读写 ====================
297
+
298
+ def read_words(self, device, start_addr, count, delay='A'):
299
+ """批量读取字元件 (WR指令).
300
+
301
+ Args:
302
+ device: 字元件类型, 'D'/'T'/'C'/'R'.
303
+ start_addr: 起始地址(十进制).
304
+ count: 读取的字数量.
305
+ delay: 消息等待时间, '0'-'F' 对应 0-150ms.
306
+
307
+ Returns:
308
+ list[int] or None: 成功返回整数列表(0-65535), 失败返回None.
309
+ """
310
+ addr_str = self._format_word_address(device, start_addr)
311
+ cmd_body = f"{self.station}FFWR{delay}{addr_str}{count:02X}"
312
+
313
+ response = self._send_and_receive(cmd_body, expect_stx=True)
314
+ if response is None or not isinstance(response, str):
315
+ return None
316
+
317
+ data = response[4:] # 去掉 station(2) + PLC_No(2)
318
+ result = []
319
+ for i in range(0, len(data), 4):
320
+ if i + 4 <= len(data):
321
+ result.append(int(data[i:i + 4], 16))
322
+ return result
323
+
324
+ def write_words(self, device, start_addr, values, delay='A'):
325
+ """批量写入字元件 (WW指令).
326
+
327
+ Args:
328
+ device: 字元件类型, 'D'/'T'/'C'/'R'.
329
+ start_addr: 起始地址(十进制).
330
+ values: 写入值列表, 每个值范围0-65535.
331
+ delay: 消息等待时间, '0'-'F' 对应 0-150ms.
332
+
333
+ Returns:
334
+ bool: True表示写入成功, False表示失败.
335
+ """
336
+ addr_str = self._format_word_address(device, start_addr)
337
+ count = len(values)
338
+ data_str = ''.join(f"{v & 0xFFFF:04X}" for v in values)
339
+ cmd_body = f"{self.station}FFWW{delay}{addr_str}{count:02X}{data_str}"
340
+
341
+ return self._send_and_receive(cmd_body, expect_stx=False)
342
+
343
+ def read_word(self, device, addr, delay='A'):
344
+ """读取单个字元件.
345
+
346
+ Args:
347
+ device: 字元件类型, 'D'/'T'/'C'/'R'.
348
+ addr: 元件地址(十进制).
349
+ delay: 消息等待时间, '0'-'F'.
350
+
351
+ Returns:
352
+ int or None: 成功返回0-65535的整数值, 失败返回None.
353
+ """
354
+ result = self.read_words(device, addr, 1, delay)
355
+ if result is None or len(result) == 0:
356
+ return None
357
+ return result[0]
358
+
359
+ def write_word(self, device, addr, value, delay='A'):
360
+ """写入单个字元件.
361
+
362
+ Args:
363
+ device: 字元件类型, 'D'/'T'/'C'/'R'.
364
+ addr: 元件地址(十进制).
365
+ value: 写入值, 范围0-65535.
366
+ delay: 消息等待时间, '0'-'F'.
367
+
368
+ Returns:
369
+ bool: True表示写入成功, False表示失败.
370
+ """
371
+ return self.write_words(device, addr, [value], delay)
372
+
373
+ # ==================== 编程口协议 (备用) ====================
374
+
375
+ def _calc_prog_port_address(self, device, addr):
376
+ """计算编程口协议中的地址值.
377
+
378
+ 字元件: address = addr * 2 + 0x1000
379
+ 位元件: address = addr // 8 + 0x0100
380
+
381
+ Args:
382
+ device: 元件类型.
383
+ addr: 十进制地址.
384
+
385
+ Returns:
386
+ int: 编程口协议使用的十六进制地址值.
387
+ """
388
+ if device.upper() in self.WORD_DEVICES:
389
+ return addr * 2 + 0x1000
390
+ else:
391
+ return addr // 8 + 0x0100
392
+
393
+ def read_device_raw(self, device, addr, byte_count):
394
+ """使用编程口协议读取设备 (命令码 '0').
395
+
396
+ Args:
397
+ device: 元件类型, 如 'D', 'M', 'X', 'Y'.
398
+ addr: 地址(十进制).
399
+ byte_count: 读取字节数.
400
+
401
+ Returns:
402
+ str or None: 成功返回响应数据字符串, 失败返回None.
403
+ """
404
+ addr_val = self._calc_prog_port_address(device, addr)
405
+ addr_hex = f"{addr_val:04X}"
406
+ bytes_hex = f"{byte_count:02X}"
407
+
408
+ body = '0' + addr_hex + bytes_hex + chr(0x03)
409
+ checksum = self._calc_checksum(body)
410
+ full_cmd = self.STX + body.encode('ascii') + checksum.encode('ascii')
411
+
412
+ self._serial.reset_input_buffer()
413
+ self._serial.write(full_cmd)
414
+
415
+ return self._read_stx_response()
416
+
417
+ def write_device_raw(self, device, addr, byte_count, data_str):
418
+ """使用编程口协议写入设备 (命令码 '1').
419
+
420
+ Args:
421
+ device: 元件类型, 如 'D', 'M', 'X', 'Y'.
422
+ addr: 地址(十进制).
423
+ byte_count: 写入字节数.
424
+ data_str: 写入数据(ASCII十六进制字符串).
425
+
426
+ Returns:
427
+ bool: True表示写入成功, False表示失败.
428
+ """
429
+ addr_val = self._calc_prog_port_address(device, addr)
430
+ addr_hex = f"{addr_val:04X}"
431
+ bytes_hex = f"{byte_count:02X}"
432
+
433
+ body = '1' + addr_hex + bytes_hex + data_str + chr(0x03)
434
+ checksum = self._calc_checksum(body)
435
+ full_cmd = self.STX + body.encode('ascii') + checksum.encode('ascii')
436
+
437
+ self._serial.reset_input_buffer()
438
+ self._serial.write(full_cmd)
439
+
440
+ ack = self._serial.read(1)
441
+ return ack == self.ACK
442
+
443
+ def force_on_raw(self, device, addr):
444
+ """使用编程口协议强制置位 (命令码 '7').
445
+
446
+ Args:
447
+ device: 位元件类型, 'X'/'Y'/'M'/'S'/'T'/'C'.
448
+ addr: 元件地址(十进制).
449
+
450
+ Returns:
451
+ bool: True表示操作成功, False表示失败.
452
+ """
453
+ addr_val = self._calc_prog_port_address(device, addr)
454
+ addr_hex = f"{addr_val:04X}"
455
+
456
+ body = '7' + addr_hex + chr(0x03)
457
+ checksum = self._calc_checksum(body)
458
+ full_cmd = self.STX + body.encode('ascii') + checksum.encode('ascii')
459
+
460
+ self._serial.reset_input_buffer()
461
+ self._serial.write(full_cmd)
462
+
463
+ ack = self._serial.read(1)
464
+ return ack == self.ACK
465
+
466
+ def force_off_raw(self, device, addr):
467
+ """使用编程口协议强制复位 (命令码 '8').
468
+
469
+ Args:
470
+ device: 位元件类型, 'X'/'Y'/'M'/'S'/'T'/'C'.
471
+ addr: 元件地址(十进制).
472
+
473
+ Returns:
474
+ bool: True表示操作成功, False表示失败.
475
+ """
476
+ addr_val = self._calc_prog_port_address(device, addr)
477
+ addr_hex = f"{addr_val:04X}"
478
+
479
+ body = '8' + addr_hex + chr(0x03)
480
+ checksum = self._calc_checksum(body)
481
+ full_cmd = self.STX + body.encode('ascii') + checksum.encode('ascii')
482
+
483
+ self._serial.reset_input_buffer()
484
+ self._serial.write(full_cmd)
485
+
486
+ ack = self._serial.read(1)
487
+ return ack == self.ACK
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "serialpro"
3
+ version = "0.1.0"
4
+ description = "串口封装"
5
+ authors = [
6
+ {name = "LiuWei",email = "183074632@qq.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "pyserial (>=3.5,<4.0)"
12
+ ]
13
+
14
+
15
+ [build-system]
16
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
17
+ build-backend = "poetry.core.masonry.api"