Consider some form of event handling execution model
The excution model for uPy on the microbit is currently a single thread executing with full control. There are idle and system "threads" behind the scenes, but these are used to update low-level things like the display and are not exposed to the Python API (except as background animations).
TD has event handlers for things like button presses, and there have been requests for such things in uPy. Eg:
def click_handler():
print("you clicked the button!")
button_a.onclick(click_handler)
A "click" is defined as a short press down and release of the button. It's not easy to write a polling loop to detect a click event, and even more difficult to do other things while checking for such a click (basically you need to write your own cooperative scheduler).
There are a few ways I can think to make the above code work. They vary significantly in complexity and use.
Simple events
Queue events in the background and expose them using simple methods. Eg: button_a.was_clicked() -- this will check to see if the button was clicked since the last time this function was called. If it was then it returns True and resets the event state. Otherwise it returns False. In this case there are no event handlers, it's really just polling (but polling for button events, not simple button status).
Explicit event handler Allow event handlers to be registered but force the user to periodically execute an "event check and handle" function. Eg:
def click_handler():
print("you clicked the button!")
button_a.onclick(click_handler)
while True:
microbit.event_handler() # check for pending events and execute the handlers if needed
# do other stuff
This is cooperative "multitasking". It makes the user aware that they need to do something special to get the event handlers to run, and gives control to the user when such callbacks are executed.
Interrupt-like handler Allow event handlers to be registered and execute them like an interrupt (or Linux signal): when the event fires a short time later the main thread is paused and execution is passed to the relevant event handler. Only when the event handler is finished does execution return to the main thread. (The event handler would execute on the same stack as the main thread and no task switching is needed.)
This leads to less problems with data locking and deadlocks, but does expose a degree of preemptive issues to the kids. Eg an event handler can modify a list that you are currently iterating through.
Technically this scheme would be implemented by the VM periodically checking for pending events and pausing the current bytecode when an event comes up. That means event handling can't preempt a C/C++ function, but I think that's a good thing (keeps things simpler) and don't think it'll be a problem since all C/C++ functions eventually return (except panic!).
Full monty Have actual threads that execute concurrently. This is way too complex (for the kids, and for us to implement), and I don't think we even need the power/flexibility that this offers.
Please raise your opinion on this very interesting topic!
From a view of someone working extensively with 11 year old kids, and also having done a lot of coding using the TD and CK event mechanism, I'd like to see at least simple events in the core API. I've discussed this at length with Howard - not just kids, even adults fall over with the event handlers. At a meeting recently, 95% of the attendees (everyone except me) painted themselves into a concurrency corner they didn't understand and got strange behaviour they could not explain.
My view is that being able to check the present state, and checking if an event has been seen since you last checked, is the minimum you need. Event handlers and callbacks get complex very quickly, and perhaps are not part of the core API that we talk about, but more part of the advanced API.
As an interesting aside, most of the TD programs I have written, I set a 'pressed' flag in the on_handler and then check the flag in the main loop. The only other design pattern I use is to increment a variable inside the on_handler so I know how many presses (useful for pressing a button that increments a number, if the top loop might block for other reasons). In TD you have to use global variables to signal as there is no mechanism for accessing the scope of the containing function. This is quite an additional tricky concept to explain to people. The CK example was a 4 line program in the event handler, and if you repeatedly pressed the button you immediately got loads of fibers running concurrently and causing very confusing glitching on the display ... on_a_pressed() { display.print(smiley); sleep(1); display.print(saddie); sleep(1) }
As discussed with some individuals at length previously (captured here now for central inspection) I believe the same simple API could be consistently used for gestures (if gestures.was_shake():...)
I'm open to discussion on event counting, perhaps the core api is was_pressed() and the flag is cleared, but there is an addtional API for how_many_presses() that returns a hidden counter, clears the counter (and clears the flag?). Those are the two most common use-cases for non polling button handling in my experience - callbacks (event handlers) make it look simple but drag people into additional concurrency complexity that teachers are not well equipped to explain, and kids don't understand unless it is well explained to them.
I don't know how practical it is, but scratch's event model seems to work well for children.
This boils down to
when (event) -> Do block
And also "broadcast(event)"
http://wiki.scratch.mit.edu/wiki/Events_Blocks
The obvious approach of aping scratch might be to just allow the user to have functions like:
def whenButtonApressed():
do stuff
That then closely mirrors the scratch stuff, but that seems to me to be the wrong route here.
Now, I'm not convinced that encouraging children to try and write "scratch
in python" is a good idea any more than writing "java in python" is a good
idea. That said, the event model would be familiar - for at least some. (At
minimum they'll all have used the "when
Rather than a callback oriented approach though - which can be difficult to map to a game - or quiz/etc - maybe a simplification of pygame's event model is a good idea. cf:
https://www.pygame.org/docs/ref/event.html
For example
microbit.events_happened -> True/False microbit.wait_events() -> Sleep until any event happens microbit.wait_events(List,Of,Event,Types) -> Sleep until any of the given event types happen microbit.what_happened() -> Returns the event microbit.broadcast_event(object) -> Posts a new event onto the event queue
- could be limited to just integers, or just named objects.
(I'm not saying this is the best place/naming, just sketching an idea :-)
The simplest events MIGHT be BANG, BUTTON, MOVE, SHAKE, etc. (ie the micro-bit equivalent of the sorts of events pygame gives you)
This is incidentally similar to what the Amiga used to use in AmigaOS's "Intuition" model of events. (The library was called that, I'm not claiming it was intuitive!)
These might be used as follows:
microbit.wait_events() event = microbit.get_event() if event == BANG: print("OUCH") if event == BUTTON: if microbit.button_was_pressed("A"): print("Button a was pressed!") if microbit.button_was_pressed("B"): print("Button B was pressed!") if event == MOVE:
Check accelerometer and do something
The broadcast_event() thing would allow a user to monitor a pin value and post the fact they've noticed a change. That then starts to allow more complex behaviours, while still using the same event model.
For example:
CATFLAPOPEN = 1 # MAGIC Numbers Bad ? CATFLAPOPEN = object() # Alternative? Seems obscure CATFLAPOPEN = Event() # Alternative? Seems bit better?
while True: while microbit.events_happened: event = microbit.what_happened() if event == CATFLAPOPEN: print("Tiddles is home!") if event == BANG: microbit.io.pin1.digitalWrite() =1 # Trigger cat feeder
if microbit.io.pin0.digitalRead() ==1: # eg catflap open microbit.broadcast_event(CATFLAPOPEN)
sleep(a_little_while)
Something like this might seem pretty trivial, but I've seen impressive things done using the scratch event system, and I think the reason is because it is naturally very simple at it's core.
In a way, thinking about it, taking a slightly pygame-y route gives them a growth model for taking what they've learnt on the micro-bit and using it beyond the micro:bit, which seems quite nice.
That said, the pygame zero people do take precisely the approach I said above which I discounted - that of having specifically named functions ( http://pygame-zero.readthedocs.org/en/latest/hooks.html). I'm not sure how that plays out if someone might want to create their own events based on bananas being plugged in as "keys", etc.
Personally, I wouldn't make it any more complex than this though.
Michael.
On 28 September 2015 at 16:24, David Whale [email protected] wrote:
From a view of someone working extensively with 11 year old kids, and also having done a lot of coding using the TD and CK event mechanism, I'd like to see at least simple events in the core API. I've discussed this at length with Howard - not just kids, even adults fall over with the event handlers. At a meeting recently, 95% of the attendees (everyone except me) painted themselves into a concurrency corner they didn't understand and got strange behaviour they could not explain.
My view is that being able to check the present state, and checking if an event has been seen since you last checked, is the minimum you need. Event handlers and callbacks get complex very quickly, and perhaps are not part of the core API that we talk about, but more part of the advanced API.
As an interesting aside, most of the TD programs I have written, I set a 'pressed' flag in the on_handler and then check the flag in the main loop. The only other design pattern I use is to increment a variable inside the on_handler so I know how many presses (useful for pressing a button that increments a number, if the top loop might block for other reasons). In TD you have to use global variables to signal as there is no mechanism for accessing the scope of the containing function. This is quite an additional tricky concept to explain to people. The CK example was a 4 line program in the event handler, and if you repeatedly pressed the button you immediately got loads of fibers running concurrently and causing very confusing glitching on the display ... on_a_pressed() { display.print(smiley); sleep(1); display.print(saddie); sleep(1) }
As discussed with some individuals at length previously (captured here now for central inspection) I believe the same simple API could be consistently used for gestures (if gestures.was_shake():...)
I'm open to discussion on event counting, perhaps the core api is was_pressed() and the flag is cleared, but there is an addtional API for how_many_presses() that returns a hidden counter, clears the counter (and clears the flag?). Those are the two most common use-cases for non polling button handling in my experience - callbacks (event handlers) make it look simple but drag people into additional concurrency complexity that teachers are not well equipped to explain, and kids don't understand unless it is well explained to them.
— Reply to this email directly or view it on GitHub https://github.com/dpgeorge/microbit-micropython/issues/36#issuecomment-143776639 .
From the examples added in #38 I see that it would be very useful to have button.was_clicked(), so that you don't have to explicitly check for button transition.
In my TouchDevelop programs, I find the way I have mostly settled on handling this has turned out to be:
on event was event := true end
Then in the main loop I have
if was event: do something was event := false
Where 'event' might be [A was pressed, B was pressed, was shake, ...]
In some of my programs I have a check_event that reads and clears the flag as a single operation.
This means if your top loop is running a bit slow, you don't miss events.
I think we are agreeing on "simple events", such as button.was_clicked(). @markshannon did you make any progress on this?
Hi all,
I've had some thoughts about what an event handling execution model might look like, and it's a bit different to what's been proposed in this thread so far. In short, I think it'd be valuable to have an explicit event loop. However, I appreciate that I'm a bit late to the party, and so I totally understand if it's too late to change tack.
If people are sufficiently interested in this, I'll put together some samples that demonstrate how code using the approach might look.
Motivation
The problem I see with the "simple events" model as I understand it is that events are not necessarily handled in the order that they occur, particularly in the presence of blocking functions. For instance, consider:
while True:
if button_a_was_pressed():
play_tune_a()
if button_b_was_pressed():
play_tune_b()
Then suppose the following things happen:
- User presses A
- This causes tune A to start playing
- While tune A is playing, the user presses A again then B
- Once tune A is finished, tune B is played, followed by tune A
This is confusing, because the user pressed A, A, B, but heard A, B, A.
Alternatively, consider:
while True:
if button_a_was_pressed():
play_tune_a()
elif button_b_was_pressed():
play_tune_b()
Then suppose the following things happen:
- User presses A
- This causes tune A to start playing
- While tune A is playing, the user presses B then A again
- Once tune A is finished, tune A is played, followed by tune B
Again, this is confusing, because the user pressed A, B, A, but heard A, A, B.
I can see this kind of behaviour leading to people writing more complicated code to handle this kind of problem.
Kinds of events
Before moving on, I think it's worth summarising the kinds of events an event loop can react to. From reading the API docs for the microbit module, it seems like it should be able to respond to the following kinds of event:
- "a button has been pressed/released"
- "the compass direction has changed"
- "the compass is calibrated"
- "the accelerometer's x/y/z has changed"
- "the value of a pin has changed"
- "the I2C bus has received bytes"
For the compass and the accelerometer, and from the point of view of a reading an analog value from a pin, it might be good to have additional events like:
- "the compass direction has become 45°"
- "the compass direction has become between 30° and 60°"
Beyond that, you could also have events relating to a user shaking the microbit, or tilting it in a certain direction.
Additionally, the event loop should be able to respond to timers popping, and to messages being broadcast from elsewhere in the code.
A proposed API for an event loop
I'd like to propose something like the following.
There would be a single global loop object, to which event handlers could be added (and possibly removed). Once all inital event handlers have been added to the loop, the loop would need to be started explicitly. Once the loop is started, execution blocks while the loops responds to events, until it is stopped or all event handlers have been removed.
Event handlers can be added to the loop at any time, via a method on the loop object method called something like when, which has the following signature:
loop.when(event, handler_fn, *args, **kwargs)
When the relevant event occurs, handler_fn is called with args and kwargs. loop.when could return an object that would allow the event handler to be removed.
If implementation were not constrained, I would expect event to be an instance of an Event class, which is instantiated by calling a function like button_a_pressed() or compass_points_between(angle1, angle2).
The event-returning functions could either "live" in an events module (like events.button_a_pressed) or on a particular object (like button_a.pressed).
Assuming the first form, you would add an event handler to the loop with something like:
loop.when(events.button_a_pressed(), handle_button_a_pressed)
loop.when(events.seconds_elapsed(5), tick)
loop.when(events.compass_points_between(355, 5), handle_pointing_northish)
Allowing events to have parameters is important, since otherwise the user has to keep track of how a particular value has changed over time, which will almost certainly require keeping track of global state somehow.
For instance if you only had a "the compass has changed" event, then to call a function once the compass points northish, you'd have to know what last known direction of the compass.
Rejected alternative APIs
I've thought about a couple of other APIs.
- We could use string/constant/enum identifiers for events. This works well for simple events without parameters, but doesn't work so well for events with parameters. I think you'd have to end up with something like:
loop.when('compass_points_between', 355, 5, handle_pointing_northish)
and it then becomes difficult to work out where the arguments that specify the even stop and the handler and its arguments begin.
- The loop could define methods for handling each event. However, this is again a problem for events with parameters. You'd have something like:
loop.when_compass_points_between(355, 5)(handle_pointing_northish)
and I think the construction of calling a function and then calling the result of that is a bit weird and likely to trip people up. For instance,
loop.when_button_a_pressed(handle_button_a_pressed)
would result in a TypeError because of the wrong number of arguments passed in.
Hi,
I don't know how far along the simple events model is at the moment, but if change is possible at the moment, I've added some thoughts below.
I'm pretty much -1 on this since it requires an understanding of not just functions, but functions used as callbacks, and callbacks in an inversion of control scenario... That's pretty advanced stuff for an 11 year old beginner. (indeed inversion of control is not really very beginner friendly IMO)
This is a shame, since things I do like are:
- The idea of ensuring events are handled in the right order
- The use of the word "when" - echoes scratch's usage
- Predefined events collection for firing off events
- It also has a means of saying "I'm waiting for these events to happen", which IS nice.
I think it's possible to take those elements forward.
Additionally, Scratch also has the concept of "broadcast" which is used in lots of scratch games. (I think recognising that scratch is beginning to be used by a lot of primary schools is useful here) If you're not familiar with scratch, see this tutorial from Maplin of all places :
http://www.maplin.co.uk/medias/Bonfire-Night-Programming-in-Scratch.pdf
Suppose we broke that up into pieces. One part apes the scratch-isms, and one part is just normal python-y stuff. (in particular, similar to pygame)
# SCRATCHY EVENTS WITH BROADCAST
# events is initialised in the background
when(events.button_a_pressed(), broadcast="button_a_pressed")
when(events.seconds_elapsed(5), broadcast="tick")
when(events.compass_points_between(355, 5), broadcast="handle_pointing_northish")
while True:
if events.happened():
for event in events:
if event == "button_a_pressed":
...
if event == "tick":
...
if event == "handle_pointing_northish":
...
That might seem a little odd, but I think bridges the scratch/python worlds quite well. (Scratch uses strings for events like this)
Inside an events.happened() method you have something like this:
def happened(self):
if events.queue == []:
events.gen.next()
if events.queue != []:
return True
return False
Which acts as an events pump. Then events.iter is something like:
def __iter__(self):
return events.queue.pop(0)
That avoids the inversion of control, but also potentially helps with understanding the leap from the concurrent world of scratch with the inherently non-concurrent world of standard programming languages.
Similarly, you /could/ also deal with the events ordering problem by doing this:
#SCRATCHY EVENTS, NO BROADCAST
# events is initialised in the background
event.when(events.button_a_pressed())
event.when(events.seconds_elapsed(5))
event.when(events.compass_points_between(355, 5))
while True:
if events.happened():
if event.button_a_pressed():
...
if event.seconds_elapsed(5):
...
if event.compass_points_between(355, 5):
...
I think that's potentially nice, and also potentially not far off the current suggested model? I think it is also potentially a little limiting, but it does also however allow ensuring ordering happens correctly.
In that case, events.happened() would work like this:
def happened(self):
if events.queue == []: # Or some other mechanism to update the events queue
events.gen.next()
if events.queue != []:
event.current_event = events.queue.pop(0)
return True
return False
Then in that situation:
# Defines a condition for an event to track
events.button_a_pressed()
# Checks to see if a specific event has happened, and clears
# the event if it did
event.button_a_pressed()
I suspect however that "events" and "event" are far too close for comfort - so they'd need to be more distinctly named IMO, but I think you get the idea.
Given these two forms, I think I prefer the broadcast version since it also allows the following:
events.broadcast("my event")
Allowing custom events to be broadcast (something very common in scratch programs).
Also, I think as I said having when/broadcast might help with bridging the scratch events model which is concurrent with a python one, which is normally single threaded.
Thinking about it, there wouldn't necessarily be a need to have to choose between the two forms. You could do this:
when(events.button_a_pressed(), broadcast="button_a_pressed")
when(events.seconds_elapsed(5), callback=tick)
when(events.compass_points_between(355, 5), broadcast="handle_pointing_northish")
Not sure I like that though :-) (though I can see advantages)
If we were targetting an older age group incidentally, I'd probably suggest something more generally practical & "tidier" based around using actors, but I personally think that's also far too complex for this age range, especially given they're beginners.
/tuppenceworth
Michael.
My 2c...
- Just because it may be hard for an 11yo doesn't mean we shouldn't do it. I actually think such event driven coding is easier to "grok" for kids if taught correctly. @sparkslabs, your evidence that there's something like this in Scratch demonstrates my point. This Guardian article from way-back-when is my attempt to clearly explain JavaScript's
whensyntax in such simple terms (although I would change my writing style if I were writing for kids): https://www.theguardian.com/info/developer-blog/2013/sep/17/deferreds-and-promises-programming-javascript - @inglesp why not just use co-routines (yield from) and/or the new
asyncandawaitkeywords? It's standard Python and if @dpgeorge tracks Python versions he'll have to implement them as core features of the language. There's alsoasynio'sFutureandTaskclasses that allow for callback based programming.
Personally, I believe the educational resources and quality of teachers has a lot more to do with a child's capacity to learn about something rather than the perceived complexity of said something (case in point: music theory, which I've taught to 5yo kids).
Hi @sparkslabs -- this looks interesting. I'll try to read it properly later in the day. One thing for now: GitHub is somehow hiding the second half of your comment, possibly because it's misparsed it and thinks that it's a response to an email. Any chance that you could edit the comment so this doesn't happen?
@dpgeorge I don't really see button.was_pressed() as an event, just as a stateful button.
@ntoll I'm a bit concerned that async and await have too many rough edges. Maybe in a few years.
If we are to have "events" could we keep it synchronous and as simple as possible? For example:
import events as e
#We are interested in button and accelerometer events, plus a tick every 100ms.
stream = e.EventStream(e.BUTTONS, e.ACCELEROMETER, tick=100)
for event in stream:
#handle event
...
As @markshannon points out, simple interfaces like button.was_pressed() are simple and useful and probably good enough for most cases. They would be the entry point for an 11yo and I think we should implement such methods (we only need them for buttons and gestures I'd say).
Then we can have "proper events" as something to graduate to when button.was_pressed() is too limiting (and when the user is ready to understand more complex event handlers).
I like the broadcast idea, posting strings to the event bus and then popping them off in order. How about simplifying the event specs to:
loop.when_button_a_pressed("click")
loop.when_seconds_elapsed(5, "tick")
loop.when_compass_points(355, 5, "north")
@inglesp I've tried editting it, but I think it thinks it's just too long. (also, I did send it in via email - which is probably part of the problem). The comment is all there though - you just have to click on the "..." to see the rest. I think what's below summarises the useful bits though.
@ntoll I strongly disagree on this point in this case. Events will be used by those making games, or things that react to events. After all, that's the major use case of the microbit :-) . A quick search of CAS shows zero discussion of callbacks, zero usage of deferreds. (I hadn't seen any discussion in the past few years I've been on there answering q's but thought it worth double checking)
On the flip side, many of the python reference cards that are used in classrooms for early KS3 (ie Y7 - Y9) tend to not even mention definition of functions (!), let alone the idea of them being first class objects, or the idea of deferreds. Also, pygame is discussed extensively all over the forums, with lots of resources created etc, suggesting any events model that's similar to pygame is a good idea (since it's a familiar model).
The argument that "if taught correctly" is true. But it's a HUGE if. After all, the target audience isn't the geek. It isn't the child in a class that is well supported. It's not really even for children of geeks like me since they're also well supported. It's children who don't know (yet) that they can find these things doable. It's the children who's teacher think "computing isn't for you" - which is sadly still out there. (Those groups are assisted by the micro:bit and will probably take it to its full potential, but they don't need encouragement to get on the rung).
If the "only" way you can handle events is a callback model, then you are instantly raising the bar well beyond anything that gets taught. Jumping into async, await and generators, again massively raises the bar. (From my work on kamaelia and guild I should be expected to argue the other way round :-) - especially since guild is a more modern simplification targetted, in part, at 14+ year old capabilities)
However, callbacks, generators, async, await, etc raise the bar so far beyond anything that the motivated and awesome teachers on CAS currently generally teach - even for A-Level that I think this is a risky approach.
That said, I DO like leaving the door open for growth - the entire microbit approach is after all predicated on that. That suggests to me that the modification I mentioned last might be a good idea - ie also allowing the callback form, but not having it as default. On that basis I think that leaves a good argument for this approach:
# events system is initialised in the background
when.button_a_pressed(broadcast="button_a_pressed")
when.compass_points_between(355, 5, broadcast="handle_pointing_northish")
while True:
if events.happened():
for received in events:
if received == "button_a_pressed":
events.broadcast("tick")
...
if received == "tick":
...
when.millis_elapsed(5000, broadcast="tick")
if received == "handle_pointing_northish":
(This factors in @dpgeorge 's comment above, and also the fact that most sleep periods use milliseconds rather than seconds)
That then allows something like this - which could be quite good for someone to grow into.
def tick():
# do something each tick
when.millis_elapsed(5000, callback=tick)
# events system is initialised in the background
when.button_a_pressed(broadcast="button_a_pressed")
when.compass_points_between(355, 5, broadcast="handle_pointing_northish")
while True:
if events.happened():
for received in events:
if received == "button_a_pressed":
when.millis_elapsed(5000, callback=tick)
...
if received == "handle_pointing_northish":
The potential problem with the callback approach is what happens when someone causes overlapping events. For example, the "tick" above example effectively says "every 5 seconds", which makes it tempting to do this:
def tick():
# do something each tick
# events system is initialised in the background
when.button_a_pressed(broadcast="button_a_pressed")
when.compass_points_between(355, 5, broadcast="handle_pointing_northish")
while True:
if events.happened():
for received in events:
if received == "button_a_pressed":
every.millis_elapsed(5000, callback=tick)
...
if received == "handle_pointing_northish":
That then leads down a difficult to debug path if they put "5" instead of 5000 and if "tick" takes longer than 5ms to execute.
I think if you actively prevented/disallowed overlapping events (by choice of how events can be raised, and avoiding things like "every" - or by only allowing an "every" to fire if the previous one has finished), then the mixed model could work. (And help provide a path to understanding callbacks and why they're useful)
@dpgeorge The reason I've stuck with "when" / "broadcast" above is because it matches the keywords used in scratch, and this event model I think is close enough for that to not be confusing. There's probably equally good arguments for loop.
@markshannon your example there is very similar to pygame's model, and I think that has obvious benefits in that it's a proven teachable model. If you went with that, that'd be awesome. I only suggested this when/broadcast/received model based on the -1 @inglesp's suggestion, and I don't like saying "no I don't like that" without providing an alternative. I think if we go beyond the approach you're suggesting this when/broadcast/recieved approach is the one I'd plump for :)
While explicitly iterating over the events has its advantages, and I'm really comfortable with how PyGame does things, I just wanted to point out that it leads to really ugly and hard to read code. Basically, you end up with chains of nested if-elif-else clauses all over your program. Defereds, callbacks, are one way around that, but there is also another way, which is commonly used in GUI toolkits -- put the event-handling code in methods with fixed names. We are in an especially good situation here, because we know exactly what kinds of events are possible, so we can design a class for that. I think it would look something like this:
class Game(microbit.Loop):
def on_button_press(self, button):
...
def on_button_A_press(self):
...
def on_tick(self):
...
def on_heading_change(self, heading):
...
Game().run()
I know that this actually introduces a lot of concepts. Classes, methods, dispatch, etc. But you can immediately start using it without initially knowing how it all works, and your code doesn't become a horrible mangled heap of steaming spaghetti. Moreover, IDEs are optimized for this kind of programming (due to it being popular in GUI frameworks) and will help you showing available methods and their arguments, etc.
I'm not necessarily advocating doing it this way, but I think it's important to remember about this pattern.
There is also a version of this popular with some web framework, where you register callbacks using a decorator. That possibly gives you better control, as you can pass parameters while registering:
@call_every_milliseconds(20)
def my_event(time):
...
I've been thinking about this a bit more this evening, and I've come to the conclusion that without introducing people to classes and objects, the callback approach I outlined last night will be unwieldy.
This is because I expect lots of callbacks will want to manipulate some kind of state (eg, when button B is pressed, move a pixel to the right) and in order for a function callback (as opposed to a method callback) to manipulate state, it's probably going to need to declare variables as global. Using global can be pretty confusing, and so I think this is enough reason to abandon the approach.
Maybe that is obvious, but it hadn't occurred to me until I tried to write some code that uses this style, eg:
y = 2
def draw():
microbit.display.clear()
microbit.display.set_pixel(2, y, 9)
def go_left():
global y
y = (y - 1) % 5
draw()
def go_right():
global y
y = (y + 1) % 5
draw()
loop.when(button_a_pressed(), go_left)
loop.when(button_b_pressed(), go_right)
draw()
loop.run()
The alternative would be to keep state in an object:
class Dot:
def __init__(self):
self.y = 2
def draw(self):
microbit.display.clear()
microbit.display.set_pixel(2, self.y, 9)
def go_left(self):
self.y = (self.y - 1) % 5
self.draw()
def go_right(self):
self.y = (self.y + 1) % 5
self.draw()
dot = Dot()
dot.draw()
loop.when(button_a_pressed(), dot.go_left)
loop.when(button_b_pressed(), dot.go_right)
loop.run()
But this requires quite a lot of new stuff to learn.
As such, I'm going to withdraw my proposal.
I've been working on an implementation of an event loop over the last few evenings. See #129 for details.
I'd like to implement some sort of event source. I've just reread this thread and it seems that my suggestion is the best (but I may be biassed :wink:).
Any implementation of events needs to be efficient (i.e. it should use very little CPU time) and should have a simple API, both in terms of size and ease of understanding.
API
To flesh out my earlier suggestion, the entire API is this:
def event_stream(*sources, tick=None, buffer_size=4, drop_newest=False)
which would return an iterable of (src, kind) tuples, where src would be the source (e.g. button_a and kind would be the type of event, e.g. "pressed" or "released".
Example of use
for src, ev in event_stream(button_a, button_b, tick=100):
if src == button_a:
if ev == "press":
move_left()
elif src == button_b:
if ev == "press":
move_right()
else: # src == "tick"
update_display()
Extension
It is perfectly possible to build event loops and class-based schemes on top of the simple iterator scheme, and to do so in Python: (The code below is not usable, merely an example)
class EventHandlerClass:
def run(self):
for src, kind in self.events:
getattr(self, "on_" + src.__class__.__name__)(src, kind)
class MyEventHandlerClass(EventHandlerClass):
def on_Button(button, ev):
if button is button_a and ev = "press":
display.scroll("Hello")
Implementation (in C):
There would be a single function pointer microbit_event(mp_obj_t source, mp_obj_t kind) which would be called by event sources (such as the buttons). By default this would be a noop. event_stream(...) would install a handler function to microbit_event which would add events to the buffer. The events iterator would simply wait for events to appear in the buffer, then yield the first in the buffer.
The above scheme is efficient:
- Negligible overhead when not in use
- No memory allocation in interrupt handlers
- Very little memory allocation (a 2-tuple per event yielded) in the main thread
- No busy waiting or polling in the event iterator.
I like the simplicity of this.
How do you generate your own events with this? Can you have multiple such iterators running at the same time? What would be the result of that? Would each get a separate copy of every new event? Can you peek for an event without removing it from the queue or somehow put it back?
Have you looked at the message bus that is already built into the micro:bit?
https://lancaster-university.github.io/microbit-docs/ubit/messageBus/
I would imagine we're most likely to build our IoT use cases around this message bus, so having some level of compatibility with this would be desirable; at least at the message event number, if not at the API function signatures.
I'm not sure that would be used as there is an ongoing effort to move away from the DAL (allowing smaller builds and the possibility of merging with upstream)
@whaleygeek The DAL message bus is closely coupled, with the rest of the DAL so we can't use it.
I see no problem in reusing the same IDs as the DAL. However, as they are just magic constants I don't how it would make any difference.
@deshipu No you can't have multiple iterators running at the same time. Nor we do have a good story for finalising the iterator to reclaim the buffer. I'd like to keep this simple and document the limitations, but if you have any suggestions on how to handle those cases, I am all ears.
Could you peek at the event?
Not with the API as it stands, but there is no fundamental reason why not. Do you think it would be useful?
Could you peek at the event?
Not with the API as it stands, but there is no fundamental reason why not. Do you think it would be useful?
I'm not sure. I don't have a particular scenario in mind for this, but I seem to remember peeking at the event queue in some of the event systems I saw in the past. I think sooner or later you will need either an ability to look at the queue without consuming events, an ability to put the events back on the queue, or an ability to filter the events and only consume the ones you are interested in. Then you can have two parts of your program handle two different kinds of events.
@markshannon A typical use case could be: MicroPython running on the micro:bit, some off-device coding experience on a PC. Application on PC wants to simulate a button press on the micro:bit, so it sends in a known event ID that goes down the message bus, and appears in the micro:bit code as if someone actually pressed the button.
Other use case might be events in MicroPython routed out via serial port. on_shake event goes down the message bus, out the serial port, and into application on PC which makes something happen.
So, regardless of the actual language, having common event ID's across languages (and they are defined in a header file anyway) would be beneficial for all.
To flesh out my earlier suggestion, the entire API is this:
def event_stream(*sources, tick=None, buffer_size=4, drop_newest=False)
This is very similar in principle to select/poll, eg:
import select
poll = select.poll()
poll.register(<entity>)
while True:
for events in poll.poll(<timeout>):
# deal with events, eg using button_a.was_pressed()
# deal with update activities
My concerns with adding more stuff is that we are getting really tight on flash space, and we should only add things that we don't already have in some other form. In this case we already have simple event queues on a per-object basis, ie button.get_presses(), accelerometer.get_gestures(). In Python there should be one (simple) way do to things, not multiple ways :)
Any news on this ? I have see that Javascript programming already have this functionality:
let Highest = 0
let value = 0
basic.forever(() => {
value = pins.analogReadPin(AnalogPin.P0)
if (value > Highest) {
Highest = value
}
})
input.omButtonPressed(Button.A, () => {
basic.showNumber(Highest)
})
Thanks !
Hi, i don't know where you are on this issue, but i haven't found any way to handle events or manage multitasking with python on the microbit.
Any update ?