| on | id | SF | period ms | first TX ms | payload | air | duty |
|---|
f.anchor
| SF | latest payload received | from | at | rx | lost |
|---|
def name(a, b) { ... return expr } # top-level code is setup: it runs once
attach(RX, fn) attach(TX_DONE, fn) attach(BUTTON, fn)
t = timer(fn) timer_start(t, delay, period) timer_stop(t) # period 0 = one-shot
radio_rx(sf) radio_off() transmit(bytes [, sf]) halt()
frame fields: f.anchor f.time f.sf f.rssi f.payload
timer fields: t.late t.missed t.period
vote(list) push(list, v) len(list) get(list, i) byte(b, i) bytes(b0..b7)
now() rx_ms() dropped() log(...) show(...) # nothing blocks: no wait, no listen
Setup runs once and must return; after that the VM only runs your callbacks, one at a
time, and a callback that never returns is killed by a watchdog. transmit() is
asynchronous: it takes the receiver off air until TX_DONE, and then the receiver
comes back on by itself. The transmit and receive spreading factors are chosen
independently: transmit(payload, sf) sends on sf and leaves the
receiver's own spreading factor untouched, while transmit(payload) sends on
whichever one the receiver is using.
Air time is real: EU868, BW 125 kHz, CR 4/8, 8-symbol preamble, 9 bytes on air — 54 ms at
SF7, 91 at SF8, 181 at SF9, 297 at SF10, 594 at SF11, 1188 at SF12. A frame is only received
if the radio is tuned to its SF for the whole transmission, so waking up late loses
it, and two transmissions overlapping on one SF destroy each other. The loop period and
jitter in the control bar stand in for everything else the board has to do, which is what
makes t.late move.