Language Forms

is (language) name is expr

At the top level, creates or rebinds a slot from a literal, existing name, fn, cells, record value, or word-call result. Inside a block, declares a lexical local. Use set to mutate an existing name after initialization.

Example

counter is 0
set counter to counter + 1
to demo [ local-value is 42; local-value ]

to (language) to name with params [ body ]

Binds a top-level word to a Code value.

Example

to double with n [ n * 2 ]
double: 21

fn (language) fn with params [ body ]

Creates a non-capturing Code value for a top-level binding. Calls resolve top-level word names; Frothy has no dynamic call word for a parameter or local holding Code.

Example

adder is fn with x, y [ x + y ]
adder: 20, 22

here (language) here name is expr

Explicitly declares a lexical local in the current block. Inside a block, name is expr means the same thing.

Example

to demo [ here speed is 10; speed ]

set (language) set place to expr

Mutates an existing name, cells element, or record field.

Example

set counter to counter + 1
set readings[0] to 11
set point -> x to 30

if (language) if cond [ then ] else [ fallback ]

Chooses between blocks and yields the chosen block’s value.

Example

if n < 2 [ n ] else [ fib: n - 1 + fib: n - 2 ]

when (language) when cond [ body ]

Runs a one-sided conditional block and yields nil when the condition is false.

Example

when adc.percent: $a0 > 50 [ led.on: ]

unless (language) unless cond [ body ]

Runs a one-sided conditional block when the condition is false.

Example

unless ready [ led.off: ]

while (language) while cond [ body ]

Repeats a block while the condition stays truthy.

Example

while x > 0 [ set x to x - 1 ]

repeat (language) repeat count as i [ body ]

Repeats a block a fixed number of times, optionally binding a zero-based index.

Example

repeat 10 as i [ set total to total + i ]

forever (language) forever [ body ]

Repeats a block until interrupted or until the body errors.

Example

forever [ led.toggle:; wait: 100 ]

cells (language) cells: length -> Cells

Creates fixed-size mutable indexed storage.

Example

readings is cells: 3
set readings[0] to 11

record (language) record Name [ fields ]

Declares a top-level record shape and constructor.

Example

record pt [ x, y ]
p is pt: 3, 4

attempt (language) attempt [ body ] rescue [ fallback ]

Runs fallback code after a catchable runtime error and yields the fallback value.

If the body succeeds, its value is the result and the rescue never runs. If it fails, the value stack is restored to the start of the attempt, the rescue block runs, and its value is the result. Inside the rescue, error.name and error.code describe the caught error. Errors raised by called words are catchable in the caller; parse errors happen before execution and an interrupt is never catchable — Ctrl-C always wins.

Example

1 + attempt [ 2 / 0 ] rescue [ 9 ]
to read-or-default with sock [
  attempt [ tcp.read: sock, 64 ] rescue [
    print: error.name
    bytes.from-text: ""
  ]
]

Deeper: errors in the language reference .


error.code (language) error.code -> Int

Reads the caught runtime error code inside a rescue block.

Example

attempt [ 2 / 0 ] rescue [ error.code ]

error.name (language) error.name -> Text

Reads the caught runtime error name inside a rescue block.

Example

attempt [ missing: ] rescue [ error.name ]

on (events) on source edge [ body ]

Registers a GPIO or Wi-Fi event body from inside a definition.

For GPIO the source is a pin and the edge is rising, falling, or changes, with an optional debounce <ms> that suppresses contact chatter from mechanical switches. The Wi-Fi sources are wifi.disconnected and wifi.reconnected, with no edge. The body runs at a safe point after the edge, not inside an interrupt handler, so it may freely print, read sensors, or call other words.

Example

to arm-button [ on $boot_button falling [ led.toggle: ] ]
to arm-button [
  on $boot_button falling debounce 25 [
    print: "pressed\n"
  ]
]

Deeper: Events module .


every (events) every millis [ body ]

Registers a repeating timer event from inside a definition.

The body runs at safe points between instructions, so it interleaves with foreground work instead of preempting it — and it waits while a native call such as wait is in progress. Output from an event body arrives as asynchronous ! lines at the prompt. The current profile allows one event body per definition: give each registration its own small word. Cancel with cancel every <same millis>.

Example

to start-ticking [ every 1000 [ print: "tick" ] ]
to heartbeat [ every 500 [ led.toggle: ] ]
to arm-all [ heartbeat: start-ticking: ]

Deeper: Events module .


after (events) after millis [ body ]

Registers a one-shot timer event from inside a definition.

Like every, the body runs once at a safe point after the delay elapses; the registration then clears itself. after and every are distinct event sources even at the same millisecond value, and cancel after <millis> removes a pending one-shot before it fires.

Example

to once [ after 500 [ led.off: ] ]

Deeper: Events module .


cancel (events) cancel source

Cancels a GPIO, timer, or Wi-Fi event by its event source identity.

Identity is the source, not the body: a GPIO cancellation uses the pin regardless of edge or debounce, while timer cancellations must name the same form (every or after) and the same millisecond value used to register. Use the events prompt command to list what is currently registered.

Example

cancel every 1000
cancel $boot_button
cancel wifi.disconnected

Deeper: Events module .

Values And Image

nil (value) nil

Represents no value and takes the false branch.

Example

if nil [ 1 ] else [ 2 ]

true (value) true

Represents boolean truth.

Example

if true [ led.on: ]

false (value) false

Represents boolean falsehood and takes the false branch.

Example

if false [ 1 ] else [ 2 ]

boot (image) boot -> Code|nil

Runs after restore when the top-level slot holds Code.

boot is an ordinary top-level name with one special property: after the saved image is restored at power-on, if it holds Code, the device runs it before opening the prompt. It is the deploy story — bind it, save, and the board runs your program standalone from then on. Reopen peripheral handles inside it (handles never survive a reset), and remember a boot forever loop is still interruptible from the console with Ctrl-C.

Example

boot is fn [ led.on: ]
save
boot is fn [
  here led is pwm.open: $led_builtin, 1000
  forever [
    pwm.write: led, (map: (adc.read: $a0), 0, 4095, 0, 10000)
    wait: 20
  ]
]
save

Deeper: the image, save, and boot .


one (image) one -> Int

Base-image literal for the integer 1.

Example

one + 41

save (persistence) save -> nil

Writes the current overlay image to persistent storage.

Words, top-level values, records, and event registrations live in an overlay over the base image. save writes that overlay to flash, so it survives power loss. Nothing persists until you run save.

Frothy reinstalls each event registration after startup or restore. A restored timer starts its interval again. Elapsed timer time and queued candidates do not persist. Live handles and transient Bytes also do not persist. Re-establish live handles in boot.

A slot holding a live handle is written as nil and named in the response; your running program keeps the handle, so saving never interrupts the hardware you are working on. Inside a larger form, save: refuses instead — see image and persistence .

Example

save

Deeper: the image, save, and boot .


restore (persistence) restore -> nil

Replaces the live overlay with the saved overlay.

Example

restore

dangerous.wipe (persistence) dangerous.wipe -> nil

Clears the live and saved overlay and returns to the base image.

Example

dangerous.wipe

words (inspection) words

Lists visible names at the prompt.

Example

words

see (inspection) see name

Renders the source form for a binding.

Example

see boot

status (inspection) status

Reports session and runtime status at the prompt.

Example

status

frothy.release (inspection) () -> Text

Returns the firmware release name baked into the running build (for example "v0.1.11").

The same value leads the status line as release=..., so tools can read it without evaluating code; frothy.release is the in-language form for scripts that want to check what they are running on. Available on text-capable profiles (every shipped board build).

Example

frothy.release

events (inspection) events

Lists active timer, GPIO, and Wi-Fi event bindings.

Example

events

mem (inspection) mem [heap|slots|objects|events]

Reports live capacity usage, optionally narrowed to one topic.

Example

mem
mem objects

clear (inspection) clear

Removes the live overlay and returns to the base image without deleting the saved overlay.

Example

clear
restore

apply (wire command) apply HEX

Decodes one serialized overlay update and installs it. This is a host-tool protocol command, not Frothy source; the decoded payload must fit the apply_bytes capacity reported by status.

Example

apply 46524f...

run (wire command) run HEX

Decodes and executes one serialized instruction stream without installing it. This is a host-tool protocol command, not a dynamic Code-call word.

Example

run 0102...

install-library (installation command) install-library

Begins replacement of the persistent library tier and sends following definitions to that tier. The CLI and editor own this lifecycle; do not use it as ordinary project source.

Example

install-library

install-user (installation command) install-user

Selects the user tier for following definitions after a library installation.

Example

install-user

wipe-user (installation command) wipe-user

Removes and commits the user tier while preserving the installed library tier. This is the project-sync primitive used by host tooling.

Example

wipe-user

commands (installation command) commands

Lists the command names you can type at the prompt, including itself.

words lists the words in the image. The commands are not words: they are names the prompt reads before it evaluates the line. This lists those.

Example

commands
status words events commands clear see apply run install-library install-user wipe-user mem

close-handles (hardware) close-handles -> Int

Closes every open handle and returns how many it closed.

When you interrupt a running program, the program stops but its handles stay open. The board keeps the pins, and the next program that wants one of them reports busy. close-handles releases all of them, so you do not have to name each handle or reach for wipe-user, which also removes your definitions.

Your words, your slot values, and your registered events are not changed. A slot that held a closed handle keeps the old value, and a program that uses it reports bad handle. Open the resource again to get a new handle.

If the board still holds a resource, a notice names the kinds that are still open. Inside a larger form close-handles: refuses with prompt only: a program must not close the handles its own words are using.

Example

close-handles
2

print (io) (Text|Bytes) -> nil

Writes raw text or bytes to the console output.

Example

print: "hello"

GPIO, ADC, And LED

$led_builtin (constant) Int

Names the board’s built-in LED pin.

Example

gpio.output: $led_builtin

$a0 (constant) Int

Names the board’s default ADC input pin.

Example

adc.read: $a0

$boot_button (constant) Int

Names the board boot button pin.

Example

gpio.read: $boot_button

$led_active_level (constant) Int

The electrical level that turns the current board’s built-in LED on.

Example

gpio.write: $led_builtin, $led_active_level

$sda (constant) Int

The board’s default I2C SDA pin.

Example

bus is i2c.open: 0, $sda, $scl, 400000

$scl (constant) Int

The board’s default I2C SCL pin.

Example

bus is i2c.open: 0, $sda, $scl, 400000

gpio.mode (gpio) (pin, mode) -> nil

Configures a GPIO pin direction: 0 for input, 1 for output, 2 for input with the internal pull-up enabled.

Mode 2 is the one for buttons wired to ground: the pull-up holds the pin high until the button pulls it low, so no external resistor is needed. A pin currently held by an open PWM channel reports busy instead of reconfiguring — changing the direction would silently detach the pin from its PWM signal. Close the channel with pwm.close first.

Example

gpio.mode: $led_builtin, 1

gpio.write (gpio) (pin, level) -> nil

Writes a GPIO output level.

A pin currently held by an open PWM channel reports busy instead of writing — a plain digital write would silently detach the pin from its PWM signal. Close the channel with pwm.close first. gpio.read stays unrestricted: sampling a pin does not disturb it.

Example

gpio.write: $led_builtin, 1

pin (gpio alias) (pin, level) -> nil

Alias for gpio.write, retained for the shortest direct pin writes.

Example

pin: $led_builtin, 1

gpio.read (gpio) (pin) -> Int

Reads a GPIO input level.

Example

gpio.read: $boot_button

gpio.high (gpio helper) (pin) -> nil

Writes level 1 to a GPIO pin.

Example

gpio.high: $led_builtin

gpio.low (gpio helper) (pin) -> nil

Writes level 0 to a GPIO pin.

Example

gpio.low: $led_builtin

gpio.toggle (gpio helper) (pin) -> nil

Writes the opposite of the pin’s current GPIO level.

Example

gpio.toggle: $led_builtin

gpio.output (gpio helper) (pin) -> nil

Configures a GPIO pin as output.

Example

gpio.output: $led_builtin

gpio.input (gpio helper) (pin) -> nil

Configures a GPIO pin as input.

Example

gpio.input: $boot_button

led.on (led helper) () -> nil

Turns on the board’s default LED.

Example

led.on:

led.off (led helper) () -> nil

Turns off the board’s default LED.

Example

led.off:

led.toggle (led helper) () -> nil

Toggles the board’s default LED.

Example

led.toggle:

blink (led helper) (pin, count, wait) -> nil

Blinks a pin by alternating high, sleep, low, sleep.

Example

blink: $led_builtin, 3, 75

led.blink (led helper) (count, wait) -> nil

Blinks the board’s default LED.

Example

led.blink: 3, 75

adc.read (adc) (pin) -> Int

Reads a raw ADC value from a pin.

The result is a raw converter count, not a voltage. The range is the converter’s resolution — 0 through 4095 on every board shipped so far, across roughly the 0–3.3 V input span — and readings are noisy by nature. Read real values from your circuit before depending on exact thresholds, and average several readings when stability matters.

Only pins on the chip’s first ADC unit are accepted; other pins fail with a bad value error. A second unit, where the chip has one, is not exposed when it shares hardware with the radio. Which pins carry analog input depends on your board (on the classic ESP32 DevKit V1, for example, GPIO 32–39); the board’s $a0 constant always names a safe analog pin, so prefer it over raw numbers.

Example

adc.read: $a0
to knob [
  map: (adc.read: $a0), 0, 4095, 0, 100
]

Deeper: Read a Sensor .


adc.above? (adc) (pin, threshold) -> Bool

Returns true when a raw ADC reading is above a threshold.

Example

adc.above?: $a0, 2000

adc.percent (adc helper) (pin) -> Int

Maps a raw 0 to 4095 ADC reading to 0 to 100.

Example

adc.percent: $a0

Timing, Math, And Random

wait (timing) (millis) -> nil

Sleeps for a nonnegative number of milliseconds while still polling interrupts.

wait sleeps in 1-millisecond steps and checks for Ctrl-C between steps, so a long wait is always interruptible. Registered event bodies do not run while a wait is in progress; they queue and run at the next safe point after it returns. Millisecond resolution is the floor — there is no finer-grained wait. Nanosecond-scale timing lives in the signal words , which capture and emit edges with dedicated hardware.

Example

wait: 75
to slow-blink [
  repeat 5 [
    led.toggle:
    wait: 2000
  ]
]

Deeper: Timing module .


millis (timing) () -> Int

Reads milliseconds since boot, wrapped to the tagged integer range.

On the 32-bit runtime the value wraps after about 12.4 days. A subtraction between two nearby readings stays correct across the wrap, so use millis for elapsed-time deltas, not as a wall clock or a forever-increasing counter.

Example

millis:
started is millis:
wait: 25
elapsed is 0
set elapsed to (millis:) - started

Deeper: Timing module .


micros (timing) () -> Int

Reads microseconds since boot, wrapped to the tagged integer range.

The wrap period is about 17.9 minutes on the 32-bit runtime, so micros is for short spans only — profiling a word, timing a sensor exchange. For sub-microsecond edge timing use the signal words , which capture and emit with 100-nanosecond hardware quantization.

Example

micros:

Deeper: Timing module .


abs (math) (x) -> Int

Returns the absolute value of an integer.

Example

abs: -5

min (math) (a, b) -> Int

Returns the smaller of two integers.

Example

min: 3, 9

max (math) (a, b) -> Int

Returns the larger of two integers.

Example

max: 3, 9

clamp (math) (x, lo, hi) -> Int

Clamps an integer to an inclusive range.

Example

clamp: 120, 0, 100

map (math) (x, in_lo, in_hi, out_lo, out_hi) -> Int

Linearly maps an integer from one range to another.

Example

map: 2048, 0, 4095, 0, 100

mod (math) (a, b) -> Int

Returns a modulo b with the runtime’s integer semantics.

Example

mod: 37, 10

sqrt (math) (x) -> Int

Returns the floor square root of a nonnegative integer.

Example

sqrt: 81

wrap (math helper) (value, size) -> Int

Returns 0 for nonpositive sizes, otherwise mod: value, size.

Example

wrap: 37, 10

sign (math helper) (n) -> Int

Clamps an integer to -1, 0, or 1.

Example

sign: -20

random.next (random) () -> Int

Returns the next pseudo-random nonnegative integer.

Example

random.next:

random.below (random) (limit) -> Int

Returns a pseudo-random integer in [0, limit).

Example

random.below: 10

random.seed (random) (seed) -> nil

Seeds the pseudo-random generator.

Example

random.seed: 123

random.chance? (random helper) (numer, denom) -> Bool

Returns true when a random draw falls inside a numerator over denominator chance.

Example

random.chance?: 1, 4

random.percent? (random helper) (percent) -> Bool

Returns true for a percentage chance from 0 to 100.

Example

random.percent?: 25

UART, I2C, And PWM

$baud_9600 (uart constant) Int

Names the UART baud-rate code for 9600 baud.

Example

uart.open: 1, $baud_9600

$baud_19200 (uart constant) Int

Names the UART baud-rate code for 19200 baud.

Example

uart.open: 1, $baud_19200

$baud_38400 (uart constant) Int

Names the UART baud-rate code for 38400 baud.

Example

uart.open: 1, $baud_38400

$baud_57600 (uart constant) Int

Names the UART baud-rate code for 57600 baud.

Example

uart.open: 1, $baud_57600

$baud_115200 (uart constant) Int

Names the UART baud-rate code for 115200 baud.

Example

uart.open: 1, $baud_115200

$baud_1200 (uart constant) Int

Names the UART baud rate 1200.

Example

uart.open: 1, $baud_1200

uart.open (uart) (port, baud) -> Handle

Opens an auxiliary UART with platform default pins.

An auxiliary UART is a serial port for talking to other devices — GPS modules, AT-command radios, another microcontroller — separate from the console UART Frothy itself lives on (which is never available here). port counts the auxiliary ports from 0; how many a board has depends on its chip (the classic ESP32 DevKit V1, for example, has two). The format is fixed 8N1 with no flow control, and baud is the plain rate — the $baud_* constants are just named integers for the common ones. This form leaves the pin choice to the chip’s per-port defaults, which may not be routed anywhere useful on your board; when in doubt, use uart.open-on and pick the pins explicitly. The handle is volatile — reopen ports in boot.

Example

aux is uart.open: 1, $baud_115200

Deeper: UART module .


uart.open-on (uart) (port, tx, rx, baud) -> Handle

Opens an auxiliary UART on caller-picked TX and RX pins.

The reliable form of uart.open: the chip’s pin matrix can route a UART to nearly any free GPIO, so pick pins that suit your wiring. TX is named from this board’s point of view — connect it to the other device’s RX, and vice versa, and share a ground. Pins already used by the console or another open UART are rejected rather than silently stolen.

Example

aux is uart.open-on: 1, 17, 16, $baud_115200

Deeper: UART module .


uart.write-byte (uart) (handle, byte) -> nil

Writes one byte to an auxiliary UART.

Example

uart.write-byte: aux, 65

uart.read-byte (uart) (handle) -> Int

Reads one byte from an auxiliary UART, or -1 when none is ready.

The read never blocks: -1 means “nothing yet”, not an error, so a polling loop stays live and interruptible. Received bytes queue in a driver buffer until read, so a periodic drain does not lose data between polls.

Example

uart.read-byte: aux
to drain [
  forever [
    here b is uart.read-byte: aux
    when b >= 0 [ pad.emit-byte: b ]
    when b < 0 [ wait: 5 ]
  ]
]

Deeper: UART module .


uart.available (uart) (handle) -> Int

Returns the count of bytes waiting on an auxiliary UART.

Example

uart.available: aux

uart.close (uart) (handle) -> nil

Closes an auxiliary UART and releases its handle.

Example

uart.close: aux

i2c.open (i2c) (port, sda, scl, freq) -> Handle

Opens an I2C bus on a port with selected pins and frequency.

I2C is a two-wire shared bus: one data line (SDA) and one clock line (SCL) carry traffic for every device wired to them, and each device answers to its own 7-bit address. That is why one i2c.open serves a whole chain of sensor breakouts. freq is the clock in Hz — 100000 (standard) works with everything; 400000 (fast mode) with most modern parts. The bus lines need pull-up resistors; nearly every breakout board ships with them soldered on, so this usually costs no thought. $sda and $scl always name your board’s conventional I2C pins — use them instead of raw pin numbers and the code stays portable. The handle is volatile — reopen the bus in boot.

Example

bus is i2c.open: 0, $sda, $scl, 400000

Deeper: I2C module .


i2c.write (i2c) (bus, addr, bytes) -> nil

Writes bytes to a 7-bit I2C address.

Example

i2c.write: bus, 104, "AT"

i2c.read (i2c) (bus, addr, count) -> Bytes

Reads count bytes from a 7-bit I2C address.

The raw transaction, for devices that stream data rather than answer the register convention — i2c.read-reg and friends cover the common register-mapped chips more directly. The result is transient Bytes: pick it apart with bytes.at in the same evaluation, or text.pack it to keep it.

Example

i2c.read: bus, 104, 2

i2c.close (i2c) (bus) -> nil

Closes an I2C bus and releases its handle.

Example

i2c.close: bus

i2c.read-reg (i2c) (bus, addr, reg) -> Int

Reads one byte from a register at a 7-bit I2C address.

Most I2C chips present themselves as a numbered array of registers — a datasheet’s register map — and this word is the whole read protocol in one step: write the register number, restart, read the value back. It is usually all you need to talk to a sensor without a driver library. The example asks an MPU-6050 motion sensor (address 104) for its WHO_AM_I register (117); the chip answers 104, a standard aliveness check.

Example

i2c.read-reg: bus, 104, 117
to sensor-alive? [
  (i2c.read-reg: bus, 104, 117) = 104
]

Deeper: I2C module .


i2c.read-reg16 (i2c) (bus, addr, reg) -> Int

Reads a big-endian 16-bit register at a 7-bit I2C address.

Example

i2c.read-reg16: bus, 104, 117

i2c.write-reg (i2c) (bus, addr, reg, value) -> nil

Writes one byte to a register at a 7-bit I2C address.

The write half of the register convention — configuration usually means writing a handful of registers from the datasheet. The example writes 0 to the MPU-6050’s power-management register (107), which wakes the chip from its power-on sleep; most sensors need one or two writes like this before their readings mean anything.

Example

i2c.write-reg: bus, 104, 107, 0

Deeper: I2C module .


i2c.write-reg16 (i2c) (bus, addr, reg, value) -> nil

Writes a big-endian 16-bit register at a 7-bit I2C address.

Example

i2c.write-reg16: bus, 104, 107, 0

pwm.open (pwm) (pin, freq) -> Handle

Opens a PWM channel on a pin at a frequency in Hz.

PWM switches the pin on and off at freq cycles per second; what you control afterwards with pwm.write is the fraction of each cycle spent on. For LED dimming, 1000 Hz is comfortably above what the eye can see. The returned handle is live working state — it does not survive a reset, so reopen channels in boot.

Opening is idempotent for an exact repeat: pwm.open on a pin that already has a channel at the same frequency returns the existing handle instead of an error, so a re-run definition just works. Asking for a different frequency on an open pin reports busy — close the channel first.

Example

led is pwm.open: $led_builtin, 1000

Deeper: PWM module , Fade an LED .


pwm.write (pwm) (handle, duty) -> nil

Sets PWM duty in the inclusive range 0 to 10000.

Duty is parts-per-ten-thousand of each cycle spent on: 0 is fully off, 5000 is half, 10000 is fully on. The fixed scale is independent of the board’s native PWM resolution — the platform maps it onto the hardware — so duty values stay portable across targets.

Example

pwm.write: led, 5000
to led.percent with handle, pct [
  pwm.write: handle, (clamp: pct, 0, 100) * 100
]

Deeper: PWM module .


pwm.close (pwm) (handle) -> nil

Closes a PWM channel and releases its handle.

Example

pwm.close: led

Text, Bytes, Network, And Power

text.length (text) (text) -> Int

Returns the byte length of a text value.

Frothy text is a sequence of bytes, and every text word measures and indexes in bytes — there is no separate character type. Plain ASCII is one byte per character; multi-byte UTF-8 characters count as their byte length.

Example

text.length: "ready"

text.equals? (text) (a, b) -> Bool

Returns true when two text values have equal bytes.

Example

text.equals?: "ok", "ok"

text.concat (text) (a, b) -> Text

Joins two text values into a new text value.

Example

text.concat: "he", "llo"

text.at (text) (text, index) -> Int

Returns the byte at an index in a text value.

Example

text.at: "A", 0

text.from-int (text) (n) -> Text

Renders an integer as decimal text.

Example

text.from-int: 42

bytes.from-text (bytes) (text) -> Bytes

Copies a text value into a transient bytes buffer.

Bytes values are scratch space: they live in a small arena that is cleared when the line (or the outermost call) finishes evaluating, so a Bytes value cannot be kept in a top-level binding across lines — a stale one fails cleanly rather than reading garbage. Build, inspect, and send bytes within one evaluation; when a result must outlive the line, copy it out with text.pack. This transience is deliberate — buffers recycle themselves, so byte-shuffling never fragments the heap.

Example

bytes.length: (bytes.from-text: "AT")

Deeper: Text, bytes, and pad .


bytes.from-byte (bytes) (byte) -> Bytes

Creates a one-byte buffer from a 0 to 255 integer.

Example

bytes.from-byte: 65

bytes.from-int (bytes) (n) -> Bytes

Converts an integer to ASCII decimal bytes.

Example

bytes.from-int: 42

bytes.length (bytes) (buf) -> Int

Returns the byte count of a bytes buffer.

Example

bytes.length: buf

bytes.at (bytes) (buf, index) -> Int

Returns the byte at an index as a 0 to 255 integer.

Example

bytes.at: buf, 0

bytes.equals? (bytes) (a, b) -> Bool

Returns true when two bytes buffers have equal contents.

Example

bytes.equals?: buf, bytes.from-text: "AT"

bytes.concat (bytes) (a, b) -> Bytes

Concatenates two bytes buffers into a new buffer.

Example

bytes.concat: bytes.from-text: "A", bytes.from-text: "T"

text.pack (text) (buf) -> Text

Copies a bytes buffer into persistent text storage.

The door out of bytes-transience: Bytes die when the line ends, but the Text this returns lives in the text pool and can sit in a binding like any other value. The usual shape is receive-then-pack — read from a socket, UART, or I2C device into transient bytes, then pack the part worth keeping.

Example

text.pack: buf
reply is text.pack: (tcp.read: sock, 64)

Deeper: Text, bytes, and pad .


wifi.save (network) (ssid, pass) -> nil

Stores Wi-Fi credentials in the Frothy Wi-Fi NVS namespace.

Example

wifi.save: "ssid", "password"

wifi.connect (network) () -> nil

Stops access-point hosting and connects Wi-Fi using stored credentials.

Example

wifi.connect:

wifi.host (network) (ssid, pass) -> nil

Starts or reconfigures a Wi-Fi access point with captive DNS.

Captive DNS answers every lookup with the device’s address. An empty password creates an open network. A nonempty password must contain 8 through 63 bytes. A password outside that range raises bad value. Hosting replaces station mode, and a later wifi.connect stops hosting.

Example

wifi.host: "frothy", ""

wifi.ready? (network) () -> Bool

Returns true when Wi-Fi is connected or hosting.

Example

wifi.ready?:

wifi.ip (network) () -> Text

Returns the active interface’s dotted-quad address.

It returns the station address while connected and the access-point address while hosting. It raises no network when neither interface is active.

Example

wifi.ip:

http.get (network) (url) -> Bytes

Fetches a plain HTTP URL and returns the complete response body.

The call blocks until the response arrives or fails, and network failures raise catchable errors — wrap unattended fetches in attempt/rescue. The response must fit FR_HTTP_MAX_BODY; an oversized response returns no partial result. The result is transient Bytes; consume or convert it in the same word. wifi.connect must have succeeded first.

Example

http.get: "http://example.com/"
to fetch-size with url [
  attempt [ bytes.length: (http.get: url) ] rescue [ -1 ]
]

Deeper: Network module .


http.post (network) (url, body) -> Bytes

Posts Text or Bytes to a plain HTTP URL and returns the complete response body.

The request uses Content-Type: text/plain. Response limits, timeout, and errors are identical to http.get, including no partial result for an oversized response. The result is transient Bytes.

Example

http.post: "http://example.com/readings", "reading=42"

Deeper: Network module .


tcp.open (network) (host, port) -> Handle

Opens a TCP connection to a host and port.

Example

sock is tcp.open: "example.com", 80

tcp.listen (network) (port) -> Handle

Opens the single TCP listener slot on a port from 1 through 65535.

Repeating the same port returns the same handle. A different port returns busy while the listener is open. tcp.close and close-handles close the listener.

Example

server is tcp.listen: 80

tcp.accept (network) (server) -> Handle|nil

Accepts one pending TCP client without blocking.

Returns nil when no client is waiting. Otherwise, the result is an ordinary TCP handle for tcp.read, tcp.write, tcp.available, and tcp.close. Poll it inside an every loop.

Example

to serve [ client is tcp.accept: server; if client [ tcp.close: client ] ]
to start-serving [ every 150 [ serve: ] ]

tcp.read (network) (sock, count) -> Bytes

Reads up to count bytes from a TCP socket.

Example

tcp.read: sock, 64

tcp.write (network) (sock, bytes) -> nil

Sends the raw bytes of a text or bytes value to a TCP socket.

Example

tcp.write: sock, "ping"

tcp.close (network) (sock) -> nil

Closes a TCP connection or listener and releases its handle.

Example

tcp.close: sock

tcp.available (network) (sock) -> Int

Returns the bytes available for immediate tcp.read.

Example

tcp.available: sock

watchdog.arm (power) (timeout_ms) -> nil

Arms the watchdog with a timeout in milliseconds.

A watchdog is a hardware countdown that resets the chip if the program stops making progress: once armed, the program must call watchdog.feed within every timeout window or the board reboots. Use it for installations that must recover from a hang without a human present. The timeout is 1000 through 60000 ms, and re-arming replaces the timeout and starts a new window.

Example

watchdog.arm: 5000
watchdog.arm: 5000
forever [
  do-one-unit-of-work:
  watchdog.feed:
]

Deeper: Power module .


watchdog.feed (power) () -> nil

Feeds an already armed watchdog.

Feeding restarts the timeout window. Feed after demonstrated progress — a completed reading, a finished frame — not unconditionally at the top of a loop that could keep spinning while the real work is stuck, which defeats the supervision. Feeding before watchdog.arm is an error.

Example

watchdog.feed:

Deeper: Power module .


sleep.deep (power) (ms) -> nil

Enters deep sleep for a duration; the chip cold-boots on wake.

Deep sleep powers down the CPU and RAM — only the low-power wake circuitry stays on, which is why a sleeping board can draw microamps instead of tens of milliamps. The cost is that nothing survives in memory: waking is a fresh start that restores the saved image and runs boot again. Save durable state and reopen peripheral handles in boot. Use it for battery projects that act briefly and sleep long; use wait when the program should simply pause and continue.

Example

sleep.deep: 60000
boot is fn [
  log-reading: (adc.read: $a0)
  sleep.deep: 3600000
]
save

Deeper: Power module .


sleep.wake-on-gpio (power) (pin, level) -> nil

Configures GPIO wake for the next deep sleep.

The configuration is pending state in RAM for the next sleep.deep call only — it is not a persistent handler, and because deep sleep cold-boots, you must call it again (typically in boot) before each sleep. The pin must support the target’s deep-sleep wake mechanism, and level is the value (0 or 1) that wakes the chip. A timed sleep with a wake pin configured wakes on whichever happens first.

Example

sleep.wake-on-gpio: $boot_button, 0
sleep.deep: 3600000

Deeper: Power module .

PAD

pad.reset (pad) () -> nil

Clears the transient pad buffer.

The pad is one shared builder buffer (64 bytes on the shipped profiles) for assembling small messages a byte at a time. Unlike Bytes values, it survives between lines and calls — bytes accumulate until you clear it — which is what makes it useful for collecting input that arrives over many loop iterations. The workflow is pad.resetpad.emit-byte as data arrives → pad.pack (keep as text) or pad.type (print). Reset before starting a new message; leftover bytes from the last one are the classic surprise.

Example

pad.reset:

Deeper: Text, bytes, and pad .


pad.emit-byte (pad) (byte) -> nil

Appends one byte to the transient pad buffer.

Example

pad.emit-byte: 65

pad.length (pad) () -> Int

Returns the transient pad buffer length.

Example

pad.length:

pad.type (pad) () -> nil

Writes the transient pad buffer to console output.

Example

pad.type:

pad.peek-byte (pad) (index) -> Int

Reads one byte from the transient pad buffer.

Example

pad.peek-byte: 0

pad.pack (pad) () -> Text

Packs the transient pad bytes into a text value.

The harvest step: whatever has accumulated in the pad becomes a durable Text value that can sit in a binding. Packing does not clear the pad — call pad.reset when you start the next message.

Example

pad.pack:
pad.reset:
pad.emit-byte: 111
pad.emit-byte: 107
status is pad.pack:  -- "ok"

Deeper: Text, bytes, and pad .


Trace And Pulse

Digital Trace

trace.open (trace) () -> Handle

Opens the single bounded digital-edge capture.

Example

capture is trace.open:

trace.watch (trace) (capture, pin) -> Int

Adds a pin to a capture and returns its channel index from 0 through 2.

Example

scl-channel is trace.watch: capture, $scl

trace.arm (trace) (capture) -> nil

Clears previous edges and arms the configured capture.

Example

trace.arm: capture

trace.wait (trace) (capture, timeout_ms) -> Bool

Waits interruptibly and returns true when the capture completes before the timeout.

Example

trace.wait: capture, 1000

trace.stop (trace) (capture) -> nil

Finishes an armed capture immediately so its collected edges can be read.

Example

trace.stop: capture

trace.count (trace) (capture) -> Int

Returns the number of captured edges.

Example

trace.count: capture

trace.channel (trace) (capture, index) -> Int

Returns the watched-channel index for one captured edge.

Example

trace.channel: capture, 0

trace.level (trace) (capture, index) -> Int

Returns the pin level after one captured edge.

Example

trace.level: capture, 0

trace.delta-ns (trace) (capture, index) -> Int

Returns nanoseconds since the previous captured edge.

Example

trace.delta-ns: capture, 1

trace.complete? (trace) (capture) -> Bool

Returns true when capture stopped, filled its edge buffer, or reached its maximum span.

Example

trace.complete?: capture

trace.dump (trace) (capture) -> nil

Prints the watched pins and captured edges for direct inspection.

Example

trace.dump: capture

trace.close (trace) (capture) -> nil

Releases the trace capture handle.

Example

trace.close: capture
set capture to nil

Timed Pulse Output

pulse.open (pulse) (pin, idle_level) -> Handle

Opens the single timed digital output with idle level 0 or 1.

Example

wave is pulse.open: 4, 0

pulse.add (pulse) (wave, level, duration_ns) -> Int

Appends one 100-nanosecond-quantized span and returns its actual duration.

Example

pulse.add: wave, 1, 800

pulse.clear (pulse) (wave) -> nil

Clears all spans while keeping the output open.

Example

pulse.clear: wave

pulse.count (pulse) (wave) -> Int

Returns the number of waveform spans.

Example

pulse.count: wave

pulse.level (pulse) (wave, index) -> Int

Returns a waveform span’s level.

Example

pulse.level: wave, 0

pulse.duration-ns (pulse) (wave, index) -> Int

Returns a waveform span’s actual quantized duration.

Example

pulse.duration-ns: wave, 0

pulse.dump (pulse) (wave) -> nil

Prints the quantized waveform.

Example

pulse.dump: wave

pulse.play (pulse) (wave) -> nil

Transmits the waveform once and returns after playback completes.

Example

pulse.play: wave

pulse.close (pulse) (wave) -> nil

Releases the timed output handle.

Example

pulse.close: wave
set wave to nil

Console Input And Routing

console.read-line (console) () -> Bytes

Blocks until one printable line arrives on the active console, removes its line ending, and returns it as data instead of evaluating it as Frothy source. A blank line returns empty Bytes. Ctrl-C interrupts the read.

The result is volatile. Consume it during the current evaluation or loop iteration, keep it in a here local during one call, or copy it into persistent Text with text.pack.

Example

answer is text.pack: console.read-line:

In the browser editor, submit the line with Input for console.read-line while the form is running. Use uart.read-byte for binary data or an auxiliary serial connection.


console.uart (console) (tx, rx, baud) -> nil

Moves the active REPL to caller-selected UART pins and a literal baud rate.

Example

console.uart: 17, 16, 115200

console.default (console) () -> nil

Restores the board’s default boot and recovery console.

Example

console.default:

console.info (console) () -> nil

Prints the active console route.

Example

console.info:

Bluetooth

Radio

ble.on (ble) () -> nil

Initializes the compiled BLE roles and waits for radio readiness.

Everything Bluetooth starts here: the radio is off until ble.on powers it, and every other ble.* word fails before it. Which roles wake up is decided at flash time — central (scan for and connect to other devices) and peripheral (advertise and accept connections) are firmware options, not runtime choices. The radio costs real RAM while on, so projects that use BLE in bursts pair it with ble.off, which tears everything down and invalidates outstanding BLE handles.

Example

ble.on:

Deeper: Bluetooth module .


ble.info (ble) () -> nil

Prints BLE roles, radio state, scan and advertising state, connection pressure, and the last raw failure reason.

Example

ble.info:

ble.off (ble) () -> nil

Closes BLE links, clears queues, invalidates BLE handles, and shuts down the radio.

Example

ble.off:

Scanning

ble.scan.start (ble scan) (interval_ms, window_ms, active, repeats, minimum_rssi) -> nil

Starts an indefinite BLE scan with bounded interval/window timing, active and duplicate-report flags, and an RSSI floor.

The radio listens for window_ms out of every interval_ms — the example’s 100, 50 listens half the time, a reasonable power/thoroughness trade. active of 1 transmits scan requests so peers can send extra response data (costs power); 0 just listens. repeats of 0 reports each peer once, 1 reports every sighting — useful when you care about RSSI over time. minimum_rssi (dBm, e.g. -90) discards weaker signals. Sightings queue as reports; drain the queue with ble.scan.next? and read each report with the other ble.scan.* words.

Example

ble.scan.start: 100, 50, 1, 0, -90

Deeper: Bluetooth module .


ble.scan.stop (ble scan) () -> nil

Stops scanning while retaining reports already queued.

Example

ble.scan.stop:

ble.scan.next? (ble scan) () -> Bool

Selects the next queued report and returns whether one was available.

Scan results follow a cursor model: this word advances to the next queued report, and ble.scan.rssi, ble.scan.peer, ble.scan.flags, and ble.scan.data all read from the report currently selected. Loop until it returns false to drain the queue.

Example

when ble.scan.next?: [ ble.scan.rssi: ]
to drain-reports [
  while ble.scan.next?: [
    print: (ble.scan.rssi:)
    print: "\n"
  ]
]

Deeper: Bluetooth module .


ble.scan.rssi (ble scan) () -> Int

Returns the current report’s RSSI in dBm.

Example

ble.scan.rssi:

ble.scan.peer (ble scan) () -> Bytes

Returns the current peer as one address-type byte followed by its six canonical address bytes.

Example

bytes.length: (ble.scan.peer:)

ble.scan.flags (ble scan) () -> Int

Returns the current report’s platform scan flags.

Example

ble.scan.flags:

ble.scan.data (ble scan) () -> Bytes

Copies the current raw advertisement payload.

Example

ble.scan.data:

Advertising

ble.advertise.start (ble advertise) (advertising_data, scan_response_data, interval_ms, connectable) -> nil

Starts legacy advertising with raw Text or Bytes AD payloads and a connectable flag of 0 or 1.

Advertising broadcasts a small payload every interval_ms for any nearby scanner to see — this is how a board announces itself before any connection exists. The payloads are raw BLE AD structures: each is a length byte, a type byte, then that many minus one payload bytes, concatenated. In the example, "\x02\x01\x06" is one structure (length 2, type 1 = flags, value 6 = general discoverable) and "\x07\x09Frothy" is another (length 7, type 9 = complete local name, then the six name bytes) — sent in the scan response, which scanners only see when scanning actively. connectable of 1 lets a central connect (pick the link up with ble.accept); 0 is broadcast-only, the beacon pattern.

Example

ble.advertise.start: "\x02\x01\x06", "\x07\x09Frothy", 100, 1

Deeper: Bluetooth module .


ble.advertise.stop (ble advertise) () -> nil

Stops active BLE advertising.

Example

ble.advertise.stop:

Connections

ble.connect (ble connection) (peer, timeout_ms) -> Handle

Connects to a peer returned by ble.scan.peer.

The central-role move: scan first, take the peer bytes of the device you want (ble.scan.peer, one address-type byte plus six address bytes), and connect. The call blocks until the link is up or timeout_ms passes — failures raise catchable errors, so unattended connects belong in attempt/rescue. The returned connection handle is volatile like every handle, and ble.off invalidates it.

Example

link is ble.connect: peer, 5000

Deeper: Bluetooth module .


ble.accept (ble connection) () -> Handle|nil

Accepts one pending peripheral connection, or returns nil when none is waiting.

The peripheral-role counterpart of ble.connect: after connectable advertising, a central’s incoming connection parks as pending until this word claims it. It never blocks — nil means “no one yet” — so the usual shape is a periodic poll that stores the handle when a link arrives.

Example

link is ble.accept:
link is nil

to watch-links [
  every 500 [
    here candidate is ble.accept:
    when candidate [ set link to candidate ]
  ]
]

Deeper: Bluetooth module .


ble.connection.ready? (ble connection) (connection) -> Bool

Returns whether a BLE connection handle is live.

Example

ble.connection.ready?: link

ble.connection.close (ble connection) (connection) -> nil

Disconnects and releases a BLE connection handle.

Example

ble.connection.close: link
set link to nil

ble.connection.info (ble connection) (connection) -> nil

Prints the peer, link parameters, security state, and raw disconnect reason.

Example

ble.connection.info: link

ble.connection.rssi (ble connection) (connection) -> Int

Reads the live connection RSSI in dBm.

Example

ble.connection.rssi: link

ble.connection.params (ble connection) (connection, min_interval_ms, max_interval_ms, latency, supervision_timeout_ms) -> nil

Requests bounded BLE connection parameters.

Example

ble.connection.params: link, 15, 30, 0, 4000

ble.connection.mtu (ble connection) (connection, requested_mtu, timeout_ms) -> Int

Exchanges the ATT MTU and returns the actual negotiated value.

Example

ble.connection.mtu: link, 128, 5000

GATT Constants

$ble.gatt.service (gatt constant) Int

Marks a local GATT table row as a service.

Example

service is GattRow: $ble.gatt.service, "FFF0", $ble.gatt.primary, 0

$ble.gatt.characteristic (gatt constant) Int

Marks a local GATT table row as a characteristic.

Example

value is GattRow: $ble.gatt.characteristic, "FFF1", $ble.gatt.read, 20

$ble.gatt.primary (gatt constant) Int

Marks a local GATT service as primary.

Example

service is GattRow: $ble.gatt.service, "FFF0", $ble.gatt.primary, 0

$ble.gatt.secondary (gatt constant) Int

Marks a local GATT service as secondary.

Example

service is GattRow: $ble.gatt.service, "FFF0", $ble.gatt.secondary, 0

$ble.gatt.read (gatt constant) Int

Adds the readable property to a local characteristic.

Example

$ble.gatt.read + $ble.gatt.notify

$ble.gatt.write (gatt constant) Int

Adds write-with-response support to a local characteristic.

Example

$ble.gatt.read + $ble.gatt.write

$ble.gatt.write-command (gatt constant) Int

Adds write-without-response support to a local characteristic.

Example

flags is $ble.gatt.write-command

$ble.gatt.notify (gatt constant) Int

Adds notification support to a local characteristic.

Example

$ble.gatt.read + $ble.gatt.notify

$ble.gatt.indicate (gatt constant) Int

Adds indication support to a local characteristic.

Example

$ble.gatt.read + $ble.gatt.indicate

$ble.gatt.read-encrypted (gatt reserved constant) Int

Names the encrypted-read flag. The current server installs the constant but rejects security flags because pairing and encryption are not yet supported.

Example

$ble.gatt.read-encrypted

$ble.gatt.write-encrypted (gatt reserved constant) Int

Names the encrypted-write flag; current GATT table installation rejects it.

Example

$ble.gatt.write-encrypted

$ble.gatt.read-authenticated (gatt reserved constant) Int

Names the authenticated-read flag; current GATT table installation rejects it.

Example

$ble.gatt.read-authenticated

$ble.gatt.write-authenticated (gatt reserved constant) Int

Names the authenticated-write flag; current GATT table installation rejects it.

Example

$ble.gatt.write-authenticated

$ble.gatt.notifications (gatt constant) Int

Selects notification mode for a remote GATT subscription.

Example

ble.gatt.subscribe: link, attribute, $ble.gatt.notifications, 5000

$ble.gatt.indications (gatt constant) Int

Selects indication mode for a remote GATT subscription.

Example

ble.gatt.subscribe: link, attribute, $ble.gatt.indications, 5000

Local GATT Server

ble.gatt.install (gatt server) (rows) -> nil

Validates and copies a declarative Cells table of GATT service and characteristic records while the radio is off.

Example

ble.gatt.install: gatt_rows

ble.gatt.info (gatt) () -> nil

Prints local-server and remote-client GATT state, capacities, queues, and raw errors.

Example

ble.gatt.info:

ble.gatt.set (gatt server) (attribute, data) -> nil

Replaces a local characteristic value by source-row attribute ID.

Example

ble.gatt.set: 1, "ready"

ble.gatt.get (gatt server) (attribute) -> Bytes

Copies a local characteristic value by source-row attribute ID.

Example

ble.gatt.get: 1

ble.gatt.notify (gatt server) (connection, attribute, data) -> nil

Sends one subscribed notification.

Example

ble.gatt.notify: link, 1, "awake"

ble.gatt.indicate (gatt server) (connection, attribute, data, timeout_ms) -> nil

Sends an indication and waits interruptibly for its acknowledgement.

Example

ble.gatt.indicate: link, 1, "awake", 5000

ble.gatt.next-write (gatt server) () -> Handle|nil

Selects the next accepted remote write and returns its connection, or nil when the queue is empty.

Example

when ble.gatt.next-write: [ ble.gatt.write-data: ]

ble.gatt.write-attribute (gatt server) () -> Int

Returns the source-row attribute ID for the selected remote write.

Example

ble.gatt.write-attribute:

ble.gatt.write-data (gatt server) () -> Bytes

Copies the selected remote write’s data.

Example

ble.gatt.write-data:

Remote GATT Client

ble.gatt.find (gatt client) (connection, service_uuid, characteristic_uuid, timeout_ms) -> Int

Finds one remote characteristic and returns its ATT handle.

Example

battery is ble.gatt.find: link, "180F", "2A19", 5000

ble.gatt.read (gatt client) (connection, attribute, timeout_ms) -> Bytes

Reads one short remote characteristic value.

Example

ble.gatt.read: link, battery, 5000

ble.gatt.write (gatt client) (connection, attribute, data, with_response, timeout_ms) -> nil

Writes one short remote characteristic value. Use 1 for a response and 0 for a write command.

Example

ble.gatt.write: link, battery, (bytes.from-byte: 42), 1, 5000

ble.gatt.subscribe (gatt client) (connection, attribute, mode, timeout_ms) -> nil

Subscribes to remote notifications or indications.

Example

ble.gatt.subscribe: link, battery, $ble.gatt.notifications, 5000

ble.gatt.unsubscribe (gatt client) (connection, attribute, timeout_ms) -> nil

Unsubscribes from one remote characteristic.

Example

ble.gatt.unsubscribe: link, battery, 5000

ble.gatt.next-notification (gatt client) () -> Handle|nil

Selects the next remote notification and returns its connection, or nil when the queue is empty.

Example

when ble.gatt.next-notification: [ ble.gatt.notification-data: ]

ble.gatt.notification-attribute (gatt client) () -> Int

Returns the ATT handle for the selected remote notification.

Example

ble.gatt.notification-attribute:

ble.gatt.notification-data (gatt client) () -> Bytes

Copies the selected remote notification data.

Example

ble.gatt.notification-data:

Internal Event Support

frothy.event-register (internal) (kind, source, debounce, body) -> nil

Internal native used by compiled event registration forms.

Example

to start-ticking [ every 1000 [ print: "tick" ] ]

frothy.event-cancel (internal) (kind, source) -> nil

Internal native used by compiled cancel forms.

Example

cancel every 1000

frothy.event-fire (internal) (kind, source, edge) -> nil

Internal test support: queues a matching registered event binding as if its source fired. kind is "every", "after", or "on"; source is the milliseconds or pin number; edge is "rising" or "falling" for "on" and nil for timers.

Example

frothy.event-fire: "every", 1000, nil