# AO_Scheduler

AO_Scheduler runs cron entries on a Kode Dot: the time expression, how an entry fires, the wake for it and the mqtt check behind the Kode channel.

## What it is

**AO_Scheduler** holds a table of **entries**. An entry is a name, a time in the five fields of cron, and some work. When the minute of an entry comes, the work runs. The work is a console line, as if somebody had typed it, or an event for an Active Object of an app. The time is read on the epoch clock as the UTC calendar that Unix seconds give, and the finest step is one minute.

The Kode Dot spends most of its life asleep (`sdk/kaos/lowpower`). AO_Scheduler is a participant of the low-power rounds. Before the board sleeps, it tells AO_LowPower the real time of its earliest entry, and the board wakes for it. The wake lights the screen and restores every owner, as a button does. When the work of that wake ends and nobody used the board, AO_Scheduler asks AO_LowPower to put it back to sleep without waiting for the idle time.

One entry is always there: **`mqtt`**, the safety net of the Kode channel. Its work is not a line. Every 15 minutes by default, AO_Scheduler hands the time of the next check to AO_Net, which looks at its sessions in a radio wake that leaves the screen dark. If the interrupt that wakes the P4 for a message is ever lost, the message still arrives at the next check. You can change the time of `mqtt` or switch it off like any other entry.

AO_Scheduler exists only with `CONFIG_KODE_SCHEDULER`. The option is off in the SDK and on in kodeOS. With it off, the calls in `kaos.h` refuse and log `the scheduler is not built`, and the console verb `cron` answers `>err code=4 msg=not_built`.

## How it is connected

```mermaid
flowchart LR
  cron["console: cron"] --> sched["AO_Scheduler"]
  code["app AO: sched_request_set_ao"] --> sched
  sched -- "ConsoleLineEvt, source SCHED" --> con["AO_Console"]
  sched -- "SchedFireEvt" --> app["app AO"]
  sched -- "net_check_at" --> net["AO_Net"]
  sched -- "lowpower_wake_at, lowpower_request_rest" --> lp["AO_LowPower"]
  con -- "sched_line_done<br/>sched_line_busy" --> sched
  net -- "sched_check_done" --> sched
```

*From an entry to its work*

AO_Scheduler owns the table, the minute timer and the order of the lines it hands to the console. It drives no device and claims no resource. It owns the NVS namespace `kcron`.

### Priority and start

AO_Scheduler runs at priority 16 (`KODE_PRIO_SCHEDULER`), the level of `KODE_PRIO_USER_HIGH`, above AO_Display. A step is a pass over sixteen entries and a post, and nothing it does touches a device, but the minute has to arrive on time while AO_Gui draws all the face's time. It is not AO_LowPower's level (24) and it does not sit under AO_Gui.

**The level is behind `CONFIG_KODE_SCHEDULER`.** An app that turns the option on gives `KODE_PRIO_USER_HIGH` up. The macro is not defined for it, so an Active Object that still starts at it fails to compile, where QP would answer a second start at an occupied level with an assert at boot. With the option off the level is free.

The queue holds `KODE_SCHED_QUEUE_LEN` (24) events. A request leaves `KODE_SCHED_REQ_MARGIN` (4) places free for the answer of a line and the timer. The stack is the small tier, 3072 bytes.

`kode_init()` starts AO_Scheduler right after AO_LowPower, or after AO_Boot when `CONFIG_KODE_LOWPOWER` is off, and before AO_Storage and AO_Console, so that the first `cron` line finds a queue to post to. At the end of `kode_init()`, once every Active Object has its queue, `sched_system_up()` posts it `SCHED_UP_SIG`. Until then the minute timer is not armed and nothing is posted to another Active Object, because a post to a queue that does not exist yet is an assert.

The Active Object subscribes to `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG` and `LOWPOWER_REFUSED_SIG`, only with `CONFIG_KODE_LOWPOWER`. Without it the scheduler still runs and the entries fire while the board is awake, since there is no sleep to wake from. The states are `Scheduler_on`, and `Scheduler_waiting` while a line is out and the console owes its answer.

### What an entry is

| Field | What it is |
| --- | --- |
| Name | 1 to `KODE_SCHED_NAME_MAX` (15) characters from `A-Z a-z 0-9 _ - .`. Unique, and the way an entry is shown, changed or removed. `mqtt` is reserved |
| Time | Five cron fields, at most `KODE_SCHED_WHEN_MAX` (63) characters |
| Work | A console line of at most 239 characters (`KODE_SCHED_CMD_MAX` is 240 bytes, NUL included), an event posted to an Active Object, or nothing (`mqtt`) |
| Owner | `console`, `code` or `builtin` |
| Stored | Whether the entry survives a reset. Only the entries of the console and the time of `mqtt` are stored |

The table has `KODE_SCHED_MAX` (16) places. The slot of `mqtt` is always taken, even when `mqtt` is off, so `mqtt` and 15 other entries fill it. A text that does not fit its buffer is refused with the name of its field and never cut.

**An entry can be changed or removed only through the door that made it.** There is one name space for all the doors.

| Owner | Made by | Stored | Changed by |
| --- | --- | --- | --- |
| `console` | The `cron` verb, from any port | Yes, in `kcron` | The `cron` verb |
| `code` | `sched_request_set_ao()`, from an Active Object of an app | No: the app makes it again at each boot | `sched_request_set_ao()` and `sched_request_remove()` |
| `builtin` | The scheduler: `mqtt` | Its time, or its being off | The `cron` verb: a new time, or `when=off` |

A request on a name that another owner holds is refused with the reason `owner`. The code door cannot take `mqtt`. The console can change or switch off `mqtt` and cannot give it a line.

### The time expression

An expression has five fields separated by spaces or tabs: minute (0 to 59), hour (0 to 23), day of month (1 to 31), month (1 to 12) and day of week (0 to 6). Sunday is 0, and 7 means Sunday too. Each field is a list of items separated by commas, and an item is `*`, a number or a range `a-b`. A `*` or a range can carry a step: `*/5`, `9-17/2`. A step must follow `*` or a range, so `5/2` is a syntax error. A step cannot be wider than the field: `*/61` on the minutes is refused and `*/60` is not.

The expression is matched against the UTC calendar. No zone is applied and daylight saving does not exist for it. Names (`JAN`, `MON`), shorthands such as `@hourly`, and the characters `L`, `W` and `#` are refused as `syntax`. A sixth field, for seconds, is refused as `fields`.

**Day of month and day of week combine as classic cron does.** When both are restricted, a day matches if either of them matches. When the text of either one begins with `*`, a day must match both. A star with a step, `*/2`, counts as a star.

The calendar is worked out in the code with `int64_t`, so leap years, the ends of months and the year 2038 behave the same on the host and on the board. The search for the next minute looks at most `SCHED_CRON_HORIZON_DAYS` (1462) days ahead. An expression that parses but matches no minute in that time, such as `0 0 31 2 *`, is refused with the reason `never`. The other reasons of a refused expression are `fields` (not five), `range` (a value outside its field), `step` and `syntax`.

### When an entry fires

An entry fires **at most once for each matching minute**, at the first second of that minute or as close to it as the wake allows.

- **One timer.** AO_Scheduler arms one `QTimeEvt` for the earliest next minute of the entries that have work. The wait is at most `KODE_SCHED_ARM_MAX_MS` (60000 ms), so a jump of the clock is noticed within a minute. At the expiry the clock is read again, and the pass runs for the minute the clock says, only if that minute is not before the one the timer was armed for.
- **Nothing fires while the clock is unknown.** The clock kept across a power cut counts as set: `time` says `state=approx` and entries run against it. While no clock is known, AO_Scheduler asks for it every `KODE_SCHED_CLOCK_POLL_MS` (1000 ms), only while an entry has work or `mqtt` is on. The first minute that fires is the one after the clock becomes known.
- **A minute is skipped, not made up.** An entry does not run late when its minute passes while the board is OFF, the clock is unknown, or the board is busy past that minute. This is what cron does. A wake that comes late inside the same minute still runs it.
- **A jump forward** fires once, for the minute it lands on, and skips the minutes it jumped over.
- **A set of the clock re-arms the timer.** Every set of the wall clock (`time set`, a time server's answer, the Time page) calls the hook kdrv_clocks keeps (`kdrv_clocks_wall_on_set()`), which posts AO_Scheduler the static `SCHED_WALL_SET_SIG` with a margin of 0. AO_Scheduler arms its timer again from the new time, so the minute after a set runs and `next=` is what fires.
- **A clock set back** never repeats a minute that already ran. A clock set back by `KODE_SCHED_BACK_MAX_MIN` (60) minutes or more is not waited for: the last minute is forgotten and the entries run from the new time, with a NOTICE that says so.
- **Nothing fires in the first `KODE_SCHED_SETTLE_MS` (5000 ms) after boot**, and nothing fires before `sched_system_up()`. A minute that falls inside either runs late, when both are over, if the clock is still inside that minute.

### The lines

The work of a console entry is a line that runs with the console source **SCHED** (`KODE_CONSOLE_SRC_SCHED`), a fourth source beside the cable, BLE and the Kode channel.

- **One at a time.** The entries that fire in the same minute are handed to AO_Console one by one, in the order of the table. The next goes out after the console has answered the one before, so at most one line is out and an answer is the answer to it. The console calls `sched_line_done()` for every line of this source, on its terminator, `>ok` or `>err`, whichever way the line ended.
- **A busy console is waited for.** A line that arrives while the console runs another command, a `screenshot` from the cable for example, is not answered `busy`. The console calls `sched_line_busy()` instead, the entry keeps its place, and AO_Scheduler gives the line again every `KODE_SCHED_RETRY_MS` (1000 ms) until the console takes it. The tries end with the bound of the minute's work, `KODE_SCHED_WORK_BOUND_MS`: a line the console never took is dropped with a WARN, `the entry '<name>' did not run: the work of its minute ran out of time`, and the entry runs again at its next minute.
- **The answers go to the log.** A line from SCHED has no reader. Each reply line is written as a NOTICE of the tag `kcli`, for example `sched: >|pong` and `sched: >ok`. Because they are NOTICE lines, `config key="log" value=w`, `e` or `off` hides them.
- **Which verbs it may run.** `cron` checks the first word of `cmd` when the entry is made: a word that is no verb of this build, or `cron` itself, is refused at once with `>err code=3 msg=cmd`, and nothing is stored. At run time a SCHED line obeys the exclusions of a line from the Kode channel: no payload verbs (`install src="terminal"`, `file_put`, `file_get`, `screenshot dst=console`, `mirror_start`) and no `install dst=c5`. These are accepted when the entry is made, because the verb alone does not say whether a line is a payload one, and they answer `>err code=5 msg=sched` each time the entry runs. It cannot run `cron` either: an entry does not edit the table it runs from. Every other verb runs, `app_run`, `sleep` and `power_off` included.
- **It is not use of the board.** A SCHED line does not start the idle time over, does not count as input and does not wake ASLEEP (`sdk/kaos/lowpower`).

**An entry whose line does not answer.** AO_Scheduler waits for the answer of a line for at most `KODE_SCHED_WORK_BOUND_MS` (10000 ms). When the bound runs out, a WARN says `a line did not answer in 10000 ms; the work of its minute is given up`, the lines of the minute that did not go out yet are dropped (one WARN each), the answer that was owed is forgotten, and the board is asked to rest if the wake was the timer's. The entry whose line did not answer skips its next minute, with one WARN, and runs again from the minute after. An entry whose next minute is much later, such as an hourly one, is not skipped.

**An entry of code.** `sched_request_set_ao(who, name, when, ao, sig)` makes an entry whose work is an event. At each match AO_Scheduler posts a `SchedFireEvt` with the entry's name (a copy) to `ao` with the signal `sig`, leaving `KODE_SCHED_POST_MARGIN` (2) places free in the queue of `ao`. The entry is done `KODE_SCHED_WORK_BOUND_MS` after the post and is skipped while its previous event is within the bound. A post that the queue would not take is a WARN, and the entry did not fire.

### Asleep: the wake for an entry

AO_Scheduler joins the rounds of AO_LowPower as `scheduler`.

- **On a round for ASLEEP**, it reads the clock again, gives AO_LowPower the real time of its earliest entry that has work with `lowpower_wake_at()`, lets the minute timer go because the QP tick stops, and answers ready. It answers busy only for work it still holds itself: a line that waits for the console, or an event of code within its bound (`sched work`), or no event was left to carry the time (`sched wake`). A line already handed to the console is the console's. A line that waits behind a `sleep` or a `power_off` does not hold the round, because that line asks for the round. When the line answers with the board at rest, the lines behind it are dropped with one WARN each. With only `mqtt` in the table it gives no time at all, because the time of `mqtt` goes to AO_Net.
- **The wake is a whole AWAKE**, with the cause `timer`. It is not a radio wake. The screen lights and every owner restores. The log says `scheduler has work due: waking for it`. On the AWAKE the minute timer is armed again, and the minute the board woke for is not lost.
- **The rest after the work.** When the wake was the timer's, AO_Scheduler calls `lowpower_request_rest(KODE_LOWPOWER_SRC_SCHED)` after a pass of a minute whose work is done, a pass that fired nothing included. It asks once and does not wait for the answer. An entry is done when the console has answered its line, or, for an entry of code, when the bound has passed. For a refusal the log says at INFO `the board stays awake: <why>`. Two refusals pass by themselves soon and the next pass asks again after them: `request: a round is running` and `request: request pending`. Any other refusal stands for the rest of the wake, so an entry every minute does not start a round a minute that meets the same answer. `input: board used` means a person has the board and the wake is theirs. A wake that a button, a cable or a message caused is not asked to rest.
- **A round for OFF** is answered ready with no time, or busy `sched work` when the scheduler holds work that has not gone out. The board boots when it wakes from OFF, and the minutes that passed are skipped.

### The `mqtt` entry

The `mqtt` entry is the net under the interrupt. The C5 wakes the P4 for a message on MQTT or BLE and the answer is immediate. The entry does not replace that. It covers the case in which the interrupt is lost.

Its time is `*/15 * * * *` on a board with nothing stored (`KODE_SCHED_MQTT_DEFAULT`). The `cron` verb changes it to any expression, with a minimum period of one minute, or switches it off with `when=off`. A switched-off `mqtt` shows as `when="off"` with `next=-`, and giving it a time switches it on again. It takes no line.

AO_Scheduler does not wake the board for `mqtt`. It hands the time of the next check to AO_Net with `net_check_at()`, and AO_Net does the rest (`sdk/kaos/net`):

- The time is sent when the system is up, sent again when `mqtt` or the clock changes, and sent after every check that AO_Net reports with `sched_check_done()`. It is `-1` while the clock is unknown or the entry is off. A time that has come stays AO_Net's until AO_Net reports the check made, for at most `SCHED_CHECK_HOLD_S` (63 s): no later time is sent over it, so a wake in that minute dwells on the check instead of losing it. While the board sleeps nothing is sent to AO_Net except the next time, as the answer to a check that was made.
- AO_Net gives AO_LowPower the earlier of this time and the deadline of the Kode channel. At that time the board makes a **radio wake**: the screen stays asleep and only the radio owners wake.
- **The check wakes the radio only when something listens**: the C5 link is up, Wi-Fi has an address, and the Kode channel is in use or a client of yours holds the MQTT session. Otherwise a check that falls due is passed at once.
- The radio wake stays for `KODE_NET_CHECK_DWELL_MS` (3000 ms), counted from the beginning of the wake, so what the C5 or the broker holds for the P4 flows in. A check that falls due in a whole wake dwells the same 3000 ms, and while it dwells AO_Net answers a round for ASLEEP busy, `check dwell`, so the rest that AO_Scheduler asks for begins again until the dwell ends. The dwell is shorter than the retry window, and `kqp_config.h` asserts it at compile time. A round for OFF is not refused for the dwell, because it closes the sessions the dwell listens to.

**The broker must keep a persistent session** for the safety net to be worth anything. A message that the broker does not hold for a Kode Dot that is not connected is lost before it looks. Holding it is the broker's side: a session that survives the disconnect and QoS 1 on the topics that matter. AO_Scheduler does not provide it.

### `cron` from the Kode channel

The verb `cron` is accepted from the Kode channel like every other verb. The risk is that a line from the channel can leave a **stored** entry. The entry runs later, with no session open and nobody at the board, and it runs every verb that a SCHED line may run. Anyone who can publish on `cmd/in` can leave work for the future. List the entries with `cron` and remove the one that should not run with `cron name="<name>" when=off`. The replies go out on `cmd/out`, as for any verb (`internals/kode-channel`).

### Storage

The console entries and the time of `mqtt` are stored in the NVS namespace `kcron`, which only AO_Scheduler writes. Each console entry has one record under the key `e<slot>` and `mqtt` has one under the key `mqtt`. A record is a `SchedStored` blob of 321 bytes: a version byte, the name, the time and the line, with a `_Static_assert` on the size.

AO_Scheduler skips a record that cannot be read, has a wrong length or version, an unterminated text, a bad name or an expression that does not parse, with a WARN that says `the stored record <key> is skipped: <why>`. A board with nothing usable stored has `mqtt` at its default. A write that fails keeps the entry in RAM with `stored=no`, and a WARN.

An **app** cannot write the store, because the apps region has a flash-write guard. In a build without `CONFIG_KODEOS_BOOT_ROLE_OS` nothing is read or written and every entry lives in RAM, `mqtt` at its default.

The table is `KODE_SCHED_MAX` entries of about 350 bytes. It is taken from PSRAM once, when the Active Object is constructed (design rule 8). If PSRAM has no room, the log says `no memory for the table; the scheduler runs nothing`, every request is refused `memory` and nothing fires.

## Specifications

### The verb

| Form | What it does |
| --- | --- |
| `cron` | Lists the entries, `mqtt` first. It is always listed |
| `cron name="x"` | Shows one entry |
| `cron name="x" when="0 7 * * *" cmd="ping"` | Makes or changes a console entry. `cmd` must start with a verb of this build other than `cron`. `mqtt` takes no `cmd` |
| `cron name="x" when=off` | Removes a console entry, or switches `mqtt` off. `off` is the one bare word `when` takes |

A record reads `>|cron name="mqtt" when="*/15 * * * *" cmd=- owner=builtin next=<epoch> stored=no`. The syntax of the verb is in `internals/command-set`.

### Signals

| Signal | How | From |
| --- | --- | --- |
| `SCHED_SET_REQ_SIG` | `sched_request_set()` | The console: add or change an entry that runs a line |
| `SCHED_SET_AO_REQ_SIG` | `sched_request_set_ao()` | An Active Object of an app: an entry that posts an event |
| `SCHED_REMOVE_REQ_SIG` | `sched_request_remove()` | An app: remove an entry of code |
| `SCHED_LIST_REQ_SIG` | `sched_request_list()` | The console: one entry or all |
| `SCHED_LINE_DONE_SIG` | `sched_line_done()`, a static event | AO_Console: the line is answered |
| `SCHED_LINE_BUSY_SIG` | `sched_line_busy()`, a static event | AO_Console: the line arrived while another command ran, and was not taken |
| `SCHED_CHECK_DONE_SIG` | `sched_check_done()` | AO_Net: the `mqtt` check was made |
| `SCHED_WALL_SET_SIG` | The hook kdrv_clocks calls (`kdrv_clocks_wall_on_set()`), a static event, margin 0 | kdrv_clocks: the wall clock was set |
| `SCHED_UP_SIG` | `sched_system_up()`, a static event | `kode_init()`: every Active Object has its queue |
| `SCHED_DUE_SIG`, `SCHED_WORK_TMO_SIG`, `SCHED_RETRY_SIG` | Its own one-shot `QTimeEvt` | Itself: the minute, the bound of the work, and the wait before a line the console was busy for goes again |
| `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG`, `LOWPOWER_REFUSED_SIG` | Subscribed | AO_LowPower |

A request is answered to the asker with zero or more `SCHED_ENTRY_SIG` (`SchedEntryEvt`: `owner`, `stored`, `next_s`, `name`, `when`, `cmd`) and then one `SCHED_END_SIG` (`SchedEndEvt`: `st`, `why`, `count`). An entry is posted with `KODE_SCHED_END_MARGIN` (1) place left in the asker's queue so the end always has room. If an entry did not fit, the end says `KODE_ERROR` with the reason `partial` and, in `count`, how many entries did arrive. The reasons of a refusal are `args name when cmd fields range step syntax never full owner unknown memory`. A reply that has no room is logged and dropped and never asserts, because the console can be driven from the Kode channel.

AO_Scheduler posts a `ConsoleLineEvt` with the source SCHED to AO_Console for each line, a `SchedFireEvt` to the Active Object of an entry of code, `NET_CHECK_AT_SIG` to AO_Net through `net_check_at()`, and `LOWPOWER_REST_SIG` and `LOWPOWER_WAKE_AT_SIG` to AO_LowPower through `lowpower_request_rest()` and `lowpower_wake_at()`.

### Calls

| Call | What it does |
| --- | --- |
| `sched_request_set(who, name, when, cmd)` | Makes or changes a console entry. `when` of `"off"` removes it, or switches `mqtt` off |
| `sched_request_set_ao(who, name, when, target, sig)` | Makes or changes an entry of code |
| `sched_request_remove(who, name)` | Removes an entry of code. A console entry or `mqtt` answers `owner` |
| `sched_request_list(who, name)` | Lists one entry, or all of them when `name` is NULL |
| `sched_line_done()` | Says the console answered the line it was given. Nothing happens when no line is out |
| `sched_line_busy()` | Says the console was busy and did not take the line. AO_Scheduler gives it again after `KODE_SCHED_RETRY_MS`. Safe from any task |
| `sched_check_done(passed_s)` | AO_Net's report that the check of that minute was made |
| `sched_system_up()` | `kode_init()`'s call, once, after the last Active Object started. Exists only with the option |

Each request returns `false` when it could not be posted: no event was left, or AO_Scheduler's queue had fewer than `KODE_SCHED_REQ_MARGIN` places free. Then no answer follows. With the option off every call refuses and logs.

### Constants

| Name | Value |
| --- | --- |
| `KODE_SCHED_MAX`, `KODE_SCHED_NAME_MAX`, `KODE_SCHED_WHEN_MAX` | 16, 15, 63 |
| `KODE_SCHED_QUEUE_LEN`, `KODE_SCHED_REQ_MARGIN`, `KODE_SCHED_POST_MARGIN`, `KODE_SCHED_END_MARGIN` | 24, 4, 2, 1 |
| `KODE_SCHED_ARM_MAX_MS`, `KODE_SCHED_CLOCK_POLL_MS`, `KODE_SCHED_SETTLE_MS` | 60000 ms, 1000 ms, 5000 ms |
| `KODE_SCHED_WORK_BOUND_MS`, `KODE_SCHED_RETRY_MS`, `KODE_SCHED_BACK_MAX_MIN` | 10000 ms, 1000 ms, 60 min |
| `KODE_NET_CHECK_DWELL_MS`, `SCHED_CHECK_HOLD_S` | 3000 ms, 63 s |
| `SCHED_CRON_HORIZON_DAYS` | 1462 |

## Gotchas

> **WARNING**
>
> **The entry runs at the wrong hour.** The time is UTC. `0 7 * * *` is 07:00 UTC whatever the zone of the face.

> **NOTE**
>
> **Nothing appears in the log when an entry runs.** The answers of a SCHED line are NOTICE lines, and `config key="log" value=w` hides them. Read the level first, then look for `entry '<name>' runs` at INFO and `sched: >ok` at NOTICE.

> **WARNING**
>
> **`cron` answers `>err code=3 msg=cmd` for a line that looks right.** The first word of `cmd` is not a verb of this build, or it is `cron`. Check the spelling against `help`, and remember that a verb behind an option the build lacks does not exist. A line that `sched_request_set()` gives from code is not checked this way and fails when it runs.

> **NOTE**
>
> **A wake for an entry lights the screen.** Only the `mqtt` check is a radio wake. Every other entry wakes the whole board, and the board sleeps again as soon as the line answers, unless somebody used it.

> **NOTE**
>
> **An app that turns the scheduler on has no `KODE_PRIO_USER_HIGH`.** Start its own Active Objects at another level, `KODE_PRIO_USER_LOW` for example.

## Example code

An app that asks for a console entry named `tick` that runs `ping` every minute. Once the wall clock is set, the log shows `sched: >|pong` and `sched: >ok` at the start of each minute. The entry lives in RAM, since an app cannot write the store, so the app makes it again at every start.

```c
/*
 * every_minute.c - 2026-11-03 - from Kode with love
 *
 * Makes a console entry named "tick" that runs the line "ping" every minute,
 * UTC. Needs CONFIG_KODE_SCHEDULER=y, the console, and a wall clock that is
 * set (join Wi-Fi, or run "time set" from the console). Once it is, the log
 * shows "sched: >|pong" and "sched: >ok" at the start of each minute. Nobody
 * waits for the answer of the request, so the asker is NULL.
 */
#include "kode_ao.h"

#define TAG        "every_minute"
#define TICK_NAME  "tick"
#define TICK_WHEN  "* * * * *"
#define TICK_LINE  "ping"

/* Start the system, then make the entry. */
void app_main(void)
{
    kode_init();

    if (!sched_request_set(NULL, TICK_NAME, TICK_WHEN, TICK_LINE)) {
        KLOG_E(TAG, "the scheduler did not take the request");
    }

    for (;;) {
        kode_delay_ms(1000U);
    }
}
```

Not compiled against the SDK. **TBD**: a build and a run on a Kode Dot. From the console, `cron name="tick" when="* * * * *" cmd="ping"` makes the same entry stored, and `cron name="tick" when=off` removes it.

## In the SDK

- [`components/kaos/scheduler`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/scheduler): `scheduler_ao.c`, `sched_cron.c` (plain C with no RTOS, which the host tests run as it is) and `scheduler_none.c` for a build without the option.
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): the calls, the owner type and the numbers of the entries.
- [`kqp_config.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kqp/include/kqp_config.h): the constants `KODE_SCHED_*` and the priority.
