|
|
|
# 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.
|
|
|
|
* Adjust voltage range on battery meter -- goes to 75% pretty much immediately.
|
|
|
|
* Figure out why Power Management isn't accurately reflecting configured timeouts.
|