How to work with multiple serial ports for R / W using twisted?

Going through a twisted finger and looking at SO questions:

However, I cannot (yet) write a twisted program that can read and write from several serial ports, especially where the protocol includes reading one or more lines and writing to the device, respectively.

What I'm trying to do is open 2 pairs (i.e. just 4) of serial ports for 2 modems. Communication with modems is used with the Hayes AT command set. Although most modem command / response exchanges are via the command port, there is diagnostic information for each modem that is available only through the diagnostic port. Diagnostic information should lead to a change in the state of the machine (device status, connection status).

Here is a rough skeletal program of what I understand as a potential approach (based on single-port examples):

class CommandProtocol(LineOnlyReceiver): def connectionMade(self): log.msg("Connected to command port") def lineReceived(self, line): print repr(line) processCommandLine(line) class DiagnosticProtocol(LineOnlyReceiver): def connectionMade(self): log.msg("Connected to diag port") def lineReceived(self, line): print repr(line) processDiagnosticLine(line) ... # modem1 ports cmdPort[0] = SerialPort(CommandProtocol, "/dev/ttyUSB0", reactor, 115200) diagPort[0] = SerialPort(DiagnosticProtocol, "/dev/ttyUSB1", reactor, 115200) # modem2 ports cmdPort[1] = SerialPort(CommandProtocol, "/dev/ttyUSB3", reactor, 115200) diagPort[1] = SerialPort(DiagnosticProtocol, "/dev/ttyUSB4", reactor, 115200) 

However, I am at a loss, how can I do the following:

  • How / where do I accept CLI input from a user who then triggers sending a set of AT commands to modems?
  • Correlation of information received in the command port for ttyUSB0 and ttyUSB1 for modem1, and similarly for another pair for modem2? Please note that each modem has its own state machine (device status and connection status).
  • Does any mechanism to control multiple state machines twist the application?
  • It is possible that the serial USB connection with the modem will be destroyed due to disconnecting the modem and re-establishing it when connected to it. How can I detect such events and add monitoring of the corresponding device ports to the reactor? I am currently doing this statically in the main application.
+5
source share
1 answer

Note in your sample code

I don’t see how you create classes before registering them in the reactor. I expect this to fail badly. Here is a similar snippet of startup code:

 # stuff to process messages coming from the serial port class SerialEater(basic.LineReceiver): statusCallback = None def __init__(self): self.keyinprocess = None def lineReceived(self, data): self.dealWithSerial(data) def connectionLost(self, reason): if(reactor.running): print "Serial lost but reactor still running! reason: " + str(reason) + " at time " + time.asctime() [...etc...] # Register the serialport into twisted serialhandler = SerialEater() # <------------- instantiate SerialPort(serialhandler, '/dev/ttyUSB0', reactor, baudrate=115200) 

How / where do I accept CLI input from a user who then triggers sending a set of AT commands to modems?

Like you can register Serial handlers in Twisted, you can register handlers for standard io, for example:

 # stuff to pull cbreak char input from stdin class KeyEater(basic.LineReceiver): def __init__(self): self.setRawMode() # Switch from line mode to "however much I got" mode def connectionLost(self, reason): if(reactor.running): self.sendLine( "Keyboard lost but reactor still running! reason: " + str(reason) + " at time " + time.asctime()) def rawDataReceived(self, data): key = str(data).lower()[0] try: if key == '?': key = "help" [...etc...] # register the stdio handler into twisted keyboardobj = KeyEater() keyboardobj.serialobj = serialhandler stdio.StandardIO(keyboardobj,sys.stdin.fileno()) 

Correlation of information received in the command port for ttyUSB0 and ttyUSB1 for modem1, and similarly for another pair for modem2? Please note that each modem has its own state machine (device status and connection status)

In normal use, each instance of the connection will have its own state machine (wrapped in an instance of the class that you register in the reactor with the connection).

You, as a programmer, choose how you want to connect class states, but often by clicking on a link to partner classes.

Below this answer contains executable code that illustrates how data is related between states / interface. It is also shown in this SO: Permanent connection in twisted


Does any mechanism for managing multiple states twist the application?

If by "application" you mean "your twisted code," then the answer is absolutely YES!

A typical Twisted application is an array of state machines, all with some surprisingly well-defined interfaces. I started my Twisted adventure, intending to write an application with two state computers (serial and keyboard), but when I felt comfortable with a perverted one, I realized that it was trivial to add additional interfaces and state machines (through the whole miracle of tx libraries). I added everything in one day on the rough web interface, the web memory interface, and then put SSL on top of both and even added SSH to the debugging interface. Once you get rolling, adding the interfaces and states of the machines becomes trivial.

In many cases (all?) The twisted model is that the state-machine will be in an instance of the class attached to the connection, and which was registered in the main event (the same thing) -loop.

With the types of connections that new state machines generate (think of http connections), you register one factory-class / state-machine along with a listening connection, which together allows the application to propagate new classes / states for each new connection. Swirling applications are usually 10 or even 100 thousand out of thousands of simultaneous instances of state when run on a scale.

Twisted is amazing if you try to glue different protocols and states (... with all that it is in the event loop of your choice (select / epoll / kqueue / etc))

The following is sample code for an example that should illustrate many of these points. Read the comments before def main() for more information on the code:

 #!/usr/bin/python # # Frankenstein-esk amalgam of example code # Key of which comes from the Twisted "Chat" example # (such as: http://twistedmatrix.com/documents/12.0.0/core/examples/chatserver.py) import sys # so I can get at stdin import os # for isatty import termios, tty # access to posix IO settings from random import random from twisted.internet import reactor from twisted.internet import stdio # the stdio equiv of listenXXX from twisted.protocols import basic # for lineReceiver for keyboard from twisted.internet.protocol import Protocol, ServerFactory class MyClientConnections(basic.LineReceiver): def __init__(self): self.storedState = "Idle" self.connectionpos = None def connectionMade(self): self.factory.clients.append(self) # <--- magic here : # protocol automagically has a link to its factory class, and # in this case that is being used to push each new connection # (which is in the form of this class) into a list that the # factory can then access to get at each of the connections self.connectionpos = str(self.factory.clients.index(self)) # figure out # where I am in the connection array print "Got new client! (index:", self.connectionpos + ")" self.transport.write("---\nYour connection: " + self.connectionpos + "\n---\n") def connectionLost(self, reason): print "Lost a client!" self.factory.clients.remove(self) # used to pretend that something was typed on a telnet connection def fakeInput(self, message): self.transport.write("FAKING Input: '" + message + "'\n") self.lineReceived(message) #this is only in a def on its own so I can lump my demo callLater def stateUpdate(self, newState, delay): self.storedState = newState # the following is a hack to fake data coming in this interface reactor.callLater(delay, self.fakeInput, newState + " DONE") def processInput(self, newState): # all the logic in here is junk to make a demo, real code may or may-not look like # this. This junk logic is an example statemachine though if self.storedState == "Idle": if newState == "start": self.stateUpdate("State A", 1) # send a message to this connection self.transport.write("starting state machine\n") # send a message to the term in which the script it running print "Connection [" + self.connectionpos + "] starting state machine" elif self.storedState == "State A": if newState == "State A DONE": self.transport.write("Beginning state B\n") self.stateUpdate("State B", 2) elif self.storedState == "State B": if newState == "State B DONE": self.transport.write("Beginning state C\n") self.stateUpdate("State C", 2) elif self.storedState == "State C": if newState == "State C DONE": self.storedState = "Idle" # send a message to this connection self.transport.write("Returning to Idle state\n") # send a message to the term in which the script it running print "Connection [" + self.connectionpos + "] return to Idle state" def lineReceived(self, line): # print "received '" + line +"' from connection", self.factory.clients.index(self) self.processInput(line) class MyServerFactory(ServerFactory): protocol = MyClientConnections def __init__(self): self.clients = [] # this gets filled from the class above def sendToAll(self, message): for c in self.clients: # Read MyClientConnections class for background c.transport.write(message) def randStart(self, width): for c in self.clients: startDelay = random() * width print "Starting client " + str(c.connectionpos) + " in " +str(startDelay) + " secs" reactor.callLater(startDelay, c.processInput, "start") # to set keyboard into cbreak mode -- just because I like it that way... class Cbreaktty(object): org_termio = None my_termio = None def __init__(self, ttyfd): if(os.isatty(ttyfd)): self.org_termio = (ttyfd, termios.tcgetattr(ttyfd)) tty.setcbreak(ttyfd) print ' Set cbreak mode' self.my_termio = (ttyfd, termios.tcgetattr(ttyfd)) else: raise IOError #Not something I can set cbreak on! def retToOrgState(self): (tty, org) = self.org_termio print ' Restoring terminal settings' termios.tcsetattr(tty, termios.TCSANOW, org) class KeyEater(basic.LineReceiver): def __init__(self, factoryObj): self.setRawMode() # Switch from line mode to "however much I got" mode # the following is one of the key connecting ideas in twisted, the object # that contains another state machine (really all of the tcp statemachines) # has been passed into this class via its init. self.factoryObj = factoryObj def rawDataReceived(self, data): key = str(data).lower()[0] if key == 's': # The following line is going to call (from within the factory object) # the random start def self.factoryObj.randStart(5) elif key == 'd': print "State Dump of connections" print "-------------------------" for c in self.factoryObj.clients: print "#" + str(c.connectionpos) + " " + c.storedState elif key == 'q': reactor.stop() else: print "--------------" print " If you haven't already, connect to this script via a" print " 'telnet localhost 5000' at least one (multiple connections" print " are better)" print "Press:" print " s - randomly start all clients" print " d - dump the state of all connected clients" print " q - to cleanly shutdown" print " Note: you can type commands in the connections, things" print " most useful of which is 'start'" print "---------------" # Custom tailored example for SO:30397425 # # This code is a mishmash of styles and techniques. Both to provide different examples of how # something can be done and because I'm lazy. Its been built and tested on OSX and linux, # it should be portable (other then perhaps termal cbreak mode). If you want to ask # questions about this code contact me directly via mail to mike at partialmesh.com # # While it isn't directly using serial ports, the tcp connections that its using are a good # parallel. # # It should be used by running the script and then opening up many windows telnet'ing into # localhost 5000. # # Once running press any key in the window where the script was run and it will give # instructions. # The normal use case would be to type "s" to queue statemachine # start-ups, then repeatedly press 'd' to dump the status of all the state machines # # 'start' can be typed into any of the telnet connections to start them by hand too. def main(): client_connection_factory = MyServerFactory() try: termstate = Cbreaktty(sys.stdin.fileno()) except IOError: sys.stderr.write("Error: " + sys.argv[0] + " only for use on interactive ttys\n") sys.exit(1) keyboardobj = KeyEater(client_connection_factory) stdio.StandardIO(keyboardobj,sys.stdin.fileno()) reactor.listenTCP(5000, client_connection_factory) reactor.run() termstate.retToOrgState() if __name__ == '__main__': main() 

It is possible that the USB-serial connection to the modem will be destroyed due to disconnecting the modem and re-establishing it when connected to it. How can I detect such events and add monitoring of the corresponding device ports to the reactor? I am currently doing this statically in the main application.

After research, I do not have a simple answer. I still suspect that the following logic will be close to a solution, but I was not lucky to find code that implements this today.

I suppose there will be a reasonable way to find out if a USB event has occurred, and work if a serial device has been added. But I doubt that there will be a good way to find out if this is one of your serial devices - much less if its a command line interface or diagnostics (if your building cannot manage USB device identifiers)

Events are triggered with serial port errors (at least from my experience with Linux), but I'm not sure how / where the USB adapter will be disconnected.


Other links that may come in handy

+6
source

All Articles