├── .gitignore ├── CHANGELOG.md ├── DONATIONS.md ├── LICENSE ├── Makefile ├── README.md ├── build └── .gitignore ├── pyRTOS ├── __init__.py ├── message.py ├── pyRTOS.py ├── scheduler.py └── task.py ├── rot_trinkey_touch.py └── sample.py /.gitignore: -------------------------------------------------------------------------------- 1 | pyRTOS/__pycache__ 2 | 3 | 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version 1.0.0 2 | 3 | New Features: 4 | 5 | - Service Rountines 6 | 7 | API Breaking Changes: 8 | 9 | - Removed anonymous locking from Mutex 10 | - Tasks no longer have mailboxes by default 11 | 12 | Triage: 13 | 14 | - All calls to Mutex.lock() and Mutex.nb_lock() require the first argument to be a reference to the calling task.
Mutex.lock() -> Mutex.lock(self)
Mutex.nb_lock() -> Mutex.nb_lock(self) 15 | - Task objects no longer have mailboxes by default. Tasks that receive messages directly must be initialized with mailbox enabled.
Task(f, priority=2) -> Task(f, priority=2, mailbox=True) 16 | -------------------------------------------------------------------------------- /DONATIONS.md: -------------------------------------------------------------------------------- 1 | I am a Brave Verified Creator on Github. If you would like to support my work on this and other projects, you can make a donation in BAT using the Brave browser. If you are using Brave, look for the triangle logo at the right end of the address bar. (If you are not using Brave, consider switching browsers, for the benefit of your own privacy.) Click that when you are on any of my Github projects, and it will give you an option to "SEND A TIP" or set a monthly contribution. (Note that if you are viewing a fork of this project, you will be donating to the owner of the fork and not the original author. When you click the Brave triangle icon, it will tell you who you are donating to. Make sure it is the person you intend on supporting. If you want to donate to the original author of pyRTOS, go to https://github.com/Rybec, and then click the triangle and submit the donation.) 2 | 3 | If you do not have/want Brave or otherwise do not want to donate in BAT, I also have a Patreon account where you can contribute to this and other open source software and hardware projects. This can be found here: https://www.patreon.com/techniumadeptus 4 | 5 | These are currently the only ways I accept donations. If there is ever enough demand, I am willing to add options to donate in certain cryptocurrencies. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ben Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #MPY_CROSS = ./mpy-cross.static-amd64-linux-6.3.0 2 | MPY_CROSS = ./mpy-cross-linux-amd64-8.2.0-69-gfb57c0801.static 3 | 4 | # Order matters here. Dependencies must come 5 | # before files that depend on them. 6 | SOURCES = pyRTOS/task.py pyRTOS/message.py pyRTOS/scheduler.py pyRTOS/pyRTOS.py 7 | 8 | 9 | build/pyRTOS.mpy: build/pyRTOS.py 10 | $(MPY_CROSS) build/pyRTOS.py -o build/pyRTOS.mpy 11 | 12 | build/pyRTOS.py: $(SOURCES) 13 | cat $(SOURCES) > build/pyRTOS.py 14 | 15 | 16 | 17 | clean: 18 | -rm build/* 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyRTOS 2 | 3 | 4 | ## Introduction 5 | 6 | pyRTOS is a real-time operating system (RTOS), written in Python. The primary goal of pyRTOS is to provide a pure Python RTOS that will work in CircuitPython. The secondary goal is to provide an educational tool for advanced CircuitPython users who want to learn to use an RTOS. pyRTOS should also work in MicroPython, and it can be used in standard Python as well. 7 | 8 | pyRTOS was modeled after FreeRTOS, with some critical differences. The biggest difference is that it uses a voluntary task preemption model, where FreeRTOS generally enforces preemption through timer interrupts. This means there is a greater onus on the user to ensure that all tasks are well behaved. pyRTOS also uses different naming conventions, and tasks have built in message passing. 9 | 10 | To the best of my knowledge, aside from voluntary preemption, the task scheduling is identical to that found in FreeRTOS. Tasks are assigned numerical priorities, the lower the number the higher the priority, and the highest priority ready task is given CPU time, where ties favor the currently running task. Alternative scheduling algorithms may be added in the future. 11 | 12 | ## Table of Contents 13 | 14 | [Basic Usage](#basic-usage) 15 | - [Tasks](#tasks) 16 | - [Notifications](#notifications) 17 | - [Messages](#messages) 18 | - [Error Handling](#error-handling) 19 | 20 | [pyRTOS API](#pyrtos-api) 21 | - [Main API](#main-api) 22 | - [Mutual Exclusion & Synchronization](#mutual-exclusion--synchronization) 23 | 24 | [Task API](#task-api) 25 | 26 | [Task Block Conditions](#task-block-conditions) 27 | 28 | [Message API](#message-api) 29 | 30 | [OS API](#os-api) 31 | - [Service Routines](#service-routines) 32 | 33 | [Templates & Examples](#templates--examples) 34 | - [Task Template](#task-template) 35 | - [Message Handling Example Template](#message-handling-example-template) 36 | - [Timeout & Delay Examples](#timeout--delay-examples) 37 | - [Messages Passing Examples](#message-passing-examples) 38 | - [Notification Examples](#notification-examples) 39 | - [Message Queue Exmaples](#message-queue-examples) 40 | - [Mutex Examples](#mutex-examples) 41 | - [Service Routine Examples](#service-routine-examples) 42 | - [Communication Setup Examples](#communication-setup-examples) 43 | 44 | [Future Additions](#future-additions) 45 | 46 | [Notes](#notes) 47 | 48 | ## Basic Usage 49 | 50 | pyRTOS separates functionality into tasks. A task is similar to a thread in a desktop operating system, except that in pyRTOS tasks cannot be migrated to other processors or cores. This is due to limitations with CircuitPython. In theory, though, it should be possible to write a scheduler with thread migration, for MicroPython, which does support hardware multithreading. 51 | 52 | A simple pyRTOS program will define some task functions, wrap them in `Task` objects, and then register them with the OS using the `add_task()` API function. Once all tasks are added, the `start()` function is used to start the RTOS. 53 | 54 | Once started, the RTOS will schedule time for tasks, giving tasks CPU time based on a priority scheduling algorithm. When the tasks are well behaved, designed to work together, and given the right priorities, the operating system will orchestrate them so they work together to accomplish whatever goal the program was designed for. 55 | 56 | See sample.py for an example task and usage. 57 | 58 | ### Tasks 59 | 60 | A pyRTOS task is composed of a `Task` object combined with a function containing the task code. A task function takes a single argument, a reference to the `Task` object containing it. Task functions are Python generators. Any code before the first yield is setup code. Anything returned by this yield will be ignored. The main task loop should follow this yield. This is the code that will be executed when the scheduler gives the task CPU time. 61 | 62 | The main task loop is typically an infinite loop. If the task needs to terminate, a return call should be used, and any teardown that is necessary should be done directly before returning. Typically though, tasks never return. 63 | 64 | Preemption in pyRTOS is completely voluntary. This means that all tasks _must_ periodically yield control back to the OS, or no other task will get CPU time, messages cannot be passed between tasks, and other administrative duties of the OS will never get done. Yields have two functions in pyRTOS. One is merely to pass control back to the OS. This allows the OS to reevaluate task priorities and pass control to a higher priority ready task, and it allows the OS to take care of administration like message passing, lock handling, and such. Yields should be fairly frequent but not so frequent that the program spends more time in the OS than in tasks. For small tasks, once per main loop may be sufficient. For larger tasks, yields should be placed between significant subsections. If a task has a section of timing dependent code though, do not place yields in places where they could interrupt timing critical processes. There is no guarantee a yield will return within the required time. 65 | 66 | Yields are also used to make certain blocking API calls. The most common will likely be delays. Higher priority processes need to be especially well behaved, because even frequent yields will not give lower priority processes CPU time. The default scheduler always gives the highest priority ready task the CPU time. The only way lower priority tasks _ever_ get time, is if higher priority tasks block when they do not need the CPU time. Typically this means blocking delays, which are accomplished in pyRTOS by yielding with a timeout generator. When the timeout generator expires, the task will become ready again, but until then, lower priority tasks will be allowed to have CPU time. Tasks can also block when waiting for messages or mutual exclusion locks. In the future, more forgiving non-real-time schedulers may be available. 67 | 68 | There are also some places tasks _should_ always yield. Whenever a message is passed, it is placed on a local queue. Messages in the local task outgoing queue are delivered when that task yields. Other places where yielding is necessary for an action to resolve will be noted with the documentation on those actions. 69 | 70 | ### Notifications 71 | 72 | Notifications are a lightweight message passing mechanic native to tasks. When a task is created, a number of notifications can be specified. These notifications can be used by other tasks or by Service Routines to communicate with the task. 73 | 74 | Notifications have a state and a value. The state is an 8-bit (signed) value used to communicate the state of the notification. The meaning of the state is user defined, but the default values for the notification functions assume 0 means the notification is not currently active and 1 means it is active. Notifications also have a 32 bit value (also signed), which can be used as a counter or to communicate a small amount of data. A series of functions are provided to send, read, and otherwise interact with notifications. 75 | 76 | A notification wait is provided as a Task Block Condition, allowing a task to wait for a notification to be set to a specific state. This blocking wait can even be used on other tasks, to wait for a notification to be set to a particular value, for example, a task may want to send a notification, but only once that notification is inactive for the target task, and thus it might block to wait for that notification state to be set to 0, before it sends. 77 | 78 | Notifications are designed for lightweight message passing, both when full messages are not necessary and for Service Routines to communicate with tasks in a very fast and lightweight manner. To communicate via notification, it is necessary to have a reference to the task you want to communicate with. 79 | 80 | ### Messages 81 | 82 | Message passing mechanics are built directly into tasks in pyRTOS, in the form of mailboxes. By default tasks are lightweight, without mailboxes, but a constructor argument can be used to give a task has its own incoming mailbox. Messages are delivered when the currently running task yields. This message passing system is fairly simple. Each message has a single sender and a single recipient. Messages also have a type, which can be pyRTOS.QUIT or a user defined type (see sample.py). User defined types start with integer values of 128 and higher. Types below 128 are reserved for future use by the pyRTOS API. Messages can also contain a message, but this is not required. If the type field is sufficient to convey the necessary information, it is better to leave the message field empty, to save memory. The message field can contain anything, including objects and lists. If you need to pass arguments into a new task that has a mailbox, one way to do this is to call `deliver()` on the newly created task object, with a list or tuple of arguments. This will add the arguments to the task's mailbox, allowing it to access the arguments during initialization. 83 | 84 | Checking messages is a critical part of any task that may receive messages. Unchecked mailboxes can accumulate so many messages that your system runs out of memory. If your task may receive messages, it is important to check the mailbox every loop. Also be careful not to send low priority tasks too many messages without periodically blocking all higher priority tasks, so they can have time to process their messages. If a task that is receiving messages never gets CPU time, that is another way to run out of memory. 85 | 86 | Messages can be addressed with a reference to the target task object or with the name of the object. Names can be any sort of comparable data, but numbers are the most efficient, while strings are the most readable. Object reference addressing _must_ target an object that actually exists, otherwise the OS will crash. Also note that keeping references of terminated tasks will prevent those tasks from being garbage collected, creating a potential memory leak. Object references are the _fastest_ message addressing method, and they may provide some benefits when debugging, but its up to the user to understand and avoid the associated hazards. Name addressing is much safer, however messages addressed to names that are not among the existing tasks will silently fail to be delivered, making certain bugs harder to find. In addition, because name addresses require finding the associated object, name addressed messages will consume significantly more CPU time to deliver. 87 | 88 | sample.py has several examples of message passing. 89 | 90 | ### Error Handling 91 | 92 | The error handling philosophy of pyRTOS is: Write good code. The OS operates on the assumption that the user will write good code that does not cause issues for the OS. If this assumption is broken, the OS _will_ crash when it comes across the broken elements, and it probably will not give you very meaningful error messages. For example, attempting to send a notification to a `Task` that does not have notifications will cause a crash, with a message about the `Task` object having no `notifications` attribute (which is actually somewhat meaningful, in this particular case...). 93 | 94 | pyRTOS is designed to be used with CircuitPython, on devices that may have _very_ limited resources. Adding OS level error handling would require significantly more code, using more flash and RAM space, as well as requiring more processing. This is unacceptable. As such, we will _not_ be adding OS error handling code to gracefully handle OS exceptions caused by incorrect use of the OS. We will also not add special OS exceptions to throw when errors occur, nor will we add preemptive error detection. These are all expensive, requiring significantly more code and processing time. This means that errors that occur within the OS may not produce high quality error messages. Users are encouraged to _write good code_, so that errors in the OS do not occur, and barring that, users can add error handling in their own code (but note that we do not condone writing poor code and then covering up the errors with error handling). Please do not file issues for crashes caused by failures to use the APIs provided correctly. Instead, fix your own code. 95 | 96 | That said, if there is a bug in the OS itself, please _do_ file an issue. Users should not have to work around bugs in pyRTOS. We apply the same standard, "Write good code" to ourselves, and if we have failed to do that, please let us know, so we can fix it. If you are having a crash, and you are not sure where the error is occurring, please do your best to check your own code first, and if you cannot find the bug in your own code, feel free to file an issue. We will do our best to track down the issue, as we have time (at the time of writing, this is a one man operation, and I am not getting paid for this, so it will likely not be immediate). Do not be offended if we find the error in your code and inform you of that. If the error is on our end, we will do our best to fix it in a timely manner (but again, one man team working for free, so no promises; this _is_ open source, so if it is urgent, please consider fixing it yourself). 97 | 98 | Similarly, if you find it difficult to correctly use the APIs, because the documentation is lacking or poorly written, please do file an issue, and we will try to improve it. Our philosophy of "Write good code" also applies to our documentation. 99 | 100 | If this sounds harsh, we sincerely apologize. We understand that this is not ideal. Unfortunately, sacrifices must be made when working on systems with extremely limited resources. Limited flash means our code has to be very small. Limited RAM means we are limited in what we can keep track of. Limited processing power means we have to weigh the value of every command we issue. The purpose of an OS is to facilitate the tasks _the user_ deems important, and the more resources the OS uses, the fewer resources are available for the user's tasks. Given such limited resources, keeping the OS as small and streamlined as possible takes precedence over error handling and debugging convenience. If your application _needs_ the error handling, and you are confident your device has the resources, you can always create a fork of pyRTOS and add error handling yourself. pyRTOS is pretty small, and it is not terribly difficult to understand, if you are familiar with Python, so this should not be very hard. 101 | 102 | ## pyRTOS API 103 | 104 | ### Main API 105 | 106 | **```add_task(task)```** 107 | 108 | 121 | 122 | **```start(scheduler=None)```** 123 | 124 | 133 | 134 | ### Mutual Exclusion & Synchronization 135 | 136 | **```class Mutex()```** 137 | 138 | 143 | 144 | 159 | 160 | 175 | 176 | 186 | 187 | **```class BinarySemaphore()```** 188 | 189 | 194 | 195 | 210 | 211 | 226 | 227 | 237 | 238 | ### Task API 239 | 240 | **```class Task(func, priority=255, name=None, notifications=None, mailbox=False)```** 241 | 242 | 267 | 268 | 278 | 279 | 301 | 302 | 324 | 325 | 339 | 340 | 358 | 359 | 377 | 378 | 392 | 393 | 403 | 404 | 414 | 415 | 433 | 434 | 444 | 445 | 455 | 456 | ### Task Block Conditions 457 | 458 | Task block conditions are generators that yield True if their conditions are met or False if they are not. When a block condition returns True, the task blocked by it is unblocked and put into the ready state. 459 | 460 | A task is blocked when a yield returns a list of block conditions. When any condition in that list returns True, the task is unblocked. This allows any blocking condition to be paired with a `timeout()` condition, to unblock it when the timeout expires, even if the main condition is not met. For example, `yield [wait_for_message(self), timeout(5)]` will block until there is a message in the incoming message queue, but it will timeout after 5 seconds and return to ready state, even if no message arrives. 461 | 462 | Note that blocking conditions _must_ be returned as lists, even if there is only one condition. Thus, for a one second blocking delay, use `yield [timeout(1)]`. 463 | 464 | **```timeout(seconds)```** 465 | 466 | 479 | 480 | **```timeout_ns(nanoseconds)```** 481 | 482 | 491 | 492 | **```delay(cycles)```** 493 | 494 | 499 | 500 | **```wait_for_message(self)```** 501 | 502 | 507 | 508 | **```wait_for_notification(task, index=0, state=1)```** 509 | 510 | 515 | 516 | ***UFunction*** 517 | 518 | 527 | 528 | ### Message API 529 | 530 | **```class Message(type, source, target, message=None)```** 531 | 532 | 553 | 554 | **```class MessageQueue(capacity=10)```** 555 | 556 | 565 | 566 | 584 | 585 | 599 | 600 | 614 | 615 | 625 | 626 | ## OS API 627 | 628 | The OS API provides tools for extending pyRTOS. Some things just do not make sense to use tasks to do. Some things need higher reliability than tasks. 629 | 630 | For the most part, messing around inside the OS is not a great idea. While part of the pyRTOS project policy is to not break userspace within a given major version, this policy does not hold for the OS API. So when deciding whether to use the OS API, keep in mind that you may be creating a dependency on a specific release or even commit. 631 | 632 | ### Service Routines 633 | 634 | Service routines are OS extensions that run every OS loop. An OS loop occurs every time a task yields. Service routines have no priority mechanic, and they run in the order they are registered. Registered service routines are intended to be permanent. While it is possible to remove them, this is part of the OS implementation that may change without warning, and there is no formal mechanic for removing a service routine. Likewise, while service routines can technically be added from within tasks, it is generally better practice to add them in the main initialization code before calling `pyRTOS.start()`†. Starting service routines outside of the main initialization code may make performance problems related to the service routine extremely difficult to debug. 635 | 636 | Service routines are simple functions, which take no arguments and return nothing. Because they run every OS loop, service routines should be small and fast, much like ISRs in RTOSs that use real-time preemption. Normally, service routines should also be stateless. Service routines that need to communicate with tasks can be created with references to global `MessageQueue` or `Task` objects. As OS extensions, it is appropriate for service routines to call `Task.deliver()` to send tasks messages, however note that creating message objects is expensive. Sending lighter messages in `MessageQueue`s is cheaper, and future features may provide even better options. 637 | 638 | Service routines that absolutely need internal state _can be_ created by wrapping a generator in a lambda function. Note that this will produce much heavier service routines than normal, so this should be used sparingly and only when necessary. To do this, first create a generator function. The function itself can take arguments, but the yield cannot. Ideally, there should be a single yield, within an infinite loop, that takes no arguments and returns nothing. Each OS loop, the service routine will begin execution directly after the yield, and it will end when it gets back to the yield. The generator must never return, or a StopIteration exception will be thrown, crashing the OS\*. Once the generator has been created by calling the function, wrap it in a lambda function like this: `lambda: next(gen)`. This lambda function is your service routine, which should be registered with `add_service_routine()`. 639 | 640 | Use cases for service routines start with the kind of things ISRs are normally used for. In CircuitPython (as of 6.3.0), there are no iterrupts. If you need to regularly check the state of a pin normally used as an interrupt source, a service routine is a good place to do that. Just like with an ISR, you should not handle the program business in the service routine. Instead, the service routine should notify a task that will handle the business associated with the iterrupt. Service routines can also be used to handle things that multiple tasks care about, to avoid the need for semaphores. For example, if multiple tasks need network communication (generally avoid this if possible), a service routine can handle routing traffic between the network and the tasks. Note though, that putting a large network stack in a service routine is a terrible idea that will starve your tasks of CPU time. If you need something bigger than a very slim traffic routing routine, it should be put into a task rather than a service routine. 641 | 642 | \* No, we will not wrap the service routine OS code in a try/except statement. This would increase the size of the OS and make it run more slowly. Instead, write good code and follow the instructions in this document, and no errors will ever get to the OS. 643 | 644 | † Attempting to start a service routine in the main initialization _after_ `pyRTOS.start()` will fail, as this function does not return in normal usage and thus no code after it will ever run. 645 | 646 | **```add_service_routine(service_routine)```** 647 | 648 | 657 | 658 | 659 | ## Templates & Examples 660 | 661 | ### Task Template 662 | 663 | ``` 664 | def task(self): 665 | 666 | # Uncomment this to get argument list passed in with Task.deliver() 667 | # (If you do this, it will crash if no arguments are passed in 668 | # prior to initialization.) 669 | # args = self.recv()[0] 670 | 671 | ### Setup code here 672 | 673 | 674 | 675 | ### End Setup code 676 | 677 | # Pass control back to RTOS 678 | yield 679 | 680 | # Main Task Loop 681 | while True: 682 | ### Work code here 683 | 684 | 685 | 686 | ### End Work code 687 | yield # (Do this at least once per loop) 688 | ``` 689 | 690 | ### Message Handling Example Template 691 | 692 | 693 | ``` 694 | msgs = self.recv() 695 | for msg in msgs: 696 | if msg.type == pyRTOS.QUIT: 697 | # If your task should never return, remove this section 698 | ### Tear Down code here 699 | 700 | 701 | 702 | ### End Tear Down Code 703 | return 704 | elif msg.type == TEMP: 705 | # TEMP is a user defined integer constant larger than 127 706 | # Temperature data will be in msg.message 707 | ### Code here 708 | 709 | 710 | 711 | ### End Code 712 | 713 | # This will silently throw away messages that are not 714 | # one of the specified types, unless you add an else. 715 | ``` 716 | 717 | ### Timeout & Delay Examples 718 | 719 | Delay for 0.5 seconds 720 | 721 | `yield [pyRTOS.timeout(0.5)]` 722 | 723 | Delay for 100 nanoseconds 724 | 725 | `yield [pyRTOS.timeout_ns(100)]` 726 | 727 | Delay for 10 OS cycles (other tasks must yield 10 times, unless all other tasks are suspended or blocked) 728 | 729 | `yield [pyRTOS.delay(10)]` 730 | 731 | ### Message Passing Examples 732 | 733 | #### Send Message 734 | 735 | Send temperature of 45 degrees to display task (TEMP constant is set to some value > 127) 736 | 737 | `self.send(pyRTOS.Message(TEMP, self, "display", 45))` 738 | 739 | This message will be delivered at the next yield. 740 | 741 | #### Read Message 742 | 743 | Instruct hum_read task to read the humidity sensor and send back the result, when wait for a message to arrive (READ_HUM constant is set to some value > 127) 744 | 745 | ``` 746 | self.send(pyRTOS.Message(READ_HUM, self, "hum_read")) 747 | yield [wait_for_message(self)] 748 | ``` 749 | 750 | ### Message Queue Examples 751 | 752 | #### Create MessageQueue 753 | 754 | Create a `MessageQueue` and pass it into some newly created tasks, so it can be retrived during initialization of the tasks 755 | 756 | ``` 757 | display = pyRTOS.Task(display_task, priority=1, "display") 758 | tsensor = pyRTOS.Task(tsensor_task, priority=2, "tsensor") 759 | 760 | temp_queue = MessageQueue(capacity=4) 761 | 762 | display.deliver(temp_queue) 763 | tsensor.deliver(temp_queue) 764 | 765 | pyRTOS.add_task(display) 766 | pyRTOS.add_task(tsensor) 767 | ``` 768 | 769 | #### Write MessageQueue 770 | 771 | Write the temperature to a `MessageQueue` (if the queue is full, this will block until it has room) 772 | 773 | `yield [temp_queue.send(current_temp)]` 774 | 775 | #### Read MessageQueue 776 | 777 | Read the temperature from a `MessageQueue` (if the queue is empty, this will block until a message is added) 778 | 779 | ``` 780 | temp_buffer = [] 781 | yield [temp_queue.recv(temp_buffer)] 782 | 783 | temp = temp_buffer.pop() 784 | ``` 785 | 786 | ### Notification Examples 787 | 788 | #### Example Task with Notification 789 | 790 | Task that runs one step each time it receives a notification at index 0 791 | 792 | ``` 793 | # This task uses one notification 794 | def task_w_notification(self): 795 | # No setup 796 | yield 797 | 798 | # Main Task Loop 799 | while True: 800 | self.wait_for_notification(index=0, state=1) 801 | 802 | # Task code here 803 | # self.notify_get_value(0) returns the value of notification 0 804 | 805 | 806 | # Create task instance 807 | task = Task(task_w_notification, notifications=1) 808 | ``` 809 | 810 | #### Set Notification with Increment 811 | 812 | Set notification 0 to a state of 1 and increment its value as a counter 813 | 814 | ``` 815 | task.notify_inc_value(index=0, step=1) 816 | ``` 817 | 818 | #### Set Notification to Value 819 | 820 | Set notification 0 to a state of 1 and value of 27 821 | 822 | ``` 823 | task.notify_set_value(index=0, value=27) 824 | ``` 825 | 826 | ### Mutex Examples 827 | 828 | #### Create Mutex 829 | 830 | Create a `Mutex` and pass it into some newly created tasks 831 | 832 | ``` 833 | temp_printer = pyRTOS.Task(temp_task, priority=3, "temp_printer") 834 | hum_printer = pyRTOS.Task(hum_task, priority=3, "hum_printer") 835 | 836 | print_mutex = pyRTOS.Mutex() 837 | 838 | temp_printer.deliver(print_mutex) 839 | hum_printer.deliver(print_mutex) 840 | ``` 841 | 842 | #### Use Mutex 843 | 844 | Use a mutex to avoid collisions when printing multiple lines of data (Note that it should never be necessary to actually do this, since no preemption occurs without a yield. This should only be necessary when at least one task yields _within_ the code that needs lock protection.) 845 | 846 | ``` 847 | yield [print_mutex.lock()] 848 | 849 | print("The last five temperature readings were:") 850 | 851 | for temp in temps: 852 | print(temp, "C") 853 | 854 | print_mutex.unlock() 855 | ``` 856 | 857 | ### Service Routine Examples 858 | 859 | #### Scheduler Delay (Simple Service Routine) 860 | 861 | When using pyRTOS within an OS, instead of as _the_ OS of an embedded microcontroller, it will likely use significantly more CPU time than expected. This is because it assumes it is the only thing running and needs to run as fast as the hardware will allow. While there are several ways to solve this, the simplest is probably to just create a service routine that introduces a delay to the scheduler. The delay probably does not need to be very long to reduce the CPU time consumed by the scheduler to almost nothing (but note that if your tasks do a lot between yields, _they_ may still use a lot of CPU time). 862 | 863 | Service routines are simple functions that do not take any arguments or return anything. If a service routine needs outside data or communication, it will need to be done through global variables. (More complex service routines can be made with generators, if internal state needs to be preserved.) 864 | 865 | 866 | ``` 867 | scheduler_delay = 0.001 # Scheduler delay in seconds (0.001 is 1 millisecond; adjust as needed) 868 | 869 | # Service Routine function 870 | def delay_sr(): 871 | global scheduler_delay 872 | time.sleep(scheduler_delay) # Don't forget to import time 873 | 874 | 875 | pyRTOS.add_service_routine(delay_sr) # Register service routine to run every scheduler loop 876 | ``` 877 | 878 | ### Communication Setup Examples 879 | 880 | Before tasks can communicate with each other, they have to know about each other. Giving tasks references to other tasks can be done in a variety of ways. 881 | 882 | #### Global Tasks 883 | 884 | Tasks are typically going to be global variables just to start with. This makes them automatically available to anything that can access global scope. For this to work though, things need to be done in the correct order. A task function cannot know about a task that does not exist yet, and a task cannot be created until the associated task function is defined. If things are done in the right order though, this can still work. 885 | 886 | ``` 887 | # We have to create the globals before we can define the task functions 888 | task0 = None 889 | task1 = None 890 | 891 | def task0_fun(self): 892 | global task1 # Give this task access to the task1 global variable 893 | # Initialization code here 894 | yield 895 | 896 | while True: 897 | # Task code here 898 | yield 899 | 900 | def task1_fun(self): 901 | global task0 # Give this task access to the task0 global variable 902 | # Initialization code here 903 | yield 904 | 905 | while True: 906 | # Task code here 907 | yield 908 | 909 | task0 = pyRTOS.Task(task0_fun) 910 | task1 = pyRTOS.Task(task1_fun) 911 | 912 | # Start tasks and then scheduler 913 | ``` 914 | 915 | #### Deliver Tasks Using Mailboxes 916 | 917 | Tasks can be delivered to other tasks using their mailboxes. Obviously this only works for tasks initialized with mailboxes. Order of events is less important here, but the tasks must explicitly read their mailboxes to get the task references. (Note that this is the accepted method for giving _any_ arguments to tasks, not just references to other tasks.) 918 | 919 | ``` 920 | def task_fun(self): 921 | target_task = self.recv()[0] 922 | yield 923 | 924 | while True: 925 | # Code here, including communication with target_task 926 | yield 927 | 928 | task = pyRTOS.Task(task_fun, priority=3) 929 | 930 | 931 | task.deliver(some_other_task) 932 | ``` 933 | 934 | #### Module Level Globals 935 | 936 | If the tasks exist within a separate module, the global nature of modules can be leveraged to provide what are essentially global references to those tasks. This can be done, simply by making the tasks global variables at the module level, and then referencing them as variables contained in the module. This eliminates the need for using the `global` directive, however that may make the code less readable, becaues the `global` directive at the begining of a task function is a clear indicator that the task is using that global. 937 | 938 | Excerpt from `mod_tasks.py` 939 | ``` 940 | task = pyRTOS.Task(task_fun) 941 | ``` 942 | 943 | Excert from external file 944 | ``` 945 | import mod_tasks 946 | 947 | def task_fun(self): 948 | # Initialization code 949 | yield 950 | 951 | while True: 952 | # Task code 953 | 954 | # Using reference to task, without needing to declare it global 955 | mod_tasks.task.[etc...] 956 | 957 | yield 958 | ``` 959 | 960 | 961 | 962 | ## Future Additions 963 | 964 | ### Mutual Exclusion 965 | 966 | We currently have a Mutex object (with priority inheritance) and a Binary Semaphore object (essentially a first-come-first-served Mutex), but this isn't really a complete set of mutual exclusion tools. FreeRTOS has Counting Semaphores and Recursive Mutexes. Because this uses voluntary preemption, these are not terribly high priority, as tasks can just _not yield_ during critical sections, rather than needing to use mutual exclusion. There are still cases where mutual exclusion is necessary though. This includes things like locking external hardware that has time consuming I/O, where we might want to yield for some time to allow the I/O to complete, without allowing other tasks to tamper with that hardware while we are waiting. In addition, some processors have vector processing and/or floating point units that are slow enough to warrant yielding while waiting, without giving up exclusive access to those units. The relevance of these is not clear in the context of Python, but we definitely want some kind of mutual exclusion. 967 | 968 | ### FreeRTOS 969 | 970 | We need to look through the FreeRTOS documentation, to see what other things a fully featured RTOS could have. 971 | 972 | ### Size 973 | 974 | Because this is intended for use on microcontrollers, size is a serious concern. The code is very well commented, but this means that comments take up a very significant fraction of the space. We are releasing in .mpy format for Circuit Python now, which is cutting the size down to around 5KB. Maybe we should include a source version with comments stripped out in future releases. 975 | 976 | ## Notes 977 | 978 | This needs more extensive testing. The Mutex class has not been tested. We also need more testing on block conditions. `sample.py` uses `wait_for_message()` twice, successfully. `timeout()` is also tested in sample.py. 979 | 980 | What we really need is a handful of example problems, including some for actual CircuitPython devices. When the Trinkey RP2040 comes out, there will be some plenty of room for some solid CircuitPython RTOS example programs. I have a NeoKey Trinkey and a Rotary Trinkey. Neither of these have much going on, so they are really only suitable for very simple examples. 981 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory (build artifacts don't belong in a Git repo) 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /pyRTOS/__init__.py: -------------------------------------------------------------------------------- 1 | from pyRTOS.pyRTOS import * 2 | from pyRTOS.message import * 3 | from pyRTOS.task import * 4 | from pyRTOS.scheduler import * 5 | -------------------------------------------------------------------------------- /pyRTOS/message.py: -------------------------------------------------------------------------------- 1 | import pyRTOS 2 | 3 | # Message Types 4 | QUIT = 0 5 | # 1-127 are reserved for future use 6 | # 128+ may be used for user defined message types 7 | 8 | 9 | class Message(object): 10 | def __init__(self, type, source, target, message=None): 11 | self.type = type 12 | self.source = source 13 | self.target = target 14 | self.message = message 15 | 16 | 17 | def deliver_messages(messages, tasks): 18 | for message in messages: 19 | if type(message.target) == pyRTOS.Task: 20 | message.target.deliver(message) 21 | else: 22 | targets = filter(lambda t: message.target == t.name, tasks) 23 | try: 24 | next(targets).deliver(message) 25 | except StopIteration: 26 | pass 27 | 28 | 29 | class MessageQueue(object): 30 | def __init__(self, capacity=10): 31 | self.capacity = capacity 32 | self.buffer = [] 33 | 34 | # This is a blocking condition 35 | def send(self, msg): 36 | sent = False 37 | 38 | while True: 39 | if sent: 40 | yield True 41 | elif len(self.buffer) < self.capacity: 42 | self.buffer.append(msg) 43 | yield True 44 | else: 45 | yield False 46 | 47 | def nb_send(self, msg): 48 | if len(self.buffer) < self.capacity: 49 | self.buffer.append(msg) 50 | return True 51 | else: 52 | return False 53 | 54 | 55 | # This is a blocking condition. 56 | # out_buffer should be a list 57 | def recv(self, out_buffer): 58 | received = False 59 | while True: 60 | if received: 61 | yield True 62 | elif len(self.buffer) > 0: 63 | received = True 64 | out_buffer.append(self.buffer.pop(0)) 65 | yield True 66 | else: 67 | yield False 68 | 69 | 70 | def nb_recv(self): 71 | if len(self.buffer) > 0: 72 | return self.buffer.pop(0) 73 | else: 74 | return None 75 | 76 | -------------------------------------------------------------------------------- /pyRTOS/pyRTOS.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pyRTOS 4 | 5 | version = 1.0 6 | 7 | 8 | tasks = [] 9 | service_routines = [] 10 | 11 | 12 | def add_task(task): 13 | global tasks 14 | 15 | if task.thread == None: 16 | task.initialize() 17 | 18 | tasks.append(task) 19 | 20 | tasks.sort(key=lambda t: t.priority) 21 | 22 | 23 | def add_service_routine(service_routine): 24 | global service_routines 25 | 26 | service_routines.append(service_routine) 27 | 28 | 29 | def start(scheduler=None): 30 | global tasks, service_routines 31 | 32 | if scheduler == None: 33 | scheduler = pyRTOS.default_scheduler 34 | 35 | run = True 36 | while run: 37 | for service in service_routines: 38 | service() 39 | 40 | messages = scheduler(tasks) 41 | pyRTOS.deliver_messages(messages, tasks) 42 | 43 | if len(tasks) == 0: 44 | run = False 45 | 46 | 47 | 48 | # Task Block Conditions 49 | 50 | # Timeout - Task is delayed for no less than the specified time. 51 | def timeout(seconds): 52 | start = time.monotonic() 53 | 54 | while True: 55 | yield time.monotonic() - start >= seconds 56 | 57 | def timeout_ns(nanoseconds): 58 | start = time.monotonic_ns() 59 | 60 | while True: 61 | yield time.monotonic_ns() - start >= nanoseconds 62 | 63 | # Cycle Delay - Task is delayed for no less than the number OS loops specified. 64 | def delay(cycles): 65 | ttl = cycles 66 | while True: 67 | if ttl > 0: 68 | ttl -= 1 69 | yield False 70 | else: 71 | yield True 72 | 73 | # Message - Task is waiting for a message. 74 | def wait_for_message(self): 75 | while True: 76 | yield self.message_count() > 0 77 | 78 | # Notification - Task is waiting for a notification 79 | def wait_for_notification(task, index=0, state=1): 80 | task.notes[0][index] = 0 81 | while task.notes[0][index] != state: 82 | yield False 83 | 84 | while True: 85 | yield True 86 | 87 | 88 | 89 | # API I/O - I/O done by the pyRTOS API has completed. 90 | # This blocking should be automatic, but API 91 | # functions may want to provide a timeout 92 | # arguement. 93 | # API Defined 94 | 95 | # UFunction - A user provided function that returns true 96 | # or false, allowing for complex, user defined 97 | # conditions. 98 | # 99 | # UFunctions must be infinite generators. They can 100 | # take take any initial arguments, but they must 101 | # must yield False if the condition is not met and 102 | # True if it is. Arguments may be passed into the 103 | # generator iterations, but pyRTOS should not be 104 | # expected to pass arguments in when checking. In 105 | # most cases, it would probably be better to 106 | # communicate with Ufunctions through globals. 107 | # User Defined 108 | 109 | 110 | # Blocking is achieved by yielding with a list argument. Each time pyRTOS 111 | # tests the task for readiness, it will iterate through the list, running 112 | # each generator function, checking the truth value of its output. If the 113 | # truth value of any element of the list is true, the task will unblock. 114 | # This allows for conditions to effectively be "ORed" together, such that it 115 | # is trivial to add a timeout to any other condition. If you need to "AND" 116 | # conditions together, write a UFunction that takes a list of conditions and 117 | # yields the ANDed output of those conditions. 118 | 119 | 120 | 121 | # API Elements 122 | 123 | # Mutex with priority inheritance 124 | # (highest priority waiting task gets the lock) 125 | class Mutex(object): 126 | def __init__(self): 127 | self.locked = False 128 | 129 | # This returns a task block condition generator. It should 130 | # only be called using something like "yield [mutex.lock(self)]" 131 | # or "yield [mutex.lock(self), timeout(1)]" 132 | def lock(self, task): 133 | while True: 134 | if self.locked == False or self.locked == task: 135 | self.locked = task 136 | yield True 137 | else: 138 | yield False 139 | 140 | def nb_lock(self, task): 141 | if self.locked == False or self.locked == task: 142 | self.locked = task 143 | return True 144 | else: 145 | return False 146 | 147 | def unlock(self): 148 | self.locked = False 149 | 150 | 151 | # Mutex with request order priority 152 | # (first-come-first-served priority for waiting tasks) 153 | class BinarySemaphore(object): 154 | def __init__(self): 155 | self.wait_queue = [] 156 | self.owner = None 157 | 158 | # This returns a task block condition generator 159 | def lock(self, task): 160 | self.wait_queue.append(task) 161 | 162 | try: 163 | while True: 164 | if self.owner == None and self.wait_queue[0] == task: 165 | self.owner = self.wait_queue.pop(0) 166 | yield True 167 | elif self.owner == self: 168 | yield True 169 | else: 170 | yield False 171 | finally: 172 | # If this is combined with other block conditions, 173 | # for example timeout, and one of those conditions 174 | # unblocks before this, we need to prevent this 175 | # from taking the lock and never releasing it. 176 | if task in self.wait_queue: 177 | self.wait_queue.remove(task) 178 | 179 | def nb_lock(self, task): 180 | if self.owner == None or self.owner == task: 181 | self.owner = task 182 | return True 183 | else: 184 | return False 185 | 186 | def unlock(self): 187 | self.owner = None 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /pyRTOS/scheduler.py: -------------------------------------------------------------------------------- 1 | import pyRTOS 2 | 3 | 4 | def default_scheduler(tasks): 5 | messages = [] 6 | running_task = None 7 | 8 | for task in tasks: 9 | if task.state == pyRTOS.READY: 10 | if running_task == None: 11 | running_task = task 12 | elif task.state == pyRTOS.BLOCKED: 13 | if True in map(lambda x: next(x), task.ready_conditions): 14 | task.state = pyRTOS.READY 15 | task.ready_conditions = [] 16 | if running_task == None: 17 | running_task = task 18 | elif task.state == pyRTOS.RUNNING: 19 | if (running_task == None) or \ 20 | (task.priority <= running_task.priority): 21 | running_task = task 22 | else: 23 | task.state = pyRTOS.READY 24 | 25 | 26 | if running_task: 27 | running_task.state = pyRTOS.RUNNING 28 | 29 | try: 30 | messages = running_task.run_next() 31 | except StopIteration: 32 | tasks.remove(running_task) 33 | 34 | return messages 35 | 36 | -------------------------------------------------------------------------------- /pyRTOS/task.py: -------------------------------------------------------------------------------- 1 | 2 | # FreeRTOS states are 3 | # 4 | # Running - Currently executing 5 | # Ready - Ready to run but task of higher or equal priority is currently in Running state 6 | # Blocked - Task is waiting for some event, for example, a delay 7 | # Other reasons for blocks include waiting for a queue, semaphore, or message. 8 | # Suspended - Task was explicitly suspended and will only be resumed by another task. 9 | 10 | # For Blocked states, some condition must be met to unblock. Is it possible that this 11 | # condition could be represented as a function that is called on the task object, that 12 | # will return True if the condition is met? This would allow the construction of 13 | # lamba or inner functions, that have built in references to any data required to 14 | # determine whether the unblocking condition is met. 15 | 16 | # See https://www.freertos.org/RTOS-task-states.html for state transition graph 17 | 18 | 19 | # Task states 20 | RUNNING = 0 # Currently executing on the processor 21 | READY = 1 # Ready to run but task of higher or equal priority is currently running 22 | BLOCKED = 2 # Task is waiting for some condition to be met to move to READY state 23 | SUSPENDED = 3 # Task is waiting for some other task to unsuspend 24 | # Why do we need this, since BLOCKED could be used? Creating an 25 | # unblocking condition for this would require the API to know 26 | # that the condition is a suspension, and if it already knows 27 | # it is a suspension, it can just remove it, instead of running 28 | # the test function. Thus there is no point in having a 29 | # function and we might as well just use a flag, since it is 30 | # cheaper. 31 | 32 | 33 | class Task(object): 34 | _out_messages = [] 35 | 36 | def __init__(self, func, priority=255, name=None, notifications=None, mailbox=False): 37 | self.func = func 38 | self.priority = priority 39 | self.name = name 40 | 41 | if notifications != None: 42 | self.notes = (array.array('b', [0] * notifications), 43 | array.array('l', [0] * notifications)) 44 | 45 | if mailbox: 46 | self._in_messages = [] 47 | 48 | self.state = READY 49 | self.ready_conditions = [] 50 | self.thread = None # This is for the generator object 51 | 52 | # If the thread function is well behaved, this will get the generator 53 | # for it, then it will start it, it will run its initialization code, 54 | # and then it will yield. 55 | def initialize(self): 56 | self.thread = self.func(self) 57 | next(self.thread) 58 | 59 | # Run task until next yield 60 | def run_next(self): 61 | state_change = next(self.thread) 62 | 63 | if state_change != None: 64 | self.ready_conditions = state_change 65 | self.state = BLOCKED 66 | 67 | msgs = Task._out_messages 68 | Task._out_messages = [] 69 | 70 | return msgs 71 | 72 | 73 | # Notification Functions # 74 | def wait_for_notification(self, index=0, state=1): 75 | self.notes[0][index] = 0 76 | while self.notes[0][index] != state: 77 | yield False 78 | 79 | while True: 80 | yield True 81 | 82 | 83 | def notify_set_value(self, index=0, state=1, value=0): 84 | self.notes[0][index] = state 85 | self.notes[1][index] = value 86 | 87 | def notify_inc_value(self, index=0, state=1, step=1): 88 | self.notes[0][index] = state 89 | self.notes[1][index] += step 90 | 91 | def notify_get_value(self, index=0): 92 | return self.notes[1][index] 93 | 94 | 95 | def notify_set_state(self, index=0, state=1): 96 | self.notes[0][index] = state 97 | 98 | def notify_inc_state(self, index=0, step=1): 99 | self.notes[0][index] += step 100 | 101 | def notify_get_state(self, index=0): 102 | return self.notes[0][index] 103 | ########################## 104 | 105 | 106 | # Mailbox functions # 107 | def send(self, msg): 108 | Task._out_messages.append(msg) 109 | 110 | def recv(self): 111 | msgs = self._in_messages 112 | self._in_messages = [] 113 | return msgs 114 | 115 | def message_count(self): 116 | return len(self._in_messages) 117 | 118 | def deliver(self, msg): 119 | if hasattr(self, "_in_messages"): 120 | self._in_messages.append(msg) 121 | else: 122 | name = self.name 123 | if self.name is None: 124 | name = str(self) 125 | raise Exception("Task, " + name + ", does not have a mailbox") 126 | ##################### 127 | 128 | 129 | def suspend(self): 130 | self.state = SUSPENDED 131 | self.ready_conditions = [] 132 | 133 | def resume(self): 134 | self.state = READY 135 | self.ready_conditions = [] 136 | 137 | -------------------------------------------------------------------------------- /rot_trinkey_touch.py: -------------------------------------------------------------------------------- 1 | import board 2 | import neopixel 3 | import touchio 4 | 5 | import pyRTOS 6 | 7 | ### 8 | # pyRTOS Sample Program for the Adafruit Rotary Trinkey 9 | # 10 | # Author: Ben Williams 11 | # 12 | # Description: 13 | # 14 | # This program uses pyRTOS to run three tasks: a touch input 15 | # handler, a rainbow color cycler, and a renderer that sends 16 | # color output to the NeoPixel. When no input is present, 17 | # the NeoPixel will cycle through a rainbow color effect. 18 | # When the touch strip on the edge of the Trinkey is being 19 | # touched, the NeoPixel will display red. When the bottom 20 | # pin of the three rotary encoder pins in touched, the 21 | # NeoPixel will display blue. When both the touch strip and 22 | # the bottom rotary encode pin are touched, the NeoPixel 23 | # will display green. When touch input is removed, the 24 | # NeoPixel will return to the rainbow cycle, wherever it 25 | # would have been, had the touch input never happened. 26 | ### 27 | 28 | 29 | # User defined message types start at 128 30 | COLOR_DATA = 128 31 | COLOR_RESUME = 129 32 | 33 | 34 | def touch_handler(self): 35 | # Task initialization 36 | 37 | touch_pad = touchio.TouchIn(board.TOUCH) 38 | # The following is the bottom rotary pin, 39 | # when the touch pad is oriented to the 40 | # right, looking at the board from the top. 41 | touch_rota = touchio.TouchIn(board.ROTA) 42 | touched = False 43 | 44 | NONE = 0b00 45 | PAD = 0b01 46 | ROTA = 0b10 47 | BOTH = 0b11 48 | 49 | yield 50 | 51 | # Main Task Loop 52 | while True: 53 | touch = touch_pad.value | touch_rota.value << 1 54 | if touch == BOTH and touched != BOTH: 55 | touched = BOTH 56 | self.send(pyRTOS.Message(COLOR_DATA, "touch", 57 | "renderer", (0b000, 0b111, 0b000))) 58 | elif touch == PAD and touched != PAD: 59 | touched = PAD 60 | self.send(pyRTOS.Message(COLOR_DATA, "touch", 61 | "renderer", (0b111, 0b000, 0b000))) 62 | elif touch == ROTA and touched != ROTA: 63 | touched = ROTA 64 | self.send(pyRTOS.Message(COLOR_DATA, "touch", 65 | "renderer", (0b000, 0b000, 0b111))) 66 | elif touch == NONE and touched != NONE: 67 | touched = NONE 68 | self.send(pyRTOS.Message(COLOR_RESUME, "touch", "renderer")) 69 | 70 | yield [pyRTOS.timeout(0.100)] 71 | 72 | 73 | def color_update(self): 74 | # Task initialization 75 | r = 0b000 76 | g = 0b000 77 | b = 0b111 78 | 79 | yield 80 | 81 | # Main Task Loop 82 | while True: 83 | if b == 0b000 and g > 0b000: 84 | c0 = g 85 | c1 = r 86 | elif g == 0b000 and r > 0b000: 87 | c0 = r 88 | c1 = b 89 | elif r == 0b000 and b > 0b000: 90 | c0 = b 91 | c1 = g 92 | 93 | if c1 < 0b111: 94 | c1 = c1 + 1 95 | else: 96 | c0 = c0 - 1 97 | 98 | if b == 0b000 and g > 0b000: 99 | g = c0 100 | r = c1 101 | elif g == 0b000 and r > 0b000: 102 | r = c0 103 | b = c1 104 | elif r == 0b000 and b > 0b000: 105 | b = c0 106 | g = c1 107 | 108 | self.send(pyRTOS.Message(COLOR_DATA, "color", 109 | "renderer", (r, g, b))) 110 | 111 | yield [pyRTOS.timeout(0.050)] 112 | 113 | 114 | 115 | def renderer(self): 116 | # Task initialization 117 | pixels = neopixel.NeoPixel(board.NEOPIXEL, 1) 118 | color = 0 119 | 120 | touched = False 121 | 122 | yield 123 | 124 | # Main Task Loop 125 | while True: 126 | # If there are multiple messages instructing 127 | # this to change the color, only the most 128 | # recent one will be applied. 129 | msgs = self.recv() 130 | for msg in msgs: 131 | if msg.source == "touch": 132 | if msg.type == COLOR_DATA: 133 | touched = True 134 | color = msg.message 135 | elif msg.type == COLOR_RESUME: 136 | touched = False 137 | else: 138 | if msg.type == COLOR_DATA and touched == False: 139 | color = msg.message 140 | 141 | pixels.fill(color) 142 | 143 | yield 144 | 145 | 146 | pyRTOS.add_task(pyRTOS.Task(touch_handler, priority=0, name="touch")) 147 | pyRTOS.add_task(pyRTOS.Task(color_update, priority=1, name="color")) 148 | pyRTOS.add_task(pyRTOS.Task(renderer, priority=2, name="renderer", mailbox=True)) 149 | 150 | pyRTOS.start() 151 | -------------------------------------------------------------------------------- /sample.py: -------------------------------------------------------------------------------- 1 | import pyRTOS 2 | 3 | 4 | # User defined message types start at 128 5 | REQUEST_DATA = 128 6 | SENT_DATA = 129 7 | 8 | 9 | # self is the thread object this runs in 10 | def sample_task(self): 11 | 12 | ### Setup code here 13 | 14 | 15 | ### End Setup code 16 | 17 | # Pass control back to RTOS 18 | yield 19 | 20 | # Thread loop 21 | while True: 22 | 23 | # Check messages 24 | msgs = self.recv() 25 | for msg in msgs: 26 | 27 | ### Handle messages by adding elifs to this 28 | if msg.type == pyRTOS.QUIT: # This allows you to 29 | # terminate a thread. 30 | # This condition may be removed if 31 | # the thread should never terminate. 32 | 33 | ### Tear down code here 34 | print("Terminating task:", self.name) 35 | print("Terminated by:", msg.source) 36 | 37 | ### End of Tear down code 38 | return 39 | elif msg.type == REQUEST_DATA: # Example message, using user 40 | # message types 41 | self.send(pyRTOS.Message(SENT_DATA, 42 | self, 43 | msg.source, 44 | "This is data")) 45 | ### End Message Handler 46 | 47 | ### Work code here 48 | # If there is significant code here, yield periodically 49 | # between instructions that are not timing dependent. 50 | # Also, it is generally a good idea to yield after 51 | # I/O commands that return instantly but will require 52 | # some time to complete (like I2C data requests). 53 | # Each task must yield at least one per iteration, 54 | # or it will hog all of the CPU, preventing any other 55 | # task from running. 56 | 57 | 58 | 59 | ### End Work code 60 | 61 | yield [pyRTOS.timeout(0.5)] 62 | 63 | if self.name == "task1": 64 | target = "task2" 65 | else: 66 | target = "task1" 67 | 68 | print(self.name, "sending quit message to:", target) 69 | self.send(pyRTOS.Message(pyRTOS.QUIT, self, target)) 70 | 71 | # Testing message passing system 72 | print(self.name, "sending quit message to:", "task3 (does not exist)") 73 | print("This should silently fail") 74 | self.send(pyRTOS.Message(pyRTOS.QUIT, self, "task3")) 75 | 76 | yield [pyRTOS.wait_for_message(self)] 77 | 78 | 79 | pyRTOS.add_task(pyRTOS.Task(sample_task, name="task1", mailbox=True)) 80 | pyRTOS.add_task(pyRTOS.Task(sample_task, name="task2", mailbox=True)) 81 | pyRTOS.add_service_routine(lambda: print("Service Routine Executing")) 82 | 83 | 84 | pyRTOS.start() 85 | --------------------------------------------------------------------------------