Skip to main content

Serial protocol with Python

A Passtrough program

Prerequisites

  • Python installed

  • Pyserial library

  • Source code on GitHub

  • Any USB to TTL converter (RS-232 serial adapter), like this from Amazon. The Serial-Servo-Hub supports 3.3v as well as 5v adapters. There are literally tons of them on Amazon or Ebay.

Install the pyserial library with pip:

pip install pyserial
tip

This code also works well on the Raspberry Pi, check out this article on how to access the serial port.

note

Depending on the nature of your application you may use more reliable solutions as USB-TTL (RS-232) interface. Check out our article about hardening Serial-Servo-Hub setups or contact us.

The code

The following snippet shows a way to read incoming data from the Serial-Servo-Hub UART protocol and send them back to the module's outputs (passthrough)

import serial

STX = '\x02'
ETX = '\x03'
FDT = ';'

port = 'COM13' #change accordingly
baud = 115200

PWM_data = ""
PWM_in = ["1500"] *5
PWM_out = ["1500"] *4

ser = serial.Serial(port, baud, timeout=None)
if ser.isOpen():
ser.close()
ser.open()


def bufferReset():
ser.reset_input_buffer()
ser.reset_output_buffer()


def syncPorts():
PWM_out[0] = PWM_in[0]
PWM_out[1] = PWM_in[1]
PWM_out[2] = PWM_in[2]
PWM_out[3] = PWM_in[3]

def writeOutputs():
SA = STX.encode('ascii') + "SA".encode('ascii') + PWM_out[0].encode('ascii') + ETX.encode('ascii')
SB = STX.encode('ascii') + "SB".encode('ascii') + PWM_out[1].encode('ascii') + ETX.encode('ascii')
SC = STX.encode('ascii') + "SC".encode('ascii') + PWM_out[2].encode('ascii') + ETX.encode('ascii')
SD = STX.encode('ascii') + "SD".encode('ascii') + PWM_out[3].encode('ascii') + ETX.encode('ascii')
ser.write(SA)
ser.write(SB)
ser.write(SC)
ser.write(SD)

bufferReset()

while True:
data_raw = ser.read(1)
if data_raw == b'\x02':
data_raw = ser.read_until(b'\x03').decode("utf-8")
PWM_data = data_raw[:-1]
PWM_in = list(filter(None, PWM_data.split(FDT)))
bufferReset()
syncPorts()
writeOutputs()

How it works

First, optional but wise step, lets declare our transmission control characters: STX, ETX and field separator.

STX = '\x02'
ETX = '\x03'
FDT = ';'

We then initialize the serial library with the actual COM port and the needed baudrate, the Serial-Servo-Hub uses 115200 baud.

port = 'COM13' #change accordingly
baud = 115200

Create an empty string for the parsed section of the incoming stream. We will fill it with valid data. We also initialize the two list which will hold the input and output values measured in microseconds. There are 5 input ports and 4 output ports. We set all to 1500μs which is the approximate center of most servos, but this value will change as soon as we capture some input data.

PWM_data = ""
PWM_in = ["1500"] *5
PWM_out = ["1500"] *4

Initialize the serial connection, in case there is an active connection close it and reopen a new one

ser = serial.Serial(port, baud, timeout=None)
if ser.isOpen():
ser.close()
ser.open()

Reset the serial ring buffer

ser.reset_input_buffer()
ser.reset_output_buffer()

This functions creates the passthrough itself. Here you can manipulate with the PWM data and add your own code between the inputs and outputs. PWM_in[5] (Port X5) is unused in our case but can be read and used without problems. Note: the values inside those lists are strings, you may need to convert them to integers in order to do math.

def syncPorts():
PWM_out[0] = PWM_in[0]
PWM_out[1] = PWM_in[1]
PWM_out[2] = PWM_in[2]
PWM_out[3] = PWM_in[3]

The last functions serves mostly to prepare out byte arrays which will be sent back to the module. It contains some repetitive code, it surely can be written in a more pythonic way but as shown here it is really easy to understand. We have to prepend to every PWM value its port on the module according to the UART protocol (SA to SD, X12 to X15 on the module). Also we have to put this between our start and end markers (STX & ETX). We then proceed to write those arrays on out serial interface.

def writeOutputs():
SA = STX.encode('ascii') + "SA".encode('ascii') + PWM_out[0].encode('ascii') + ETX.encode('ascii')
SB = STX.encode('ascii') + "SB".encode('ascii') + PWM_out[1].encode('ascii') + ETX.encode('ascii')
SC = STX.encode('ascii') + "SC".encode('ascii') + PWM_out[2].encode('ascii') + ETX.encode('ascii')
SD = STX.encode('ascii') + "SD".encode('ascii') + PWM_out[3].encode('ascii') + ETX.encode('ascii')
ser.write(SA)
ser.write(SB)
ser.write(SC)
ser.write(SD)

Before we begin with any serial port operation we need to clear the ring buffer with the function declared before

bufferReset()

Next we run the program loop itself.

while True:
data_raw = ser.read(1)
if data_raw == b'\x02':
data_raw = ser.read_until(b'\x03').decode("utf-8")
PWM_data = data_raw[:-1]
PWM_in = list(filter(None, PWM_data.split(FDT)))
bufferReset()
syncPorts()
writeOutputs()

In detail, what happens inside this while:

In an infinte cycle, read one byte a time looking for the STX marker

while True:
data_raw = ser.read(1)
if data_raw == b'\x02':

Once found STX, read until ETX and store in data_raw

        data_raw = ser.read_until(b'\x03').decode("utf-8")

Proceed to cut the last byte (ETX marker) in order to have a clean content, store it in PWM_data

        PWM_data = data_raw[:-1]

PWM_data is a long string containing our cleaned up sequence from the module, like this:

 -> print(PWM_data)
-> 1504;1505;1506;1502;1004;

In the next step we need to tokenize the PWM_data string using out before declared field divider (semicolon). We also eliminate empty tokens by using the filter option. This is necessary because of the last semicolon in the PWM_data string. These tokens are then assigned to the PWN_in variable. This happens in this step:

        PWM_in = list(filter(None, PWM_data.split(FDT)))

Our PWM_in list looks then like this:

 -> print(PWM_in)
-> ['1504', '1505', '1506', '1502', '1004']

Next we reset the serial ring buffer again, preparing it for the upcoming while cycle. We also call the syncPorts() function which fills the PWM_out list and send those values to the serial interface using the writeOutputs() function

        bufferReset()
syncPorts()
writeOutputs()

Just run the program in your terminal window. You may press ctrl+c to abort it.

The syncPorts() function

This function is also the ideal entry point for the user in order to customize the Serial-Servo-Hub. Note that the values contained in the lists are strings, we eventually need to convert into integers. It would then look like this:

def syncPorts():
PWM_out[0] = str(int(PWM_in[0]))
...
...
...

Some type conversion happens there but a for a modern day computer or even microcontroller this is no big deal. Furthermore we would like some variables to work with, lets create X1_int like port X1 integer and convert it back as string to the output:

def syncPorts():
X1_int = int(PWM_in[0])
PWM_out[0] = str(X1_int)

Example: we want listen to port X5 and invert port X12 if X5 is greater than 1500μs assuming a range from 1000μs to 2000μs.

def syncPorts():
X1_int = int(PWM_in[0])
X5_int = int(PWM_in[4])

if(X5_int <= 1500):
PWM_out[0] = str(X1_int)
else:
PWM_out[0] = str(1500*2 - X1_int) #inverting like this works if the servo center is known as exactly 1500μs