Skip to content

Commit 7573aa4

Browse files
committed
[feat] add ModbusTcpClient and GcodeClient
1. ModbusTcpClient: xarm/tools/modbus_tcp.py 2. GcodeClient: xarm/tools/gcode.py
1 parent 1ca478d commit 7573aa4

File tree

3 files changed

+273
-1
lines changed

3 files changed

+273
-1
lines changed

xarm/tools/gcode.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# !/usr/bin/env python3
2+
# Software License Agreement (BSD License)
3+
#
4+
# Copyright (c) 2024, UFACTORY, Inc.
5+
# All rights reserved.
6+
#
7+
8+
9+
import os
10+
import re
11+
import sys
12+
import socket
13+
import logging
14+
import threading
15+
16+
logger = logging.Logger('gcode')
17+
logger_fmt = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - - %(message)s'
18+
logger_date_fmt = '%Y-%m-%d %H:%M:%S'
19+
stream_handler = logging.StreamHandler(sys.stdout)
20+
stream_handler.setLevel(logging.DEBUG)
21+
stream_handler.setFormatter(logging.Formatter(logger_fmt, logger_date_fmt))
22+
logger.addHandler(stream_handler)
23+
logger.setLevel(logging.INFO)
24+
25+
GCODE_PATTERN = r'([A-Z])([-+]?[0-9.]+)'
26+
CLEAN_PATTERN = r'\s+|\(.*?\)|;.*'
27+
28+
29+
class GcodeClient(object):
30+
def __init__(self, robot_ip):
31+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
32+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
33+
self.sock.setblocking(True)
34+
self.sock.connect((robot_ip, 504))
35+
self._lock = threading.Lock()
36+
37+
def execute(self, cmd):
38+
data = re.sub(CLEAN_PATTERN, '', cmd.strip().upper())
39+
if not data:
40+
# logger.warning('[E] null after clean {}'.format(cmd))
41+
return -1, []
42+
if data[0] == '%':
43+
# logger.warning('[E] starts with % ({})'.format(cmd))
44+
return -2, []
45+
if not re.findall(GCODE_PATTERN, data):
46+
# logger.warning('[E] not found {}'.format(cmd))
47+
return -3, []
48+
data = data.encode('utf-8', 'replace')
49+
with self._lock:
50+
self.sock.send(data + b'\n')
51+
ret = self.sock.recv(5)
52+
code, mode_state, err = ret[0:3]
53+
state, mode = mode_state & 0x0F, mode_state >> 4
54+
cmdnum = ret[3] << 8 | ret[4]
55+
if code != 0 or err != 0:
56+
logger.error('[{}], code={}, err={}, mode={}, state={}, cmdnum={}'.format(cmd, code, err, mode, state, cmdnum))
57+
elif state >= 4:
58+
logger.warning('[{}], code={}, err={}, mode={}, state={}, cmdnum={}'.format(cmd, code, err, mode, state, cmdnum))
59+
return code, [mode, state, err, cmdnum]
60+
61+
def execute_file(self, filepath):
62+
if not os.path.exists(filepath) or os.path.isdir(filepath):
63+
return -99
64+
with open(filepath, 'r') as f:
65+
for line in f.readlines():
66+
cmd = line.strip()
67+
if not cmd:
68+
continue
69+
code, info = self.execute(cmd)
70+
if code < 0:
71+
continue
72+
if code != 0 or info[2] != 0:
73+
if code != 1 and code != 2:
74+
return code
75+
if cmd in ['M2', 'M02', 'M30']:
76+
logger.info('[{}] Program End'.format(cmd))
77+
break
78+
return 0

xarm/tools/modbus_tcp.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# !/usr/bin/env python3
2+
# Software License Agreement (BSD License)
3+
#
4+
# Copyright (c) 2024, UFACTORY, Inc.
5+
# All rights reserved.
6+
#
7+
8+
9+
import sys
10+
import time
11+
import socket
12+
import struct
13+
import logging
14+
import threading
15+
16+
logger = logging.Logger('modbus_tcp')
17+
logger_fmt = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - - %(message)s'
18+
logger_date_fmt = '%Y-%m-%d %H:%M:%S'
19+
stream_handler = logging.StreamHandler(sys.stdout)
20+
stream_handler.setLevel(logging.DEBUG)
21+
stream_handler.setFormatter(logging.Formatter(logger_fmt, logger_date_fmt))
22+
logger.addHandler(stream_handler)
23+
logger.setLevel(logging.INFO)
24+
25+
26+
class ModbusTcpClient(object):
27+
def __init__(self, ip, port=502, unit_id=0x01):
28+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
29+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
30+
self.sock.setblocking(True)
31+
self.sock.connect((ip, port))
32+
self._transaction_id = 0
33+
self._protocol_id = 0x00
34+
self._unit_id = unit_id
35+
self._func_code = 0x00
36+
self._lock = threading.Lock()
37+
38+
def __wait_to_response(self, transaction_id=None, unit_id=None, func_code=None, timeout=3):
39+
expired = time.monotonic() + timeout
40+
recv_data = b''
41+
length = 0
42+
code = -3 # TIMEOUT
43+
send_transaction_id = transaction_id if transaction_id is not None else self._transaction_id
44+
send_unit_id = unit_id if unit_id is not None else self._unit_id
45+
send_func_code = func_code if func_code is not None else self._func_code
46+
while time.monotonic() < expired:
47+
if len(recv_data) < 7:
48+
recv_data += self.sock.recv(7 - len(recv_data))
49+
if len(recv_data) < 7:
50+
continue
51+
if length == 0:
52+
length = struct.unpack('>H', recv_data[4:6])[0]
53+
if len(recv_data) < length + 6:
54+
recv_data += self.sock.recv(length + 6 - len(recv_data))
55+
if len(recv_data) < length + 6:
56+
continue
57+
transaction_id = struct.unpack('>H', recv_data[0:2])[0]
58+
protocol_id = struct.unpack('>H', recv_data[2:4])[0]
59+
unit_id = recv_data[6]
60+
func_code = recv_data[7]
61+
if transaction_id != send_transaction_id:
62+
logger.warning('Receive a reply with a mismatched transaction id (S: {}, R: {}), discard it and continue waiting.'.format(send_transaction_id, transaction_id))
63+
length = 0
64+
recv_data = b''
65+
continue
66+
elif protocol_id != self._protocol_id:
67+
logger.warning('Receive a reply with a mismatched protocol id (S: {}, R: {}), discard it and continue waiting.'.format(self._protocol_id, protocol_id))
68+
length = 0
69+
recv_data = b''
70+
continue
71+
elif unit_id != send_unit_id:
72+
logger.warning('Receive a reply with a mismatched unit id (S: {}, R: {}), discard it and continue waiting.'.format(send_unit_id, unit_id))
73+
length = 0
74+
recv_data = b''
75+
continue
76+
elif func_code != send_func_code and func_code != send_func_code + 0x80:
77+
logger.warning('Receive a reply with a mismatched func code (S: {}, R: {}), discard it and continue waiting.'.format(send_func_code, func_code))
78+
length = 0
79+
recv_data = b''
80+
continue
81+
else:
82+
code = 0
83+
break
84+
if code == 0 and len(recv_data) == 9:
85+
logger.error('modbus tcp data exception, exp={}, res={}'.format(recv_data[8], recv_data))
86+
return recv_data[8], recv_data
87+
elif code != 0:
88+
logger.error('recv timeout, len={}, res={}'.format(len(recv_data), recv_data))
89+
return code, recv_data
90+
91+
def __pack_to_send(self, pdu_data, unit_id=None):
92+
self._transaction_id = self._transaction_id % 65535 + 1
93+
unit_id = unit_id if unit_id is not None else self._unit_id
94+
data = struct.pack('>HHHB', self._transaction_id, self._protocol_id, len(pdu_data) + 1, unit_id)
95+
data += pdu_data
96+
self.sock.send(data)
97+
98+
def __request(self, pdu, unit_id=None):
99+
with self._lock:
100+
self._func_code = pdu[0]
101+
self.__pack_to_send(pdu)
102+
return self.__wait_to_response(unit_id=unit_id, func_code=pdu[0])
103+
104+
def __read_bits(self, addr, quantity, func_code=0x01):
105+
assert func_code == 0x01 or func_code == 0x02
106+
pdu = struct.pack('>BHH', func_code, addr, quantity)
107+
code, res_data = self.__request(pdu)
108+
if code == 0 and len(res_data) == 9 + (quantity + 7) // 8:
109+
return code, [(res_data[9 + i // 8] >> (i % 8) & 0x01) for i in range(quantity)]
110+
else:
111+
return code, res_data
112+
113+
def __read_registers(self, addr, quantity, func_code=0x03, signed=False):
114+
assert func_code == 0x03 or func_code == 0x04
115+
pdu = struct.pack('>BHH', func_code, addr, quantity)
116+
code, res_data = self.__request(pdu)
117+
if code == 0 and len(res_data) == 9 + quantity * 2:
118+
return 0, list(struct.unpack('>{}{}'.format(quantity, 'h' if signed else 'H'), res_data[9:]))
119+
else:
120+
return code, res_data
121+
122+
def read_coil_bits(self, addr, quantity):
123+
"""
124+
func_code: 0x01
125+
"""
126+
return self.__read_bits(addr, quantity, func_code=0x01)
127+
128+
def read_input_bits(self, addr, quantity):
129+
"""
130+
func_code: 0x02
131+
"""
132+
return self.__read_bits(addr, quantity, func_code=0x02)
133+
134+
def read_holding_registers(self, addr, quantity, signed=False):
135+
"""
136+
func_code: 0x03
137+
"""
138+
return self.__read_registers(addr, quantity, func_code=0x03, signed=signed)
139+
140+
def read_input_registers(self, addr, quantity, signed=False):
141+
"""
142+
func_code: 0x04
143+
"""
144+
return self.__read_registers(addr, quantity, func_code=0x04, signed=signed)
145+
146+
def write_single_coil_bit(self, addr, on):
147+
"""
148+
func_code: 0x05
149+
"""
150+
pdu = struct.pack('>BHH', 0x05, addr, 0xFF00 if on else 0x0000)
151+
return self.__request(pdu)[0]
152+
153+
def write_single_holding_register(self, addr, reg_val):
154+
"""
155+
func_code: 0x06
156+
"""
157+
pdu = struct.pack('>BHH', 0x06, addr, reg_val)
158+
return self.__request(pdu)[0]
159+
160+
def write_multiple_coil_bits(self, addr, bits):
161+
"""
162+
func_code: 0x0F
163+
"""
164+
datas = [0] * ((len(bits) + 7) // 8)
165+
for i in range(len(bits)):
166+
if bits[i]:
167+
datas[i // 8] |= (1 << (i % 8))
168+
pdu = struct.pack('>BHHB{}B'.format(len(datas)), 0x0F, addr, len(bits), len(datas), *datas)
169+
return self.__request(pdu)[0]
170+
171+
def write_multiple_holding_registers(self, addr, regs):
172+
"""
173+
func_code: 0x10
174+
"""
175+
pdu = struct.pack('>BHHB{}H'.format(len(regs)), 0x10, addr, len(regs), len(regs) * 2, *regs)
176+
return self.__request(pdu)[0]
177+
178+
def mask_write_holding_register(self, addr, and_mask, or_mask):
179+
"""
180+
func_code: 0x16
181+
"""
182+
pdu = struct.pack('>BHHH', 0x16, addr, and_mask, or_mask)
183+
return self.__request(pdu)[0]
184+
185+
def write_and_read_holding_registers(self, r_addr, r_quantity, w_addr, w_regs, r_signed=False, w_signed=False):
186+
"""
187+
func_code: 0x17
188+
"""
189+
pdu = struct.pack('>BHHHHB{}{}'.format(len(w_regs), 'h' if w_signed else 'H'), 0x17, r_addr, r_quantity, w_addr, len(w_regs), len(w_regs) * 2, *w_regs)
190+
code, res_data = self.__request(pdu)
191+
if code == 0 and len(res_data) == 9 + r_quantity * 2:
192+
return 0, struct.unpack('>{}{}'.format(r_quantity, 'h' if r_signed else 'H'), res_data[9:])
193+
else:
194+
return code, res_data

xarm/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.14.0'
1+
__version__ = '1.14.1'

0 commit comments

Comments
 (0)