As mentioned in a previous post I will be using python Twisted architecture for the asynchronous communications that are required I chose to use twisted partly because I was interested in learning more about it but also because it supports all the functionality that I needed serial communication(for RS-232RS-232 and RS-422 protocols FTP and SMTP all in one package Twisted certainly is capable of much more but these are the communication channels I need to support
All of the popular Linux distributions maintain a python-twisted package as well as packaged versions of Twisted’s dependencies. To install Twisted on a dpkg-based system, run:
sudo apt-get install python-twisted
To verify that the installation worked and that you have the desired version of Twisted installed, import the twisted module from a Python prompt:
$ python >>> Python 2.7.3 (default, Mar 8 2014, 05:13:23) Type "help", "copyright", "credits" or "license" for more information. >>> import twisted >>> twisted.__version__ '1.0.0' [GCC 4.6.3] on linux2
If the import twisted statement runs with no errors, you have a working Twisted installation.
RS-422 communication
I am communicating to a Fenner M-Trim motor controller over a RS-422 serial connection. The protocol can be found in the User Manual in chapter 7 (http://www.contrexinc.com/PDF/UserManuals/M-TrimUserManual.pdf). The protocol is a simple 12 byte ASCII string and uses ‘\x02’ as the STX (start of transmission) and ‘\x03’ as the ETX (end of transmission).
The protocol looks like this:
Char# | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Desc | STX | DEV# 10’s | DEV# 1’s | MSG Type | Par# 10’s | Par# 1’s | Data 1000’s | Data 100’s | Data 10’s | Data 1’s | Data Format | ETX |
The MSG Type determines whether it is a read or write packet. The Data format determines the decimal position for the data.
The code for this portion of the project can be found at https://github.com/frellwan/SciFy-Pi.git in the Serial/MTrim folder.
The finished code will differ slightly due to the fact that the communication on this port will be driven by the SLC 5/04 processor, but for testing purposes I have added code to read and write to parameters to show the concept.
As mentioned in a previous post I am using a USB to RS422 cable(USB-RS422-WE-1800-BTUSB-RS422-WE-1800-BT) to be able to communicate to the Fenner M-Trim controller. The data sheet can be found here: USB-RS422-FTDI
The cable pinout can be seen below:
The TXD-/TXD+ on the cable is connected to the RX-/RX+ on the MTrim and the RX-/RX+ on the cable is connected to the TX-/TX+ on the MTrim. The GND on the cable is connected to the GND on the MTrim. Once connected it works very well. The USB side of the cable has 2 LED's - a red for transmit and a green for receive. So it is very easy to see when signal transmission is happening. With the exception of the price of this cable, I am very pleased with its performance.
I borrowed heavily from the Pymodbus source code. This package is well proven and seemed like a good starting point. The serialport instantiating is very simple
class SerialMTrimClient(serialport.SerialPort):
def __init__(self, factory, *args, **kwargs):
''' Setup the client and start listening on the serial port
:param factory: The factory to build clients with
'''
protocol = factory.buildProtocol()
self.decoder = ClientDecoder()
serialport.SerialPort.__init__(self, protocol, *args, **kwargs)
Options = Options()
config = SafeConfigParser()
config.read([options['config']])
framer = AsciiFramer(ClientDecoder())
factory = MTrimFactory(framer)
RS422port = config.get('RS-422', 'host')
RS422baud = config.getint('RS-422', 'baudrate')
SerialMTrimClient(factory, RS422port, reactor, baudrate = RS422baud)
log.debug("Starting the client")
reactor.run()
At this point the MTrimProtocol is now responsible for encoding and sending the ASCII string to the MTrin controller.
The Mtrim has 3 types of commands – read, write and control command. Each command type has a different way to encode the data, so they each have their own code for encoding and decoding messages.
class ParameterSendRequest(PDU):
'''
Class for writing a MTrim register
'''
_frame_size = 12
def __init__(self, address=1, parameter=1, value=0, **kwargs):
''' Initializes a new instance
:param address: The address of the device to write to
:paramater: The parameter to write to
:value: The value to write to the parameter
'''
PDU.__init__(self, **kwargs)
self.address = address
self.parameter = parameter
self.msgType = '3'
self.value = value
self.Data = ''
self.dataFormat = '0'
self.packet = ''
def encode(self):
''' Encodes the request packet
:return: The encoded packet
'''
if (not self.skip_encode):
self.packet = struct.pack('>B', 0x02) #STX
if (self.address <10):
self.packet += '0' + str(self.address)
else:
self.packet += str(self.address)
self.packet += self.msgType
if (self.parameter < 10):
self.packet += '0' + str(self.parameter)
else:
self.packet += str(self.parameter)
strValue = str(self.value)
if(self.parameter in [20,21,22,23]):
if (self.value >= 0):
if strValue.find('.') == -1:
self.dataFormat = '0'
else:
self.dataFormat = str((len(strValue)-1)-strValue.find('.'))
if (self.value < 0):
if strValue.find('.') == -1:
self.dataFormat = '4'
else:
self.dataFormat = str((len(strValue)-1)-strValue.find('.')+4)
else:
self.dataFormat = '0'
if strValue.find('.') == -1:
for c in range(4-len(strValue)):
self.Data += '0'
self.Data += strValue
else:
for c in range(5-len(strValue)):
self.Data += '0'
self.Data += "".join(strValue.split('.'))
self.packet += self.Data
self.packet += self.dataFormat
self.packet += struct.pack('>B', 0x03) #ETX
return self.packet
def decode(self, data):
''' Decode a register request packet
:param data: The request to decode
'''
self.address = int(data[1])*10 + int(data[2])
self.msgType = data[3]
self.parameter = int(data[4])*10 + int(data[5])
self.dataFormat = int(data[10])
self.value = int(data[6:10])
if (self.dataFormat <= 3):
self.Data = self.value/(10**self.dataFormat)
else:
self.Data = -1 * self.value/(10**(self.dataFormat-4))
return self
def __str__(self):
''' Returns a string representation of the instance
:returns: A string representation of the instance
'''
return "ParameterSendRequest (%d,%d,%d)" % (self.address, self.parameter, self.Data)
Here is some video - it didn't turn out as well as I had hoped - hopefully I can get a better video soon
