Gpiozero: issue with is_held

Created on 23 Oct 2017  路  8Comments  路  Source: gpiozero/gpiozero

I'd like to save seconds during a button is held. But I get an exception after few pushes. Why is _held_from in this case None? Is there a hardcoded threshold? Or do I have a mind mistake regarding parameter delivery to the counting_time function?

from gpiozero import Button
import signal
from functools import partial
import datetime


def counting_time(button):
    counted_time = datetime.timedelta(seconds=0)
    while button.is_held:
        counted_time = datetime.timedelta(seconds=button.held_time)
        #print('held_time: {}'.format(str(datetime.timedelta(seconds=button.held_time))),  end="\r", flush=True)
    #return(counted_time)
    print(counted_time)


def main():
    try:
        with Button(21, hold_time=0) as button:
            button.when_held = partial(counting_time, button)
            signal.pause()
    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    main()

Error

0:00:01.898711
0:00:01.186122
0:00:02.229292
0:00:00.131410
0:00:00.108006
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.4/threading.py", line 868, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.4/dist-packages/gpiozero/mixins.py", line 460, in held
    parent._fire_held()
  File "/usr/local/lib/python3.4/dist-packages/gpiozero/mixins.py", line 364, in _fire_held
    self.when_held()
  File "test.py", line 16, in counting_time
    counted_time = datetime.timedelta(seconds=button.held_time)
  File "/usr/local/lib/python3.4/dist-packages/gpiozero/mixins.py", line 432, in held_time
    return time() - self._held_from
TypeError: unsupported operand type(s) for -: 'float' and 'NoneType'
question

All 8 comments

Is there any update on this? Ideas, @bennuttall?

Sorry for the delay.

I don't have an issue with the following usage:

from gpiozero import Button
import signal
from functools import partial
import datetime

def counting_time(button):
    counted_time = datetime.timedelta(seconds=0)
    while button.is_held:
        counted_time = datetime.timedelta(seconds=button.held_time)
        print('held_time: {}'.format(str(datetime.timedelta(seconds=button.held_time))),  end="\r", flush=True)

button = Button(21)
button.when_held = counting_time

signal.pause()

except that the counter does not appear until after 1 second (the default hold_time).

Note that button.is_held can return False even if the button is pressed. Not until the button has been pressed and held for hold_time will it be True. And the same goes for held_time - that will be None until the hold threshold has been reached:

while True:
     print('Pressed' if btn.is_pressed else 'Not pressed')
     print('Held for {}'.format(btn.held_time) if btn.is_held else 'Not held')
     sleep(0.1)

this will print:

  • Not pressed and Not held while the button is unpressed
  • Pressed and Not held for 1 second
  • Pressed and Held for {time} while the button is held

Perhaps this will help?

If not, it could be related to your use of partial. Sorry but I don't see the purpose of this - could you explain? Are you aware that the Button object is passed into the function implicitly (if the callback takes an argument):

def pressed(btn):
    print("button {} was pressed".format(btn.pin.number))

btn2 = Button(2)
btn3 = Button(3)

btn2.when_pressed = pressed
btn3.when_pressed = pressed

Now when pressed:

button 2 was pressed
button 2 was pressed
button 3 was pressed
button 2 was pressed

I can replicate your error when using partial. An internal comparison takes place and it throws up an error as you posted. We have seen some odd behaviour when using partial - see #436.

My guess is that the partial callback is held as a weakref (in a similar way to #617) and is being gc-ed somewhere in the loop. With @bennuttall I don't believe you need the partial, but I'll look through the code to see if I can spot a weakref issue anyway.

A button must be pressed for at least minimum one second to use held_time()? hold_time=1
There are many cases where it doesn't make sense to press a button longer than a second.

So I'll have to build something myself to measure time.
held_time() would be a good and simple method in this case but maybe I can do it with pigpio or someone else has a better idea.

edit:

Sorry but I don't see the purpose of this - could you explain?

For a more complex program I have to save the time how long a button was pressed. It is possible that the button is pressed for only 0.01 seconds or that the button is pressed for 2 seconds or somthin else. Depending on this, other actions / functions are execute.

You can set hold_time to whatever you want. But you can also just use pressed_time to see how long it's been pressed, irrespective of the hold_time threshold.

>>> b = Button(2) # now press the button
>>> b.pressed_time
1.23456...

Sorry but I don't see the purpose of this - could you explain?

I actually meant why are you using partial when the button object is passed into the callback anyway?

I actually meant why are you using partial when the button object is passed into the callback anyway?

Because I dont't like global variables. But without partial and with pressed_time it is the same effect.
The problem, IMHO, is after or during leaving the while loop. print(counted_time)
Your example and my example works fine with print() in same indentation level as while loop. But in my case I need a retun value of counted_time. Maybe it helps if I put the value of counted_time in a Queue.

from gpiozero import Button
import signal
import datetime

button = Button(21, hold_time=0.0)

def counting_time():
    counted_time = datetime.timedelta(seconds=0)
    while button.is_held:
        counted_time = datetime.timedelta(seconds=button.pressed_time)
        # print('held_time: {}'.format...) <--- works but I need the value after ending/exit while
    print(counted_time)
   #return(counted_time)


def main():
    try:
        with button:
            button.when_held = counting_time
            signal.pause()
    except KeyboardInterrupt:
        pass


if __name__ == '__main__':
    main()
0:00:02.941736
0:00:02.536089
0:00:00.292461
Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.4/threading.py", line 868, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.4/dist-packages/gpiozero/mixins.py", line 460, in held
    parent._fire_held()
  File "/usr/local/lib/python3.4/dist-packages/gpiozero/mixins.py", line 364, in _fire_held
    self.when_held()
  File "zeit.py", line 13, in counting_time
    counted_time = datetime.timedelta(seconds=button.held_time)
TypeError: unsupported type for timedelta seconds component: NoneType

I surrender. I do not find a solution, Only try ... except statement keeps the program running.

        try:
            #counted_time = datetime.timedelta(seconds=(button.held_time) if not None else 0)
            counted_time = datetime.timedelta(seconds=button.held_time)
        except TypeError:
            print("TypeError")
            continue
0:00:00.337018
TypeError
0:00:00.959058
0:00:00.319658
0:00:00.145064
0:00:00.150357
0:00:00.446121
TypeError
0:00:01.663353
0:00:01.123233
0:00:00.517127
0:00:00.170076
TypeError
0:00:00.201232
0:00:00.306588
0:00:00.873242
0:00:00.163394
0:00:00.157065
TypeError
0:00:00.153106
0:00:00.137756
0:00:00.244023
TypeError
0:00:00.254869
TypeError
0:00:00.186886
TypeError
0:00:00.187203
0:00:00.175504
TypeError
0:00:00.701250
0:00:00.659084
TypeError
0:00:00.858553
0:00:00.463715
0:00:01.045953

I'm afraid you are mixing paradigms and expecting things to just work. Please understand the four different approaches used in gpiozero:

  • procedural with polling:

    while True:
      if button.is_pressed:
          led.on()
      else:
          led.off()
    
  • procedural with blocking:

    while True:
      button.wait_for_press()
      led.on()
      button.wait_for_release()
      led.off()
    
  • event-driven:

    button.when_pressed = led.on
    button.when_pressed = led.off
    
  • declarative:

    led.source = button.values
    

Each has its own advantages/disadvantages, and you should understand the constraints of your chosen paradigm. If you're happy using a procedural approach, that's fine, but remember that's you're now stuck in a while loop and can't do anything else at the same time. If you're using event-driven, don't put blocking while loops in your callbacks!

However, I think we can avoid the issue you've been hitting by changing the way held_time is calculated. I'm closing this issue and I'll open a new one to address this particular change.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lurch picture lurch  路  6Comments

claussoegaard picture claussoegaard  路  10Comments

bsimmo picture bsimmo  路  6Comments

driesdepoorter picture driesdepoorter  路  9Comments

waveform80 picture waveform80  路  11Comments