10. MaxArm Serial Communication

10.1 Principle of Device Master-Slave Communication

10.1.1 Preface

This chapter aims to introduce the detailed information about the master-slave relationship during the communication between MaxArm and various devices such as STM32, 51 microcontroller, Arduino, and Raspberry Pi, which can help users to master how MaxArm functions as a slave device to communicate with other devices and how other devices act as master controllers to control MaxArm.

Throughout this chapter, MaxArm serves as a slave device, communicating with other devices via UART serial communication for data transmission.

10.1.2 Master-Slave Relationship

  • The Functions of MaxArm as the Slave Device

(1) Receive and parse the received signal from the master:

Waiting for serial port signals, if the data received from the serial port is not none, the serial data is parsed according to the communication protocol, and the corresponding functions can be invoked based on the data information.

(2) Call the functionalities of MaxArm according to the received data.

When the signals are parsed, the corresponding MaxArm requires to be invoked. For example, the motion control of bus servo, the motion control of the PWM servo at the end-effector, the working mode of the suction cup at the end-effector.

(3) Data encapsulation and feedback:

When a read command is received, the corresponding read function needs to be invoked. After reading the data, it is encapsulated into a data packet according to the communication protocol and send to the master device.

  • Other Devices as the Masters

(1) Command Encapsulation and Transmission:

The master needs to encapsulate control commands and data into data packets according to the communication protocol and send them to the device.

(2) Control Coordination :

The master needs to manage the overall system’s collaborative operation, ensuring the smooth communication and operation between MaxArm and other devices, maintaining a goods working state.

(3) Receive Data:

When the master reads the servo status,it sends a read command and then receives status information send by MaxArm to ensure the integrity and correctness of the data. Consequently, it parses the data packet to extract the useful information.

10.1.3 Hardware Wiring

Here will use an example of MaxArm connected to a PC:

  • Connect the RXD, TXD, GND of USB adapter to IO32, IO33, GND ports of ESP32 expansion board respectively using Dupont wires.

(1) Connect the USB adapter into your computer.

10.1.4 Data Transmission Format

The default UART serial port data transmission format for MaxArm is as follow:

Baud rate 9600
Data bits 8
Parity bit None
Stop bit 1

10.1.5 Communication Protocol

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

Frame header: when the frame header is received, it indicates data is transmitting. The fixed frame header length is 2 bytes.

Function code: Used to indicate the purpose of an information frame.

Data length: Indicate the amount of the subsequent data.

Data information: Represents the transmitted data.

Parity bit: (Function Code + Data Length + Data) is negated and the low byte is taken.

10.1.6 Notice

  • The power source for the master and MaxArm robotic arm can be different, but they must share a common ground when connected to ensure the stable communication voltage levels.

  • When connecting devices, please note that the TX and RX pins of UART serial port must be crossed, otherwise communication will not be possible.

10.2 Underlying File Analysis

10.2.1 Slave End (MaxArm)

  • Low-level Parsing at the Slave End (Arduino Version)

Source Code

This lesson will explain the low-level program of MaxArm’s slave-side communication control functions, analyzing MaxArm’s reception of data from other devices, parsing the data to control MaxArm, and implementing the functionality to send data to other devices.

(1) Communication Connection

① Hardware Connection

MaxArm’s serial communication supports two interfaces, one is the 4-pin interface, suitable for devices with pin interfaces for UART communication, commonly used with Arduino development boards and STM32 development board. The other interface is the micro-USB interface, suitable for devices with USB master interfaces, such as Raspberry Pi, Jetson Nano and others.

The communication connection for the micro-USB interface is as follow:

The communication connection for 4Pin interfaces is as follow:

② Communication Protocol

The communication protocol for both the master and slave devices in the MaxArm communication routines follows the following format:

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

The annotations of each part of the protocol are as follows:

Frame header: if 0xAA and 0x55 are received sequentially, it indicates that there is data to be received, consisting of a fixed 2 bytes.

Function Code: Used to indicate the purpose of an information frame, consists of 1 byte.

Data Length: Indicates the number of data bits carried by the data frame.

Check Bit: Verifies the correctness of the data frame. If correct, the corresponding function is called; otherwise, the data frame is skipped.

The calculation method for the check bit is: calculate the sum of the function code, data length, and data, then take the complement, and finally, take the low byte, which serves as the checksum.

③ Functions and Corresponding Function Code Instructions

Function Name Instruction Function Code
FUNC_SET_ANGLE Set servo angle 0x01
FUNC_SET_XYZ Set the robotic arm coordinate position 0x03
FUNC_SET_PWMSERVO Control PWM servo for nozzle 0x05
FUNC_SET_SUCTIONNOZZLE Control the air pump state 0x07
FUNC_READ_ANGLE Control servo angle 0x11
FUNC_READ_XYZ Read robotic arm coordinate position 0x13

(2) Program Interface Parsing

The routines analyzed in this document are located in “Communication Routine Low-Level Files (Arduino Version)->MaxArm_Arduino_microUSB” in the same directory. The routines for communication using the micro-USB interface are provided, and similar routines for communication using the 4-pin interface are also available, with the difference being the use of different IO ports for communication. This section primarily analyzes the files “PC_rec.h” and “PC_rec.c”, which are the implementation files for the slave-level communication of MaxArm.

In the “MaxArm_Arduino_microUSB.ino” file, which serves as the entry point for the Arduino program, the program flow is as follows:

① Create a Serial Communication Control Object

Create a serial communication object globally, as shown in the following figure:

7
PC_REC pc_rec;

The constructor of the object is invoked during its creation to initialize the serial port baud rate to 9600. This is implemented in the “PC_rec.c” file as shown below:

17
18
19
20
PC_REC::PC_REC(void)
{
  Serial.begin(9600);
}

② Initialize MaxArm Control Interface

In the void setup() function, the buzzer, bus servo interface, end effector interface, PWM servo interface, etc., are initialized, and the robotic arm is reset, as shown in the following figure:

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void setup() {
  Serial.begin(115200);
  pc_rec.begin();
  // 初始化
  Buzzer_init(); //蜂鸣器
  ESPMax_init(); //总线舵机
  Nozzle_init(); //吸嘴
  PWMServo_init(); //PWM舵机
  Valve_on(); //
  go_home(2000);
  Valve_off();
  delay(100);
  SetPWMServo(1, 1500, 1000);
  Serial.println("start...");
}

③ Receiving, Parsing, and Controlling

In the void loop() function, continuously call the rec_data() function in a loop to wait for serial port information reception and parse the information to control the MaxArm robotic arm.

The void rec_data(void) function is implemented in the “PC_rec.c” file. Its function is to receive serial port information, parse the serial port information, and control MaxArm or return the read information based on the information. The function flow is as follows: Check if data is received on the serial port. If so, copy the data to the pk_ctl.data[] array.

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  uint32_t len = Serial.available();
  while(len--)
  {
    int rd = Serial.read();
    pk_ctl.data[pk_ctl.index_tail] = (char)rd;
    pk_ctl.index_tail++;
    if(BUFFER_SIZE <= pk_ctl.index_tail)
    {
      pk_ctl.index_tail = 0;
    }
    if(pk_ctl.index_tail == pk_ctl.index_head)
    {
      pk_ctl.index_head++;
      if(BUFFER_SIZE <= pk_ctl.index_head)
      {
        pk_ctl.index_head = 0;
      }
    }else{
      pk_ctl.len++;
    }
  }

Iterate through the bytes of data in pk_ctl.data[], parse the data according to the communication protocol, and save the correctly received function code, data, etc., in the pk_ctl.frame structure variable. Part of the program is as follows:

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
  uint8_t crc = 0;
  //Parse data (解析数据)
  while(pk_ctl.len > 0)
  {
    switch(pk_ctl.state)
    {
      case STATE_STARTBYTE1: /* Handle start byte 1 (处理帧头标记1) */
        pk_ctl.state = CONST_STARTBYTE1 == pk_ctl.data[pk_ctl.index_head] ? STATE_STARTBYTE2 : STATE_STARTBYTE1;
        break;
      case STATE_STARTBYTE2: /*Handle start byte 2 (处理帧头标记2)*/
        pk_ctl.state = CONST_STARTBYTE2 == pk_ctl.data[pk_ctl.index_head] ? STATE_FUNCTION : STATE_STARTBYTE1;
        break;
      case STATE_FUNCTION: /* Handle frame function (处理帧功能号) */
        pk_ctl.state = STATE_LENGTH;
        if(FUNC_SET_ANGLE != pk_ctl.data[pk_ctl.index_head])
          if(FUNC_SET_XYZ != pk_ctl.data[pk_ctl.index_head])
            if(FUNC_SET_PWMSERVO != pk_ctl.data[pk_ctl.index_head])
              if(FUNC_SET_SUCTIONNOZZLE != pk_ctl.data[pk_ctl.index_head])
                if(FUNC_READ_ANGLE != pk_ctl.data[pk_ctl.index_head])
                  if(FUNC_READ_XYZ != pk_ctl.data[pk_ctl.index_head])
                  {
                    pk_ctl.state = STATE_STARTBYTE1;
                  }
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
      case STATE_LENGTH: /* Handle frame data length (处理帧数据长度) */
        if(pk_ctl.data[pk_ctl.index_head] >= DATA_SIZE) //If the (specific) information data length > DATA_SIZE, then there is an issue(若(包含具体信息)信息数据长度>DATA_SIZE,则有问题)
        {
          pk_ctl.state = STATE_STARTBYTE1;
          continue;
        }else{
          pk_ctl.frame.data_length = pk_ctl.data[pk_ctl.index_head];
          pk_ctl.state = (0 == pk_ctl.frame.data_length) ? STATE_CHECKSUM : STATE_DATA;
          pk_ctl.data_index = 0;
          break;
        }
      case STATE_DATA: /* Handle frame data (处理帧数据) */
        pk_ctl.frame.data[pk_ctl.data_index] = pk_ctl.data[pk_ctl.index_head];
        ++pk_ctl.data_index;
        if(pk_ctl.data_index >= pk_ctl.frame.data_length) {
            pk_ctl.state = STATE_CHECKSUM;
            pk_ctl.frame.data[pk_ctl.data_index] = '\0';
        }
        break;

Compare the received checksum value with the calculated checksum value to determine whether the received data frame is correct. If received correctly, pass the parsed content to the deal_command() function, as shown in the figure:

 99
100
101
102
103
104
105
106
107
        pk_ctl.frame.checksum = pk_ctl.data[pk_ctl.index_head];
        crc = checksum_crc8((uint8_t*)&pk_ctl.frame.function, pk_ctl.frame.data_length + 2);
        // Serial.println(crc);
        if(crc == pk_ctl.frame.checksum) { /* If checksum fails, skip execution (校验失败, 跳过执行) */
            deal_command(&pk_ctl.frame); //Process data (处理数据)
        }
        memset(&pk_ctl.frame, 0, sizeof(struct PacketRawFrame)); //Clear frame (清除)
        pk_ctl.state = STATE_STARTBYTE1;
        break;

The deal_command() function selects the corresponding function to execute based on the function code of the incoming information. The parsing programs for each function are as follows:

  • For the “Set Angle” function, the incoming bytes are converted into three angle values and one time value. Then, each of the three bus servos is controlled to move according to the specified angles.

127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
    case FUNC_SET_ANGLE: //Set angle 0x01 (设置角度 0x01)
      {
        Angle_Ctl_Data msg;
        if(len == 8)
        {
          memcpy(&msg , ctl_com->data , sizeof(msg));
          set_servo_in_range(1,msg.pul[0],msg.time);
          delay(2);
          set_servo_in_range(2,msg.pul[1],msg.time);
          delay(2);
          set_servo_in_range(3,msg.pul[2],msg.time);
          delay(2);
        }
      }
      break;
  • For the “Set XYZ Axis Coordinates” function, the incoming bytes are converted into spatial coordinate values and one time value. Then, the coordinate setting interface is called to set the end-point angle coordinates of MaxArm.

142
143
144
145
146
147
148
149
150
151
152
    case FUNC_SET_XYZ: //Set xyz axis 0x03 (设置xyz轴 0x03)
      {
        XYZ_Ctl_Data msg;
        if(len == 8)
        {
          memcpy(&msg , &ctl_com->data , sizeof(msg));
          float p[3] = {msg.pos[0],msg.pos[1],msg.pos[2]};
          set_position(p , msg.time);
        }
      }
      break;
  • When setting the pulse width of the end-point PWM servo, the incoming byte data is converted into PWM pulse width and time values. Subsequently, the PWM servo setting function is called to set the motion of the PWM servo.

155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
    case FUNC_SET_PWMSERVO: //Set PWM servo 0x05 (设置PWM舵机 0x05)
      {
        PWM_Ctl_Data msg;
        if(len == 4)
        {
          memcpy(&msg , &ctl_com->data , sizeof(msg));
          SetPWMServo(1, msg.pul, msg.time);
        }
      }
      break;    case FUNC_SET_PWMSERVO: //Set PWM servo 0x05 (设置PWM舵机 0x05)
      {
        PWM_Ctl_Data msg;
        if(len == 4)
        {
          memcpy(&msg , &ctl_com->data , sizeof(msg));
          SetPWMServo(1, msg.pul, msg.time);
        }
      }
      break;
  • When setting the end effector nozzle function, the incoming bytes are converted into sub-function codes, and based on the sub-function code, the state of the nozzle is set.

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
    case FUNC_SET_SUCTIONNOZZLE: //Set suction nozzle 0x07 (设置吸嘴 0x07)
      {
        SN_Ctl_Data msg;
        if(len == 1)
        {
          memcpy(&msg , &ctl_com->data , sizeof(msg));
          switch(msg.cmd)
          {
            case 1:
              Pump_on();
              break;
            case 2:
              Valve_on();
              break;
            case 3:
              Valve_off();
              break;
          }
        }
      }
      break;
  • When reading angle values, the read_angles() function is called, and the read angle values are sent to the host device via the serial port.

128
129
130
131
132
133
134
135
136
137
138
    case FUNC_READ_ANGLE: //Read angles 0x11 (读取角度 0x11)
      {
        read_Angle_Data msg;
        int16_t angles[3];
        read_angles(angles);
        uint8_t send_data[20] = {0xAA , 0x55 , 0x11 , 0x06};
        memcpy(&send_data[4] , angles , 6);
        send_data[10] = checksum_crc8(&send_data[2] , 8);
        Serial.write(send_data , 11);
      }
      break;
  • When reading XYZ values, the read_position() function is called, and the read coordinate values are sent to the host device via the serial port.

200
201
202
203
204
205
206
207
208
209
210
    case FUNC_READ_XYZ: //Read xyz axis 0x13 (读取xyz轴 0x13)
      {
        float pos_f[3];
        read_position(pos_f);
        int16_t pos[3] = {(int16_t)pos_f[0] , (int16_t)pos_f[1] , (int16_t)pos_f[2]};
        uint8_t send_data[20] = {0xAA , 0x55 , 0x13 , 0x06};
        memcpy(&send_data[4] , pos , 6);
        send_data[10] = checksum_crc8(&send_data[2] , 8);
        Serial.write(send_data , 11);
      }
      break;
  • Low-level Parsing at the Slave End (MicroPython Version)

Source Code

The lesson will explain the MicroPython version of low-level program of MaxArm’s slave-side communication control functions, analyzing MaxArm’s reception of data from other devices, parsing the data to control MaxArm, and implementing the functionality to send data to other devices.

(1) Communication Connection

① Hardware Connection

MaxArm’s serial communication supports two interfaces, one is the 4-pin interface, suitable for devices with pin interfaces for UART communication, commonly used with Arduino development boards and STM32 development board. The other interface is the micro-USB interface, suitable for devices with USB master interfaces, such as Raspberry Pi, Jetson Nano and others.

The communication connection for the micro-USB interface is as follow:

The communication connection for 4Pin interfaces is as follow:

② Communication Protocol

The communication protocol for both the master and slave devices in the MaxArm communication routines follows the following format:

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

The annotations of each part of the protocol are as follows:

Frame header: if 0xAA and 0x55 are received sequentially, it indicates that there is data to be received, consisting of a fixed 2 bytes.

Function Code: Used to indicate the purpose of an information frame, consists of 1 byte.

Data Length: Indicates the number of data bits carried by the data frame.

Check Bit: Verifies the correctness of the data frame. If correct, the corresponding function is called; otherwise, the data frame is skipped.

The calculation method for the check bit is: calculate the sum of the function code, data length, and data, then take the complement, and finally, take the low byte, which serves as the checksum.

③ Functions and Corresponding Function Code Instructions

Function Name Instruction Function Code
FUNC_SET_ANGLE Set servo angle 0x01
FUNC_SET_XYZ Set the robotic arm coordinate position 0x03
FUNC_SET_PWMSERVO Control PWM servo for nozzle 0x05
FUNC_SET_SUCTIONNOZZLE Control the air pump state 0x07
FUNC_READ_ANGLE Control servo angle 0x11
FUNC_READ_XYZ Read robotic arm coordinate position 0x13

(2) Program Interface Parsing

The routines analyzed in this document are located in “Communication Routine Low-Level Files (MicroPython Version) File->MaxArm_microPython_microUSB” in the same directory. This lesson will primary analyze the “MaxArm_ctl.py” file, which is the program file implementing the underlying communication of the MaxArm robot’s slave device.

① Constructor

def _init_(self) is the constructor of MaxArm message receiving class, where a protocol parser, a buffer for receiving data, a bus servo control object, and a MaxArm robotic arm control object are created, and initializes the protocol parser to start parsing from the first byte of the data packet.

82
83
84
85
86
87
88
  # Constructor (构造函数)
  def __init__(self):
    self.__pk_ctl = PacketController()
    self.data = []
    self.bus_servo = BusServo()
    self.arm = ESPMax(self.bus_servo)
    self.__pk_ctl.state = PacketControllerState.STARTBYTE1

Communication Protocol Format Enumeration

This class mainly defines 6 constants, corresponding to the frame header, function code, data length, data and check bit of the communication protocol respectively. These constants are sued to provide parsing status when parsing the data packets.

34
35
36
37
38
39
40
41
42
# Format enumeration of communication protocol (通信协议的格式枚举)
class PacketControllerState:
  # 0xAA 0x55 Length Function Data Checksum
  STARTBYTE1 = const(0)
  STARTBYTE2 = const(1)
  FUNCTION = const(2)
  LENGTH = const(3)
  DATA = const(4)
  CHECKSUM = const(5)

Command Packet Construction

Encapsulate control commands and data into a data packet, and subsequently communicate with the master directly in the form of data packets. In the constructor of this class, all variables created in the format of the communication protocol are initialized to 0x00.

53
54
55
56
57
58
59
60
61
62
# Command packet structure (命令包结构)
class PacketRawFrame:
  def __init__(self):
    self.start_byte1 = 0x00
    self.start_byte2 = 0x00
    self.function = 0x00
    self.data_length = 0x00
    # self.data = [0x00] * 256
    self.data = []
    self.checksum = 0x00

Protocol Parser

Construct variables related to protocol parsing and buffer:

state: Controls the current state of data packet parsing.

frame: An object representing a data packet, used for receiving or sending data packets.

data_index: Records the number of data processed.

len: Length of received data.

Data: buffer for receiving data.

64
65
66
67
68
69
70
71
72
73
74
75
# Protocol parser (协议解析器)
class PacketController:
  def __init__(self):
    # Related variables for parsing the protocol (解析协议的相关变量)
    self.state = PacketControllerState()
    self.frame = PacketRawFrame()
    self.data_index = 0
    self.index = 0
    # Buffer-related variables (缓冲区相关变量)
    self.len = 0
    # data = [0x00] * 256
    self.data = []

② Serial Port Activation Function

The begin () function identifies the serial port to be activated based on the parameter “port” passed to it.

If the value is PORT_FOR_USB, it opens the serial port for MicroUSB, where the corresponding pins are: TX->10, RX->9.

If the value is PORT_FOR_4Pin, it opens the serial port for the 4-pin interface, where the corresponding pins are: TX->33, RX->32.

Finally, it creates an object for the air pump and sets the servo controlling the nozzle to rotate to the position of 1500 (i.e., the central position).

 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
  # Serial port opening function (串口开启函数)
  # If using USB port, use SELECT_PORT.PORT_FOR_USB;If using 4Pin port, use SELECT_PORT.PORT_FOR_4Pin (若接USB口通讯,则用SELECT_PORT.PORT_FOR_USB;用4pin口通讯,则用SELECT_PORT.PORT_FOR_4Pin)
  def begin(self , port):
    if port == SELECT_PORT.PORT_FOR_USB:
      print("begin in USB")
      self.__uart = UART(1, 9600, tx=10, rx=9)
    else:
      print("begin in 4Pin")
      self.__uart = UART(1, 9600, tx=33, rx=32)
    self.nozzle = SuctionNozzle()
    self.arm.go_home(1500)
    time.sleep_ms(2000)

The enumeration for Selecting the Open Serial Port

The value of the parameter “port” can be used to select one of two serial ports for activation, with the corresponding parameter values being:

0x01: Activate the USB serial port

0x03: Activate the serial port for the 4-pin interface

21
22
23
class SELECT_PORT:
  PORT_FOR_USB = const(0x01)
  PORT_FOR_4Pin = const(0x03)

③ Reception and Parsing Function

First, use __uart.read() to read the data packet from the serial port. If a non-empty data packet is received, assign the length of the received data packet to the variable data_len. Then, create two intermediate variables:

index: Index for reading the data packet, used to read the data packet sequentially.

data_index: Index for reading the data part, used to read the data part of the data packet sequentially.

104
105
106
107
108
109
110
  # Receiving and parsing function (接收解析函数)
  def rec_data(self):
    readbuffer = self.__uart.read()
    if readbuffer is not None:
      data_len = len(readbuffer)
      index = 0
      data_index = 0

By using a while loop, start processing the received data packet byte by byte. First, two frame headers are processed. The current part of the data frame being processed is updated based on the state of self.__pk_ctl.state.

If the first data frame being processed is CONST_STARTBYTE1 (that is, 0xAA), indicating it is identified successfully, and then the state is updated to STARTBYTE2 (that is, switching to identify the second frame header). If failing to identify, the first frame header will be identified again.

111
112
113
114
115
116
117
    while (data_len > 0):
        # Process frame header marker 1 (处理帧头标记1)
        if PacketControllerState.STARTBYTE1 == self.__pk_ctl.state:
          if CONST_STARTBYTE1 == readbuffer[index]:
            self.__pk_ctl.state = PacketControllerState.STARTBYTE2
          else:
            self.__pk_ctl.state = PacketControllerState.STARTBYTE1

When recognizing the second frame header, if it is recognized successfully, the state is set as FUNCTION (which is the function code recognition state). If the recognition fails, it goes back to recognize the first frame header.

119
120
121
122
123
124
       # Process frame header marker 2 (处理帧头标记2)
        elif PacketControllerState.STARTBYTE2 == self.__pk_ctl.state:
          if CONST_STARTBYTE2 == readbuffer[index]:
            self.__pk_ctl.state = PacketControllerState.FUNCTION
          else:
            self.__pk_ctl.state = PacketControllerState.STARTBYTE1

If the current state is FUNCTION (e.i. function code recognition state), it enters the function code recognition state. Here, the state will be first updated to LENGTH (e.i. data length recognition state), and then through nested if statements, each function code is recognized one by one. If a valid function code is identified, it is temporarily stored in the variable frame.function. Otherwise, the state is updated back to recognizing the first frame header.

126
127
128
129
130
131
132
133
134
135
136
137
       # Process function code (处理帧功能号)
        elif PacketControllerState.FUNCTION == self.__pk_ctl.state:
          self.__pk_ctl.state = PacketControllerState.LENGTH
          if PACKET_FUNCTION.FUNC_READ_ANGLE != readbuffer[index]:
            if PACKET_FUNCTION.FUNC_READ_XYZ != readbuffer[index]:
              if PACKET_FUNCTION.FUNC_SET_ANGLE != readbuffer[index]:
                if PACKET_FUNCTION.FUNC_SET_PWMSERVO != readbuffer[index]:
                  if PACKET_FUNCTION.FUNC_SET_XYZ != readbuffer[index]:
                    if PACKET_FUNCTION.FUNC_SET_SUCTIONNOZZLE != readbuffer[index]:
                      self.__pk_ctl.state = PacketControllerState.STARTBYTE1
          if self.__pk_ctl.state == PacketControllerState.LENGTH:
            self.__pk_ctl.frame.function = readbuffer[index]

If the current state is LENGTH (i.e., processing the frame header data length state), the data length in data frame is temporarily stored in the variable frame.data_length, then check if the data length is 0. If it is 0, it indicates that the data packet is empty, so the state is directly set as CHEKSUM ( processing checksum bit ), and then it jumps directly to the part of handling the checksum. If the data length is not 0, then set the state to DATA (processing the frame data state), and proceed to handle the data part.

139
140
141
142
143
144
145
146
        # Process data length (处理帧数据长度)
        elif PacketControllerState.LENGTH == self.__pk_ctl.state:
          self.__pk_ctl.frame.data_length = readbuffer[index]
          if 0 == self.__pk_ctl.frame.data_length:
            self.__pk_ctl.state = PacketControllerState.CHECKSUM
          else:
            self.__pk_ctl.state = PacketControllerState.DATA
          data_index = 0

If the current state is DATA, it successfully enters the part of processing the frame data. According to the index variable, start reading the data byte by byte from the first data and store it in the variable frame.data. When data_index (i.e., the actual received data length) is greater than or equal to the sent data length, it indicates that the reception is complete. Then, set the state to CHECKSUM (i.e., processing the checksum state).

148
149
150
151
152
153
        # Process frame data (处理帧数据)
        elif PacketControllerState.DATA == self.__pk_ctl.state:
          self.__pk_ctl.frame.data.append(readbuffer[index])
          data_index += 1
          if data_index >= self.__pk_ctl.frame.data_length:
            self.__pk_ctl.state = PacketControllerState.CHECKSUM

When it comes to processing the checksum state, we first extract the checksum part from the data frame. Then, based on the previously read function code, data length, and the sum of all values in the data part, we calculate the checksum using the checksum_crc8 function in the format of a checksum and assign it to the variable crc.

155
156
157
158
159
160
        # Process checksum (处理校验值)
        elif PacketControllerState.CHECKSUM == self.__pk_ctl.state:
          self.__pk_ctl.frame.checksum = readbuffer[index]
          crc = checksum_crc8(self.__pk_ctl.frame.function , 
                              self.__pk_ctl.frame.data_length , 
                              self.__pk_ctl.frame.data)

If the calculated checksum matches the checksum in the received data frame, then the checksum is successful. We return the frame object to the robotic arm control function for control. After completing these operations, reset the state to the recognition of the first frame header state, and clear all data in the frame object to wait for the next data reception or transmission.

161
162
163
164
165
166
          # If checksum is valid (若校验成功)
          if self.__pk_ctl.frame.checksum == crc:
            self.deal_command(self.__pk_ctl.frame)
          self.__pk_ctl.state = PacketControllerState.STARTBYTE1
          # Clear storage variables (清除存储变量)
          self.__pk_ctl.frame.data.clear()

If an error occurs during execution, reset the state to recognize the first frame header. Finally, after processing the content of a data frame, decrement the data_len variable (remaining data frame length) by one, and increment the index variable (data frame index during processing) by one.

168
169
170
171
172
173
174
        # Runtime error (运行错误)
        else:
          self.__pk_ctl.state = PacketControllerState.STARTBYTE1
          
        # Index processing (下标处理)
        data_len -= 1
        index += 1

④ Robotic Arm Control Function

The deal_command() function has one parameter: ctl_com, which is an object representing a data packet in the same format as the communication protocol. It is used to control the robotic arm. At the beginning of the function, the length of the data in the packet is obtained to facilitate the subsequent data reading.

180
181
182
183
184
  ''' 
  User function (用户使用函数)
  '''
  def deal_command(self , ctl_com):
    len = ctl_com.data_length

Then, based on the function code in the data packet, the function determines the functionality that the current data packet implements.

If the recognized function code is invalid, the function passes and exits.

Set Servo Control Angle

If the data length is 8, it is considered valid data. The function reads the angles of the three servos from the data packet using the angles array. In the ctl_com.data, indices 0, 2, 4 represent the low 8-bit values of the angles for each servo, while indices 1, 3, 5 represent the high 8-bit values of the angles for each servo. Indices 6 and 7 represent the low 8-bit and high 8-bit values of the servo rotation time, respectively.

Finally, the arm.set_servo_in_range() function is called to control the servos to rotate to the corresponding positions based on the read angles and time.

182
183
184
185
186
187
188
189
190
191
192
193
194
195
    if ctl_com.function == PACKET_FUNCTION.FUNC_SET_ANGLE:
      # print("FUNC_SET_ANGLE")
      if len == 8:
        angles = []
        angles.append((ctl_com.data[0] & 0x00FF) | ((ctl_com.data[1] << 8 ) & 0xFF00))
        angles.append((ctl_com.data[2] & 0x00FF) | ((ctl_com.data[3] << 8 ) & 0xFF00))
        angles.append((ctl_com.data[4] & 0x00FF) | ((ctl_com.data[5] << 8 ) & 0xFF00))
        time_count = (ctl_com.data[6] & 0x00FF) | ((ctl_com.data[7] << 8 ) & 0xFF00)
        self.arm.set_servo_in_range(1 , angles[0] , time_count)
        time.sleep_ms(10)
        self.arm.set_servo_in_range(2 , angles[1] , time_count)
        time.sleep_ms(10)
        self.arm.set_servo_in_range(3 , angles[2] , time_count)
        time.sleep_ms(10)

Control Servo by Setting Coordinates

Convert the data at indices 0 to 6 of ctl_com.data to spatial coordinate values, then combine the data at indices 6 and 7 into a 16-bit runtime, and control the robotic arm to rotate in the spatial coordinate system using the set_position() function.

197
198
199
200
201
202
203
    elif ctl_com.function == PACKET_FUNCTION.FUNC_SET_XYZ:
      # print("FUNC_SET_XYZ")
      if len == 8:
        xyz = bytes(ctl_com.data[:7])
        time_count = (ctl_com.data[6] & 0x00FF) | ((ctl_com.data[7] << 8 ) & 0xFF00)
        unpacked_data = struct.unpack('<hhh', xyz) 
        self.arm.set_position(unpacked_data , time_count)

Control PWM Servo on Nozzle

The data length for controlling PWM servos in the data packet is only 4. In ctl_com.data, indices 0 and 1 contain the low 8-bit and high 8-bit values of the servo rotation angle, respectively, while indices 2 and 3 contain the low 8-bit and high 8-bit values of the timing time. Finally, the data is output to the PWM servo using the nozzle.set_pwmservo_pul() function.

205
206
207
208
209
210
    elif ctl_com.function == PACKET_FUNCTION.FUNC_SET_PWMSERVO:
      # print("FUNC_SET_PWMSERVO")
      if len == 4:
        pul = (ctl_com.data[0] & 0x00FF) | ((ctl_com.data[1] << 8 ) & 0xFF00)
        time_count = (ctl_com.data[2] & 0x00FF) | ((ctl_com.data[3] << 8 ) & 0xFF00)
        self.nozzle.set_pwmservo_pul(pul , time_count)

Control Air Pump

We can categorize the pump into three states:

State 1: Pump on, in this state, the pump starts suction. Corresponding control command: nozzle.on_uart().

State 2: Pump off while opening the air valve to release air. In this state, the pump releases air. Corresponding control command: nozzle.on_uart1().

State 3: Close the air valve, and the pump is completely powered off. Corresponding control command: nozzle.on_uart2().

212
213
214
215
216
217
218
219
220
    elif ctl_com.function == PACKET_FUNCTION.FUNC_SET_SUCTIONNOZZLE:
      # print("FUNC_SET_SUCTIONNOZZLE")
      if len == 1:
        if ctl_com.data[0] == 1:
          self.nozzle.on_uart()
        elif ctl_com.data[0] == 2:
          self.nozzle.off_uart_1()
        elif ctl_com.data[0] == 3:
          self.nozzle.off_uart_2()

Read Servo Angle

When the function code is for reading servo angles, the arm.read_angle() function is used to read the positions of the servos. Then, the data is encapsulated into a data packet format according to the communication protocol, and this data packet is returned to the host.

222
223
224
225
226
227
228
229
    elif ctl_com.function == PACKET_FUNCTION.FUNC_READ_ANGLE:
      # print("FUNC_READ_ANGLE")
      angles = self.arm.read_angles()
      send_data = bytearray([0xAA,0x55,0x11,0x06])
      send_data += struct.pack('<hhh', angles[0] , angles[1] , angles[2])
      check_num = checksum_crc8(0,0,send_data)
      send_data.append(check_num)
      self.__uart.write(send_data)

Read Robotic Arm Coordinate Position

When the recognized function code is for reading the position of the robotic arm in the coordinate system, we use the arm.read_position() function to read the position in the coordinate system. Then, we encapsulate it into a data packet format and send it back to the master.

231
232
233
234
235
236
237
238
    elif ctl_com.function == PACKET_FUNCTION.FUNC_READ_XYZ:
      # print("FUNC_READ_ANGLE")
      (x, y, z) = self.arm.read_position()
      send_data = bytearray([0xAA,0x55,0x13,0x06])
      send_data += struct.pack('<hhh', x, y, z)
      check_num = checksum_crc8(0,0,send_data)
      send_data.append(check_num)
      self.__uart.write(send_data)

10.2.2 Master End

  • Arduino Development Board Communication Routine Analysis

Source Code

This lesson explains the Arduino-based low-level program for communication control as the host, analyzing the implementation of Arduino’s encapsulation and transmission of data to MaxArm.

(1) Communication Connection

① Hardware Wiring

MaxArm has two types of serial communication interfaces: one is the 4-pin interface, which is suitable for devices with pin interfaces for UART communication, commonly used for Arduino development boards, STM32 development boards. The other is the microUSB interface, suitable for devices with USB host interfaces, such as Raspberry Pi, Jetson Nano, etc. Here, we need to connect to MaxArm’s 4-pin interface for communication.

The communication connection for 4Pin interfaces is as follow:

The specific pin connections are as follow:

MaxArm’s IO32 pin serves as the RX signal pin, connected to pin 6 of the Arduino development board.

MaxArm’s IO33 pin serves as the TX signal pin, connected to pin 7 of the Arduino development board.

MaxArm’s GND pin is connected to the GND pin of the Arduino development board.

② Communication Protocol

The communication protocol for both the master and slave devices in the MaxArm communication routines follows the following format:

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

The annotations of each part of the protocol are as follows:

Frame header: if 0xAA and 0x55 are received sequentially, it indicates that there is data to be received, consisting of a fixed 2 bytes.

Function Code: Used to indicate the purpose of an information frame, consists of 1 byte.

Data Length: Indicates the number of data bits carried by the data frame.

Check Bit: Verifies the correctness of the data frame. If correct, the corresponding function is called; otherwise, the data frame is skipped.

The calculation method for the check bit is: calculate the sum of the function code, data length, and data, then take the complement, and finally, take the low byte, which serves as the checksum.

③ Functions and Corresponding Function Code Instructions

Function Name Instruction Function Code
FUNC_SET_ANGLE Set servo angle 0x01
FUNC_SET_XYZ Set the robotic arm coordinate position 0x03
FUNC_SET_PWMSERVO Control PWM servo for nozzle 0x05
FUNC_SET_SUCTIONNOZZLE Control the air pump state 0x07
FUNC_READ_ANGLE Control servo angle 0x11
FUNC_READ_XYZ Read robotic arm coordinate position 0x13

(2) Program Interface Parsing

The example routine analyzed in this document, demonstrated in the “arduino_4Pin_MaxArm” project in the same directory, primarily focuses on the “MaxArm_ctl.h” and “MaxArm_ctl.c” files. Their main purpose is to encapsulate and send control information to MaxArm.

① Instantiate Serial Port

In MaxArm_ctl.h file, define rxPin and txPin intefaces as 7 and 6 respectively.

15
16
#define rxPin 7      //Serial port between Arduino and MaxArm (Arduino与MaxArm通讯串口)
#define txPin 6  

In the MaxArm_ctl.cpp file, instantiate a software serial port and set the communication baud rate to 9600.

11
static SoftwareSerial SerialM(rxPin, txPin);  //Instantiate software serial (实例化软串口)
31
32
33
34
void MaxArm_ctl::serial_begin(void)
{
  SerialM.begin(9600);
}

② Calculate Checksum and Function

The function checksum_crc8() takes two parameters: *buf, which represents the data to be sent, and len, which indicates the length of the data. It calculates the checksum by summing up the values of the data bytes and adding the length of the data. Then, it takes the bitwise complement of the sum and extracts the lower 8 bits to obtain the checksum value.

14
15
16
17
18
19
20
21
22
23
/* CRC check (CRC校验)  */
static uint16_t checksum_crc8(const uint8_t *buf, uint16_t len)
{
    uint8_t check = 0;
    while (len--) {
        check = check + (*buf++);
    }
    check = ~check;
    return ((uint16_t) check) & 0x00FF;
}

③ Encapsulate Data Packet

Set Servo Angle

The variable msg[20] represents the encapsulated data packet. First, the values of CONST_STARTBYTE1 and CONST_STARTBYTE2 are added as the frame header to the first two positions of the msg variable. The value of CONST_STARTBYTE1 is 0xAA, and the value of CONST_STARTBYTE2 is 0x55. Then, the servo function code and data length are added to the data packet (the function code for this functionality is FUNC_SET_ANGLE).

42
43
44
45
  uint16_t pul[3];
  uint8_t msg[20] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_ANGLE;
  msg[3] = 8;

Map the angles of each servo from the range of 0 to 240 to the range of 0 to 1000 using map() function, and add them to the data packet. Then, split the servo runtime into two bytes in low-high order and add them to the data packet. Finally, add the checksum to the data packet.

46
47
48
49
50
51
52
53
54
55
  pul[0] = angles[0] > 240 ? 240 : angles[0];
  pul[1] = angles[1] > 240 ? 240 : angles[1];
  pul[2] = angles[2] > 240 ? 240 : angles[2];
  pul[0] = map(pul[0] , 0 , 240 , 0 , 1000);
  pul[1] = map(pul[1] , 0 , 240 , 0 , 1000);
  pul[2] = map(pul[2] , 0 , 240 , 0 , 1000);
  memcpy(&msg[4] , pul , 6);
  msg[10] = time & 0x00ff;
  msg[11] = (time>>8)&0x00FF;
  msg[12] = checksum_crc8((uint8_t*)&msg[2] , 10);

The encapsulation of a data packet is now complete. By calling the write() function, the data packet can be sent via the serial port to the slave device.

56
  SerialM.write((char*)&msg , 13);

Set Servo Angle

The set_xyz() function takes two parameters: *pos, which represents the data value to be sent, and time, which indicates the time for controlling the servo rotation.

59
60
61
62
63
64
65
66
67
68
69
70
//Set xyz (设置xyz)
void MaxArm_ctl::set_xyz(int16_t* pos , uint16_t time)
{
  uint8_t msg[20] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_XYZ;
  msg[3] = 8;
  memcpy(&msg[4] , pos , 6);
  msg[10] = time & 0x00ff;
  msg[11] = (time>>8)&0x00FF;
  msg[12] = checksum_crc8((uint8_t*)&msg[2] , 10);
  SerialM.write((char*)&msg , 13);
}

In this function, first, the frame header, function code, and data length are stored in the data packet. Specifically, the function code is FUNC_SET_XYZ.

62
63
64
  uint8_t msg[20] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_XYZ;
  msg[3] = 8;

Copy the position of the coordinate system to the data packet, and simultaneously add the servo runtime (time) and the checksum_crc8 checksum to the data packet.

65
66
67
68
  memcpy(&msg[4] , pos , 6);
  msg[10] = time & 0x00ff;
  msg[11] = (time>>8)&0x00FF;
  msg[12] = checksum_crc8((uint8_t*)&msg[2] , 10);

Finally, use the write function to send the 13-byte data packet via the serial port to the slave device.

69
  SerialM.write((char*)&msg , 13);

Set End-effector Servo Angle

The set_pwmservo() function has two parameters: angle for the servo rotation angle, and time for the servo rotation time.

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//Set end servo angle 0~180 degrees (设置末端舵机角度 0~180度)
void MaxArm_ctl::set_pwmservo(uint8_t angle , uint16_t time)
{
  uint16_t pul = angle > 180 ? 180 : angle;
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_PWMSERVO;
  msg[3] = 4;
  pul = map(pul , 0 , 180 , 500, 2500);
  msg[4] = pul & 0x00ff;
  msg[5] = (pul>>8)&0x00FF;
  msg[6] = time & 0x00ff;
  msg[7] = (time>>8)&0x00FF;
  msg[8] = checksum_crc8((uint8_t*)&msg[2] , 6);
  SerialM.write((char*)&msg , 9);
}

First, add the frame header, function code, and data length to the data packet.

76
77
78
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_PWMSERVO;
  msg[3] = 4;

Limit the servo angle to a valid range, where if the servo angle is greater than 180 degrees, it is set to 180 degrees.

75
  uint16_t pul = angle > 180 ? 180 : angle;

Map the servo angle from the range of 0 to 180 to the range of 500 to 2500. Then, store the servo angle and runtime in the data packet in the order of low 8 bits and high 8 bits. Finally, store the checksum in the data packet.

79
80
81
82
83
84
  pul = map(pul , 0 , 180 , 500, 2500);
  msg[4] = pul & 0x00ff;
  msg[5] = (pul>>8)&0x00FF;
  msg[6] = time & 0x00ff;
  msg[7] = (time>>8)&0x00FF;
  msg[8] = checksum_crc8((uint8_t*)&msg[2] , 6);

Finally, use the write function to send the 9-byte data packet via the serial port to the slave device.

85
  SerialM.write((char*)&msg , 9);

Set Nozzle Function

The set_SuctionNozzle() function has one parameter, func, which represents the status of the suction nozzle. There are three states:

State 1: Turn on the air pump, it starts suction.

State 2: Turn off the air pump and open the air valve to release air, the pump is in the exhaust state.

State 3: Close the air valve, the pump is completely powered off.

The function encapsulates the air pump status into a data packet in the format of the communication protocol, including frame header, function code, data length, data, and checksum. The function code is FUNC_SET_SUCTIONNOZZLE.

 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
//Set suction nozzle function (设置喷嘴功能)
void MaxArm_ctl::set_SuctioNnozzle(int func)
{
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_SET_SUCTIONNOZZLE;
  msg[3] = 1;
  // memcpy(&msg[4] , pul , 2);
  if(func < 0 && func > 3)
  {
    Serial.println("set_SuctioNnozzle ERROR");
    return;
  }
  msg[4] = func;
  msg[5] = checksum_crc8((uint8_t*)&msg[2] , 3);
  SerialM.write((char*)&msg , 6);
}

Read Angle

The read_angles() function takes one parameter, *angle, which stores the servo angles returned from the slave device. This function is mainly used to send a command to the slave device to read the angles, receive and parse the data from the slave device, and then store the parsed servo angle values in the angle variable.

174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Read angles (读取角度)
bool MaxArm_ctl::read_angles(int* angles)
{
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_READ_ANGLE;
  msg[3] = 0;
  msg[4] = checksum_crc8((uint8_t*)&msg[2] , 2);
  while(SerialM.available())
  {
    SerialM.read();
  }
  SerialM.write((char*)&msg , 5);
  uint16_t count = 3;
  delay(300);
  int16_t res[3];
  if(rec_handle((uint8_t*)res , 0x11))
  {
    angles[0] = map(res[0] , 0 , 1000 , 0 , 240);
    angles[1] = map(res[1] , 0 , 1000 , 0 , 240);
    angles[2] = map(res[2] , 0 , 1000 , 0 , 240);
    return true;
  }
  return false;
}

Here, a data packet with a function code of FUNC_READ_ANGLE is first sent, with a data length of 0.

177
178
179
180
181
182
183
184
185
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_READ_ANGLE;
  msg[3] = 0;
  msg[4] = checksum_crc8((uint8_t*)&msg[2] , 2);
  while(SerialM.available())
  {
    SerialM.read();
  }
  SerialM.write((char*)&msg , 5);

After sending the data packet, the function starts to receive the data packet from the slave device. It parses the data packet using the rec_handle() function and checks if the function code is 0x11 (0x11 is the function code for reading angles). If the read is successful and the function code is for reading angles, the data read is mapped from the range of 0 to 1000 to the range of 0 to 240 using the map() function, and then assigned to the angle array. Finally, if the read is successful, it returns true; otherwise, it returns false.

189
190
191
192
193
194
195
196
197
  if(rec_handle((uint8_t*)res , 0x11))
  {
    angles[0] = map(res[0] , 0 , 1000 , 0 , 240);
    angles[1] = map(res[1] , 0 , 1000 , 0 , 240);
    angles[2] = map(res[2] , 0 , 1000 , 0 , 240);
    return true;
  }
  return false;
}

Read xyz

The read_xyz() function takes one parameter, *pos, which stores the position of the spatial coordinate system returned from the slave device. This function is primarily used to send a command to the slave device to read the position of the spatial coordinate system, receive and parse the data returned from the slave device, and then store the parsed servo position values in the pos variable.

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//读取xyz
bool MaxArm_ctl::read_xyz(int* pos)
{
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_READ_XYZ;
  msg[3] = 0;
  msg[4] = checksum_crc8((uint8_t*)&msg[2] , 2);
  while(SerialM.available())
  {
    SerialM.read();
  }
  SerialM.write((char*)&msg , 5);
  delay(300);
  int16_t res[3];
  if(rec_handle((uint8_t*)res , 0x13))
  {
    pos[0] = res[0];
    pos[1] = res[1];
    pos[2] = res[2];
    return true;
  }
  return false;
}

Here, a data packet with a function code of FUNC_READ_XYZ is first sent, with a data length of 0.

202
203
204
205
206
207
208
209
210
  uint8_t msg[10] = {CONST_STARTBYTE1 , CONST_STARTBYTE2};
  msg[2] = FUNC_READ_XYZ;
  msg[3] = 0;
  msg[4] = checksum_crc8((uint8_t*)&msg[2] , 2);
  while(SerialM.available())
  {
    SerialM.read();
  }
  SerialM.write((char*)&msg , 5);

After sending the data packet, the function starts to receive the data packet returned from the slave device. It parses the data packet using the rec_handle() function and checks if the function code is 0x13 (0x13 is the function code for reading XYZ). If the read is successful and the function code is for reading XYZ, the data read is directly copied to the pos array. Finally, if the read is successful, it returns true; otherwise, it returns false.

211
212
213
214
215
216
217
218
219
220
221
  delay(300);
  int16_t res[3];
  if(rec_handle((uint8_t*)res , 0x13))
  {
    pos[0] = res[0];
    pos[1] = res[1];
    pos[2] = res[2];
    return true;
  }
  return false;
}

④ Parse Data Packet

The rec_handle() function primarily parses the data packets sent by the slave device. It takes two parameters: *res is used to store the received data portion, and func is used to identify the functionality of the received data packet.

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
bool MaxArm_ctl::rec_handle(uint8_t* res , uint8_t func)
{
  int len = SerialM.available();
  // Limit the length of data read (限制读取的数据长度)
  len = len > 30 ? 30 : len;
  uint8_t step = 0;
  uint8_t data[8] , index = 0;
  while(len--)
  {
    int rd = SerialM.read();
    switch(step)
    {
      case 0:
        if(rd == 0xAA)
        {
          step++;

First, the function retrieves the length of the data in the serial port and limits the read data length to 30 bytes. It creates a variable step as a parsing stage variable and also creates data[8] as a buffer for receiving data. index is used as a pointer to specify a byte of data in the data packet during parsing.

107
108
109
110
  int len = SerialM.available();
  // Limit the length of data read (限制读取的数据长度)
  len = len > 30 ? 30 : len;
  uint8_t step = 0;

Through a while loop, the function starts parsing based on the length of the received data packet. It reads one byte of data into the variable rd and begins parsing according to the step variable. Initially, when step = 0, it is the phase for parsing the first frame header. If the data read is 0xAA, it is the first frame header, so step is incremented to move to the next phase, which is identifying the second frame header. If the identification fails, step is reset to 0 to restart the recognition process from the first phase.

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
  while(len--)
  {
    int rd = SerialM.read();
    switch(step)
    {
      case 0:
        if(rd == 0xAA)
        {
          step++;
        }
        break;
      case 1:
        if(rd == 0x55)
        {
          step++;
        }else{
          step = 0;
        }
        break;

Here’s the parsing for the function code, data length, and data content:

case 2: This phase is for identifying the function code. Based on the func parameter passed, it checks if it matches the specified function code. If it does, the function code is stored in the data data buffer. If not, step is set to 0, returning to phase 1.

case 3: This phase is for identifying the data length. If the received data length is 6 bytes, the value of the data length is stored in the data data buffer. Otherwise, it returns to phase 1.

case 4: This phase is for parsing the data content. The function directly stores the read data into the data variable. When the number of stored data reaches or exceeds 8 bytes, it moves to the next phase: identifying the checksum.

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
      case 2:
        if(rd == func)
        {
          data[index++] = rd;
          step++;
        }else{
          step = 0;
        }
        break;
      case 3: //The received data length must be 6 bytes (接收的数据长度必须为6个字节)
        if(rd == 6)
        {
          data[index++] = rd;
          step++;
        }else{
          step = 0;
        }
        break;
      case 4:
        data[index++] = rd;
        if(index >= 8)
        {
          step++;
        }
        break;

In phase 5, the function calculates the checksum value for the previously obtained data length and data content using the checksum_crc8() checksum function. It then compares this checksum value with the checksum read from the received packet. If they are the same, the data parsing is successful; otherwise, it fails, and the function returns to phase 1 to restart the parsing process.

156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
      case 5:
        if(checksum_crc8(data , 8) == rd)
        {
          memcpy(res , &data[2] , 6);
          return true;
        }else{
          return false;
        }
        break;
      default:
        step = 0;
        break;
    }
  }
  return false;
}
  • Raspberry Pi Communication Routine Analysis

Source Code

(1) Communication Connection

① Hardware Wiring

Please ensure the MaxArm robotic arm connects to the USB port of Raspberry Pi.

② Communication Protocol

The communication protocol for both the master and slave devices in the MaxArm communication routines follows the following format:

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

The annotations of each part of the protocol are as follows:

Frame header: if 0xAA and 0x55 are received sequentially, it indicates that there is data to be received, consisting of a fixed 2 bytes.

Function Code: Used to indicate the purpose of an information frame, consists of 1 byte.

Data Length: Indicates the number of data bits carried by the data frame.

Check Bit: Verifies the correctness of the data frame. If correct, the corresponding function is called; otherwise, the data frame is skipped.

The calculation method for the check bit is: calculate the sum of the function code, data length, and data, then take the complement, and finally, take the low byte, which serves as the checksum.

(2) Program Interface Parsing

The routines analyzed in this document are located in folder of “Raspberry Pi Routines”, utilizing examples of communication via microUSB interface. This section mainly analyzes the “MaxArm_ctl.py” file, which is program file for the Raspberry Pi’s communication underlying implementation and has been encapsulated into Raspberry Pi communication class MaxArm_ctl:

42
43
44
45
'''
Raspberry Pi and MaxArm communication class (树莓派与MaxArm的通信类)
'''
class MaxArm_ctl:

The usage and parsing of the member functions is as follow:

① Constructor

The constructor of MaxArm_ctl class can takes two parameters. Parameter 1 is the port number for the connection between Raspberry Pi and MaxArm, and parameter 2 is the communication baud rate, which is set to 9600.

47
48
49
50
51
52
53
54
55
    # Constructor (构造函数)
    def __init__(self, device = "/dev/ttyUSB0", baudrate=115200):
        self.__uart = serial.Serial(
                                        port=device,
                                        baudrate=baudrate,
                                        bytesize=serial.EIGHTBITS,
                                        parity=serial.PARITY_NONE,
                                        stopbits=serial.STOPBITS_ONE,
                                    )

You can enter the command “ls /dev/ttyUSB*” in terminal of Raspberry Pi to view the port number for the connection between Raspberry Pi and MaxArm. When initializing this communication class, it is necessary to first select the appropriate port number for communication, otherwise, the communication fails.

② Function to Set the Angles of Three Bus Servos

The set_angles() function is used to set the angles of three bus servos on MaxArm, requiring two parameters. Parameter one is a collections of angles for three servos, and parameter 2 is the servo runtime.

This function decomposes the passed three angle values and runtime into control data of 8 bytes. It construct a complete control frame by adding a frame header, function code 0x01, data length 0x08, control data, and the calculated checksum to a byte array. Consequently, this frame data is sent to MaxArm via a serial port transmission function, enabling control of MaxArm movement.

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    def set_angles(self, angles, time):
        # Map angle values to the range 0~1000 (将角度值映射到0~1000的范围)
        mapped_angles = [self.map_func(angle, 0, 180, 0, 1000) for angle in angles]

        # Split angle values into two bytes in little-endian format (将角度值拆分为小端格式的两个字节)
        angles_bytes = []
        for angle in mapped_angles:
            angle_bytes = struct.pack('<H', angle)
            angles_bytes.extend(angle_bytes)

        # Split time into two bytes in little-endian format (将时间拆分为小端格式的两个字节)
        time_bytes = struct.pack('<H', time)

        # Build the data frame to be sent (构建要发送的数据帧)
        data = bytearray([0xAA, 0x55, PACKET_FUNCTION.FUNC_SET_ANGLE, 0x08])
        data.extend(angles_bytes)  # Add angle data (添加角度数据)
        data.extend(time_bytes)    # Add time (添加时间)
        checksum = checksum_crc8(data[2:]) # Calculate checksum (计算校验位)
        data.append(checksum)  # Add checksum (添加校验位)

        # Send data frame (发送数据帧)
        self.serial_send(data)

③ Function to Set End-effector’ XYZ Coordinates

The set_xyz() function sets the movement of MaxArm to specific XYZ coordinates, requiring two parameters. Parameter 1 is a collection of XYZ coordinates, and Parameter 2 is the duration of servo motion.

The function decomposes the passed XYZ coordinates and motion time into control data of 8 bytes. It constructs a complete control frame by adding a frame header, function code 0x03, data length 0x08, control data, and the calculated checksum to a byte array. This frame data is then sent to MaxArm via a serial port transmission function, enabling control of MaxArm movement to the corresponding XYZ axis coordinates.

 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
    def set_xyz(self , pos , time):        
        # Pack position data and time into a byte string (将位置数据和时间打包为字节串)
        pos_bytes = struct.pack('<hhh', *pos)
        time_bytes = struct.pack('<H', time)

        # Build the data frame to be sent (构建要发送的数据帧)
        msg = bytearray([0xAA, 0x55, PACKET_FUNCTION.FUNC_SET_XYZ, 0x08])
        msg.extend(pos_bytes)
        msg.extend(time_bytes)
        
        # Calculate checksum (计算校验位)
        checksum = checksum_crc8(msg[2:]) # Calculate checksum (计算校验位)
        
        msg.append(checksum)

        # Send data frame (发送数据帧)
        self.serial_send(msg)
        # print(msg)

④ Function to Set the Position of PWM Servo at End-effector

The set_pwmservo()function sets the movement of end-effector’s PWM servo to the specific angle. It requires two parameters. Parameter 1 is represents the servo angle value, and Parameter 2 denotes the duration of servo motion.

The function transforms the passed servo angle value into the pulse width of the servo, and decomposes the duration of servo motion into control data of 4 bytes.

It constructs a complete control frame by adding a frame header, function code 0x05, data length 0x04, control data, and the calculated checksum to a byte array. This frame data is then sent to MaxArm via a serial port transmission function, enabling control of MaxArm to rotate to the corresponding angle.

114
115
116
117
118
119
120
121
122
123
124
125
126
127
    def set_pwmservo(self , angle , time):
        angle = min(angle, 180)
        pul = self.map_func(angle, 0, 180, 500, 2500)

        # Build the data frame to be sent (构建要发送的数据帧)
        data = bytearray([0xAA, 0x55, PACKET_FUNCTION.FUNC_SET_PWMSERVO, 0x04, pul & 0xFF, (pul >> 8) & 0xFF, time & 0xFF, (time >> 8) & 0xFF])

        # Calculate checksum (计算校验位)
        checksum = checksum_crc8(data[2:])
        data.append(checksum)

        # Send data frame (发送数据帧)
        self.serial_send(data)
#       print(data)

⑤ Function to Control the Working Status of the Suction Cup at End-effector

The set_SuctioNnozzle() function is used to control the working status of the end-effector’s suction cup. It requires one parameter only, which represents the sub-function code value of the working status of the suction cup. The value involves 1, 2 and 3 corresponding to opening the air pump, closing the air pump and opening the air valve, and closing the air value, respectively.

The sequence of operation the air pump is as follow: when the pump is opened, the suction cup will generate suction force. Then, when the pump is closed and the valve is opened, negative pressure still exists inside the suction cup after the pump is turned off. Opening the valve allows external air to enter, eliminating the negative pressure and completely eliminating the suction force. Finally, the valve is closed.

The function checks whether the passed sub-function code is correct. If correct, it constructs a complete control frame by adding a frame header, function code 0x07, data length 0x01, sub-function code, and the calculated checksum to a byte array. This frame data is then sent to MaxArm via a serial port transmission function, enabling control of the working status of the end effector’s suction nozzle on MaxArm.

130
131
132
133
134
135
136
137
138
139
    def set_SuctioNnozzle(self , func):
        if func not in [1,2,3]:
            return
        # Build the data frame to be sent (构建要发送的数据帧)
        data = bytearray([0xAA, 0x55, PACKET_FUNCTION.FUNC_SET_SUCTIONNOZZLE, 0x01, func & 0xFF])

        crc = checksum_crc8(data[2:])
        data.append(crc)
        self.serial_send(data)
#        print(data)

⑥ Function to Read Bus Servo Angle

The read_angles() function reads the angles of the bus servos on MaxArm. This function returns a collection of angles containing three elements, each representing the angle of one bus servo.

The program flow of this function is as follows: first, it sends a command to read the angle values. After waiting for 0.1 seconds, it receives the data frame sent through the serial port. Then, it extracts and parses the angle values from the received data frame, and finally returns the results.

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
    def read_angles(self):
        # Build the command frame for reading angles (构建要发送的读取角度的命令帧)
        command = bytearray([0xAA, 0x55, 0x11, 0x00 , 0xEE])
        # Send the command frame to read angles (发送读取角度的命令帧)
        self.serial_send(command)
        
        time.sleep(0.1)
        
        # Receive data from serial (从串口接收数据)
        response = self.__uart.read(12)
        
#        print(response)
        
        # Call the parsing function to process received data (调用解析函数处理接收到的数据)
        rec = self.rec_handle(response , 0x11)
        
        if rec:
            angles = struct.unpack('<hhh', rec)
        else:
            return
        return angles

⑦ Function to Read XYZ Coordinates of End-effector

The read_xyz() function reads the XYZ coordinates of the end effector on MaxArm. This function returns a collection with three elements representing the values of x, y, and z.

The program of this function is as follow:

  1. Send a command to request the XYZ coordinates from MaxArm.

  2. Wait for 0.1 seconds, and then receive the data frame sent via the serial port.

  3. Extract and parse the XYZ coordinates from the received data frame.

  4. Return the XYZ coordinates as a collection of three elements.

165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
    def read_xyz(self):
        # Build the command frame for reading xyz coordinates (构建要发送的读取角度的命令帧)
        command = bytearray([0xAA, 0x55, 0x13, 0x00 , 0xEC])
        # Send the command frame to read xyz (发送读取角度的命令帧)
        self.serial_send(command)
        
        time.sleep(0.1)
        
        # Receive data from serial (从串口接收数据)
        response = self.__uart.read(11)
        
#        print(response)
        
        # Call the parsing function to process received data (调用解析函数处理接收到的数据)
        rec = self.rec_handle(response , 0x13)
        
        if rec:
            print(len(rec))
            xyz = struct.unpack('<hhh', rec)
        else:
            return
        return xyz

10.3 Control Example

10.3.1 Raspberry Pi Control Routine

The document demonstrates the control of MaxArm’s bus servo angles, setting of XYZ coordinates, control of nozzle functionality, and reading of bus servo angles and XYZ coordinates using a Raspberry Pi.

  • Working Principle

The source code of the program is located in “10.Serial Communication/ Control Routine/ Raspberry Pi Control Routine/ Routine”. The specific underlying implementation can be view in the “MaxArm_ctl.py” program file stored in “10.Serial Communication/ Control Routine/ Raspberry Pi Routine Control/ Raspberry Pi Routine”`.

(1) After hardware connection, enter the command ls /dev/ttyUSB* via Raspberry Pi’s terminal to view the corresponding device port connecting to MaxArm. The following program uses “/dev/ttyUSB0” as an example.

(2) Import MaxArm_ctl library, then invoke the MaxArm_ctl.MaxArm_ctl() function. Input the device port checked in step 1, "/dev/ttyUSB0" and initialize the serial baud rate to 9600, otherwise communication cannot be established normally.

5
6
7
import  MaxArm_ctl
import  time
ma = MaxArm_ctl.MaxArm_ctl(device = "/dev/ttyUSB0", baudrate=9600)

(3) By calling the ma.set_SuctioNnozzle() function, the control of air pump and solenoid valve can be achieved. For example, take the code ma.set_SuctioNnozzle(1) as example, where the parameter “1” represents the current mode of operation as opening the air pump. The duration of the venting operation is very short, so the delay for the venting operation can be reduced.

10
11
12
13
14
15
16
17
    #Set suction nozzle function (设置吸嘴功能)
    print("set suction nnozzle")
    ma.set_SuctioNnozzle(1) #Turn on the pump (打开气泵)
    time.sleep(2)
    ma.set_SuctioNnozzle(2) #Open solenoid valve and turn off pump (打开电磁阀并关闭气泵)
    time.sleep(0.2)
    ma.set_SuctioNnozzle(3) #Close solenoid valve (关闭电磁阀)
    time.sleep(2)

(4) By calling the ma.set_angles() function, the control of the bus servo angles can be achieved. For example, using the code ma_ctl.set_angles(angles , 1000), where the first parameter “angles” represents the rotation angles. This parameter is a list of length 3, corresponding to the angles required for the rotation of the 3 bus servos. The range of rotation angles is 0°-240°, and the second parameter “1000” represents the running time (in milliseconds), with a duration of 1000 milliseconds in this case.

19
20
21
22
23
24
25
26
27
28
    #Set bus servo angles (设置总线舵机角度)
    print("set angles")
    angles = [90,90,90]
    #Set bus servo angles to 90/90/90 degrees, with run time of 1000ms (将总线舵机的角度分别设置为90/90/90度,运行时间为1000ms)
    ma.set_angles(angles , 1000) 
    time.sleep(2)
    angles = [45,90,90]
    #Set bus servo angles to 45/90/90 degrees, with run time of 1000ms (将总线舵机的角度分别设置为45/90/90度,运行时间为1000ms)
    ma.set_angles(angles , 1000) 
    time.sleep(2)

(5) By calling the ma.set_xyz() function, you can set the XYZ coordinates. For example, using the code ma.set_xyz(xyz , 1000), where the first parameter “xyz” represents the XYZ coordinates to be set, and it is a list of length 3, and the second parameter “1000” represents the duration of operation (in milliseconds), with a duration of 1000 milliseconds in this case.

30
31
32
33
34
35
36
37
    #Set xyz coordinates (设置xyz坐标)
    print("set xyz")
    xyz = {120 , -180 , 85} # Set xyz coordinates to 120/-180/85, with run time of 1000ms (将xyz坐标设置为120/-180/85,运行时间为1000ms)
    ma.set_xyz(xyz , 1000)
    time.sleep(2)
    xyz = {-120,-180,85} #Set xyz coordinates to -120/-180/85, with run time of 1000ms (将xyz坐标设置为-120/-180/85,运行时间为1000ms)
    ma.set_xyz(xyz , 1000)
    time.sleep(2)

(6) By calling the ma.set_pwmservo() function, you can control the angle of PWM servos. For example, using the code ma.set_pwmservo(135, 1000), where the first parameter “135” represents the angle of rotation for the PWM servo, and the second parameter “1000” represents the duration of operation (in milliseconds), with a duration of 1000 milliseconds in this case.

39
40
41
42
43
44
    #Set PWM servo angle (设置PWM舵机角度)
    print("set pwm servo")
    ma.set_pwmservo(135,1000) #Set PWM servo angle to 135°, with run time of 1000ms (将PWM舵机角度设置为135度,运行时间为1000ms)
    time.sleep(2)
    ma.set_pwmservo(45,1000) #Set PWM servo angle to 45°, with run time of 1000ms (将PWM舵机角度设置为45度,运行时间为1000ms)
    time.sleep(2)

(7) By calling the ma.read_xyz() function, you can obtain the current XYZ coordinates of MaxArm. Retrieve the current XYZ coordinate information and print it out.

46
47
48
49
50
    #Read and print current xyz coordinates (读取并打印出当前状态的xyz坐标)
    print("read xyz")
    xyz = ma.read_xyz()
    print(xyz)
    time.sleep(2)

(8) By calling the ma.read_angles() function, you can obtain the current angles of the three bus servos on MaxArm. Retrieve the current information of the bus servo angles and print it out.

52
53
54
55
56
    #Read and print current bus servo angles (读取打印出当前状态的总线舵机角度)
    print("read angles")
    angles = ma.read_angles()
    print(angles)
    time.sleep(2)
  • Preparation

Please ensure that MaxArm is connected to the USB port of Raspberry Pi.

  • Operation Instructions

Note

  • The entered command should be case sensitive and “Tab” can be used to complement key words.

(1) Start MaxArm robotic arm and connect to Raspberry Pi system desktop via VNC. The, transfer the routine code to the /home/MaxArm/ directory.

(2) Click on in the upper left corner of the desktop, or press “Ctrl+Alt+T” to open the LX terminal.

(3) Input the command to view the corresponding device port.

ls /dev/ttyUSB*

(4) Replace the device port number “/dev/ttyUSB0” in the main.py file with the port number obtained in step 3).

5
6
7
import  MaxArm_ctl
import  time
ma = MaxArm_ctl.MaxArm_ctl(device = "/dev/ttyUSB0", baudrate=9600)

(5) Enter the command and press Enter to access the directory where the program is stored.

cd /home/MaxArm

(6) Enter the command and press Enter to start the program.

cd /home/MaxArm

(7) If you want to close this file, enter “Ctrl+C” in the LX terminal. If the operation fails, you can try pressing “Ctrl+C” multiple times

  • Program Outcome

While the program is running:

(1) Open the air pump to start suction of an object, then release the object after 2 seconds.

(2) Rotate the three bus servos to 90 degrees, then wait for 2 seconds before rotating them to 45, 90 , 90 degrees respectively.

(3) Change the XYZ coordinates of MaxArm to 120/-180/85, then wait for two seconds before changing the XYZ coordinates to -120, -180, 85.

(4) Rotate the PWM servo angle to 135 degrees, then wait for 2 seconds before rotating it to 45 degrees.

(5) Finally, print the current angles of the bus servos and XYZ coordinates in the serial port.

10.3.2 Arduino Control Routine

The document demonstrates the control of MaxArm’s bus servo angles, setting of XYZ coordinates, control of nozzle functionality, and reading of bus servo angles and XYZ coordinates using an Arduino controller.

  • Working Principle

The source code of the program is located in “10.Serial Communication->Control Routine->Arduino Control Routine->Arduino Routine->arduino_and_MaxArm->arduino_and_MaxArm”

The specific underlying implementation can be view in the “MaxArm_ctl.cpp” program file stored in “10.Serial Communication->Control Routine->Arduino Routine Control->Arduino Routine->arduino_and_MaxArm->arduino_and_MaxArm”.

(1) Set the debugging print serial port baud rate to 115200, call the ma_ctl.serial_begin() function from the MaxArm_ctl.h library to initialize the software serial port communication at a baud rate of 9600.

13
14
15
16
void setup() {
  Serial.begin(115200);
  ma_ctl.serial_begin();
}

(2) By calling the ma_ctl.set_angles() function, you can control the angles of the bus servos. The example of ma_ctl.set_angles(angles , 1000) is involved in here.

The first parameter “angles” represents the rotation angles. This parameter is a one-dimensional array of length 3, corresponding to the rotation angle for the 3 bus servos. The rotation range is 0°-240°.

The second parameter “1000” represents the duration of operation (in milliseconds), with a duration of 1000 milliseconds in this case.

23
24
25
26
27
28
29
void loop() {
  //Set bus servo angles (设置总线舵机角度)
  Serial.println("set angles");
  uint8_t angles[3] = {120,100,120};
  //Set the bus servo angles to 120/100/120 degrees respectively, run time 1000ms (将总线舵机的角度分别设置为120/100/120度,运行时间为1000ms)
  ma_ctl.set_angles(angles , 1000); 
  delay(1500);

(3) By calling the ma_ctl.set_xyz() function, the XYZ coordinates can be set. The example of ma_ctl.set_xyz(xyz[0] , 1000) is involved in here.

The first parameter “xyz[0]” represents the XYZ coordinates to be set, and it is a one-dimensional array of length 3.

The second parameter “1000” represents the duration of operation (in milliseconds), with a duration of 1000 milliseconds in this case.

31
32
33
34
35
36
37
38
39
  //Set XYZ coordinates (设置xyz坐标)
  Serial.println("set xyz");
  int16_t xyz[2][3] = {{120,-180,85},{-120,-180,85}};
  //Set XYZ coordinates to 120/-180/85, run time 1000ms (将xyz坐标设置为120/-180/85,运行时间为1000ms)
  ma_ctl.set_xyz(xyz[0] , 1000);  
  delay(1500);
  //Set XYZ coordinates to -120/-180/85, run time 1000ms (将xyz坐标设置为-120/-180/85,运行时间为1000ms)
  ma_ctl.set_xyz(xyz[1] , 1000);  
  delay(1500);

(4) The example of ma_ctl.set_pwmservo(90,1000) is involved in here.

The first parameter “90” represents the angle of rotation for the PWM servo.

The second parameter “1000” denotes the duration of operation (in milliseconds), set here to 1000 milliseconds.

41
42
43
44
45
46
47
48
49
50
51
  //Set PWM servo angles (设置PWM舵机角度)
  Serial.println("set pwm servo");
  //Set PWM servo angle to 90 degrees, run time 1000ms (将PWM舵机角度设置为90度,运行时间为1000ms)
  ma_ctl.set_pwmservo(90,1000); 
  delay(1500);
  //Set PWM servo angle to 45 degrees, run time 1000ms (将PWM舵机角度设置为45度,运行时间为1000ms)
  ma_ctl.set_pwmservo(45,1000); 
  delay(1500);
  //Set PWM servo angle to 135 degrees, run time 1000ms (将PWM舵机角度设置为135度,运行时间为1000ms)
  ma_ctl.set_pwmservo(135,1000);  
  delay(1500);

(5) Utilizing the ma_ctl.set_SuctioNnozzle() function enables the control of the air pump and solenoid valve. Consider the code "ma_ctl.set_SuctioNnozzle(1)" as an example, where the parameter “1” signifies the current operational mode as opening the pump. Given the brief duration of the venting operation, it is advisable to reduce the delay for the venting operation.

53
54
55
56
57
58
59
60
  //Set suction nozzle function (设置吸嘴功能)
  Serial.println("set suction nnozzle")
  ma_ctl.set_SuctioNnozzle(1);  //Turn on the pump (打开气泵)
  delay(1500);
  ma_ctl.set_SuctioNnozzle(2);  //Turn on solenoid valve and turn off the pump (打开电磁阀并关闭气泵)
  delay(200);
  ma_ctl.set_SuctioNnozzle(3);  //Turn off solenoid valve (关闭电磁阀)
  delay(1500);

(6) By invoking the ma_ctl.read_angles() function, you can retrieve the angle information of the three current bus servos on MaxArm. Store the read servo angles into the "read_a" array and then print them out via the serial port.

62
63
64
65
66
67
68
69
70
71
72
73
74
75
  //Read and print current bus servo angles (读取并打印出当前状态的总线舵机角度)
  Serial.println("read angles");
  int16_t read_a[3] = {0,0,0};
  if(ma_ctl.read_angles(read_a))
  {
    Serial.print("angles: ");
    Serial.print(read_a[0]);
    Serial.print(" ");
    Serial.print(read_a[1]);
    Serial.print(" ");
    Serial.print(read_a[2]);
    Serial.println(" ");
  }
  delay(100);

(7) Utilizing the ma_ctl.read_xyz() function allows you to obtain the current XYZ coordinates of MaxArm. Store the retrieved XYZ coordinates into the “read_x” array and then print them out via the serial port.

77
78
79
80
81
82
83
84
85
86
87
88
89
90
  //Read and print current XYZ coordinates (读取并打印出当前状态的xyz坐标)
  Serial.println("read xyz");
  int16_t read_x[3] = {0,0,0};
  if(ma_ctl.read_xyz(read_x))
  {
    Serial.print("xyz: ");
    Serial.print(read_x[0]);
    Serial.print(" ");
    Serial.print(read_x[1]);
    Serial.print(" ");
    Serial.print(read_x[2]);
    Serial.println(" ");
  }
  delay(100);
  • Preparation

(1) Hardware

MaxArm has two types of serial communication interfaces: one is the 4-pin interface, which is suitable for devices with pin interfaces for UART communication, commonly used for Arduino development boards, STM32 development boards. The other is the microUSB interface, suitable for devices with USB host interfaces, such as Raspberry Pi, Jetson Nano, etc. Here, we need to connect to MaxArm’s 4-pin interface for communication.The communication connection for 4Pin interfaces is as follow:

The specific pin connections are as follow:

MaxArm’s IO32 pin serves as the RX signal pin, connected to pin 6 of the Arduino development board.

MaxArm’s IO33 pin serves as the TX signal pin, connected to pin 7 of the Arduino development board.

MaxArm’s GND pin is connected to the GND pin of the Arduino development board.

(2) Software

Connect MaxArm robotic arm to Arduino editor according to “4. Underlying Program Learning->Arduino Development->Set Arduino Development Environment”.

  • Program Download

Source Code

(1) Double click to open Arduino IDE.

(2) Click “File->Open” in sequence.

(3) Select the program “arduino_and_MaxArm.ino” in the folder “Serial Communication/ Control Routine/ Arduino Control Routine/ Arduino Routine/ arduino_and_MaxArm”`, then click “Open”.

(4) Select the correct model for development board. Click “Tool->Development Board” to select “arduino_and_MaxArm”. (If the development board model and port number have already been configured in the development environment, you can skip the step.)

(5) Choose the corresponding port number for ESP32 controller in “Tool -> Port”. (In this case, the port number of COM5 is used as example. It may vary on different computers, so it is necessary to select the correct one according to your own computer. Do not choose COM1if it appears in here, as it is typically the system communication port instead of the actual port of the development board.)

(6) If you’re unsure about the port number, you can right-click on “This PC”, then click on “Properties -> Device Manager”, and check the port number corresponding to the control board (the one with CH340 is our device).

(7) After the selection, confirm the development board model of “ESP32 Dev Module” and port number of “COM5” at bottom right corner.

(8) Once confirmed, click on to verify the program. If the program is error-free, the status area will sequentially display “Compiling project -> Compilation complete”. After compilation, it will display information such as the number of bytes used by the current project and program storage space.

(9) After compiling without errors, we click on to upload the program to the development board. The status area will sequentially display “Compiling project -> Uploading -> Upload successful”. Once the upload is successful, the status area will stop printing upload information.

  • Project Outcome

While the program is running:

(1) Three bus servo rotate to 120, 100 and 120 degrees, respectively.

(2) Change MaxArm’s coordinates to 120/-180/85, and then change to -120/-180/85 after 1.5 seconds.

(3) The PWM servo smoothly transitions its angle from 90 to 45 degrees, and then to 135 degrees.

(4) After a delay of 1.5 seconds, the air pump is activated to start suctioning the object, and after 1.5 seconds, the object is released.

(5) Finally, print the current angles of the bus servos and XYZ coordinates in the serial port.

10.3.3 PC Control Routine

The document demonstrates the control of MaxArm’s bus servo angles, setting of XYZ coordinates, control of nozzle functionality, and reading of bus servo angles and XYZ coordinates using an Arduino controller.

  • Working Principle

(1) After MaxArm is serially connected to TTL, and then connected to a PC for serial communication, enabling serial control of MaxArm through the serial port.

(2) Communication protocol:

Following uses communication protocols as examples. The format of the protocol instruction packet is as follow:

Frame header Function code Data length Data information Check bit
0xAA 0x55 func len data check

Frame header: if 0xAA and 0x55 are received sequentially, it indicates that there is data to be received, consisting of a fixed 2 bytes.

Function code: Used to indicate the purpose of a message frame.

Data length: Indicates the amounts of the consequent data

Data information: indicate the transmission data information.

Checksum: The checksum is calculated by taking the complement of the sum of the Function Code, Data Length, and Data, and then taking the lower byte.

  • Preparation

(1) Hardware

Please make sure to properly connect MaxArm robotic arm to PC, otherwise, it will affect the normal communication.

① Connect the RXD, TXD, and GND pins of the USB adapter to the IO32, IO33, and GND interfaces of the ESP32 expansion board, respectively, using Dupond wires.

② Connect the USB adapter to the computer.

(2) Software

Please ensure that the baud rate of the serial port assistant is set to 9600, parity is set to NONE, data bits are set to 8, stop bits are set to 1, and hexadecimal sending is selected, as shown in the configuration below:

  • Project Outcome

Control MaxArm by sending hexadecimal commands:

(1) Command name: FUNC_SET_ANGLE, command value 0x01,data length 8:

Description: Controls the rotation of the bus servos of MaxArm

Data information: First, set the angles of the three bus servos, followed by the servo’s operating time, totaling 4 values. Each value is split into 2 bytes, with the low byte first and the high byte last.

Example: Taking the hexadecimal sequence AA 55 01 08 c8 00 f4 01 f4 01 d0 07 6d as an example, the data information is c8 00 f4 01 f4 01 d0 07. Among them, c8 00 represents servo 1 rotating to position 200, the following two f4 01 correspond to servo 2 and 3 rotating to position 500 respectively, and d0 07 indicates the operating time is 2000ms.

(2) Command Name: FUNC_SET_XYZ, command value 0x03, data length 8:

Description: Controls the XYZ coordinates of MaxArm.

Data Information: First are the coordinates corresponding to XYZ, followed by the operating time, totaling 4 values. Each value is split into 2 bytes, with the low byte first and the high byte last.

Example: Taking the hexadecimal sequence AA 55 03 08 78 00 4c ff 55 00 e8 03 f1 as an example, the data information is 78 00 4c ff 55 00 e8 03. Among them, 78 00 represents X coordinate as 120, 4c ff represents Y coordinate as -180, 55 00 represents Z coordinate as 85, and e8 03 represents the operating time as 1000ms.

(3) Command Name: FUNC_SET_PWMSERVO, command value 0x05, data length 4:

Description: Controls the rotation of the PWM servo.

Data Information: There are 2 parameters in total. The first parameter is the angle of rotation for the PWM servo, followed by the operating time, totaling 2 values. Each value is split into 2 bytes, with the low byte first and the high byte last.

Example: Taking the hexadecimal sequence AA 55 05 04 D0 07 e8 03 34 as an example, the data information is D0 07 e8 03. Among them, D0 07 represents the servo rotating to position 2000, and e8 03 represents the operating time as 1000ms.

(4) Command Name: FUNC_SET_SUCTIONNOZZLE, command value 0x07, data length 1:

Description: Controls the suction nozzle function.

Data Information: 1 indicates turning on the vacuum pump, 2 indicates turning off the vacuum pump and opening the air valve, 3 indicates closing the air valve.

Example: Taking the hexadecimal sequence AA 55 07 01 02 f6 as an example, the data information is 02, which indicates that the suction nozzle will perform the operation of turning off the air pump and opening the air valve.

(5) Command Name: FUNC_READ_ANGLE, command value 0x11, data length 0:

Description: Reads the current angles of the bus servos on MaxArm.

Data Information: Since it is a read operation, there is no data information.

Example: Taking the hexadecimal sequence AA 55 11 00 EE as an example.

After sending the data, MaxArm returns the current angles of the three bus servos. Taking the received data AA 55 11 06 60 03 9A 01 C9 02 20 as an example, the data information is 60 03 9A 01 C9 02. Here, 60 03 represents the current position of servo 1 as 873, 9A 01 represents the current position of servo 2 as 410, and C9 02 represents the current position of servo 3 as 713.

(6) Instruction Name: FUNC_READ_XYZ, command value 0x13, data length 0:

Description: Reads the current XYZ coordinates of MaxArm.

Data Information: Since it is a read operation, there is no data information.

Example: Taking the hexadecimal sequence AA 55 13 00 EC as an example.

After sending the data, MaxArm returns the current XYZ coordinates. Taking the received data AA 55 13 06 61 FF FA FF 60 00 2E as an example, the data information is 61 FF FA FF 60 00. Here, 61 FF represents the X-axis as -159, FA FF represents the Y-axis as -6, and 60 00 represents the Z-axis as 96.