Rybec / pyRTOS

RTOS written in pure Python, designed for use with CircuitPython
MIT License
150 stars 28 forks source link

Using notifications #13

Open jwise-acton opened 1 year ago

jwise-acton commented 1 year ago

I'm looking for a basic working example using notifications to communicate between two tasks. In the simplest case, I want to send an incrementing integer, as the notification value, from one task to another task once a second.

Thanks for any help!
...Jeff

Rybec commented 1 year ago

https://github.com/Rybec/pyRTOS#notification-examples

I just added that. Please let me know how that works for you, so I can either close the issue or fix whatever I got wrong! If it is not detailed enough, please let me know.

Note that if all you want to do is unblock a task once per second, it might be better to use a yield [timeout(1)] in that task. If you need greater stability though (as that will lose a little bit of time each iteration), using a notification in this way is certainly a better option, if you write you timing code correctly. In this case, you could use a service routine for handling the timing and sending the notifications, to optimize response time.

jwise-acton commented 1 year ago

Hi Ben Thanks for getting back to me!  You are correct that I am interested in timing precision in the notification sending. 

What you've written is perfect as far as it goes - the notification receiver task.  I have one more issue - at the pyRTOS level with the sender task of the notification. ...Pardon my inexperience with pyRTOS...

I still need clarification/confirmation on how the notification sender task is to be written and instantiated so that it knows the receiving task.  I believe that senders of notifications interact directly with the receiver's notification variables, but how does the sender know the task to which it is supposed to send a notification?  I have the impression that at the pyRTOS level, I instantiate all the tasks, and before adding the tasks to pyRTOS, I send a message to the task sender that gives it the python object of the receiver task,  Once the receiving task runs its initialization code (before the first yield), it reads its message(s) and gets the receiving task's object.  I.e., the sender task reads it mailbox before the first 'yield' and takes the message value field as the receiver task object. Later when pyRTOS is running, the sender uses this rcvr-task-obj reference to 'send' the notification.   Is this anything like correct?  Please add the code that sets up the notification relationship between the sending and receiving tasks, and shows the sender's code.   

If you'd like, when I have a simple example running in CPYTHON, I'll send you the code for possible incorporation in the pyRTOS  documentation...

Thanks again! ...Jeff

On Wednesday, January 11, 2023 at 05:08:13 PM EST, Ben Williams ***@***.***> wrote:  

https://github.com/Rybec/pyRTOS#notification-examples

I just added that. Please let me know how that works for you, so I can either close the issue or fix whatever I got wrong! If it is not detailed enough, please let me know.

Note that if all you want to do is unblock a task once per second, it might be better to use a yield [timeout(1)] in that task. If you need greater stability though (as that will lose a little bit of time each iteration), using a notification in this way is certainly a better option, if you write you timing code correctly. In this case, you could use a service routine for handling the timing and sending the notifications, to optimize response time.

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

Rybec commented 1 year ago

There are several ways to "inform" tasks about the existence of other tasks. The mailbox mechanic should work exactly as you describe it for this. Another option is global variables. This is commonly used with FreeRTOS and other embedded RTOS applications. In Python, you would create your global task variables (empty, since the task instances don't exist yet) first. Then you would create your task functions, with the global x line at the top, to bring the global variable into local scope. As long as you assign the tasks to the empty global variables before starting the tasks, this should work very smoothly. Yet another option is to make a global singleton class that all of the task functions use global to bring into local scope, and put everything that tasks need to know about there.

In my experience, just using global variables tends to be the most elegant option, if you only have one instance of each task. This eliminates the startup cost of passing in through the mailbox, and it makes it very clear what tasks interact with other tasks.

If you are going to create multiple instances of a task and need each instance to communicate with a different task, the mailbox method is the best choice. It can also be a good choice when there is some reason you can't do things in the order necessary for just using globals directly.

There's actually also a method that can work really well, if your tasks are handled in their own modules, by making the module level globals. This is kind of advanced, and it might make your code harder to read. If you want to know though, ask and I'll explain how to do it.

(Please don't use the singleton method. There might be places where it is appropriate, but singletons almost always make code way harder to read, and they tend to encourage terrible coding practices.)

Maybe I should add an "intertask communication" section to the documentation discussing this topic...

Rybec commented 1 year ago

Ok, so I've written up a new section in the examples section:

https://github.com/Rybec/pyRTOS/blob/main/README.md#communication-setup-examples

This has examples of how to get references to another task into a task. I hope this helps!

jwise-acton commented 1 year ago

Ben, Another home run!  Thanks for addressing these other issues.  It's a great accelerator for me, and I presume will help others dive into pyRTOS with reliable success.  Thanks!

I have a few questions and comments:

Service Routine: Interesting your point on pyRTOS running within an OS, vs. as the OS. 

Can the CircuitPython KeyPad module operate within the pyRTOS environment or is it it's own OS?  I understand that KeyPad on its own scans the button keys for pressed/unpressed state changes.  Does KeyPad operate as a 'background' task?  Would it require the scheduler delay example you've written up to give it processor time to function?  Or is KeyPad incompatible with pyRTOS?  Do I need to implement the KeyPad functionality in a service routine - using KeyPad code, or perhaps code tailored to this exact situation? 

Also, what is the minimum time interval in the time.sleep( scheduler_delay) call that is reflected as an actual time interval.  I.e., what is the minimum time delay quanta upon which the interval timing is based? 

Communication Setup Examples:

This section is very clear. 

Is it true that in task methods that can accept the name (string) of a target task, that if given a task-object reference, they will run faster/fastest?  This could be an additional motivation for using the global task concept broadly. 

...Global Tasks:

When defining a task in a sub-module and importing that module into the main global name-space, does the task object itself need to be imported, with its embedded task function already installed, or can one import just the task function, and then instantiate the task in the main global name space?  This is a small point, but it may be cleaner to instantiate all the tasks in one place, while leaving the details of the task functions in other modules. 

Deliver Tasks Using Mailboxes: In your example code, I would add a line after the "task.deliver( some_other_task )" that documents the pyRTOS.add_task() operation that needs to follow the delivery of the mail message, so the reader sees the full sequence of operations. 

Module Level Globals:

You provide a very clear example of importing a task from another module using "import ". 

When I've done importing from other modules of mine, I usually use     "from import or [ ]".  This gives me visibility at the top of the file of all the imported items and which module they each came from all in one area.  (I NEVER use "from import * "!!!!  This provides no back-tracing of the origin of names within the file.)  Another commonly seen import command is     "import as " as a way to use a few characters to preface the imported nodule's item with a short abbreviation.  For example, I see this typically with pandas - "pd".  Lots of import style options! 

I don't know if it is worth it or even appropriate to document such alternatives in this pyRTOS document at this point.  People that are new to Python might benefit, and experienced Python users will already have their own preferences on this! 

Thanks again for being so responsive and helpful.  You've really helped me build my pyRTOS knowledge up to a working level!  Now to put it to use...

...Jeff

On Thursday, January 12, 2023 at 03:26:18 AM EST, Ben Williams ***@***.***> wrote:  

Ok, so I've written up a new section in the examples section:

https://github.com/Rybec/pyRTOS/blob/main/README.md#communication-setup-examples

This has examples of how to get references to another task into a task. I hope this helps!

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

jwise-acton commented 1 year ago

Ben, more to think about!  Thanks for broadening my perspective on the various techniques. 

Regarding a singleton class with all the global tasks and other global information as a way to create a global name-space that transcends modules, I try not to do this in my multi-module python applications.  When I have to have a global name-space (rather than function arg. passed down data) with read and write capability, I use a separate "global" module that various modules in the application can import and access.  But I try to avoid this too - it is so unpythonic!  So your suggestion to not use a singleton class certainly aligns with my preferences! 

You make a very good point about multi-instance tasks that need to communicate with different target tasks.  Mailboxes are the way to go.  I hadn't thought that far ahead! 

Module-level imports of globals is something I've done lots of times over the years.  I use the emacs editor running the python language mode, and it has good multiple-open-files/multiple-windows capability that works well with this paradigm.  I presume several other editors can work well with multiple open files too...

A discussion of intertask communication would be a great addition.  It would help coders to jump right to good approaches that are appropriate to their situation - considering the various tradeoffs and chances of achieving 'first time' success! 

Another helper that has occurred to me in my early days with pyRTOS is a special debugging version of pyRTOS that would catch many of the errors beginners might make.  This would add code to pyRTOS, but presumably the streamlined version of pyRTOS would be used once the prototype was running and tested. 

...Jeff

On Thursday, January 12, 2023 at 01:13:52 AM EST, Ben Williams ***@***.***> wrote:  

There are several ways to "inform" tasks about the existence of other tasks. The mailbox mechanic should work exactly as you describe it for this. Another option is global variables. This is commonly used with FreeRTOS and other embedded RTOS applications. In Python, you would create your global task variables (empty, since the task instances don't exist yet) first. Then you would create your task functions, with the global x line at the top, to bring the global variable into local scope. As long as you assign the tasks to the empty global variables before starting the tasks, this should work very smoothly. Yet another option is to make a global singleton class that all of the task functions use global to bring into local scope, and put everything that tasks need to know about there.

In my experience, just using global variables tends to be the most elegant option, if you only have one instance of each task. This eliminates the startup cost of passing in through the mailbox, and it makes it very clear what tasks interact with other tasks.

If you are going to create multiple instances of a task and need each instance to communicate with a different task, the mailbox method is the best choice. It can also be a good choice when there is some reason you can't do things in the order necessary for just using globals directly.

There's actually also a method that can work really well, if your tasks are handled in their own modules, by making the module level globals. This is kind of advanced, and it might make your code harder to read. If you want to know though, ask and I'll explain how to do it.

(Please don't use the singleton method. There might be places where it is appropriate, but singletons almost always make code way harder to read, and they tend to encourage terrible coding practices.)

Maybe I should add an "intertask communication" section to the documentation discussing this topic...

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>