Skip to content

Commit 5f941a9

Browse files
authored
Merge pull request #118 from Mrkun5018/main
Add conveyor belt interface
2 parents 2c78552 + bcb8fe3 commit 5f941a9

File tree

2 files changed

+217
-3
lines changed

2 files changed

+217
-3
lines changed

pymycobot/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from pymycobot.pro400 import Pro400
3939
from pymycobot.pro400client import Pro400Client
4040
from pymycobot.myarmm_control import MyArmMControl
41-
41+
from conveyor_api import ConveyorAPI
4242
__all__ = [
4343
"MyPalletizer260",
4444
"MechArm270",
@@ -75,8 +75,8 @@
7575
"Pro400Client",
7676
"MyCobot280Socket",
7777
"MyCobot320Socket",
78-
"MyArmMControl"
79-
78+
"MyArmMControl",
79+
"ConveyorAPI"
8080
]
8181

8282
if sys.platform == "linux":

pymycobot/conveyor_api.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import logging
2+
import threading
3+
import time
4+
5+
from serial import Serial
6+
from common import *
7+
import struct
8+
9+
10+
class CommandGenre(object):
11+
SET_SERVO_DIRECTION = 0XA0
12+
GET_SERVO_DIRECTION = 0XA2
13+
14+
SET_SERVO_SPEED = 0XA5
15+
GET_SERVO_SPEED = 0XA3
16+
17+
# WRITE_SERVO_ANGLE = 0XA6
18+
# WRITE_SERVO_STEP = 0XA7
19+
20+
READ_FIRMWARE_VERSION = 0XAA
21+
22+
23+
class Command(object):
24+
HEADER = 0XFF
25+
26+
def __init__(self, genre, address, check_digit, params, length=None):
27+
self.__header = [0xff, 0xff]
28+
self.__length = length or len(params)
29+
self.__address = address
30+
self.__params = params
31+
self.__genre = genre
32+
self.__check_digit = check_digit
33+
34+
@property
35+
def genre(self):
36+
return self.__genre
37+
38+
def to_bytes(self):
39+
return bytes([*self.__header, self.__address, self.__length, *self.__params, self.__genre, *self.__check_digit])
40+
41+
def get_params(self):
42+
return self.__params[0] if self.__length == 1 else self.__params
43+
44+
def __str__(self):
45+
return " ".join(map(lambda bit: hex(bit).upper(), self.to_bytes()))
46+
47+
def __bytes__(self):
48+
return self.to_bytes()
49+
50+
@classmethod
51+
def packing(cls, genre: int, addr: int, *params):
52+
check_code = cls.check_digit(genre, params)
53+
return cls(genre=genre, address=addr, check_digit=(check_code, ), params=(*params,))
54+
55+
@classmethod
56+
def parsing(cls, buffer: bytes):
57+
# header, header, addr, length, params, genre, check_bits
58+
if len(buffer) < 6:
59+
return None
60+
if buffer[0] != cls.HEADER or buffer[1] != cls.HEADER:
61+
return None
62+
length = buffer[3]
63+
return cls(genre=buffer[-2], address=buffer[2], length=length, params=(*buffer[4:4+length], ), check_digit=buffer[4+length:4+length+1])
64+
65+
@classmethod
66+
def unpack_args(cls, *parameters):
67+
bits_pack_list = []
68+
for param in parameters:
69+
pair = struct.pack('>h', param)
70+
if len(pair) == 2:
71+
bits_pack_list.extend(list(pair))
72+
else:
73+
bits_pack_list.clear()
74+
print(bits_pack_list)
75+
return bits_pack_list
76+
77+
@classmethod
78+
def check_digit(cls, genre, params):
79+
"""
80+
Calculate the check-code for the command.
81+
:param genre: int, function genre
82+
:param params: bytes, function parameters
83+
:return: int, check-code
84+
"""
85+
return sum([genre, *params]) & 0xff
86+
87+
@classmethod
88+
def has_header(cls, buffer: bytes):
89+
"""
90+
Check if the buffer contains a header.
91+
:param buffer:
92+
:return:
93+
"""
94+
if len(buffer) < 2:
95+
return False
96+
return buffer[0] == cls.HEADER and buffer[1] == cls.HEADER
97+
98+
99+
class SerialProtocol(object):
100+
101+
def __init__(self, comport, baudrate, timeout=0.5):
102+
self._comport = comport
103+
self._baudrate = baudrate
104+
self._timeout = timeout
105+
self._serial_port = Serial(port=comport, baudrate=baudrate, timeout=timeout)
106+
self._serial_port.rts = True
107+
self._serial_port.dtr = True
108+
109+
def open(self):
110+
if self._serial_port.is_open is False:
111+
self._serial_port.open()
112+
113+
def close(self):
114+
if self._serial_port.is_open is True:
115+
self._serial_port.close()
116+
117+
def write(self, data: bytes):
118+
self._serial_port.write(data)
119+
self._serial_port.flush()
120+
121+
def read(self, size=None):
122+
return self._serial_port.read(size or self._serial_port.in_waiting)
123+
124+
def flush(self):
125+
self._serial_port.flush()
126+
127+
128+
class ConveyorAPI(SerialProtocol):
129+
class MotorModel:
130+
STEPPER_MOTOR_42 = 0x30
131+
STEPPER_MOTOR_57 = 0x31
132+
133+
def __init__(self, comport, baudrate="115200", timeout=0.1, debug=False):
134+
super().__init__(comport, baudrate, timeout)
135+
self._debug = debug
136+
self.open()
137+
self._mutex = threading.Lock()
138+
self._log = logging.getLogger("conveyor_api")
139+
self._log.setLevel(logging.DEBUG if debug else logging.INFO)
140+
handler = logging.StreamHandler()
141+
handler.setLevel(logging.DEBUG if debug else logging.INFO)
142+
formatter = logging.Formatter(
143+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
144+
datefmt='%Y-%m-%d %H:%M:%S'
145+
)
146+
handler.setFormatter(formatter)
147+
self._log.addHandler(handler)
148+
149+
def _wait_for_reply(self, timeout=None):
150+
buffers = b""
151+
timeout = timeout or self._timeout
152+
start_time = time.perf_counter()
153+
while time.perf_counter() - start_time < timeout:
154+
if self._serial_port.in_waiting <= 0:
155+
continue
156+
buffers = self.read(self._serial_port.in_waiting)
157+
if not Command.has_header(buffers):
158+
continue
159+
break
160+
161+
return Command.parsing(buffers) if Command.has_header(buffers) else None
162+
163+
def _merge(self, genre, address, *parameters, has_reply=False):
164+
command = Command.packing(genre, address, *parameters)
165+
self._log.debug(f"write > {command}")
166+
with self._mutex:
167+
self.write(command.to_bytes())
168+
169+
if not has_reply:
170+
return
171+
172+
reply_command = self._wait_for_reply(timeout=0.07) # WaitForAReply
173+
self._log.debug(f"read < {reply_command}")
174+
if not reply_command:
175+
return None
176+
177+
result = reply_command.get_params()
178+
if reply_command.genre == CommandGenre.READ_FIRMWARE_VERSION:
179+
return result / 10
180+
return result
181+
182+
def set_motor_direction(self, direction, motor_model=MotorModel.STEPPER_MOTOR_57):
183+
"""Modify the direction of movement of the conveyor belt"""
184+
if direction not in (0, 1):
185+
raise ValueError("direction must be 0 or 1")
186+
self._merge(CommandGenre.SET_SERVO_DIRECTION, motor_model, direction)
187+
188+
def get_motor_direction(self, motor_model=MotorModel.STEPPER_MOTOR_57):
189+
"""Get the direction of movement of the conveyor belt"""
190+
return self._merge(CommandGenre.GET_SERVO_DIRECTION, motor_model, has_reply=True)
191+
192+
def get_motor_speed(self, motor_model=MotorModel.STEPPER_MOTOR_57):
193+
"""Get the speed of the conveyor belt"""
194+
return self._merge(CommandGenre.GET_SERVO_SPEED, motor_model, has_reply=True)
195+
196+
def set_motor_speed(self, status, speed: int, motor_model=MotorModel.STEPPER_MOTOR_57):
197+
"""Modify the speed of the conveyor belt"""
198+
if status not in (0, 1):
199+
raise ValueError("status must be 0 or 1")
200+
201+
if not 0 <= speed <= 100:
202+
raise ValueError("speed must be in range [0, 100]")
203+
return self._merge(CommandGenre.SET_SERVO_SPEED, motor_model, status, speed)
204+
205+
# def write_motor_angle(self, carry, angle: int, speed, motor_model=MotorModel.STEPPER_MOTOR_57):
206+
# return self._merge(CommandGenre.WRITE_SERVO_ANGLE, motor_model, carry, angle, speed)
207+
#
208+
# def write_motor_step(self, carry, step: int, speed, direction, motor_model=MotorModel.STEPPER_MOTOR_57):
209+
# return self._merge(CommandGenre.WRITE_SERVO_STEP, motor_model, carry, step, speed, direction)
210+
211+
def read_firmware_version(self, motor_model=MotorModel.STEPPER_MOTOR_57):
212+
"""Get the firmware version of the conveyor belt"""
213+
return self._merge(CommandGenre.READ_FIRMWARE_VERSION, motor_model, has_reply=True)
214+

0 commit comments

Comments
 (0)