New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Button.when_held(function_ref, seconds) #115
Comments
I like the idea - could be handy. I'm not sure how it would work as you can't set up multiple events for different lengths of time, you'd have to wrap it in something, and then timeout on release...? |
Any ideas @waveform80? Would this be possible? |
Erm ... yeah, it's certainly possible - the question is how to do it reasonably safely (it'll necessarily involve a background thread to note when a time-threshold has elapsed) which then throws up interesting questions like "is the threshold configurable after construction", and "if the threshold is configurable, what happens if someone changes it while the held-thread is executing after a button press but before event firing", etc. I'll put my thinking cap on and have a play with this but it'll be a little while yet - I want to plough through a few more 1.2 tickets first. |
Ok let's leave it for now - there are plenty more to do for 1.2 (I just tagged a few more). Though as before, we can postpone a few if necessary. |
I might be adding this to RPi.GPIO in the next few months. |
What if the thread itself wasn't configurable after construction, but the thread only gets created (if the user has set up a |
It's an interesting idea but if you create lots of threads you've got to be careful about join()ing them again or you quite quickly exhaust the thread limit of the process (in other words, it's certainly do-able but one needs still needs to be a little careful). |
Sadly, even if it were added to RPi.GPIO natively I doubt we could take advantage of that; with pins now abstracted into their own hierarchy we'd have a choice if RPi.GPIO supports "when_held" but everyone else doesn't: add it to the pin abstraction and then implement an emulation for it in the RPIO and native pin modules (and pigpiod and potentially wiringpi in future), or just implement it ourselves at the device level (probably in DigitalInputDevice). Frankly I suspect we'll wind up going the latter route simply because it's less code. |
I needed this function, so I have created a 'HoldableButton' class which inherits from Button. from gpiozero import Button
from threading import Timer
class HoldableButton(Button):
def __init__(self, pin=None, pull_up=True, bounce_time=None, hold_time=1, repeat=False):
super(HoldableButton, self).__init__(pin, pull_up, bounce_time)
# Set Button when_pressed and when_released to call local functions
# cant use super() as it doesn't support setters
Button.when_pressed.fset(self, self._when_button_pressed)
Button.when_released.fset(self, self._when_button_released)
self._when_held = None
self._when_pressed = None
self._when_released = None
self.hold_time = hold_time
self.repeat = repeat
self._held_timer = None
#override button when_pressed and when_released
@property
def when_pressed(self):
return self._when_pressed
@when_pressed.setter
def when_pressed(self, value):
self._when_pressed = value
@property
def when_released(self):
return self._when_released
@when_released.setter
def when_released(self, value):
self._when_released = value
@property
def when_held(self):
return self._when_held
@when_held.setter
def when_held(self, value):
self._when_held = value
def _when_button_pressed(self):
if self._when_pressed != None:
self._when_pressed()
self._start_hold()
def _when_button_released(self):
if self._when_released != None:
self.when_released()
self._stop_hold()
def _start_hold(self):
if self._when_held != None:
self._held_timer = Timer(self.hold_time, self._button_held)
self._held_timer.start()
def _stop_hold(self):
if self._held_timer != None:
self._held_timer.cancel()
def _button_held(self):
self._when_held()
if self.repeat == True and self.is_pressed == True:
self._start_hold()
if __name__ == "__main__":
from time import sleep
def button_pressed():
print("pressed")
def button_released():
print("released")
def button_held():
print("held")
pin = 26
but = HoldableButton(pin, hold_time = 0.5, repeat = False)
but.when_held = button_held
but.when_pressed = button_pressed
but.when_released = button_released
while True:
sleep(0.1) This feels like a more 'appropriate' implementation, rather than complicating Button which should itself be simple. @bennuttall @waveform80 - Thoughts? |
Interesting stuff - this could definitely form the basis of a holdable button implementation. Only one thing's immediately jumped out at me: if the user assigns a long-running when_pressed handler (e.g. just for the sake of argument |
Good spot @waveform80. |
I wonder if it'd be worth also adding a if self._when_released != None:
try:
self.when_released(time.time() - self._press_time)
except TypeError:
self.when_released()
finally:
self._press_time = None (while leaving all the other Timer stuff in there too) and then the caller could choose to use either Hmmmm... although I guess my modification above is 'incompatible' with the logic of https://github.com/RPi-Distro/python-gpiozero/blob/master/gpiozero/input_devices.py#L153 - I can't quite tell how |
@lurch - interesting query: good catch on |
I've just been thinking about the 'repeat' aspect of this and how it interacts with 'hold_time'... (similar to what @waveform80 was talking about in #115 (comment) ) If the user sets a def _stop_hold(self):
if self._held_timer != None:
self._held_timer.cancel()
self._held_timer = None (which might be worth doing anyway?), and then modify def _start_hold(self):
if self._when_held != None:
if self._held_timer != None:
hold_time = self._held_timer.interval
else:
hold_time = self.hold_time
self._held_timer = Timer(hold_time, self._button_held)
self._held_timer.start() ? And I wonder if a similar argument could be made for the Apologies if I'm over-complicating things, but it's always worth considering edge-cases. |
I think your over complicating the problem. A typical use case would be a volume up button with a very short hold time so you don't have to press a button 100 times to get from 0% to 100%, its unlikely that properties would be changed at run time. However from my perspective it works as intended. At initiation (ie the button being pressed) the properties in place should be used, changing behaviour part way through an execution would be confusing. |
That sort of ties in with what I was saying about |
I've made a couple of changes to my HoldableButton class, most notably adding a is_held property. I've put it on a gist - https://gist.github.com/martinohanlon/20cee570d6ea4ca0b7ad |
It looks like if the |
@lurch good spot... I hadnt thought of that. I'll make a change. Edit - change made! |
Cool! It's only a minor point, but I guess def _button_held(self):
self._is_held = True
if self._when_held is not None:
if self.repeat and self.is_pressed:
self._start_hold()
self._when_held() i.e. the And your idea of a @property
def pressed_time(self):
if self.is_pressed:
return time.time() - self._press_time
else:
return 0 |
@waveform80 is this one for v1.2 or v1.3? |
I think 1.2 - it's looking good to me I just haven't had 5 minutes spare to play/torture it myself yet. One quick question: is there any particular reason this shouldn't just be in the Button class (I can't remember if I mentioned this before). I'm also need to test @lurch's suggestions involving repetition as that's quite an important use-case (the holding volume up/down button is an excellent example that probably ought to become a recipe albeit with a PWMLED instead of actual volume). Let me polish off these tests, get the robot-keyboard example up to scratch (that'll take me up to 5, then I've got baby-sitting duties) then I'll try and look at this. |
No reason why it couldn't go in the Button class. My initial views were that it might be worth keeping Button simple and inheriting from Button made the implementation simpler, but no strong views, from a interface perspective adding this capability to Button doesn't change its existing simple interface providing you default hold_time and repeat in the constructor. A few of @lurch 's suggestion have been incorporated in the class I've been using in my project, see the gist. I did ponder making hold_time and repeat private properties as it seemed an odd use case to change the hold_time and repeat after you had initialised the object, if they are kept public, I would leave the implementation as is, as the implementer can decide whether the values of held_time and repeat can be updated based on whether the button is held down. |
What's the status of this? Are we waiting for a PR from @martinohanlon? |
I'm going to look into this tomorrow now I've gotten most of the other tickets done. As it happens I think the use-case of changing hold_time is actually going to be rather common: consider how keyboards handle repetition. There's a long initial delay, then a shorter inter-repeat delay (to avoid accidental repetition). If people want to emulate that (e.g. for holding a volume-up button) their first thought is going to be something like: from gpiozero import Button
volume_up = Button(4, hold_time=0.5)
def volume_up_held():
volume_up.hold_time = 0.1
# turn the music up!
def volume_up_released():
volume_up.hold_time = 0.5
volume_up.when_held = volume_up_held
volume_up.when_released = volume_up_released So I want to put some rigorous testing into edge cases of that (I have a sneaking suspicion there's a race condition in there still, and I think I might be able to make a slightly simpler implementation that doesn't ghost all the properties by overriding _fire_events, but we'll see). |
I've had a play with this today and here's some thoughts and I'll PR an alternate implementation in a mo... It's a rather different design to @martinohanlon's original gist and incorporates some thoughts that came up after @lurch's notes above. Martin's design is good in that it affords predictable, precise repeat intervals by spawning a new thread each time a hold-wait needs to be undertaken. Unfortunately, this means it's possible (even likely in some scenarios, which I'll get to below) to have multiple Let's consider a long running
Note the handlers are nicely fired on the 0.0, 0.5, 1.0, etc. boundaries, but that each is effectively a new thread. Provided there's no global state or inter-thread interactions that's fine, but if there are, things are going to blow up in interesting and unpredictable ways (multi-threaded debugging is always horrid and not something we want to force down people's throats too quickly). If anyone's wondering how likely this sort of scenario is, here's an example from my reams of picamera e-mail. A picamera user wanted to implement something like the burst mode on his hand-held camera so that pressing the button would snap a picture, but holding it would repeatedly capture images. Bear in mind that the camera has to be a global or shared object in this scenario, and that high quality still capture typically takes somewhere in the region of 1 second with the Pi's camera module (due to mode switching). It should be easy to see how this would lead to a crash with the model above. So, can we come up with a better model? Well ... I've got a friendlier model, but while it's easier I hesitate to say it's actually "better". In this model we fire up a single thread when the button is pressed. This thread waits on the hold time, fires the
If the event handler is fast enough there'll be precious little difference to Martin's model but obviously longer handlers (like a camera capture) result in substantial differences (in other words, it's probably worth documenting if we go with this model). Still, it works nicely with the camera example, and with the use-case of changing the hold time while the handler's running (simple in this model as it just applies to the next waiting time after the handler's finished). Comments? |
As long as when_held and is_held are implemented im cool.
|
Doh - I forgot is_held. I'll just bung that in and then push it. |
That made me laugh...
|
Adds when_held event hook to Button (via extension of the EventsMixin class)
There we go, it's in PR #260 |
Eurgh, and I've forgotten to properly implement hold_repeat as well ... bah. Oh well, that'll have to wait until tomorrow - I've got a horribly early start so I've gotta head off now but I'll finish it off tomorrow. |
Any scope for adding the pressed_time idea I had? Or is that something that it'd be better for the user to implement themselves using when_pressed and when_released, rather than adding extra bloat to the API? |
Yeah, why not - it's simple enough and this is the "Button" class after all (not some abstract parent) so let's throw in all the functionality we want. |
Actually, now I've looked at - that's kinda useful in a whole series of scenarios (not just buttons) ... I might just rename it |
Adds when_held event hook to Button (via extension of the EventsMixin class)
Cool! :-D |
Adds when_held event hook to Button (via extension of the EventsMixin class)
So, what about my other idea of the |
I think I'll pass on that one for now. It's a little too big a change for 1.2 and I've got plenty of work to do on the test suite today (got a couple of persistent failures on the Pi and the real-pin PWM tests are being extremely annoying with their segfaults). Worth re-visiting in 1.3 though. |
Huh... just noticed the codecov stuff doesn't seem to be working either (was just checking what coverage was missing for mixins) |
Okay, let's merge this to let people play with it - I'll do another PR for the tests in a bit. |
I'm looking for some pointers... I think it would be a good to add Button.when_held which would fire a function when a button has been held down for a period of time. Its achievable by timing when_pressed and when_released but its a function I use a lot to determine between a short press and a long press. Presumably it would also be a good idea to have a wait_for _held(seconds, timeout=None)?
Providing you agree Im happy to make the change and issue a pull but could I get some pointers as to where the change should be made as Im struggling to find my path through the modules structure.
The text was updated successfully, but these errors were encountered: