Search code examples
twisted

Creating an "On Demand" event?


I am writing a plugin for existing software that will use twisted to communicate with a Denon AV receiver. I've created a method, powerDenonOff, in my protocol for turning off the device, and have the reactor start in a thread when the application launches via runConcurrentThread. I have another method, powerOff, which will be called when the user selects to turn the device off in the software. I'm having trouble figuring out how to call my powerDenonOff method in the protocol from the Plugin class.

from twisted.internet.protocol import Factory, Protocol
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor

class DenonProtocol(LineReceiver):
    delimiter='\r'

    def lineReceived(self, line):
        pass

    def connectionMade(self):
        pass

    def powerDenonOff(self):
        self.transport.write("PWSTANDBY")

    def __call__(self):
        pass

class DenonFactory(Factory):

    def __init__(self):
        pass

    def startedConnecting(self, connector):
        pass

    def clientConnectionFailed(self, one, two):
        pass

    def connectionMade(self):
        pass

    def buildProtocol(self, addr):
        return DenonProtocol()

class Plugin(software.PluginClass):

    def powerOff(self):
        reactor.callInThread(powerDenonOff) #I think this may need to be callFromThread but
                                            #but I left it since that was where the poorly
                                            #worded version of my question left off.

    def runConcurrentThread(self):
        try:
            while True:
                #hardcoded for testing
                port = 23
                host = "111.11.11.11"

                denon=DenonFactory()
                reactor.connectTCP(host, port, denon)
                reactor.run()
        except self.StopThread:
            pass

    def stopConcurrentThread(self):
        reactor.callFromThread(reactor.stop)

How do I reach the method in my protocol class? Is there a better way to accomplish this?

Thanks


Solution

  • You can't call reactor.run more than once, so the loop in runConcurrentThread won't work. You need to start the reactor exactly once per process. You can call connectTCP any number of times and they'll work as long as the reactor gets started at some point.

    If runConcurrentThread is called in a non-main thread, then you also need to start the reactor without child process support (child processes are only supported when the reactor is run in the main thread).

    If stopConcurrentThread is called in the same thread as runConcurrentThread then it doesn't need to use reactor.callFromThread. However, if runConcurrentThread is supposed to block until the plugin is done, then there's no way stopConcurrentThread could be called in the same thread, so the use of reactor.callFromThread must be correct.

    reactor.run will never raise StopThread. It will just return.

    As far as how to call methods on the protocol instance "from" the plugin, this is just a question about managing references in Python. Recall that protocol instances are given a reference to their factory (your factory doesn't do this, but it could) and your plugin class is the code that creates the factory. This means you should be able to write plugin code that refers to the protocol via the factory.

    However, you could also use one of the more convenient APIs - for example, endpoints:

    endpoint = TCP4ClientEndpoint(reactor, host, port)
    factory = ...
    d = endpoint.connect(factory)
    def connected(protocol):
        protocol.powerDenonOff()
        protocol.transport.loseConnection()
    d.addCallback(connected)
    

    This is just one possible approach - I don't know what kind of connection management makes sense to your application. If you want to maintain a single long-lived connection, then you might save the protocol reference for later use rather than calling powerDenonOff and then disconnecting it. I suspect that since the method here is for turning power off, the connection might not live much longer anyway though...

    Crochet will help you do most of the more boring and finnicky parts of this. I suggest taking a look at it sooner rather than later.

    For the rest, you just need to be very clear about what code runs in what thread. The only Twisted API you are allowed to use in a different thread than reactor.run was called in is reactor.callFromThread. You can use this to call other Twisted APIs in the reactor thread. Also keep in mind that you can only run one reactor per process, and you can only run it once. The rest is just keeping track of references to the objects you need to use.