Adam Pippin
4 years ago
1 changed files with 239 additions and 0 deletions
@ -0,0 +1,239 @@ |
|||||
|
# watchos |
||||
|
|
||||
|
A simple framework for developing on the M5StickC. |
||||
|
|
||||
|
## Features |
||||
|
|
||||
|
* Kernel which handles task scheduling, running, signalling, and more. |
||||
|
* Eeprom abstraction layer which creates an allocation table to allow multiple |
||||
|
modules to share storage without hardcoding offsets or addresses everywhere. |
||||
|
* User Input module which handles tracking button presses, debouncing, and |
||||
|
converting the inputs into inter-process signals to provide a unified way |
||||
|
for handling events. |
||||
|
* Interface library which provides an easy way to handle creating tiling layouts |
||||
|
and rotation. |
||||
|
* Power management library to handle screen dimming and putting the processor |
||||
|
into deep sleep to conserve battery while giving tasks an opportunity to |
||||
|
save their state. |
||||
|
* The base system uses less than 10% of the ESP32's storage and 5% of its RAM. |
||||
|
|
||||
|
### Kernel |
||||
|
|
||||
|
The Kernel is the core of watchos. It provides a kind of cooperative multitasking. |
||||
|
|
||||
|
Tasks can be registered and set to run on a schedule or based on interrupts. Tasks |
||||
|
are given the opportunity for clean startup and shutdown, to handle user input, |
||||
|
to handle updating the display, and more. Tasks can be paused and resumed and |
||||
|
exited tasks can return an exit code consumable by other processes. |
||||
|
|
||||
|
The Kernel tries to intelligently conserve power by putting the processor into |
||||
|
light sleep in between scheduled executions, while making use of hardware features |
||||
|
to wake up sooner if events of interest occur. |
||||
|
|
||||
|
### EAT |
||||
|
|
||||
|
The "EEPROM Allocation Table" a la FAT/File Allocation Table. A few bytes are |
||||
|
reserved at the start of flash storage to track how many bytes have been allocated |
||||
|
and to which module. A read/write abstraction is provided to allow modules to |
||||
|
read/write from their allocated space. This serves to prevent conflicts between |
||||
|
multiple independent pieces of code attempting to use the storage device. |
||||
|
|
||||
|
### User Input |
||||
|
|
||||
|
A simple module that tracks if either button has been pressed, handles debouncing |
||||
|
(not detecting another press until they have first been released), and sends |
||||
|
interprocess signals to other processes when the events occur. This simplifies |
||||
|
input handling and prevents the need for duplication of things like debounce |
||||
|
logic. |
||||
|
|
||||
|
### Interface |
||||
|
|
||||
|
A take on a tiling window manager supplies a simple way to lay out multiple |
||||
|
modules on the display in a flexible way as well as provide for adjusting the |
||||
|
layout for various display rotations. Each module can register as many screen |
||||
|
sections as it wants as well as as many nested tiles as it wants in order to |
||||
|
create a layout. |
||||
|
|
||||
|
### Power Management |
||||
|
|
||||
|
A simple power management library which handles tracking idle time and taking |
||||
|
appropriate actions when the device is no longer in use. After a short delay, |
||||
|
the display's backlight is dimmed. After a longer delay it will signal all |
||||
|
running tasks to exit (giving them a chance to save state to eeprom or quickly |
||||
|
finish any outstanding tasks) then the processor will be placed into deep sleep |
||||
|
and the display and backlight powered off. This, combined with the Kernel's |
||||
|
intelligent use of light sleep helps to extend the M5StickC's battery life well |
||||
|
beyond what is typically expected. |
||||
|
|
||||
|
### Resource Efficient |
||||
|
|
||||
|
The base system (Kernel, EAT, User Input, User Interface, Power Management) uses |
||||
|
only around 10% of available program flash and 5% of dynamic memory leaving |
||||
|
plenty of room to build on top of. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
### Your App |
||||
|
|
||||
|
Include all of the watchos files in your project, then simply initialize the |
||||
|
kernel and register your tasks in your `setup()` method, then call the kernel |
||||
|
during each invocation of your `loop()` method. For example: |
||||
|
|
||||
|
``` |
||||
|
#include <M5StickC.h> |
||||
|
#include "Kernel.h" |
||||
|
|
||||
|
int MyTask(int pid, unsigned int signal) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
void setup() |
||||
|
{ |
||||
|
M5.begin(); |
||||
|
Kernel_setup(); |
||||
|
Kernel_start(&MyTask, 5000); |
||||
|
} |
||||
|
|
||||
|
void loop() |
||||
|
{ |
||||
|
Kernel_tick(); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
If your tasks make use of the EAT module, it will need to be initialized before |
||||
|
first use and a call to load the allocation table will need to be made on startup. |
||||
|
You can either add the initialization call to run once then remove from your code |
||||
|
later, or provide the ability to trigger initialization by, for example, holding |
||||
|
a button during power up. For example: |
||||
|
|
||||
|
``` |
||||
|
void setup() |
||||
|
{ |
||||
|
M5.begin(); |
||||
|
Kernel_setup(); |
||||
|
Kernel_start(&MyTask, 5000); |
||||
|
|
||||
|
M5.update(); |
||||
|
if (M5.BtnB.isPressed()) |
||||
|
{ |
||||
|
EAT_initialize(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
EAT_load(); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
If the side button is held during power up, the EEPROM will be reinitialized with |
||||
|
an empty allocation table, otherwise the existing table will be loaded (causing a |
||||
|
panic if no table exits or is valid). |
||||
|
|
||||
|
### Writing Your Task |
||||
|
|
||||
|
Each task method requires a simple signature: |
||||
|
|
||||
|
``` |
||||
|
int MyTask(int pid, unsigned int signal) |
||||
|
{ |
||||
|
[...] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
This method takes its own process id and any pending signals as arguments, and |
||||
|
is expected to return an exit code as its return value. |
||||
|
|
||||
|
The id is generated each time the task is started. If you expect to start a |
||||
|
task multiple times, this allows you to manage keeping independent data stores |
||||
|
for each invocation. |
||||
|
|
||||
|
The signals are a way for the kernel and other tasks to notify you of events. |
||||
|
This will tell you whether your task was just started, is being requested to |
||||
|
shutdown, has reached its invocation interval, needs to redraw the display, etc. |
||||
|
|
||||
|
The returned exit code should always be `0` if your program expects to continue. |
||||
|
Any other value will signal the kernel to stop executing your task, and that value |
||||
|
will be available to other running tasks. |
||||
|
|
||||
|
#### Signals |
||||
|
|
||||
|
Signals are defined in `Kernel.h` and include: |
||||
|
|
||||
|
* `SIGNAL_TICK` - your task's configured run interval has elapsed |
||||
|
* `SIGNAL_START` - this is the first invocation of your task method and it |
||||
|
should perform first-startup initialization; this will only be sent once |
||||
|
* `SIGNAL_STOP` - your task is requested to shutdown; you *should* return a |
||||
|
non-zero exit code in response to this, though if your task needs to continue |
||||
|
running for a brief time to finish shutting down cleanly it may return 0 and |
||||
|
quit on a later invocation |
||||
|
* `SIGNAL_INPUT_A` - the main (home) button has been pressed; only delivered |
||||
|
once per press |
||||
|
* `SIGNAL_INPUT_B` - the secondary (side) button has been pressed; only delivered |
||||
|
once per press |
||||
|
* `SIGNAL_REDRAW` - the display needs to be redrawn and you should redraw |
||||
|
anything you have on the screen -- you should only ever perform draws in |
||||
|
response to this signal |
||||
|
|
||||
|
To see the full benefit of this system, you should read and understand the |
||||
|
further documentation on the kernel and how exactly you should use and respond |
||||
|
to these signals. |
||||
|
|
||||
|
##### Signal Mask |
||||
|
|
||||
|
Your application may not be interested in all of these signals. To avoid your |
||||
|
method being called unnecessarily, you can set a signal mask -- this tells the |
||||
|
kernel what signals you're interested in. The mask should include all signals |
||||
|
you *do* want to receive. For example: |
||||
|
|
||||
|
``` |
||||
|
Kernel_signal_mask(mypid, SIGNAL_START | SIGNAL_STOP | SIGNAL_TICK); |
||||
|
``` |
||||
|
|
||||
|
#### Example |
||||
|
|
||||
|
A simple framework for a method: |
||||
|
|
||||
|
``` |
||||
|
#include "Kernel.h" |
||||
|
|
||||
|
int MyTask(int pid, unsigned long signal) |
||||
|
{ |
||||
|
if (signal & SIGNAL_START) |
||||
|
{ |
||||
|
// Perform startup here |
||||
|
} |
||||
|
|
||||
|
if (signal & SIGNAL_STOP) |
||||
|
{ |
||||
|
// Perform shutdown here |
||||
|
return 255; |
||||
|
} |
||||
|
|
||||
|
if (signal & SIGNAL_TICK) |
||||
|
{ |
||||
|
// Your requested run interval has transpired; perform any regular |
||||
|
// logic here to update state. |
||||
|
|
||||
|
// If you need to update the screen, do *not* do it here, instead trigger |
||||
|
// a redraw so the entire screen can be cleared and redrawn: |
||||
|
Kernel_signal(SIGNAL_REDRAW); |
||||
|
// Your application will also receive this REDRAW signal and can perform its |
||||
|
// draws in response to that. |
||||
|
} |
||||
|
|
||||
|
if (signal & SIGNAL_REDRAW) |
||||
|
{ |
||||
|
// Update the screen from your state variables here. |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## TODO: |
||||
|
|
||||
|
* Better kernel module system to support EAT as well as wifi/bluetooth/etc. |
||||
|
* More efficient draws: if we're using the tiling wm, no reason we need to |
||||
|
clear the entire screen; if we know which parts need a redraw we could |
||||
|
instead drawRect and only redraw those. |
Loading…
Reference in new issue