Search code examples
pythoncallbackraspberry-pirobotics

Python: Passing a class member function to another class's callback


Can I pass class A into class B so that B can run a callback using A's member function?

I am trying to write a Python leg class for a robot I am building. I am using a Raspberry Pi as the main computer, and Martin O'Hanlon's KY040 rotary encoder library KY040 to detect each 1/4 rotation of the leg. To this end, I watch for the first of several clicks, sleep for a short time, stop the servo, and now a 1/4 rotation has been achieved. In standalone, unthreaded code this works fine, but creating a class has been a challenge.

Details:

A threaded sentinel loop watches a boolean (quarterTurn) to signal that a rotation must be carried out.

def run(self):
    print "leg running"
    while self._running:
        sleep(.0001)
        if self.quarterTurn:
            print "quarterTurn is: " + str(self.quarterTurn)
            self.qTurn(self.quarterCount)

qTurn accesses a pwm controller to activate the motors, and reset quarterTurn to false.

def qTurn(self, quarters):
    count = 0

    while count < quarters:
        sleep(.0001)
        self.setMotor(self.maxPulse)

        if self.ClickedOnce:
            count = count + 1
            sleep(.17)
            self.parkMotor()
            sleep(.04)
            self.clickedOnce = False

    self.quarterTurn = False

The trick is that O'Hanlon's class is already threaded. On one hand, it is convenient, on the other, it makes my class more complex. The KY040 makes use of a callback function to provide feedback, but using this within my class is the source of my trouble.

I need the callback to modify a a boolean in my leg class, but this function is only called by the KY040 class, which tries to pass itself into the function.

def rotaryChange(self, pin):
    self.clickedOnce = True

Since the code is open source (thank you, O'Hanlon), I thought I could modify the constructor of the KY040 to let me pass my leg class into it, so that I could modify the correct data.

O'Hanlon's Original Constructor:

def __init__(self, clockPin, dataPin, switchPin=None, rotaryCallback=None, switchCallback=None,rotaryBouncetime=250, switchBouncetime=300):
    # persist values
    self.clockPin = clockPin
    self.dataPin = dataPin
    self.switchPin = switchPin
    self.rotaryCallback = rotaryCallback
    self.switchCallback = switchCallback
    self.rotaryBouncetime = rotaryBouncetime
    self.switchBouncetime = switchBouncetime

    #setup pins
    GPIO.setup(clockPin, GPIO.IN)
    GPIO.setup(dataPin, GPIO.IN)

    if None != self.switchPin:
        GPIO.setup(switchPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

I added a "host" variable, into which I pass the leg class:

def __init__(self, clockPin, dataPin, switchPin=None, rotaryCallback=None, switchCallback=None, host=None, rotaryBouncetime=250, switchBouncetime=300):
    # persist values
    self.clockPin = clockPin
    self.dataPin = dataPin
    self.switchPin = switchPin
    self.rotaryCallback = rotaryCallback
    self.switchCallback = switchCallback
    self.rotaryBouncetime = rotaryBouncetime
    self.switchBouncetime = switchBouncetime

    # My Change
    self.host = host

    #setup pins
    GPIO.setup(clockPin, GPIO.IN)
    GPIO.setup(dataPin, GPIO.IN)

    if None != self.switchPin:
        GPIO.setup(switchPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

The modified constructor would be called like so:

self.encoder = KY040(self.clockPin, self.dataPin, rotaryCallback=self.rotaryChange, host=self)

O'Hanlon's callback now passes the host along:

def _clockCallback(self, pin):
        # My change
        self.rotaryCallback(pin, self.host)

My new callback:

def rotaryChange(pin, host):
    host.clickedOnce = True

Unfortunately, after making sure the modified code is installed with the setup script, it doesn't seem to acknowledge my new additions. I run my program and receive the follwing error:

    Traceback (most recent call last):
    File "ctf.py", line 18, in <module>
      LR = leg.leg(lr_chan, lr_max, lr_park, lr_clk, lr_data);
    File "/home/[user]/hexacrescentapod/leg.py", line 47, in __init__
      self.encoder = KY040(self.clockPin, self.dataPin, 
    rotaryCallback=self.rotaryChange, host=self)
    TypeError: __init__() got an unexpected keyword argument 'host'

Solution

  • This is a little confusing because of your wording. Are you actually trying to pass a class in as you say, or an instance of that class as you seem to be doing? Which class is rotaryChange defined in?

    Anyway, it looks like what you're actually trying to do is pass self.rotaryChange as a callback.

    This already works, without any changes. self.rotaryChange is a bound method, meaning it knows what that self was when it was created, and will pass it when it's called. This may be easier to see with an example:

    >>> class Spam:
    ...     def eggs(self):
    ...         pass
    >>> spam = Spam()
    >>> spam
    <__main__.Spam at 0x119947630>
    >>> spam.eggs
    <bound method Spam.eggs of <__main__.Spam object at 0x119947630>>
    

    Notice that it's a bound method of the spam object. When you call spam.eggs(), that spam object will be passed as the self argument.

    This means you don't need to pass a host in, because it's already available as self. And, since that's the only thing you do with host, you don't need to pass around host in the first place. Which means you can revert all of your changes to the library code.

    You do need to define your callback method as a proper method, with self as the first argument. But that's it. Then you can just pass rotaryCallback=self.rotaryChange to the constructor, and everything will work.