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