【问题标题】:How to deal with multiple serial ports for R/W using twisted?如何使用twisted处理多个R/W串口?
【发布时间】:2015-05-22 13:01:48
【问题描述】:

浏览扭曲手指教程并查看 SO 问题:

但是,我(还)不能编写一个可以从多个串行端口读取和写入的扭曲程序,尤其是在协议涉及读取单行或多行并相应地写回设备的情况下。

我要做的是为 2 个调制解调器打开 2 对(即总共 4 个)串行端口。与调制解调器的通信使用 Hayes AT 命令集。虽然与调制解调器的大多数命令/响应交换是通过命令端口进行的,但对于每个调制解调器,只有很少的诊断信息只能通过诊断端口获得。诊断信息应导致状态机(设备状态、连接状态)被修改。

这是我理解为潜在方法的粗略骨架程序(基于单端口示例):

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)

但是,我不知道如何执行以下操作:

  • 如何/在哪里接受来自用户的 CLI 输入,然后触发向调制解调器发送一组 AT 命令?
  • 将命令端口上接收到的信息关联起来,用于调制解调器 1 的 ttyUSB0 和 ttyUSB1 以及调制解调器 2 的另一对?请注意,每个调制解调器都有自己的状态机(设备状态和连接状态)
  • twisted 是否提供任何机制来按应用程序管理多个状态机?
  • 与调制解调器的 USB 串行连接可能由于调制解调器被拔出而被破坏,并在重新插入时重新建立。如何检测此类事件并将相应设备端口的监控添加到 reactor ?目前,我在主应用程序中进行静态处理。

【问题讨论】:

  • 必须是Twisted吗?
  • 并非如此。如果有更简单的解决方案,我宁愿从它开始。性能可能是一个问题,但不是一个直接的问题。异步事件模型是 Twisted 吸引我的地方,但我不得不承认这有点困难(尤其是对于赶时间的人)。
  • 我想建议你看看使用 circuits 来解决这个问题,因为它是事件驱动的,具有更好的架构和一组 API(s ) 来处理这种“并发”和“协调”,而不会迷失在“回调”中。
  • 这正是twisted擅长的项目类型。它是一个具有陡峭倾斜曲线的框架,但是当掌握它时,它为 python 中的多方面 IO 提供了几乎最佳的速度和灵活性。我找到的最好的教程是 krondo Twisted Introduction krondo.com/?page_id=1327
  • @JamesMills,首先感谢您向我指出电路。在缩小扭曲范围之前,我已经完成了我认为相当彻底的研究(尽管主要是理论上的,即没有编写任何代码),而且“电路”甚至没有出现在我的雷达上。我已经完成了它的初学者教程,我不得不承认它非常容易上手。我发现有一个串行通信组件,还有一个例子,虽然通知它未经测试,但有点令人担忧。电路是否有活跃的社区和用户群?

标签: python serial-port twisted


【解决方案1】:

注意您的示例代码

我没有看到您在将类注册到反应器之前实例化它们。我预计那会失败得很惨。这是我的运行代码的类似sn-p:

# 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)

我如何/在哪里接受来自用户的 CLI 输入,然后触发向调制解调器发送一组 AT 命令?

就像您如何将串行处理程序注册到 Twisted 中一样,您可以为标准 io 注册处理程序,例如:

# 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())

将调制解调器 1 的 ttyUSB0 和 ttyUSB1 命令端口接收到的信息相关联,调制解调器2 的另一对也类似?请注意,每个调制解调器都有自己的状态机(设备状态和连接状态)

在正常使用中,每个连接实例都有自己的状态机(包装在与连接一起注册到反应器中的类的实例中)。

作为程序员,您可以选择如何连接类的状态,但通常是通过推送对合作伙伴类的引用。

下面,此答案包含可运行的代码,将说明数据如何在状态机/接口之间连接。此 SO 中也说明了这一点:Persistent connection in twisted


twisted 是否提供任何机制来管理应用程序的多个状态机?

如果“应用程序”是指“你的扭曲代码”,那么答案是肯定的!

典型的 Twisted 应用程序是一组状态机,所有这些都具有一些非常明确的接口。我开始了我的 Twisted 冒险,打算用两个状态机(一个串行和键盘)编写一个应用程序,但是当我对 twisted 感到满意时,我意识到添加额外的接口和状态机是微不足道的(通过所有的奇迹的 tx 库)。在一个下午,我添加了一个粗略的 Web 界面,一个 websocket 界面,然后在两者上放置了 SSL,甚至还添加了一个 SSH 调试界面。一旦你得到一个滚动,添加接口和状态机就变得微不足道了。

在许多(所有?)情况下,扭曲模型是状态机将驻留在实例化类中,该类与连接绑定并已注册到(唯一的)主事件中-循环。

使用产生新状态机的连接类型(想想 http 连接),您注册一个工厂类/状态机以及侦听连接,它们共同使应用程序能够为每个新状态机产生新的类/状态机联系。 Twisted 应用程序在大规模运行时通常有 10 甚至 100 万个并发状态实例。

如果您尝试将不同的协议和状态粘合在一起,Twisted 会非常棒(......所有这些都在您选择的事件循环中(select/epoll/kqueue/etc))

以下是可运行的示例代码,可以说明其中的许多要点。阅读def main() 之前的 cmets 了解更多代码背景:

#!/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()

调制解调器的 USB 串行连接可能由于调制解调器被拔出而被破坏,并在重新插入时重新建立。如何检测此类事件并将相应设备端口的监控添加到 reactor ?目前,我在主应用程序中进行静态处理。

经过研究,我没有一个简单的答案。我仍然怀疑以下逻辑将接近解决方案,但我今天没有找到实现此功能的代码。

我的猜测是有一种合理的方法可以确定是否发生了 USB 事件,并确定是否添加了串行设备。但我怀疑是否有一种好方法可以确定它是否是您的串行设备之一 - 如果它是您的命令或诊断接口则更不用说(除非您的构建硬件并且可以控制设备的 USB ID)

事件会因串行端口错误而触发(至少从我在 linux 上的经验来看),但我不确定 USB 拔出将如何/在何处注册。


其他可能对您有用的链接

【讨论】:

  • 非常感谢 Mike 提供了一个相当全面的答案,并分享了你相当鼓舞人心的学习经历。已经了解对于每个连接实例都有一个特定于接口的状态,但是,我认为我需要在 Twisted 方面取得一些重大进展,然后才能完全理解和欣赏 2 个连接(在不同接口上)之间的状态相关性如何成立。
  • @icarus74 谢谢!我的目标是(至少)对这个答案再做一次刷新,当我这样做时,我将尝试考虑我将如何在状态机之间、键盘状态机和串行机之间进行通信的示例(这将适用于两台串行机器之间)
  • 优秀。敬请期待。
  • @icarus74 我添加了一个扩展的(可运行的)示例代码块,它应该说明任何其他模糊点。请务必阅读def main() 上方的 cmets 块并运行代码以更好地了解它的用途。总而言之,我认为这代表了对您问题的一个相当完整的答案,所以如果您同意,如果您将此标记为答案,我将不胜感激。
  • @icarus74 看起来很棒!我将不得不调查它是否会帮助我(乍一看它看起来会,感谢您指出这一点!)。在查看其他库时,请记住您必须将它们扭曲化,否则它们会冻结扭曲的反应器。搜索“pyudev”和twisted yeilded pastebin.com/TnrsS15s,这可能会成功
猜你喜欢
  • 1970-01-01
  • 2016-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多