You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
7.9 KiB
240 lines
7.9 KiB
4 years ago
|
# 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.
|