# Frothy: full text for AI models > Frothy is a small language that runs on ESP32 and RP2040 boards. You send code to the board over USB serial while it runs. Frothy is not Forth. This file matches Frothy v0.1.23. It holds the agent guide, the limits, the boards, the language reference, the word catalog, the error codes and the module pages. The site index is at https://frothy.dev/llms.txt. --- Source: https://frothy.dev/agent-guide/ # Agent Guide Frothy is not Forth. It has no data stack and no `: name ... ;` definitions. Do not write Forth, Arduino C or MicroPython. This is Frothy code: ```frothy to blink-twice with pin, delay [ repeat 2 [ gpio.high: pin wait: delay gpio.low: pin wait: delay ] ] blink-twice: $led_builtin, 100 ``` This guide matches Frothy v0.1.23. The [language reference](/reference/language/) and the [word catalog](/reference/words/) give the rest, and [llms-full.txt](/llms-full.txt) holds them in one file. ## Syntax Rules 1. **Define a word with `to`.** Write `to name with a, b [ ... ]`, or `to name [ ... ]` for a word with no parameters. The last expression of the body is the result. There is no `return`. 2. **Call a word with a colon.** Write `name: a, b`, and `name:` for a word with no arguments, for example `led.on:`. A name with no colon gives its value and does not call it. At the prompt, a word with no colon gives notice 102. 3. **A call takes every argument after its colon.** Put a call in parentheses when more arguments follow it: `max: (abs: -5), 3`. A call that is the last argument needs no parentheses: `print: abs: -42`. 4. **Compare integers with `=` and `<>`.** The other comparisons are `<`, `>`, `<=` and `>=`. `==` and `!=` give `invalid (8)`. Comparisons do not chain: write `1 < x and x < 3`. Compare Text with `text.equals?`. Test `nil` or a Bool with `if`, `when` or `unless`. 5. **Comments.** `--` starts a comment that ends at the line end. `-*` starts a block comment, and `*-` ends it. 6. **Line breaks.** Inside `[ ... ]`, a line break or a `;` ends an expression. At the top level, one line holds one form: `a is 1; b is 2` is an error. 7. **Integers only.** An integer is from -1073741824 to 1073741823. `3.14` gives `invalid (8)`. `0x68`, `0b101` and `1_000` work. There are no bit operators: `&`, `|`, `^`, `~`, `<<` and `>>` do not exist. The math operators are `+`, `-`, `*`, `/` and `%`, and the math words include `abs:`, `min:`, `max:`, `clamp:`, `map:`, `mod:`, `sqrt:` and `sign:`. 8. **Text.** Write Text in double quotes. The escapes are `\n`, `\r`, `\t`, `\"`, `\\` and `\xHH`. `+` does not join Text: write `text.concat: "a", "b"`. `print:` takes an integer, Text, Bytes or a buffer. Outside an event body, it adds no line end. 9. **Names.** `count is 0` binds a top-level name, and `set count to count + 1` changes it. `set` does not make a name. Inside a block, `v is 1` or `here v is 1` makes a local. A parameter cannot change. 10. **Control.** The forms are `if c [ ... ] else [ ... ]`, `when c [ ... ]`, `unless c [ ... ]`, `while c [ ... ]`, `repeat n [ ... ]`, `repeat n as i [ ... ]` (`i` counts from 0) and `forever [ ... ]`. `attempt [ ... ] rescue [ ... ]` catches an error. There is no `break`, `for` or `case`. Only `nil`, `false` and `0` are false. 11. **Values.** `scores is cells: 3` makes a Cells value. Read it with `scores[0]`, and change it with `set scores[0] to 7`. `record Point [ x, y ]` defines a record: `p is Point: 1, 2`, then `p->x` and `set p->y to 5`. `flash is fn [ led.toggle: ]` holds Code, and `flash:` runs it. 12. **No captured locals.** Code reads its own parameters and locals, and the top-level names. An event body cannot read the locals of the word that registered it. 13. **Events.** Register an event in a word: `every 1000 [ ... ]`, `after 500 [ ... ]`, `on pin falling debounce 25 [ ... ]` (also `rising` and `changes`), or `on wifi.disconnected [ ... ]` on a board with Wi-Fi. One word holds one event body. A board holds 16 event bindings. `cancel every 1000`, `cancel pin` and `cancel wifi.disconnected` remove them. 14. **Board constants start with `$`.** They are `$led_builtin`, `$led_active_level`, `$a0`, `$boot_button`, `$sda`, `$scl`, the `$baud_*` constants and the `$ble.*` constants. Do not invent others, such as `$button`. `$led_builtin` is `nil` on a board with no built-in LED, and the RP2040 boards have no `$boot_button`. For another pin, write its GPIO number. 15. **Save and boot.** `save` writes the words to flash. Only the prompt can run `save`, `restore` and `dangerous.wipe`; a word that calls them gives `prompt only (26)`. A word named `boot` runs when the board starts. `clear` removes the changes since the last `save`. `save` writes a handle as `nil`, and a top-level name cannot hold Bytes. ## Words That Do Not Exist | Do not write | Write | | --- | --- | | `delay(100)`, `delay: 100`, `ms: 100` | `wait: 100` | | `pinMode`, `digitalWrite`, `digitalRead` | `gpio.mode`, `gpio.write`, `gpio.read` | | `analogRead`, `analogWrite` | `adc.read`, then `pwm.open` and `pwm.write` | | `HIGH`, `LOW` | `1`, `0` | | `OUTPUT`, `INPUT`, `INPUT_PULLUP` | modes `1`, `0` and `2` of `gpio.mode` | | `Serial.println(x)` | `print.value: "x", x`, or `print: x` and then `print: "\n"` | | `i2c.scan`, `spi.*` | nothing in v0.1.23 | | `neopixel.show` | nothing; on ESP32 boards, the `pulse.*` words build a waveform | | `break`, `return`, `for`, `case` | `while`, `repeat`, `forever`, `if` | | `: name ... ;`, `DUP`, `SWAP` | Frothy has no data stack | ## Look Before You Write The board answers questions. `words` lists the words on the board, and a word that is not in the list does not exist there. `see name` prints the code of a word. `status` prints the release and the profile, `mem` prints how full each table is, and `events` prints the event bindings. ## Print A Number Outside an event body, `print:` writes an integer with no line end. `print.value:` writes a label, a colon, an integer and a line end in one write, so an event cannot print inside the line. Neither word makes a Text or Bytes value, so both are safe in a long loop: ```frothy repeat 20 [ print.value: "a0", (adc.read: $a0) wait: 200 ] ``` A label has 1 to 48 bytes: a letter or `_` first, then letters, digits, `_`, `.` or `-`. ## Send Code - The [browser editor](https://app.frothy.dev/editor) sends code with **Run Line** and **Run File**. `frothy send file.fr` sends a file, and `frothy connect` opens a prompt. - The board reads one line of at most 511 bytes. The editor and the CLI send a form with line breaks as one line. A longer line gives `capacity exceeded (4)` and does not run. - In another serial terminal, write each form on one line, and separate its expressions with `;`. - Read the answer after each form. A success ends with `ok`. An error starts with `error: name (code)`, and `source:`, `note:` and other lines can follow. A notice starts with `notice:` and ends with `ok`. ## Common Errors | Answer | Cause | Fix | | --- | --- | --- | | `not found (7)` | the name does not exist | check `words` and the spelling | | `invalid (8)`, `unexpected token` | a syntax error | look for `!=`, a bit operator, or two forms on one line | | `invalid (8)`, `expected ':' before the argument to a word` | a call with no colon | write `name: arg` | | `invalid (8)`, `expected 3 arguments, got 1` | a call with the wrong number of arguments | give each argument; put a nested call in parentheses | | `wrong type (2)` | a value of the wrong kind | for example, `text.concat:` with an integer | | `capacity exceeded (4)` | a fixed table is full; the `note:` line names it | see [Limits](/limits/) | | `busy (25)` | a pin or port is in use | close its handle first | | `prompt only (26)` | a word called `save`, `restore` or `dangerous.wipe` | run it at the prompt | ## Limits An ESP32 board holds 256 of your names, and an RP2040 board holds 192. About 4,178 bytes of compiled code and parameter names can wait for `save`, or 1,114 bytes on the XIAO ESP32C3. One form must fit in a 511-byte line and in 64 parse nodes. A new Text value stays until the next `save`, `clear` or restart. Keep a value that changes in a buffer. Each loop pass frees the Bytes values that it made. [Limits](/limits/) lists every size. --- Source: https://frothy.dev/limits/ # Limits A Frothy board keeps your program in tables of fixed size. When a table is full, the board stops the form with error 4. A `note:` line names the table and its size: ```text error: capacity exceeded (4) note: pending code is full (limit 4178 bytes) -- a save that succeeds frees it ``` The numbers on this page are for Frothy v0.1.23. All ESP32 boards have the same limits, except the code area of the XIAO ESP32C3. The two RP2040 boards have the same limits. `mem` shows how much of each table is in use. | What | ESP32 boards | RP2040 boards | | --- | --- | --- | | Names that you define, saved or not | 256 | 192 | | Compiled code and parameter names that wait for `save` | 4,178 bytes (1,114 on the XIAO ESP32C3) | 4,178 bytes | | One line | 511 bytes | 511 bytes | | Parse nodes in one form | 64 | 64 | | Object table entries (Text, Cells, records, buffers) | 128 | 96 | | New Text and the capacities of buffers (RAM Text pool) | 4,096 bytes | 4,096 bytes | | Cells in use | 128 | 128 | | Elements in one Cells value | 32 | 32 | | Fields in one record | 4 | 4 | | Open handles | 16 | 12 | | Event bindings | 16 | 16 | | Bytes values in use | 8 | 8 | | PAD | 64 bytes | 64 bytes | | Parameters in one word | 8 | 8 | | Locals in one word, with one for each loop level | 16 | 16 | | Expressions in one block | 24 | 24 | | Levels of parentheses | 15 | 15 | | Nested calls | 24 | 24 | | Bytes in one name | 48 | 48 | | Integers | -1,073,741,824 to 1,073,741,823 | the same | `save` does not free a name that you defined. It frees a reserved name when no saved code calls it. `clear` removes the names that you did not save. `wipe-user` removes all of your names and your saved program. The [network words](/reference/modules/wifi/) have their own limits. ## Code That Waits For `save` Each definition adds its compiled code and its parameter names to an area of 4,178 bytes, or 1,114 bytes on the XIAO ESP32C3. The code stays there until `save` moves it into the saved image. When you define a word again, the board adds the new code, and the old code also stays until `save`. This word uses 51 bytes. On most boards, 81 definitions of it fit, and the 82nd fails. On the XIAO ESP32C3, the 22nd definition fails: ```frothy to pulse with pin [ gpio.write: pin, 1; gpio.write: pin, 0; gpio.write: pin, 1; gpio.write: pin, 0 ] ``` When the area is full, type `save`. Then send your definitions again, from the one that failed. In a long file, put a `save` line after each group of words. `mem code` shows how much of the area is in use. ## Long Words The board reads at most 511 bytes in one line. The editor and `frothy send` send each form, for example one `to` definition, as one line. In a form with line breaks, each line break and each backslash count as 2 bytes, and the tools add 12 bytes. Indentation also counts. The board does not run a line that is too long: ```text error: capacity exceeded (4) note: the line limit is 511 bytes ``` A form must also fit in 64 parse nodes. A word with 20 calls such as `gpio.write: pin, 1` fits, and a word with 21 of these calls does not. Split a long word into smaller words, and let one word call the next. ## Text And Bytes In A Loop Each new Text value uses one entry of the object table. The entry stays in use until the next `save`, `clear` or restart, also when your program does not keep the value. This loop stops after 128 values on an ESP32 board, and after 96 values on an RP2040 board: ```frothy repeat 200 as i [ text.from-int: i ] ``` ```text error: capacity exceeded (4) note: object table is full (limit 128) ``` To print numbers in a loop, use `print:` or `print.value:`. They make no Text value. To keep a value that changes, such as the latest reply, use a [buffer](/reference/language/#buffers). Bytes values do not fill a table in a loop. Each pass of `repeat`, `while` and `forever` frees the Bytes values that the pass made. So a loop that calls `i2c.read` or `http.get` can run for as long as you want. At one time, 8 Bytes values with 4,096 bytes of data in total can be in use. Each loop level uses one of the 16 locals of its word. A word with 16 locals and a loop answers `capacity exceeded (4)` with the note `space for locals in one form is full (limit 16)`. --- Source: https://frothy.dev/boards/ # Boards Frothy has official firmware for seven boards. Install it once with the [browser flasher](https://app.frothy.dev/flash). Then program the board in the [browser editor](https://app.frothy.dev/editor). | Board | Chip | USB connection | `$led_builtin` | Radio | | --- | --- | --- | --- | --- | | ESP32 DevKit V1 | ESP32 | USB-to-UART chip | LED pin | Wi-Fi, Bluetooth LE | | NodeMCU ESP-32S | ESP32 | USB-to-UART chip | `nil` | Wi-Fi, Bluetooth LE | | Seeed Studio XIAO ESP32C3 | ESP32-C3 | USB Serial/JTAG | `nil` | Wi-Fi, Bluetooth LE | | Seeed Studio XIAO ESP32C6 | ESP32-C6 | USB Serial/JTAG | LED pin | Wi-Fi, Bluetooth LE | | Seeed Studio XIAO ESP32S3 | ESP32-S3 | USB Serial/JTAG | LED pin | Wi-Fi, Bluetooth LE | | Seeed Studio XIAO RP2040 | RP2040 | native USB | LED pin | none | | Arduino Nano RP2040 Connect | RP2040 | native USB | LED pin | Wi-Fi, Bluetooth LE | All seven boards have `$a0`, `$sda` and `$scl`, and the ESP32 boards also have `$boot_button`. Use these names, not pin numbers, and the same program runs on more than one board. When `$led_builtin` is `nil`, the `led.*` words stop with an error. Connect an LED to a pin, and use `gpio.write: pin, 1`. The pins of these boards use 3.3 V. Do not connect a 5 V signal to a pin. ## ESP32 Boards The flasher writes Frothy to an ESP32 board over USB. If it cannot connect, start the board in download mode, as [Install](/install/#if-no-port-appears) shows. When an ESP32 board starts, it prints `boot: Ctrl-C or BOOT skips saved code`. Press Ctrl-C or the BOOT button in the next 600 ms, and the board starts without your saved program. Use this when your saved program does not let the prompt appear. The DevKit V1 and the NodeMCU ESP-32S connect through a USB-to-UART chip, CP210x or CH340. On some computers, this chip needs a driver. The XIAO ESP32C3, ESP32C6 and ESP32S3 connect through the USB Serial/JTAG unit in the chip. This connection has no flow control. When you paste more than about 1 KB into a terminal or `frothy connect`, the board can lose bytes with no error. The editor and `frothy send` wait for the prompt after each form, so they do not send too much at one time. The XIAO ESP32C3 has no `trace.*` words. ## RP2040 Boards For an RP2040 board, the flasher downloads a UF2 file. Restart the board in BOOTSEL mode, and drag the file to the `RPI-RP2` drive. The flasher can restart the board in BOOTSEL mode over USB. If the drive does not appear: - XIAO RP2040: hold BOOT, press and release RESET, then release BOOT. - Nano RP2040 Connect: with USB connected, connect REC to GND and press RESET. Remove the jumper when the `RPI-RP2` drive appears. An RP2040 board has no safe-boot window. Press Ctrl-C to stop a `boot` word that does not end. RP2040 boards do not have the `uart.*`, `trace.*`, `pulse.*`, `sleep.*` and `watchdog.*` words, or `console.uart`, `console.default` and `console.info`. The Nano RP2040 Connect has the Wi-Fi, HTTP and TCP words. Its Bluetooth words scan and advertise, but they do not connect. The XIAO RP2040 has no radio words. RP2040 boards also have smaller tables. See [Limits](/limits/). ## Boards That Do Not Work Frothy has no firmware for the Arduino UNO, UNO R4, Nano, Nano Every, Nano 33 IoT or Nano R4. Their chips are not ESP32 or RP2040 chips. The Arduino Nano ESP32 has an ESP32-S3 chip, but it has no official Frothy firmware yet. --- Source: https://frothy.dev/reference/language/ # Frothy in Y Minutes Frothy is a small live language for programmable devices. You send it source, the device installs that source into its current image, and you can inspect, replace, save, and run the result without rebuilding firmware. This is the whole language, from the first expression to the edges of the profile your board runs. Read it top to bottom once, then use the [word catalog](/reference/words/) when you only need a signature. ## The Thirty-Second Version ```frothy counter is 0 to blink-and-count with pin, delay [ gpio.high: pin wait: delay gpio.low: pin set counter to counter + 1 counter ] blink-and-count: $led_builtin, 75 save ``` That sample contains most of the model: - `counter is 0` binds a top-level name to a value. - `to ... [ ... ]` defines a word. Other languages usually say “function.” - `with pin, delay` declares parameters. - `gpio.high: pin` calls a word with `:`. - Newlines separate expressions inside a block. - The last expression is the block's result. - `set` changes an existing place. - `save` persists the user image. ## Comments A line comment begins with two hyphens and ends at the newline. ```frothy led.on: -- the rest of this line is ignored ``` A block comment begins with `-*` and ends with `*-`. ```frothy -* This can span several lines. *- led.off: ``` Comments are recognized after whitespace or punctuation that can precede a comment. Hyphens may also be part of names, so this remains one name: ```frothy read-next-byte ``` ## Integers And Arithmetic Frothy has integers, not floating-point numbers. Decimal, hexadecimal, and binary literals are accepted. ```frothy 42 -7 0x2a 0b101010 1_000_000 ``` Underscores may group digits in any base. There is no duration type and no unit suffix on literals: a duration is a plain integer in the unit the word documents. Most timing words take milliseconds (`wait: 500`); words with a different unit carry it in their name, such as `pulse.duration-ns`. Integers are whole numbers with a range set by the profile. On a 32-bit profile — which every shipped board uses today — that range is `-1073741824` through `1073741823`. An out-of-range literal or arithmetic result is an error; values do not silently wrap. `status` reports the running board's `int_min` and `int_max`. The arithmetic operators are `+`, `-`, `*`, `/`, and `%`. ```frothy 2 * 3 + 4 -- 10 (2 + 3) * 4 -- 20 37 % 10 -- 7 ``` `*`, `/`, and `%` bind more tightly than `+` and `-`. Operators at the same precedence associate from left to right. Division by zero is an error. `%` and the word `mod` use C-style truncating remainder semantics. The [math module](/reference/modules/math-and-random/) also provides `abs`, `min`, `max`, `clamp`, `map`, and integer `sqrt`. ## Comparisons And Boolean Logic Integer comparisons use `<`, `>`, `<=`, `>=`, `=`, and `<>`. ```frothy 2 < 3 -- true 10 >= 10 -- true 4 <> 5 -- true ``` `not`, `and`, and `or` use Frothy truthiness and produce `Bool` results. `and` and `or` short-circuit, so the right side only runs when needed. ```frothy not false -- true not temperature = 0 -- not (temperature = 0) 2 > 1 and 3 < 4 -- true ready or attempt-connect: -- skips the call when ready is truthy ``` A comparison cannot be chained. Write `1 < x and x < 3`, not `1 < x < 3`. `not` negates a complete comparison; parenthesize it when using its result inside arithmetic. Precedence, from tightest to loosest, is: 1. `*`, `/`, `%` 2. `+`, `-` 3. `<`, `>`, `<=`, `>=`, `=`, `<>` 4. `not` 5. `and` 6. `or` Use parentheses when the grouping is not immediately obvious. ## Values A profile chooses which value families it can afford. The profile every shipped board runs exposes all of these: | Value | Example | Lifetime | | --- | --- | --- | | `Nil` | `nil` | persistable | | `Bool` | `true`, `false` | persistable | | `Int` | `42`, `0xff` | persistable | | `Text` | `"ready"` | persistable | | `Code` | `fn [ 1 + 2 ]` | persistable | | `Cells` | `cells: 4` | persistable mutable storage | | buffer | `buffer: 64` | persistable mutable storage | | record shape/value | `record Point [ x, y ]` | persistable | | `Bytes` | `bytes.from-text: "AT"` | transient | | `Handle` | `i2c.open: ...` | transient | Only `nil`, `false`, and integer `0` are falsy. Every other value is truthy, including empty text. ```frothy if 5 [ "yes" ] else [ "no" ] -- "yes" if 0 [ "yes" ] else [ "no" ] -- "no" if nil [ "yes" ] else [ "no" ] -- "no" ``` `0x10` is the integer `16`; it is not a Bytes value. Create `Bytes` explicitly with a `bytes.*` word or an I/O operation that returns bytes. Square brackets delimit bodies used by language forms. A standalone `[ 1 + 2 ]` is not a `Code` literal; use `fn [ 1 + 2 ]`. ## Text Text is immutable and byte-oriented. A literal is enclosed in double quotes. ```frothy greeting is "hello" print: greeting text.length: greeting -- 5 text.at: greeting, 1 -- 101, the byte for e ``` The supported escapes are: | Escape | Meaning | | --- | --- | | `\n` | newline | | `\r` | carriage return | | `\t` | tab | | `\"` | double quote | | `\\` | backslash | | `\xNN` | one byte written as two hexadecimal digits | ```frothy print: "first\nsecond\n" packet is "\x02\x01\x06" ``` Text cannot contain a literal source newline. Use `\n`. One Text value holds at most 4,096 bytes. The Text values made since the last `save` and the capacities of all buffers share a RAM pool of 4,096 bytes. Use [Text, Bytes & PAD](/reference/modules/text-bytes-pad/) for conversion and the lifetime of Bytes. ## Names Names are intentionally permissive. Dots, hyphens, question marks, underscores, and exclamation marks are ordinary name characters. ```frothy sensor.ready? read-next-byte all_my.friends! ``` Whitespace and syntax punctuation end a name. A name cannot contain `(`, `)`, `[`, `]`, `,`, `:`, `;`, `+`, `*`, `/`, `%`, comparison operators, or `->`. A hyphen stays inside a name unless it is clearly subtraction or the start of a negative integer. ```frothy x - y -- subtraction; spaces make the intent clear x-y -- one name ``` Names are case-sensitive. The current profile permits up to 48 bytes per name. Language keywords cannot be used in positions where they would be ambiguous. ## Top-Level Bindings Bind a top-level name with `is`. ```frothy count is 10 message is "ready" ``` Running another `is` definition for the same name rebinds its stable slot. ```frothy count is 20 ``` Use `set` when the intent is mutation rather than replacement of a definition. ```frothy set count to count + 1 ``` `set` only changes an existing name or place. It does not create one. A top-level `is` definition accepts literals, an existing name, `fn`, `cells`, a record value, or the result of a word call. An operator or control-flow expression is evaluated with `set` after the place exists: ```frothy total is 0 set total to 20 + 22 ``` This boundary keeps image installation explicit. A call result may be bound when its value kind is slot-safe; `Bytes` is the exception and must be packed to Text first. ## Words And Calls Frothy calls functions “words.” Define a word with `to` and call it with a colon. ```frothy to add-three [ 1 + 2 ] add-three: -- 3 to add with a, b [ a + b ] add: 5, 9 -- 14 ``` A bare name reads its value. It does not call it. ```frothy add -- displays a Code value at the prompt add: 5, 9 -- runs it ``` The last expression in a word body is its result. ```frothy to describe with n [ print: "value: " n ] ``` Arguments are comma-separated. Parenthesize a nested call when it makes the argument boundary explicit. ```frothy print: (abs: -42) total is add: 1, (add: 2, 3) ``` Without parentheses, a following call begins a new expression at every operator boundary. This makes recursive code read naturally: ```frothy fib: n - 1 - fib: n - 2 gpio.write: pin, (1 - gpio.read: pin) ``` The parentheses in the second line deliberately keep the inner call inside `gpio.write`'s second argument. Calls are checked against the word's arity. Too few or too many arguments are errors. Words may call themselves recursively. The call-depth limit is a profile setting; it is 24 nested calls on the shipped profile. ```frothy to fib with n [ if n < 2 [ n ] else [ fib: n - 1 + fib: n - 2 ] ] ``` ## Code Values `fn` creates a `Code` value. `to name ...` is the convenient top-level spelling for binding that code to a name. ```frothy double is fn with n [ n * 2 ] to double with n [ n * 2 ] ``` Code can be rebound to another top-level name, and either name can call the same code object. ```frothy double is fn with n [ n * 2 ] also-double is double also-double: 21 -- 42 ``` Code is non-capturing. It can read its own parameters, locals declared in its own body, and top-level names. It cannot capture a local from another word. ```frothy delay is 50 -- This word can read the top-level delay. to blink-once with pin [ gpio.high: pin wait: delay gpio.low: pin ] ``` When a word needs a value that is not top-level, pass that value as an argument. `fn` values themselves are currently installed from top-level definitions; Frothy does not build closures inside a running word. Calls also resolve a top-level word name: a Code value held only in a parameter, local, Cell, or record field is not dynamically callable, and there is no separate `call` word. ## Parameters And Locals Parameters are immutable. Inside a block, `is` declares a lexical local; `here` is the explicit spelling of the same declaration. Mutate that local with `set`. ```frothy to countdown with start [ n is start while n > 0 [ set n to n - 1 ] n ] ``` Locals use lexical block scope. Lookup chooses the nearest local, then an outer local, then a parameter, then a top-level name. ```frothy speed is 100 to demo [ speed is 20 if true [ here speed is 5 speed -- 5 ] speed -- 20 ] ``` The local declared inside the `if` block stops existing at its closing `]`. The current profile allows 8 parameters and 16 locals in one parsed definition. Each loop level uses one of the 16 locals. `set` of a local refuses a Bytes value with `not saved (13)`. Make the value with `here` in the loop body. ## Blocks And Expression Separation A body is enclosed in `[ ... ]`. It must contain at least one expression. Newlines separate expressions naturally. ```frothy to pulse [ led.on: wait: 50 led.off: ] ``` A semicolon separates expressions on the same line. ```frothy to pulse [ led.on:; wait: 50; led.off: ] ``` A trailing semicolon is allowed. In normal source, prefer newlines and use semicolons only when a compact one-line form is clearer. ## Conditional Expressions `if` chooses a block and returns that block's value. ```frothy if temperature > 30 [ "hot" ] else [ "comfortable" ] ``` `else if` can be repeated. ```frothy if percent > 80 [ "high" ] else if percent > 50 [ "medium" ] else [ "low" ] ``` Without `else`, a false `if` yields `nil`. `when` is the one-sided positive form; `unless` is the one-sided negative form. ```frothy when wifi.ready?: [ print: "online\n" ] unless wifi.ready?: [ print: "offline\n" ] ``` ## Loops `while` checks its condition before each pass. ```frothy while count > 0 [ set count to count - 1 ] ``` `repeat` evaluates a count and runs a block that many times. A negative count is an error. Add `as name` for a zero-based loop index. ```frothy repeat 3 [ print: "tick\n" ] repeat 3 as i [ print: i print: "\n" ] ``` The index is a local scoped to the repeat body. `forever` runs until the body errors or the evaluation is interrupted. ```frothy forever [ led.toggle: wait: 100 ] ``` `while`, `repeat`, and `forever` yield `nil` when they finish normally. ## Cells Cells are fixed-size mutable indexed storage. Create them at the top level with a positive literal length. ```frothy readings is cells: 3 ``` New cells begin as `nil`. Indexes are zero-based. ```frothy set readings[0] to 11 set readings[1] to 22 readings[0] + readings[1] -- 33 ``` An out-of-bounds index is an error. Both sizes are profile settings: the shipped profile allows up to 32 elements in one Cells value and 128 cell elements across the live image. Cells and their persistable contents are included by `save`. Transient `Bytes` and `Handle` values are rejected as cell contents. ## Buffers A buffer keeps bytes that change many times, such as the latest reply or status line. Create it at the top level with a positive literal capacity: ```frothy reply is buffer: 64 buffer.set: reply, "status: ok" buffer.append: reply, "!" print: reply -- status: ok! buffer.length: reply -- 11 ``` A buffer takes its capacity from the Text pool when you define it. It takes no new storage after that, so a loop or an event body can change it for as long as it runs. `buffer.set` and `buffer.append` take an integer, Text, Bytes or a buffer. A value that does not fit answers `capacity exceeded (4)` with the note `buffer is full (limit 64 bytes)`, and the contents do not change. `buffer.length`, `buffer.capacity`, `buffer.at` and `buffer.equals?` read a buffer. `print`, `i2c.write`, `tcp.write`, `http.post` and the BLE words that send data take a buffer where they take Text or Bytes. A definition with the same capacity keeps the buffer and empties it, so a file can run again. A Cells element or a record field cannot hold a buffer. `buffer` is a reserved word: a parameter, a local or a `repeat ... as` name cannot be `buffer`. `save` keeps the contents of a buffer; see [Buffers and `save`](/reference/device/image-and-persistence/#buffers-and-save). ## Records Records give names to small fixed shapes. Declare a shape at the top level. ```frothy record Point [ x, y ] ``` The declaration creates a constructor word with the same name. Construct a record at the top level by calling it. ```frothy point is Point: 3, 9 ``` Read and write fields with `->`. ```frothy point -> x -- 3 set point -> y to 12 ``` Field names cannot contain dots. The field count is a profile setting; the shipped profile permits 4 per shape. Record construction inside a word body is not currently supported; construct records at the top level and pass them into words. Record shapes and values are persistable. Transient `Bytes` and `Handle` values cannot be stored in record fields. ## Errors And Rescue An error returns control to the prompt instead of wedging the device. A diagnostic names the error and shows its numeric code. Reader and parser diagnostics can also point at the offending source span. ```text error: bad value: 0 (3) ``` Type, domain, and range failures include the value that was rejected. Extra lines may identify the expected kind, native argument position, source span, or valid bound. Values are escaped and bounded for the transcript, and secret arguments are redacted. See [Error and notice codes](/errors/) for the full wire shape and recovery table. `attempt [ ... ] rescue [ ... ]` catches runtime errors. ```frothy attempt [ 10 / divisor ] rescue [ 0 ] ``` If the attempt succeeds, the entire expression yields its value. If it fails, Frothy restores the value stack to the beginning of the attempt, runs the rescue block, and yields the rescue value. Inside the rescue block, `error.code` and `error.name` describe the caught error. ```frothy to safe-divide with a, b [ attempt [ a / b ] rescue [ print: error.name error.code ] ] ``` Errors raised by called words can be caught by the caller's attempt. Parse and compile errors happen before execution and cannot be caught. An interrupt is also never catchable. ## Events Event forms register code that the device runs at safe points. Registration belongs inside a word body, not as a bare prompt expression. Register a repeating or one-shot timer: ```frothy to start-ticking [ every 1000 [ print: "tick\n" ] ] to stop-later [ after 5000 [ led.off: ] ] ``` Register a GPIO edge handler: ```frothy to watch-button [ on $boot_button falling debounce 25 [ led.toggle: ] ] ``` The edge is `rising`, `falling`, or `changes`. `debounce ` is optional. Wi-Fi exposes two event sources: ```frothy to watch-network-down [ on wifi.disconnected [ led.off: ] ] to watch-network-up [ on wifi.reconnected [ led.on: ] ] ``` The current profile permits one event body per definition. Use one small registration word for each event source, then call those words during setup. Cancel a registration with the same source identity: ```frothy cancel every 1000 cancel after 5000 cancel $boot_button cancel wifi.disconnected ``` For GPIO, cancellation uses the pin regardless of edge or debounce settings. For timers, `every` and `after` are distinct and the millisecond value must match. Event output appears as asynchronous `! ` lines at the prompt. See the [events module](/reference/modules/events/) for lifecycle and capacity details. ## Bytes And Handles `Bytes` and `Handle` are real Frothy values, but they represent live working state rather than saved project data. A `Bytes` value holds transient bytes. Words such as `i2c.read`, `tcp.read`, `http.get`, and BLE reads return one. ```frothy bytes.length: (http.get: "http://example.com/") ``` Bytes cannot be installed in top-level names, Cells, or record fields. A `here` local can hold them during one word call. Copy bytes into persistent Text when the content must survive the current evaluation or loop pass: ```frothy reply-text is text.pack: (http.get: "http://example.com/") ``` `text.pack` reuses an equal Text value. It makes a new Text value only when no equal Text value exists. Keep bytes that change many times in a [buffer](#buffers). A `Handle` refers to an open platform resource such as I2C, UART, PWM, TCP, trace, pulse, or BLE state. ```frothy bus is i2c.open: 0, $sda, $scl, 400000 i2c.close: bus set bus to nil ``` Handles are volatile. Close them and remove them from top-level project slots before `save`. Reopen required resources from `boot` after restore. ## The Image, Save, And Boot Frothy starts with a firmware-owned base image and adds your top-level bindings to an overlay image. ```frothy threshold is 500 to alarm [ when adc.read: $a0 > threshold [ led.on: ] ] ``` `save` writes the overlay to persistent storage. `restore` replaces the live overlay with that saved overlay. `clear` removes the live overlay but leaves the saved copy available. `dangerous.wipe` removes both. ```frothy save clear restore ``` If the top-level `boot` slot holds Code after restore, Frothy runs it before returning to the prompt. ```frothy boot is fn [ led.off: wifi.connect: ] save ``` On an ESP32 board, safe boot starts the board without a bad saved program. On an RP2040 board, Ctrl-C stops a `boot` that does not end. The [image and persistence reference](/reference/device/image-and-persistence/) covers the full lifecycle. ## The Prompt Is Part Of The Language Experience The prompt evaluates expressions and installs definitions. It also provides inspection commands without colons. ```frothy words see blink status events mem ``` - `words` lists visible names. - `see name` reconstructs a definition or describes a value. - `status` reports the current session and runtime profile. - `events` lists active event bindings. - `mem` reports heap, slots, pending code, objects, and event capacity. Use `mem heap`, `mem slots`, `mem code`, `mem objects`, or `mem events` for one group. - `clear` removes the live overlay and returns to the base image. Host tooling also uses `apply HEX`, `run HEX`, `install-library`, `install-user`, and `wipe-user`. They are prompt protocol or installation-tier commands rather than source-language forms; their exact contracts are in the [word catalog](/reference/words/#apply). Press Ctrl-C to abandon incomplete multiline input or interrupt running code. Frothy checks interrupts at safe points and returns the prompt to a usable state. ## Source Files And Includes A `.fr` file contains the same top-level definitions and expressions you type at the prompt. ```frothy to main [ led.blink: 3, 75 ] main: ``` `include "helper.fr"` is a host-side `frothy` tool directive. The CLI resolves it before sending source to the device. It is not device language syntax and does not create a runtime word. ## What To Open Next - [Word catalog](/reference/words/) — every current form, command, constant, helper, and ESP32 word in a searchable list. - [Modules](/reference/modules/) — broad usage guides for GPIO, Wi-Fi, I2C, text, signals, BLE, and the rest of the built-in surface. - [Device & sessions](/reference/device/) — prompt behavior, persistence, diagnostics, recovery, and tooling sessions. - [Toolchain](/reference/toolchain/) — projects, source files, CLI commands, builds, flashing, and editor support. --- Source: https://frothy.dev/reference/words/ # Word Catalog ## 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy to demo [ here speed is 10; speed ] ``` --- **`set`** *(language)* `set place to expr` Mutates an existing name, cells element, or record field. **Example** ```frothy 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** ```frothy 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** ```frothy when adc.percent: $a0 > 50 [ led.on: ] ``` --- **`unless`** *(language)* `unless cond [ body ]` Runs a one-sided conditional block when the condition is false. **Example** ```frothy unless ready [ led.off: ] ``` --- **`while`** *(language)* `while cond [ body ]` Repeats a block while the condition stays truthy. **Example** ```frothy 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** ```frothy repeat 10 as i [ set total to total + i ] ``` --- **`forever`** *(language)* `forever [ body ]` Repeats a block until interrupted or until the body errors. **Example** ```frothy forever [ led.toggle:; wait: 100 ] ``` --- **`cells`** *(language)* `cells: length -> Cells` Creates fixed-size mutable indexed storage. **Example** ```frothy readings is cells: 3 set readings[0] to 11 ``` --- **`record`** *(language)* `record Name [ fields ]` Declares a top-level record shape and constructor. **Example** ```frothy 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** ```frothy 1 + attempt [ 2 / 0 ] rescue [ 9 ] ``` ```frothy to read-or-default with sock [ attempt [ tcp.read: sock, 64 ] rescue [ print: error.name bytes.from-text: "" ] ] ``` Deeper: [errors in the language reference](/reference/language/#errors-and-rescue). --- **`error.code`** *(language)* `error.code -> Int` Reads the caught runtime error code inside a rescue block. **Example** ```frothy attempt [ 2 / 0 ] rescue [ error.code ] ``` --- **`error.name`** *(language)* `error.name -> Text` Reads the caught runtime error name inside a rescue block. **Example** ```frothy 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 ` 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** ```frothy to arm-button [ on $boot_button falling [ led.toggle: ] ] ``` ```frothy to arm-button [ on $boot_button falling debounce 25 [ print: "pressed\n" ] ] ``` Deeper: [Events module](/reference/modules/events/). --- **`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 `. **Example** ```frothy to start-ticking [ every 1000 [ print: "tick" ] ] ``` ```frothy to heartbeat [ every 500 [ led.toggle: ] ] to arm-all [ heartbeat: start-ticking: ] ``` Deeper: [Events module](/reference/modules/events/). --- **`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 ` removes a pending one-shot before it fires. **Example** ```frothy to once [ after 500 [ led.off: ] ] ``` Deeper: [Events module](/reference/modules/events/). --- **`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** ```frothy cancel every 1000 cancel $boot_button cancel wifi.disconnected ``` Deeper: [Events module](/reference/modules/events/). ## Values And Image **`nil`** *(value)* `nil` Represents no value and takes the false branch. **Example** ```frothy if nil [ 1 ] else [ 2 ] ``` --- **`true`** *(value)* `true` Represents boolean truth. **Example** ```frothy if true [ led.on: ] ``` --- **`false`** *(value)* `false` Represents boolean falsehood and takes the false branch. **Example** ```frothy 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 it opens the prompt. Bind it and `save`, and the board runs it after each reset. Handles do not survive a reset, so open them in `boot`. Use `boot` for work that must happen at start: open handles, set pin modes, and register events. An event keeps the prompt available while it runs. A `forever` loop in `boot` keeps the prompt closed until you press Ctrl-C. Use an event, not a loop in `boot`, when you can. **Example** ```frothy boot is fn [ led.on: ] save ``` ```frothy to arm-button [ on $boot_button falling debounce 25 [ led.toggle: ] ] boot is fn [ arm-button: ] save ``` Deeper: [the image, save, and boot](/reference/language/#the-image-save-and-boot). --- **`one`** *(image)* `one -> Int` Base-image literal for the integer `1`. **Example** ```frothy 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](/reference/device/image-and-persistence/). **Example** ```frothy save ``` Deeper: [the image, save, and boot](/reference/language/#the-image-save-and-boot). --- **`restore`** *(persistence)* `restore -> nil` Replaces the live overlay with the saved overlay. **Example** ```frothy restore ``` --- **`dangerous.wipe`** *(persistence)* `dangerous.wipe -> nil` Clears the live and saved overlay and returns to the base image. **Example** ```frothy dangerous.wipe ``` --- **`words`** *(inspection)* `words` Lists visible names at the prompt. **Example** ```frothy words ``` --- **`see`** *(inspection)* `see name` Renders the source form for a binding. After `save` and a restart or `restore`, `see` shows the parameter names of a word, but not its local names, comments or layout. A local shows as `local0`, `local1` and so on, and `and` and `or` show as nested `if` forms. So keep your `.fr` source. **Example** ```frothy see boot ``` --- **`status`** *(inspection)* `status` Reports session and runtime status at the prompt. **Example** ```frothy 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 frothy.release: ``` --- **`events`** *(inspection)* `events` Lists active timer, GPIO, and Wi-Fi event bindings. **Example** ```frothy events ``` --- **`mem`** *(inspection)* `mem [heap|slots|code|objects|events]` Reports live capacity usage, optionally narrowed to one topic. `mem code` shows `code.pending.used` and `code.pending.total`: the bytes of user code that wait for the next `save`, and the room for them, without the base library. **Example** ```frothy mem mem code ``` --- **`clear`** *(inspection)* `clear` Removes every change since the last `save`: the saved words and values come back, and the saved image does not change. `clear` also closes the open handles and cancels all events. Without a saved program, only the base words stay. **Example** ```frothy clear ``` --- **`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** ```text 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** ```text 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** ```text install-library ``` --- **`install-user`** *(installation command)* `install-user` Selects the user tier for following definitions after a library installation. **Example** ```text 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** ```text 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** ```text 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** ```frothy close-handles 2 ``` --- **`print`** *(io)* `(Int|Text|Bytes|Buffer) -> nil` Writes an integer, Text, Bytes or a buffer to the console output. Outside an event body, it adds no line end. An event body writes complete lines that start with `! `. **Example** ```frothy print: "hello" print: 42 ``` --- **`print.value`** *(io)* `(label, value) -> nil` Writes the label, a colon, the integer value and a line end in one write, so an event cannot print inside the line. The label is Text of 1 to 48 bytes: a letter or `_` first, then letters, digits, `_`, `.` or `-`. The word makes no Text or Bytes value, so a loop can call it for as long as it runs. **Example** ```frothy print.value: "temp", 21 ``` ## GPIO, ADC, And LED **`$led_builtin`** *(constant)* `Int` Names the board's built-in LED pin. **Example** ```frothy gpio.output: $led_builtin ``` --- **`$a0`** *(constant)* `Int` Names the board's default ADC input pin. **Example** ```frothy adc.read: $a0 ``` --- **`$boot_button`** *(constant)* `Int` Names the board boot button pin. **Example** ```frothy gpio.read: $boot_button ``` --- **`$led_active_level`** *(constant)* `Int` The electrical level that turns the current board's built-in LED on. **Example** ```frothy gpio.write: $led_builtin, $led_active_level ``` --- **`$sda`** *(constant)* `Int` The board's default I2C SDA pin. **Example** ```frothy bus is i2c.open: 0, $sda, $scl, 400000 ``` --- **`$scl`** *(constant)* `Int` The board's default I2C SCL pin. **Example** ```frothy 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** ```frothy 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** ```frothy gpio.write: $led_builtin, 1 ``` --- **`gpio.read`** *(gpio)* `(pin) -> Int` Reads a GPIO input level. **Example** ```frothy gpio.read: $boot_button ``` --- **`gpio.high`** *(gpio helper)* `(pin) -> nil` Writes level `1` to a GPIO pin. **Example** ```frothy gpio.high: $led_builtin ``` --- **`gpio.low`** *(gpio helper)* `(pin) -> nil` Writes level `0` to a GPIO pin. **Example** ```frothy gpio.low: $led_builtin ``` --- **`gpio.toggle`** *(gpio helper)* `(pin) -> nil` Writes the opposite of the pin's current GPIO level. **Example** ```frothy gpio.toggle: $led_builtin ``` --- **`gpio.output`** *(gpio helper)* `(pin) -> nil` Configures a GPIO pin as output. **Example** ```frothy gpio.output: $led_builtin ``` --- **`gpio.input`** *(gpio helper)* `(pin) -> nil` Configures a GPIO pin as input. **Example** ```frothy gpio.input: $boot_button ``` --- **`led.on`** *(led helper)* `() -> nil` Turns on the board's default LED. **Example** ```frothy led.on: ``` --- **`led.off`** *(led helper)* `() -> nil` Turns off the board's default LED. **Example** ```frothy led.off: ``` --- **`led.toggle`** *(led helper)* `() -> nil` Toggles the board's default LED. **Example** ```frothy led.toggle: ``` --- **`blink`** *(led helper)* `(pin, count, wait) -> nil` Blinks a pin by alternating high, sleep, low, sleep. **Example** ```frothy blink: $led_builtin, 3, 75 ``` --- **`led.blink`** *(led helper)* `(count, wait) -> nil` Blinks the board's default LED. **Example** ```frothy 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** ```frothy adc.read: $a0 ``` ```frothy to knob [ map: (adc.read: $a0), 0, 4095, 0, 100 ] ``` Deeper: [Read a Sensor](/tutorials/read-a-sensor/). --- **`adc.above?`** *(adc)* `(pin, threshold) -> Bool` Returns true when a raw ADC reading is above a threshold. **Example** ```frothy adc.above?: $a0, 2000 ``` --- **`adc.percent`** *(adc helper)* `(pin) -> Int` Maps a raw 0 to 4095 ADC reading to 0 to 100. **Example** ```frothy 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](/reference/modules/signals/), which capture and emit edges with dedicated hardware. **Example** ```frothy wait: 75 ``` ```frothy to slow-blink [ repeat 5 [ led.toggle: wait: 2000 ] ] ``` Deeper: [Timing module](/reference/modules/timing/). --- **`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** ```frothy millis: ``` ```frothy started is millis: wait: 25 elapsed is 0 set elapsed to (millis:) - started ``` Deeper: [Timing module](/reference/modules/timing/). --- **`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](/reference/modules/signals/), which capture and emit with 100-nanosecond hardware quantization. **Example** ```frothy micros: ``` Deeper: [Timing module](/reference/modules/timing/). --- **`abs`** *(math)* `(x) -> Int` Returns the absolute value of an integer. **Example** ```frothy abs: -5 ``` --- **`min`** *(math)* `(a, b) -> Int` Returns the smaller of two integers. **Example** ```frothy min: 3, 9 ``` --- **`max`** *(math)* `(a, b) -> Int` Returns the larger of two integers. **Example** ```frothy max: 3, 9 ``` --- **`clamp`** *(math)* `(x, lo, hi) -> Int` Clamps an integer to an inclusive range. **Example** ```frothy 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** ```frothy map: 2048, 0, 4095, 0, 100 ``` --- **`mod`** *(math)* `(a, b) -> Int` Returns `a` modulo `b` with the runtime's integer semantics. **Example** ```frothy mod: 37, 10 ``` --- **`sqrt`** *(math)* `(x) -> Int` Returns the floor square root of a nonnegative integer. **Example** ```frothy sqrt: 81 ``` --- **`wrap`** *(math helper)* `(value, size) -> Int` Returns `0` for nonpositive sizes, otherwise `mod: value, size`. **Example** ```frothy wrap: 37, 10 ``` --- **`sign`** *(math helper)* `(n) -> Int` Clamps an integer to `-1`, `0`, or `1`. **Example** ```frothy sign: -20 ``` --- **`random.next`** *(random)* `() -> Int` Returns the next pseudo-random nonnegative integer. **Example** ```frothy random.next: ``` --- **`random.below`** *(random)* `(limit) -> Int` Returns a pseudo-random integer in `[0, limit)`. **Example** ```frothy random.below: 10 ``` --- **`random.seed`** *(random)* `(seed) -> nil` Seeds the pseudo-random generator. **Example** ```frothy random.seed: 123 ``` --- **`random.chance?`** *(random helper)* `(numer, denom) -> Bool` Returns true when a random draw falls inside a numerator over denominator chance. **Example** ```frothy random.chance?: 1, 4 ``` --- **`random.percent?`** *(random helper)* `(percent) -> Bool` Returns true for a percentage chance from 0 to 100. **Example** ```frothy random.percent?: 25 ``` ## UART, I2C, And PWM **`$baud_9600`** *(uart constant)* `Int` Names the baud rate 9600. Its value is the integer `9600`. **Example** ```frothy uart.open: 1, $baud_9600 ``` --- **`$baud_19200`** *(uart constant)* `Int` Names the baud rate 19200. Its value is the integer `19200`. **Example** ```frothy uart.open: 1, $baud_19200 ``` --- **`$baud_38400`** *(uart constant)* `Int` Names the baud rate 38400. Its value is the integer `38400`. **Example** ```frothy uart.open: 1, $baud_38400 ``` --- **`$baud_57600`** *(uart constant)* `Int` Names the baud rate 57600. Its value is the integer `57600`. **Example** ```frothy uart.open: 1, $baud_57600 ``` --- **`$baud_115200`** *(uart constant)* `Int` Names the baud rate 115200. Its value is the integer `115200`. **Example** ```frothy uart.open: 1, $baud_115200 ``` --- **`$baud_1200`** *(uart constant)* `Int` Names the baud rate 1200. Its value is the integer `1200`. **Example** ```frothy 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** ```frothy aux is uart.open: 1, $baud_115200 ``` Deeper: [UART module](/reference/modules/uart/). --- **`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** ```frothy aux is uart.open-on: 1, 17, 16, $baud_115200 ``` Deeper: [UART module](/reference/modules/uart/). --- **`uart.write-byte`** *(uart)* `(handle, byte) -> nil` Writes one byte to an auxiliary UART. **Example** ```frothy 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 wait in the receive queue of the driver until read, so a periodic drain does not lose data between polls. **Example** ```frothy uart.read-byte: aux ``` ```frothy 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](/reference/modules/uart/). --- **`uart.available`** *(uart)* `(handle) -> Int` Returns the count of bytes waiting on an auxiliary UART. **Example** ```frothy uart.available: aux ``` --- **`uart.close`** *(uart)* `(handle) -> nil` Closes an auxiliary UART and releases its handle. **Example** ```frothy 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** ```frothy bus is i2c.open: 0, $sda, $scl, 400000 ``` Deeper: [I2C module](/reference/modules/i2c/). --- **`i2c.write`** *(i2c)* `(bus, addr, bytes) -> nil` Writes bytes to a 7-bit I2C address. **Example** ```frothy 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** ```frothy i2c.read: bus, 104, 2 ``` --- **`i2c.close`** *(i2c)* `(bus) -> nil` Closes an I2C bus and releases its handle. **Example** ```frothy 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** ```frothy i2c.read-reg: bus, 104, 117 ``` ```frothy to sensor-alive? [ (i2c.read-reg: bus, 104, 117) = 104 ] ``` Deeper: [I2C module](/reference/modules/i2c/). --- **`i2c.read-reg16`** *(i2c)* `(bus, addr, reg) -> Int` Reads a big-endian 16-bit register at a 7-bit I2C address. **Example** ```frothy 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** ```frothy i2c.write-reg: bus, 104, 107, 0 ``` Deeper: [I2C module](/reference/modules/i2c/). --- **`i2c.write-reg16`** *(i2c)* `(bus, addr, reg, value) -> nil` Writes a big-endian 16-bit register at a 7-bit I2C address. **Example** ```frothy 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** ```frothy led is pwm.open: $led_builtin, 1000 ``` Deeper: [PWM module](/reference/modules/pwm/), [Fade an LED](/tutorials/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** ```frothy pwm.write: led, 5000 ``` ```frothy to led.percent with handle, pct [ pwm.write: handle, (clamp: pct, 0, 100) * 100 ] ``` Deeper: [PWM module](/reference/modules/pwm/). --- **`pwm.close`** *(pwm)* `(handle) -> nil` Closes a PWM channel and releases its handle. **Example** ```frothy 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** ```frothy text.length: "ready" ``` --- **`text.equals?`** *(text)* `(a, b) -> Bool` Returns true when two text values have equal bytes. **Example** ```frothy text.equals?: "ok", "ok" ``` --- **`text.concat`** *(text)* `(a, b) -> Text` Joins two text values into a new text value. **Example** ```frothy text.concat: "he", "llo" ``` --- **`text.at`** *(text)* `(text, index) -> Int` Returns the byte at an index in a text value. **Example** ```frothy text.at: "A", 0 ``` --- **`text.from-int`** *(text)* `(n) -> Text` Renders an integer as decimal text. **Example** ```frothy text.from-int: 42 ``` --- **`bytes.from-text`** *(bytes)* `(text) -> Bytes` Copies a text value into a new Bytes value. `Bytes` values use a small transient arena. A Bytes value cannot stay in a top-level binding. Use each value during its evaluation or loop pass. Copy a result with `text.pack` when it must stay. For a result that changes, use a buffer. [Text, Bytes & PAD](/reference/modules/text-bytes-pad/#bytes) gives the lifetime rules. **Example** ```frothy bytes.length: (bytes.from-text: "AT") ``` Deeper: [Text, bytes, and pad](/reference/modules/text-bytes-pad/). --- **`bytes.from-byte`** *(bytes)* `(byte) -> Bytes` Makes a Bytes value of one byte from a 0 to 255 integer. **Example** ```frothy bytes.from-byte: 65 ``` --- **`bytes.from-int`** *(bytes)* `(n) -> Bytes` Converts an integer to ASCII decimal bytes. **Example** ```frothy bytes.from-int: 42 ``` --- **`bytes.length`** *(bytes)* `(bytes) -> Int` Returns the number of bytes in a Bytes value. **Example** ```frothy bytes.length: (bytes.from-text: "AT") ``` --- **`bytes.at`** *(bytes)* `(bytes, index) -> Int` Returns the byte at an index as a 0 to 255 integer. **Example** ```frothy bytes.at: (bytes.from-text: "AT"), 0 ``` --- **`bytes.equals?`** *(bytes)* `(a, b) -> Bool` Returns true when two Bytes values hold the same bytes. **Example** ```frothy bytes.equals?: (bytes.from-text: "AT"), bytes.from-text: "AT" ``` --- **`bytes.concat`** *(bytes)* `(a, b) -> Bytes` Joins two Bytes values into a new Bytes value. **Example** ```frothy bytes.concat: (bytes.from-text: "A"), bytes.from-text: "T" ``` --- **`text.pack`** *(text)* `(bytes) -> Text` Copies a Bytes value into Text. Frothy reuses an equal Text value when one exists. 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. Use a buffer and `buffer.set` for bytes that change many times. **Example** ```frothy reply is text.pack: (bytes.from-text: "ready") ``` Deeper: [Text, bytes, and pad](/reference/modules/text-bytes-pad/). --- **`buffer.set`** *(buffer)* `(buffer, value) -> nil` Replaces the contents of a buffer with the bytes of the value. The value is an integer, Text, Bytes or a buffer; an integer gives its decimal digits. A value that does not fit answers `capacity exceeded (4)` with the note `buffer is full (limit N bytes)`, and the contents do not change. Make a buffer at the top level with `name is buffer: N`. **Example** ```frothy reply is buffer: 64 buffer.set: reply, "status: ok" ``` --- **`buffer.append`** *(buffer)* `(buffer, value) -> nil` Adds the bytes of the value after the contents of a buffer. A value that does not fit changes nothing. **Example** ```frothy buffer.append: reply, "!" ``` --- **`buffer.length`** *(buffer)* `(buffer) -> Int` Returns the number of bytes in a buffer. **Example** ```frothy buffer.length: reply ``` --- **`buffer.capacity`** *(buffer)* `(buffer) -> Int` Returns the most bytes that a buffer can hold. **Example** ```frothy buffer.capacity: reply ``` --- **`buffer.at`** *(buffer)* `(buffer, index) -> Int` Returns the byte at an index as a 0 to 255 integer. **Example** ```frothy buffer.at: reply, 0 ``` --- **`buffer.equals?`** *(buffer)* `(buffer, value) -> Bool` Returns true when a buffer holds the bytes of the value. The value is an integer, Text, Bytes or a buffer. **Example** ```frothy buffer.equals?: reply, "status: ok" ``` Deeper: [Buffers](/reference/language/#buffers). --- **`wifi.save`** *(network)* `(ssid, pass) -> nil` Stores Wi-Fi credentials in the Frothy Wi-Fi NVS namespace. **Example** ```frothy wifi.save: "ssid", "password" ``` --- **`wifi.connect`** *(network)* `() -> nil` Stops access-point hosting and connects Wi-Fi using stored credentials. **Example** ```frothy 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** ```frothy wifi.host: "frothy", "" ``` --- **`wifi.ready?`** *(network)* `() -> Bool` Returns true when Wi-Fi is connected or hosting. **Example** ```frothy 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** ```frothy 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** ```frothy http.get: "http://example.com/" ``` ```frothy to fetch-size with url [ attempt [ bytes.length: (http.get: url) ] rescue [ -1 ] ] ``` Deeper: [Network module](/reference/modules/wifi/). --- **`http.post`** *(network)* `(url, body) -> Bytes` Posts Text, Bytes or a buffer 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** ```frothy http.post: "http://example.com/readings", "reading=42" ``` Deeper: [Network module](/reference/modules/wifi/). --- **`tcp.open`** *(network)* `(host, port) -> Handle` Opens a TCP connection to a host and port. **Example** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy tcp.read: sock, 64 ``` --- **`tcp.write`** *(network)* `(sock, bytes) -> nil` Sends the raw bytes of a Text, Bytes or buffer value to a TCP socket. **Example** ```frothy tcp.write: sock, "ping" ``` --- **`tcp.close`** *(network)* `(sock) -> nil` Closes a TCP connection or listener and releases its handle. **Example** ```frothy tcp.close: sock ``` --- **`tcp.available`** *(network)* `(sock) -> Int` Returns the bytes available for immediate `tcp.read`. **Example** ```frothy 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** ```frothy watchdog.arm: 5000 ``` ```frothy watchdog.arm: 5000 forever [ do-one-unit-of-work: watchdog.feed: ] ``` Deeper: [Power module](/reference/modules/power/). --- **`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** ```frothy watchdog.feed: ``` Deeper: [Power module](/reference/modules/power/). --- **`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** ```frothy sleep.deep: 60000 ``` ```frothy boot is fn [ log-reading: (adc.read: $a0) sleep.deep: 3600000 ] save ``` Deeper: [Power module](/reference/modules/power/). --- **`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** ```frothy sleep.wake-on-gpio: $boot_button, 0 sleep.deep: 3600000 ``` Deeper: [Power module](/reference/modules/power/). ## PAD **`pad.reset`** *(pad)* `() -> nil` Empties the PAD. The PAD is one shared scratch area (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.reset` → `pad.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** ```frothy pad.reset: ``` Deeper: [Text, bytes, and pad](/reference/modules/text-bytes-pad/). --- **`pad.emit-byte`** *(pad)* `(byte) -> nil` Adds one byte to the PAD. **Example** ```frothy pad.emit-byte: 65 ``` --- **`pad.length`** *(pad)* `() -> Int` Returns the number of bytes in the PAD. **Example** ```frothy pad.length: ``` --- **`pad.type`** *(pad)* `() -> nil` Writes the bytes of the PAD to the console output. **Example** ```frothy pad.type: ``` --- **`pad.peek-byte`** *(pad)* `(index) -> Int` Reads one byte from the PAD. **Example** ```frothy 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** ```frothy pad.pack: ``` ```frothy pad.reset: pad.emit-byte: 111 pad.emit-byte: 107 status is pad.pack: -- "ok" ``` Deeper: [Text, bytes, and pad](/reference/modules/text-bytes-pad/). --- ## Trace And Pulse ### Digital Trace **`trace.open`** *(trace)* `() -> Handle` Opens the single bounded digital-edge capture. **Example** ```frothy 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** ```frothy scl-channel is trace.watch: capture, $scl ``` --- **`trace.arm`** *(trace)* `(capture) -> nil` Clears previous edges and arms the configured capture. **Example** ```frothy trace.arm: capture ``` --- **`trace.wait`** *(trace)* `(capture, timeout_ms) -> Bool` Waits interruptibly and returns true when the capture completes before the timeout. **Example** ```frothy trace.wait: capture, 1000 ``` --- **`trace.stop`** *(trace)* `(capture) -> nil` Finishes an armed capture immediately so its collected edges can be read. **Example** ```frothy trace.stop: capture ``` --- **`trace.count`** *(trace)* `(capture) -> Int` Returns the number of captured edges. **Example** ```frothy trace.count: capture ``` --- **`trace.channel`** *(trace)* `(capture, index) -> Int` Returns the watched-channel index for one captured edge. **Example** ```frothy trace.channel: capture, 0 ``` --- **`trace.level`** *(trace)* `(capture, index) -> Int` Returns the pin level after one captured edge. **Example** ```frothy trace.level: capture, 0 ``` --- **`trace.delta-ns`** *(trace)* `(capture, index) -> Int` Returns nanoseconds since the previous captured edge. **Example** ```frothy trace.delta-ns: capture, 1 ``` --- **`trace.complete?`** *(trace)* `(capture) -> Bool` Returns true when capture stopped, filled its edge storage, or reached its maximum span. **Example** ```frothy trace.complete?: capture ``` --- **`trace.dump`** *(trace)* `(capture) -> nil` Prints the watched pins and captured edges for direct inspection. **Example** ```frothy trace.dump: capture ``` --- **`trace.close`** *(trace)* `(capture) -> nil` Releases the trace capture handle. **Example** ```frothy 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** ```frothy 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** ```frothy pulse.add: wave, 1, 800 ``` --- **`pulse.clear`** *(pulse)* `(wave) -> nil` Clears all spans while keeping the output open. **Example** ```frothy pulse.clear: wave ``` --- **`pulse.count`** *(pulse)* `(wave) -> Int` Returns the number of waveform spans. **Example** ```frothy pulse.count: wave ``` --- **`pulse.level`** *(pulse)* `(wave, index) -> Int` Returns a waveform span's level. **Example** ```frothy pulse.level: wave, 0 ``` --- **`pulse.duration-ns`** *(pulse)* `(wave, index) -> Int` Returns a waveform span's actual quantized duration. **Example** ```frothy pulse.duration-ns: wave, 0 ``` --- **`pulse.dump`** *(pulse)* `(wave) -> nil` Prints the quantized waveform. **Example** ```frothy pulse.dump: wave ``` --- **`pulse.play`** *(pulse)* `(wave) -> nil` Transmits the waveform once and returns after playback completes. **Example** ```frothy pulse.play: wave ``` --- **`pulse.close`** *(pulse)* `(wave) -> nil` Releases the timed output handle. **Example** ```frothy 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** ```frothy 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** ```frothy console.uart: 17, 16, 115200 ``` --- **`console.default`** *(console)* `() -> nil` Restores the board's default boot and recovery console. **Example** ```frothy console.default: ``` --- **`console.info`** *(console)* `() -> nil` Prints the active console route. **Example** ```frothy 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** ```frothy ble.on: ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`ble.info`** *(ble)* `() -> nil` Prints BLE roles, radio state, scan and advertising state, connection pressure, and the last raw failure reason. **Example** ```frothy ble.info: ``` --- **`ble.off`** *(ble)* `() -> nil` Closes BLE links, clears queues, invalidates BLE handles, and shuts down the radio. **Example** ```frothy 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** ```frothy ble.scan.start: 100, 50, 1, 0, -90 ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`ble.scan.stop`** *(ble scan)* `() -> nil` Stops scanning while retaining reports already queued. **Example** ```frothy 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** ```frothy when ble.scan.next?: [ ble.scan.rssi: ] ``` ```frothy to drain-reports [ while ble.scan.next?: [ print: (ble.scan.rssi:) print: "\n" ] ] ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`ble.scan.rssi`** *(ble scan)* `() -> Int` Returns the current report's RSSI in dBm. **Example** ```frothy 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** ```frothy bytes.length: (ble.scan.peer:) ``` --- **`ble.scan.flags`** *(ble scan)* `() -> Int` Returns the current report's platform scan flags. **Example** ```frothy ble.scan.flags: ``` --- **`ble.scan.data`** *(ble scan)* `() -> Bytes` Copies the current raw advertisement payload. **Example** ```frothy ble.scan.data: ``` ### Advertising **`ble.advertise.start`** *(ble advertise)* `(advertising_data, scan_response_data, interval_ms, connectable) -> nil` Starts legacy advertising with raw Text, Bytes or buffer 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** ```frothy ble.advertise.start: "\x02\x01\x06", "\x07\x09Frothy", 100, 1 ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`ble.advertise.stop`** *(ble advertise)* `() -> nil` Stops active BLE advertising. **Example** ```frothy 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** ```frothy link is ble.connect: peer, 5000 ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`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** ```frothy link is ble.accept: ``` ```frothy link is nil to watch-links [ every 500 [ here candidate is ble.accept: when candidate [ set link to candidate ] ] ] ``` Deeper: [Bluetooth module](/reference/modules/bluetooth/). --- **`ble.connection.ready?`** *(ble connection)* `(connection) -> Bool` Returns whether a BLE connection handle is live. **Example** ```frothy ble.connection.ready?: link ``` --- **`ble.connection.close`** *(ble connection)* `(connection) -> nil` Disconnects and releases a BLE connection handle. **Example** ```frothy 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** ```frothy ble.connection.info: link ``` --- **`ble.connection.rssi`** *(ble connection)* `(connection) -> Int` Reads the live connection RSSI in dBm. **Example** ```frothy 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** ```frothy 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** ```frothy ble.connection.mtu: link, 128, 5000 ``` ### GATT Constants **`$ble.gatt.service`** *(gatt constant)* `Int` Marks a local GATT table row as a service. **Example** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy $ble.gatt.read + $ble.gatt.notify ``` --- **`$ble.gatt.write`** *(gatt constant)* `Int` Adds write-with-response support to a local characteristic. **Example** ```frothy $ble.gatt.read + $ble.gatt.write ``` --- **`$ble.gatt.write-command`** *(gatt constant)* `Int` Adds write-without-response support to a local characteristic. **Example** ```frothy flags is $ble.gatt.write-command ``` --- **`$ble.gatt.notify`** *(gatt constant)* `Int` Adds notification support to a local characteristic. **Example** ```frothy $ble.gatt.read + $ble.gatt.notify ``` --- **`$ble.gatt.indicate`** *(gatt constant)* `Int` Adds indication support to a local characteristic. **Example** ```frothy $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** ```frothy $ble.gatt.read-encrypted ``` --- **`$ble.gatt.write-encrypted`** *(gatt reserved constant)* `Int` Names the encrypted-write flag; current GATT table installation rejects it. **Example** ```frothy $ble.gatt.write-encrypted ``` --- **`$ble.gatt.read-authenticated`** *(gatt reserved constant)* `Int` Names the authenticated-read flag; current GATT table installation rejects it. **Example** ```frothy $ble.gatt.read-authenticated ``` --- **`$ble.gatt.write-authenticated`** *(gatt reserved constant)* `Int` Names the authenticated-write flag; current GATT table installation rejects it. **Example** ```frothy $ble.gatt.write-authenticated ``` --- **`$ble.gatt.notifications`** *(gatt constant)* `Int` Selects notification mode for a remote GATT subscription. **Example** ```frothy ble.gatt.subscribe: link, attribute, $ble.gatt.notifications, 5000 ``` --- **`$ble.gatt.indications`** *(gatt constant)* `Int` Selects indication mode for a remote GATT subscription. **Example** ```frothy 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** ```frothy ble.gatt.install: gatt_rows ``` --- **`ble.gatt.info`** *(gatt)* `() -> nil` Prints local-server and remote-client GATT state, capacities, queues, and raw errors. **Example** ```frothy ble.gatt.info: ``` --- **`ble.gatt.set`** *(gatt server)* `(attribute, data) -> nil` Replaces a local characteristic value by source-row attribute ID. **Example** ```frothy ble.gatt.set: 1, "ready" ``` --- **`ble.gatt.get`** *(gatt server)* `(attribute) -> Bytes` Copies a local characteristic value by source-row attribute ID. **Example** ```frothy ble.gatt.get: 1 ``` --- **`ble.gatt.notify`** *(gatt server)* `(connection, attribute, data) -> nil` Sends one subscribed notification. **Example** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy ble.gatt.write-attribute: ``` --- **`ble.gatt.write-data`** *(gatt server)* `() -> Bytes` Copies the selected remote write's data. **Example** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy 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** ```frothy ble.gatt.notification-attribute: ``` --- **`ble.gatt.notification-data`** *(gatt client)* `() -> Bytes` Copies the selected remote notification data. **Example** ```frothy 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** ```frothy to start-ticking [ every 1000 [ print: "tick" ] ] ``` --- **`frothy.event-cancel`** *(internal)* `(kind, source) -> nil` Internal native used by compiled `cancel` forms. **Example** ```frothy 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 frothy.event-fire: "every", 1000, nil ``` --- Source: https://frothy.dev/errors/ # Error and Notice Codes ## Status Codes **`0 — ok`** *(success)* The operation completed. Code 0 appears as an `ok` completion rather than an error headline. --- **`1 — out of range`** *(runtime)* A number or index crossed an ordered bound. Use the reported value and any bound detail to choose an in-range value. --- **`2 — wrong type`** *(runtime)* A value has the wrong kind for this operation. Pass the kind named by the detail line. --- **`3 — bad value`** *(runtime)* The kind is acceptable, but that particular value is invalid in this domain. Argument details identify the rejected position. --- **`4 — capacity exceeded`** *(runtime)* A fixed table, buffer, image, or other bounded store is full. Store less data. Many of these errors add a note line that names the full store and its limit. The note gives a remedy only when one always works. For example: `note: pending code is full (limit 4178 bytes) -- a save that succeeds frees it`. A line longer than the device reads answers this code with `note: the line limit is bytes`. A value that does not fit a buffer answers it with `note: buffer is full (limit bytes)`, and the buffer does not change. [Limits](/limits/) lists each table and its size. --- **`5 — overflow`** *(runtime)* A result or stack would exceed its representable capacity. Reduce the input or the amount of nested work. --- **`6 — underflow`** *(runtime)* An operation needed a value that was not available, commonly on a stack. Supply the expected values or fix the calling shape. --- **`7 — not found`** *(runtime)* A requested name, field, or item does not exist. Check the reported name or define it first. --- **`8 — invalid`** *(language)* Source could not be read, parsed, or compiled. Fix the token identified by the message and caret. --- **`9 — unsupported`** *(device)* The operation or value is not supported by the active firmware or profile. Use a supported operation or a build that provides it. --- **`10 — interrupted`** *(runtime)* Evaluation was deliberately interrupted and is not catchable. At the prompt this is `interrupted` followed by `ok`; while restored `boot` code is running it is `error: interrupted (10)`. --- **`11 — corrupt data`** *(device)* Persisted or incoming data failed validation: a bad magic number, a bad checksum, or a bad length. The data is damaged, so restore, wipe, rebuild, or reacquire it instead of using a partial result. An image with a different format or profile hash reports `other release (27)` instead. That answer does not check the rest of the image. --- **`12 — i/o failed`** *(device)* A peripheral or storage operation failed. Check power, wiring, media, and device state, then retry when safe. --- **`13 — not saved`** *(persistence)* Volatile state cannot be made durable. A rejected store is an error. At the prompt, a bare `save` or `save:` stores slots holding handle values as nil and reports [notice 100](#notice-100) instead. It still refuses with this code when the value cannot be made nil safely: a live Bluetooth connection, a slot installed in library mode, or more handle-bound slots than the device can hold at once. Rebind the named slot and retry. `set` of a local to a Bytes value also answers this code, because the next loop pass can free the value. Make the value with `here` in the loop body. Inside a program, `save:` does not reach this check at all — see [code 26](#code-26). --- **`14 — bad handle`** *(resource)* A handle is closed, stale, or belongs to the wrong driver. Reopen the resource and pass a live handle of the expected kind. --- **`15 — no network`** *(network)* The network interface is disconnected. Reconnect it before retrying the operation. --- **`16 — timed out`** *(network)* A network operation did not complete in time. Check reachability and retry according to the application. --- **`17 — dns failed`** *(network)* A hostname could not be resolved. Check the hostname, DNS service, and network connection. --- **`18 — refused`** *(network)* The remote endpoint rejected the connection. Check the address, port, and server state. --- **`19 — too large`** *(network)* A network payload or response exceeds the supported bound. Reduce or split it. --- **`20 — bad protocol`** *(network)* A peer returned malformed or unexpected protocol data. Check that the endpoint and protocol agree. --- **`21 — ble not ready`** *(bluetooth)* The required Bluetooth state has not been established. Enable or configure the prerequisite state first. --- **`22 — ble busy`** *(bluetooth)* Another Bluetooth procedure is active or stopping. Wait for it to finish, or stop it before retrying. --- **`23 — ble timed out`** *(bluetooth)* A Bluetooth operation did not finish in time. Check range and peer state, then reconnect or retry. --- **`24 — ble disconnected`** *(bluetooth)* The Bluetooth peer disconnected. Reconnect before using peer-owned state. --- **`25 — busy`** *(resource)* A valid exclusive resource is already claimed. Reuse or close its existing handle, or select a different resource. --- **`26 — prompt only`** *(persistence)* This operation replaces the running program; issue it as its own complete prompt form. `save`, `restore`, and `dangerous.wipe` swap the image the running program is executing from, so they refuse while any word, event body, or `boot` routine is running. Catchable with `attempt`/`rescue`. Close the form first and run the word on its own. --- **`27 — other release`** *(persistence)* The saved image has a different image format or profile hash than this firmware. The device reads an image only when both match its own (see [An Image From Another Release](/reference/device/image-and-persistence/#an-image-from-another-release)). You see this at boot, when the device finds a saved image it cannot load, and from `restore`. Send your source again. Then type `save`. A `save` before you send your source writes an empty program over the old image. --- ## Notice Codes A notice reports something worth saying about a form that succeeded. Notice codes start at 100 so they can never be confused with a status code; they never appear as an error and cannot be caught. **`100 — saved; handle values stored as nil`** *(persistence)* `save` at the prompt kept your program running and wrote everything the image can carry. The slots named in the detail line hold handles — live connections to hardware — and no saved image can carry those, so they were stored as `nil`. Open those resources in `boot` and a reboot brings them back. **`101 — handles are still open`** *(handles)* `close-handles` could not close the kinds of handle that the detail line names. The platform keeps them, and a later `close-handles` tries again. **`102 — word not called`** *(prompt)* The line was a bare word, such as `led.on`, so the prompt shows the word and runs nothing. The detail says how to call it: `led.on:`. **`103 — base word replaced`** *(prompt)* A binding gave a new value to a name that held a base or library word, such as `wait is 5`. The binding stays. In the user tier, `clear` brings the old word back before `save`; after `save`, run `wipe-user` and restart the board, and note that `wipe-user` also removes the rest of the saved program. In library mode (after `install-library`) the binding is saved at once, and `clear` and `wipe-user` keep it: install the library again without it, then restart the board. The detail says to use another name to keep the word. Binding `boot` never gives this notice. --- Source: https://frothy.dev/reference/modules/board/ # Board Constants & Helpers Every firmware image starts with core words, target-native words, board constants, and a small Frothy source library. This page is the map of that ready-to-use layer. ## First Example ```frothy gpio.output: $led_builtin led.blink: 3, 75 adc.percent: $a0 ``` The constants let the same source describe intent without copying pin numbers. The helpers are ordinary Frothy words layered over the raw native surface. ## Board Constants | Name | Meaning | | --- | --- | | [`$led_builtin`](/reference/words/#led-builtin) | Built-in LED pin | | [`$led_active_level`](/reference/words/#led-active-level) | Electrical level that turns the built-in LED on | | [`$boot_button`](/reference/words/#boot-button) | Board boot-button pin | | [`$a0`](/reference/words/#a0) | Default analog input | | [`$sda`](/reference/words/#sda) | Default I2C data pin | | [`$scl`](/reference/words/#scl) | Default I2C clock pin | The two supported ESP32 board definitions install the same names but may bind them to different pin numbers or LED polarity. Use the names unless the circuit requires a specific pin. ## GPIO Helpers The raw primitives are `gpio.mode`, `gpio.write`, and `gpio.read`. The source library adds intent-revealing wrappers: ```frothy gpio.output: 4 gpio.high: 4 gpio.toggle: 4 gpio.low: 4 gpio.input: 4 ``` ## Built-In LED Helpers The LED helpers account for the board's active level: ```frothy led.on: led.toggle: led.off: led.blink: 3, 75 ``` Use `blink: pin, count, wait_ms` when you want the same behavior on another pin. ## ADC, Math, And Random Helpers The source library also installs: - [`adc.percent`](/reference/words/#adc-percent), mapping a raw 0–4095 read to 0–100 - [`wrap`](/reference/words/#wrap), safe modulo for cyclic indexes - [`sign`](/reference/words/#sign), producing -1, 0, or 1 - [`random.chance?`](/reference/words/#random-chance) and [`random.percent?`](/reference/words/#random-percent) These definitions live in the base image and can be inspected on the device: ```frothy see led.blink see adc.percent ``` Go to [GPIO & ADC](/reference/modules/gpio/) for the electrical I/O contract or the [word catalog](/reference/words/) for every exact signature. --- Source: https://frothy.dev/reference/modules/gpio/ # GPIO & ADC GPIO is the shortest path from source to a physical result. Start on the built-in LED and boot button, then move to circuit-specific pins. ## Blink And Read A Button ```frothy gpio.output: $led_builtin gpio.input: $boot_button to show-button [ if (gpio.read: $boot_button) = 0 [ led.on: ] else [ led.off: ] ] show-button: ``` The common ESP32 boot button is active-low: pressed reads as `0`. Other inputs depend on the circuit, pull resistors, and selected pin. The RP2040 boards have no `$boot_button`, and some boards have no built-in LED; see [Boards](/boards/). ## Pull-Up Inputs Mode `2` makes a pin an input with its internal pull-up resistor on. Use it for a button that connects the pin to ground, with no other resistor. The pin reads `1` when the button is up and `0` when you push it: ```frothy to button.pressed? with pin [ gpio.mode: pin, 2 (gpio.read: pin) = 0 ] ``` The modes `0`, `1`, and `2` work on ESP32 and RP2040 boards. Another mode gives `bad value (3)`. ## Digital Word Table | Word | Result | Use | | --- | --- | --- | | [`gpio.mode`](/reference/words/#gpio-mode) | `nil` | Set mode `0` for input, `1` for output, or `2` for input with the internal pull-up | | [`gpio.write`](/reference/words/#gpio-write) | `nil` | Write level `0` or `1` | | [`gpio.read`](/reference/words/#gpio-read) | `Int` | Read digital level | | [`gpio.input`](/reference/words/#gpio-input) | `nil` | Configure input mode | | [`gpio.output`](/reference/words/#gpio-output) | `nil` | Configure output mode | | [`gpio.high`](/reference/words/#gpio-high) | `nil` | Write level `1` | | [`gpio.low`](/reference/words/#gpio-low) | `nil` | Write level `0` | | [`gpio.toggle`](/reference/words/#gpio-toggle) | `nil` | Read and invert a level | The `gpio.*` helpers expose logical levels. Board LED helpers additionally use `$led_active_level`, so `led.on:` works on both active-high and active-low LEDs. ## React To Edges Register edge handlers inside a word, then call that word once: ```frothy to arm-button [ on $boot_button falling debounce 25 [ led.toggle: ] ] arm-button: ``` Use `rising`, `falling`, or `changes`. Cancel every GPIO registration for the pin with `cancel $boot_button`. See [Events](/reference/modules/events/) for identity, output, and capacity rules. ## Analog Input ```frothy raw is adc.read: $a0 percent is adc.percent: $a0 above-half? is adc.above?: $a0, 2047 ``` | Word | Result | Use | | --- | --- | --- | | [`adc.read`](/reference/words/#adc-read) | `Int` | Read the platform ADC value | | [`adc.above?`](/reference/words/#adc-above) | `Bool` | Compare one reading with a threshold | | [`adc.percent`](/reference/words/#adc-percent) | `Int` | Map the converter's full range to 0–100 | Analog reads accept only pins on the chip's first ADC unit; any other pin fails with `bad value`. A second unit, where the chip has one, is not exposed when it shares hardware with the radio — reads there fail or return noise whenever the radio is on. 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. An unconnected analog pin floats. A stable application circuit needs the appropriate source impedance, reference, filtering, and calibration; the language helper cannot replace those physical details. --- Source: https://frothy.dev/reference/modules/timing/ # Timing Timing has two jobs: pause a straightforward sequence, or measure elapsed time. Use events when code should react later without holding the foreground evaluation. ## Blink With A Delay ```frothy repeat 3 [ led.on: wait: 75 led.off: wait: 75 ] ``` `wait` accepts a nonnegative millisecond count, polls for Ctrl-C once per millisecond, and returns `nil`. Later expressions in the same word do not run until the delay finishes. ## Measure A Short Span ```frothy started is millis: wait: 25 finished is millis: elapsed is 0 set elapsed to finished - started ``` | Word | Result | Current 32-bit wrap period | | --- | --- | --- | | [`wait`](/reference/words/#wait) | `nil` | not applicable | | [`millis`](/reference/words/#millis) | `Int` | about 12.4 days | | [`micros`](/reference/words/#micros) | `Int` | about 17.9 minutes | The visible clocks wrap at the tagged integer ceiling. Short modular deltas across the wrap remain useful; do not treat either clock as a wall-clock date or an indefinitely increasing persisted counter. ## Milliseconds Are The Unit Durations are plain integers, and the unit belongs to the word: `wait` and the timer events take milliseconds, and a word with a different unit carries it in its name (`pulse.duration-ns`, `trace.delta-ns`). Millisecond resolution is also the floor for delays — `wait` sleeps in 1-millisecond steps. Nanosecond-scale timing exists only where dedicated hardware provides it: the [signal words](/reference/modules/signals/) capture and emit edges with 100-nanosecond quantization. ## Delay, Timer Event, Or Signal Hardware? - Use `wait` for a short, linear sequence. - Use `after` or `every` when work should run at a later safe point while the prompt remains live. - Use `trace.*` or `pulse.*` when edge timing must be captured or emitted with 100-nanosecond hardware quantization. Continue with [Events](/reference/modules/events/) or [Digital signals](/reference/modules/signals/). --- Source: https://frothy.dev/reference/modules/events/ # Events Events let the device react later while the prompt stays available. Frothy records a small handler, receives timer or hardware candidates, and runs the handler at a safe statement boundary. ## Timers And A Button Event registration belongs inside a word: ```frothy to start-ticks [ every 1000 [ print: "tick\n" ] ] to stop-later [ after 10000 [ led.off: ] ] to watch-button [ on $boot_button falling debounce 25 [ led.toggle: ] ] start-ticks: stop-later: watch-button: ``` `every` repeats. `after` removes itself before its body runs. The GPIO handler can use `rising`, `falling`, or `changes`; `debounce` is optional. The current profile allows one event body per definition, so each source gets one small registration word. ## Event Forms | Form | Identity | Behavior | | --- | --- | --- | | [`every ms [body]`](/reference/words/#every) | kind plus millisecond value | Repeats until cancelled or cleared | | [`after ms [body]`](/reference/words/#after) | kind plus millisecond value | Runs once, then disappears | | [`on pin edge [body]`](/reference/words/#on) | GPIO pin | Runs on the selected edge | | [`on pin edge debounce ms [body]`](/reference/words/#on) | GPIO pin | Drops candidates inside the debounce window | | [`on wifi.disconnected [body]`](/reference/words/#on) | Wi-Fi state | Runs after a previously working connection drops | | [`on wifi.reconnected [body]`](/reference/words/#on) | Wi-Fi state | Runs when that connection returns | | [`cancel ...`](/reference/words/#cancel) | matching source identity | Removes one registration | ## Cancel And Replace Use the same identity used at registration: ```frothy cancel every 1000 cancel after 10000 cancel $boot_button cancel wifi.disconnected cancel wifi.reconnected ``` Timer identity includes both kind and period, so `every 1000` and `after 1000` are different registrations. GPIO identity is the pin alone: registering a new edge handler for the same pin replaces the old one, and `cancel pin` removes it regardless of edge or debounce. Each Wi-Fi lifecycle kind has one binding. The current runtime holds 16 event bindings in total. Re-registering a matching identity replaces it without consuming another slot. `mem events` shows used and total capacity; `events` lists the active registrations. ## Handler Scope Event bodies do not capture the parameters or `here` locals of the word that registered them. Use top-level names or call another top-level word: ```frothy presses is 0 to count-press [ set presses to presses + 1 ] to arm-button [ on $boot_button falling debounce 25 [ count-press: ] ] ``` This keeps a handler valid after the registration call has returned. ## Dispatch And Output Candidates arrive asynchronously, but handler code runs in the Frothy runtime at safe points: statement boundaries, loop back-edges, and word returns. A long single expression can delay delivery until the next such point. Handlers do not interrupt one another recursively. Output produced outside the foreground reply is prefixed with `! ` on the wire: ```text ! tick ``` If the platform queue overflows, Frothy reports `! events dropped: N`. Keep handlers short and move sustained work into ordinary foreground code when possible. ## Wi-Fi Lifecycle Register Wi-Fi handlers before connecting: ```frothy to watch-wifi-down [ on wifi.disconnected [ print: "wifi down\n" ] ] to watch-wifi-up [ on wifi.reconnected [ print: "wifi back\n" ] ] watch-wifi-down: watch-wifi-up: wifi.connect: ``` The first successful connection is not a `wifi.reconnected` event. These forms describe a drop and recovery after the device has already obtained an address. See [Wi-Fi, HTTP & TCP](/reference/modules/wifi/) for connection behavior. ## RP2040 Support The Nano RP2040 Connect and the XIAO RP2040 support timer and GPIO events. The Nano also supports Wi-Fi lifecycle events through its NINA radio. The XIAO RP2040 has no Wi-Fi radio. On both RP2040 boards, `save` disables interrupts during each flash erase and program step. A timer can run late during these steps. The device can miss GPIO edges during a flash step. Do not run `save` while each GPIO edge is critical. ## Persistence And Clearing Event registrations are part of the project overlay. `save` persists them, `restore` reinstalls them, and `clear` or a project wipe removes their live platform registrations. Use `events` after restore to inspect what is active. Timers and GPIO handlers are for human-scale asynchronous work. For captured or emitted edges with 100-nanosecond quantization, use [Digital signals](/reference/modules/signals/). --- Source: https://frothy.dev/reference/modules/math-and-random/ # Math & Random Frothy math is integer math. These words keep common sensor scaling, bounds, cyclic indexes, and pseudo-random choices readable without introducing a floating-point value kind. ## Scale A Sensor Reading ```frothy raw is adc.read: $a0 percent is map: raw, 0, 4095, 0, 100 limited is clamp: percent, 10, 90 ``` ## Word Table | Word | Result | Use | | --- | --- | --- | | [`abs`](/reference/words/#abs) | `Int` | Absolute value | | [`min`](/reference/words/#min) | `Int` | Smaller input | | [`max`](/reference/words/#max) | `Int` | Larger input | | [`clamp`](/reference/words/#clamp) | `Int` | Limit a value to an inclusive range | | [`map`](/reference/words/#map) | `Int` | Linearly remap between two ranges | | [`mod`](/reference/words/#mod) | `Int` | C-style truncating remainder | | [`sqrt`](/reference/words/#sqrt) | `Int` | Floor square root of a nonnegative integer | | [`wrap`](/reference/words/#wrap) | `Int` | Cyclic remainder, or `0` for a nonpositive size | | [`sign`](/reference/words/#sign) | `Int` | Clamp to `-1`, `0`, or `1` | | [`random.next`](/reference/words/#random-next) | `Int` | Next nonnegative pseudo-random integer | | [`random.below`](/reference/words/#random-below) | `Int` | Random integer in `[0, limit)` | | [`random.seed`](/reference/words/#random-seed) | `nil` | Reset the pseudo-random sequence | | [`random.chance?`](/reference/words/#random-chance) | `Bool` | Numerator-over-denominator chance | | [`random.percent?`](/reference/words/#random-percent) | `Bool` | Percentage chance | ## Bounds And Mapping ```frothy min: 4, 9 max: 4, 9 clamp: 120, 0, 100 map: 2048, 0, 4095, 0, 100 ``` `clamp` rejects a lower bound greater than its upper bound. `map` uses integer division, so fractional results truncate toward zero. It permits ascending or descending ranges, rejects a zero-width input range, and reports a range error if the final result cannot fit a Frothy integer. `abs` reports a range error for the one most-negative integer because its positive counterpart is outside the tagged integer range. ## Remainders, Wrapping, And Roots ```frothy mod: 37, 10 mod: -3, 10 wrap: 37, 10 sign: -20 sqrt: 80 ``` `mod` follows C truncating remainder semantics and rejects a zero divisor. `wrap` is the small source helper `if size <= 0 [ 0 ] else [ value % size ]`; it does not normalize every negative remainder into a positive index. `sqrt` returns the floor root (`8` for `80`) and rejects negative inputs. ## Repeatable Random Sequences ```frothy random.seed: 12345 first is random.next: 1 + random.below: 6 coin? is random.percent?: 50 ``` This is deterministic pseudo-randomness, suitable for variation, simulation, and repeatable tests—not cryptographic keys or security decisions. The same seed starts the same sequence on a given implementation. `random.below` requires a positive limit. Use numerator and denominator values that describe a meaningful probability: ```frothy random.chance?: 1, 4 random.percent?: 25 ``` `random.chance?` returns false for a nonpositive denominator. Percent callers normally stay in `0..100`; the helper itself is ordinary Frothy source and does not clamp the input. --- Source: https://frothy.dev/reference/modules/text-bytes-pad/ # Text, Bytes & PAD Frothy has four byte tools with different lifetimes: | Kind | Best for | Lifetime | | --- | --- | --- | | `Text` | fixed strings and copied I/O results | object value; persistable | | `Bytes` | live I/O and temporary binary transforms | current evaluation or loop pass | | buffer | a value that changes many times | fixed capacity; persistable | | PAD | one tiny hand-built scratch sequence | one shared 64-byte area until reset | Each new Text value stays until the next `save`, `clear` or restart. Keep a value that changes in a buffer. Show a number with `print:` or `print.value:`; they make no Text value. ## Copy An HTTP Body Into Text An I/O read returns `Bytes`. It cannot be installed in a top-level binding: ```frothy body is text.pack: (http.get: "http://example.com/") text.length: body ``` Inside a word, a `here` local can hold Bytes for the duration of the call: ```frothy to fetch-size with url [ here body is http.get: url bytes.length: body ] ``` ## Keep The Latest HTTP Body In A Buffer A program that fetches a reply again and again keeps it in a buffer. `text.pack` makes a new Text value when no equal Text value exists. `buffer.set` writes into the same storage: ```frothy reply is buffer: 512 to fetch with url [ buffer.set: reply, (http.get: url) ] ``` A body longer than 512 bytes answers `capacity exceeded (4)`, and `reply` keeps the body before it. ## Text | Word | Result | Use | | --- | --- | --- | | [`text.length`](/reference/words/#text-length) | `Int` | Byte length | | [`text.equals?`](/reference/words/#text-equals) | `Bool` | Byte-for-byte equality | | [`text.concat`](/reference/words/#text-concat) | `Text` | New joined Text | | [`text.at`](/reference/words/#text-at) | `Int` | Byte at an index | | [`text.from-int`](/reference/words/#text-from-int) | `Text` | Decimal integer rendering | | [`text.pack`](/reference/words/#text-pack) | `Text` | Copy Bytes into Text storage | Text is immutable and indexed by byte, not Unicode character. Source literals support `\n`, `\r`, `\t`, `\"`, `\\`, and `\xHH` for one byte in hexadecimal. One Text value holds at most 4,096 bytes. The Text values made since the last `save` and the capacities of all buffers share a RAM pool of 4,096 bytes. Text shares the object table with Cells, records and buffers: 128 entries on ESP32 boards and 96 on RP2040 boards. ```frothy greeting is text.concat: "hello, ", "Frothy" text.at: greeting, 0 text.equals?: greeting, "hello, Frothy" ``` Each new Text value uses an object entry and room in the Text pool until the next `save`, `clear`, or restart, also when the program does not keep it. Reuse stable values. In a long loop, print a number with `print:` or `print.value:`. They make no Text value. ## Bytes | Word | Result | Use | | --- | --- | --- | | [`bytes.from-text`](/reference/words/#bytes-from-text) | `Bytes` | Copy Text into the transient arena | | [`bytes.from-byte`](/reference/words/#bytes-from-byte) | `Bytes` | Create one byte from `0..255` | | [`bytes.from-int`](/reference/words/#bytes-from-int) | `Bytes` | Decimal integer as ASCII bytes | | [`bytes.length`](/reference/words/#bytes-length) | `Int` | Byte count | | [`bytes.at`](/reference/words/#bytes-at) | `Int` | Byte at an index | | [`bytes.equals?`](/reference/words/#bytes-equals) | `Bool` | Byte-for-byte equality | | [`bytes.concat`](/reference/words/#bytes-concat) | `Bytes` | Join two Bytes values | | [`console.read-line`](/reference/words/#console-read-line) | `Bytes` | Read one printable console line as data | Eight Bytes values can be in use at one time, with 4,096 bytes of data in total. Each pass of `repeat`, `while`, and `forever` frees the Bytes values that the pass made. An event body and a failed `attempt` body free theirs when they end, and the prompt frees all Bytes values after each form. So a loop can make Bytes values for as long as it runs. A Bytes value cannot stay from one pass to the next. `set` of a local refuses a Bytes value with `not saved (13)`. The next pass can free the value while the local still names it. Make the value with `here` in the loop body. Bytes cannot be written to top-level slots, Cells, or record fields, and cannot be saved. Consume them immediately, keep them in a `here` local during one call, or pack them: ```frothy to make-frame [ here head is bytes.from-text: "value=" here value is bytes.from-int: 42 here frame is bytes.concat: head, value text.pack: frame ] frame-text is make-frame: ``` Words such as `print`, `i2c.write`, `tcp.write`, and BLE write operations accept Text, Bytes or a buffer directly. `console.read-line`, `i2c.read`, `tcp.read`, `http.get`, and BLE read operations return Bytes. ## Buffers | Word | Result | Use | | --- | --- | --- | | [`buffer.set`](/reference/words/#buffer-set) | `nil` | Replace the contents | | [`buffer.append`](/reference/words/#buffer-append) | `nil` | Add bytes after the contents | | [`buffer.length`](/reference/words/#buffer-length) | `Int` | Byte count | | [`buffer.capacity`](/reference/words/#buffer-capacity) | `Int` | Most bytes it can hold | | [`buffer.at`](/reference/words/#buffer-at) | `Int` | Byte at an index | | [`buffer.equals?`](/reference/words/#buffer-equals) | `Bool` | Byte-for-byte equality | `name is buffer: N` makes a buffer at the top level. Its capacity uses the RAM Text pool. `mem objects` shows the total capacity of the buffers in the row `objects.buffers.used`. [Buffers](/reference/language/#buffers) gives the rules. ## PAD PAD is one global scratch area. It is useful when bytes are assembled one at a time without allocating a Bytes entry: ```frothy pad.reset: pad.emit-byte: 65 pad.emit-byte: 84 pad.type: command is pad.pack: ``` | Word | Result | Use | | --- | --- | --- | | [`pad.reset`](/reference/words/#pad-reset) | `nil` | Empty the PAD | | [`pad.emit-byte`](/reference/words/#pad-emit-byte) | `nil` | Append one `0..255` byte | | [`pad.length`](/reference/words/#pad-length) | `Int` | Current byte count | | [`pad.peek-byte`](/reference/words/#pad-peek-byte) | `Int` | Read one byte by index | | [`pad.type`](/reference/words/#pad-type) | `nil` | Write PAD bytes to the console | | [`pad.pack`](/reference/words/#pad-pack) | `Text` | Copy PAD into persistent Text | The current PAD holds 64 bytes. `pad.emit-byte` reports capacity when full; `pad.peek-byte` reports range for an invalid index. `pad.pack` does not clear the PAD, so call `pad.reset` before constructing the next value. --- Source: https://frothy.dev/reference/modules/i2c/ # I2C I2C is the usual path to small sensors, displays, and converters. Frothy keeps the bus itself explicit: open it once, pass its `Handle` to each transfer, and close it when the project no longer needs it. ## Read A Sensor Register The following shape works for devices with an 8-bit identity register. Replace the address and register with values from the device datasheet. ```frothy bus is i2c.open: 0, $sda, $scl, 400000 identity is i2c.read-reg: bus, 104, 117 i2c.close: bus set bus to nil ``` `104` is the decimal form of 7-bit address `0x68`; `117` is register `0x75`. Frothy also reads hexadecimal (`0x68`) and binary (`0b1101000`) literals, so you can type datasheet values as they are. ## Word Table | Word | Result | Use | | --- | --- | --- | | [`i2c.open`](/reference/words/#i2c-open) | `Handle` | Open a port on selected SDA/SCL pins and frequency | | [`i2c.write`](/reference/words/#i2c-write) | `nil` | Send Text, Bytes or a buffer to a 7-bit address | | [`i2c.read`](/reference/words/#i2c-read) | `Bytes` | Read bytes as a transient Bytes value | | [`i2c.read-reg`](/reference/words/#i2c-read-reg) | `Int` | Read one 8-bit register | | [`i2c.write-reg`](/reference/words/#i2c-write-reg) | `nil` | Write one 8-bit register | | [`i2c.read-reg16`](/reference/words/#i2c-read-reg16) | `Int` | Read one big-endian 16-bit register | | [`i2c.write-reg16`](/reference/words/#i2c-write-reg16) | `nil` | Write one big-endian 16-bit register | | [`i2c.close`](/reference/words/#i2c-close) | `nil` | Close the bus and release its handle | ## Open And Close ```frothy bus is i2c.open: 0, $sda, $scl, 100000 -- transfers go here i2c.close: bus set bus to nil ``` The board constants `$sda` and `$scl` select the default pins. Use numeric pin values when a circuit is wired elsewhere. Common frequencies are `100000` and `400000` Hz, subject to the devices, wiring, pull-ups, and target support. `i2c.open` returns a live runtime `Handle`, not an integer port number. A closed or stale handle cannot be reused. ## Raw Transfers `i2c.write` accepts Text, Bytes or a buffer, so short fixed command sequences can be written directly: ```frothy i2c.write: bus, 104, "AT" to inspect-reply [ here reply is i2c.read: bus, 104, 2 bytes.at: reply, 0 bytes.at: reply, 1 ] inspect-reply: ``` `i2c.read` returns transient `Bytes`. Copy a reading into persistent Text before the current evaluation ends or when the value must survive a save: ```frothy saved-reply is text.pack: (i2c.read: bus, 104, 2) ``` The `count`, address, register, and register values must fit their respective byte or 16-bit ranges. A device NACK, invalid address, unavailable port, or bus failure is reported as an error. ## Register Helpers The register words perform the common write-register-then-read transaction: ```frothy i2c.write-reg: bus, 104, 107, 0 who-am-i is i2c.read-reg: bus, 104, 117 i2c.write-reg16: bus, 72, 1, 32768 config is i2c.read-reg16: bus, 72, 1 ``` The 16-bit helpers use big-endian byte order. If a component uses little-endian values, multi-byte register addresses, repeated-start rules outside this contract, or a larger transaction, compose it with raw `i2c.write` and `i2c.read` instead. ## Persistence Pattern Handles are volatile: `save` writes a slot holding one as `nil` and names it, so the image never carries a bus that will not exist after a reboot. Keep the durable binding non-handle and open the bus from `boot`: ```frothy bus is false to sensor.open [ set bus to i2c.open: 0, $sda, $scl, 400000 ] to sensor.close [ i2c.close: bus set bus to false ] boot is fn [ sensor.open: ] ``` See [Persist the Recipe, Not the Handle](/guide/10-snapshots-and-persistence/#persist-the-recipe-not-the-handle) for the generalized close/save/reopen lifecycle and the important `save` commit-boundary behavior. See [Text, Bytes & PAD](/reference/modules/text-bytes-pad/) for byte lifetimes and [Board constants & helpers](/reference/modules/board/) for the named pins. --- Source: https://frothy.dev/reference/modules/uart/ # UART Auxiliary UARTs connect Frothy to GPS receivers, modems, motor controllers, and other byte-oriented serial devices. They are separate from the active human console that carries the REPL. ## Send And Receive One Byte ```frothy aux is uart.open-on: 1, 17, 16, $baud_115200 uart.write-byte: aux, 65 when (uart.available: aux) > 0 [ uart.read-byte: aux ] uart.close: aux set aux to nil ``` `65` is ASCII `A`. UART words work with individual integers from `0` through `255`; build a loop when a protocol has a longer frame. ## Word Table | Word | Result | Use | | --- | --- | --- | | [`uart.open`](/reference/words/#uart-open) | `Handle` | Open a port on its target-default pins | | [`uart.open-on`](/reference/words/#uart-open-on) | `Handle` | Open a port on explicit TX and RX pins | | [`uart.available`](/reference/words/#uart-available) | `Int` | Count bytes ready to read without waiting | | [`uart.read-byte`](/reference/words/#uart-read-byte) | `Int` | Read one byte, or `-1` when none is ready | | [`uart.write-byte`](/reference/words/#uart-write-byte) | `nil` | Write one byte | | [`uart.close`](/reference/words/#uart-close) | `nil` | Close the port and release its handle | ## Baud Constants Each constant is the baud rate as an integer. You can also give another integer, for example `uart.open: 1, 4800`. | Constant | Baud | | --- | ---: | | [`$baud_1200`](/reference/words/#baud-1200) | 1200 | | [`$baud_9600`](/reference/words/#baud-9600) | 9600 | | [`$baud_19200`](/reference/words/#baud-19200) | 19200 | | [`$baud_38400`](/reference/words/#baud-38400) | 38400 | | [`$baud_57600`](/reference/words/#baud-57600) | 57600 | | [`$baud_115200`](/reference/words/#baud-115200) | 115200 | ```frothy aux is uart.open: 1, $baud_9600 ``` `uart.open` uses the port's target-default pins. Use `uart.open-on` when the board exposes a different route. The chosen port and pins must be supported and must not conflict with the active console or another peripheral. ## Exclusive Ports And Busy Errors A UART port has one owner. Opening a valid port that is already claimed reports the rejected port in a `busy` error: ```text error: busy: 0 (25) detail: uart.open argument 1 was rejected ``` Reuse the existing handle, close that handle before opening the port again, or select a different supported port. Code 25 is distinct from a bad port number: it means the resource exists but is currently exclusive to another owner. See [Error and notice codes](/errors/#code-25) for the general recovery contract. ## Nonblocking Reads `uart.read-byte` does not wait forever. It returns the next byte or `-1` when the receive queue is empty: ```frothy to drain-uart with handle [ while (uart.available: handle) > 0 [ print.value: "byte", (uart.read-byte: handle) ] ] ``` Check `uart.available` when `-1` would overlap an application sentinel. Read bytes are unsigned `0..255` integers. ## Auxiliary Port Or Console? Use `uart.*` for application data while the REPL remains on its current route. Use [`console.read-line`](/reference/modules/console/#read-input-as-data) when a running form needs one printable data line from the human console. Use console routing only when you intentionally want the prompt and all console output to move to another UART. UART handles are volatile, so reopen required ports from `boot`. A top-level UART handle does not block a save: the prompt's `save` writes that slot as `nil`, names it in a [`saved; handle values stored as nil (100)` notice](/errors/#notice-100), and leaves your open port running. `save` inside a word or larger form does not save at all — it answers [`prompt only (26)`](/errors/#code-26), because saving replaces the image the running program is executing from. --- Source: https://frothy.dev/reference/modules/pwm/ # PWM PWM repeatedly switches a pin at a selected frequency. It is useful for LED brightness, buzzers, and devices that accept a duty-controlled input. ## Fade The Built-In LED ```frothy channel is pwm.open: $led_builtin, 1000 repeat 21 as i [ pwm.write: channel, i * 500 wait: 30 ] pwm.write: channel, 0 pwm.close: channel set channel to nil ``` The electrical brightness direction follows the LED's polarity. On an active-low built-in LED, a larger duty may look dimmer rather than brighter. ## Word Table | Word | Result | Use | | --- | --- | --- | | [`pwm.open`](/reference/words/#pwm-open) | `Handle` | Open a channel on a pin at a frequency in Hz | | [`pwm.write`](/reference/words/#pwm-write) | `nil` | Set duty from `0` through `10000` | | [`pwm.close`](/reference/words/#pwm-close) | `nil` | Stop output and release the handle | ## Frequency And Duty ```frothy channel is pwm.open: 4, 5000 pwm.write: channel, 2500 -- 25 percent duty pwm.write: channel, 7500 -- 75 percent duty ``` The duty scale is always Frothy's inclusive `0..10000` contract. Frequency is specified in hertz. The target maps those values onto its hardware PWM peripheral; unsupported pins, frequencies, or exhausted channels return an error. `pwm.write` changes the duty of an open channel. Values below `0` or above `10000` are rejected rather than clamped. ## Which Pins Can PWM Any output-capable GPIO can carry PWM — the chip routes its PWM peripheral to the pin, so there is no special "PWM pin" set the way analog input is tied to particular converter pins. Two limits apply on every board: - **Input-only pins can't.** A pin with no output driver rejects `pwm.open`. Which pins those are is a property of your board — on the classic ESP32 DevKit V1, for example, GPIO 34, 35, 36, and 39 are input-only. - **Never drive the flash pins.** The pins wired to the module's SPI flash chip count as output-capable, so `pwm.open` accepts them — but driving them corrupts the firmware's own flash bus and typically crashes the board. Check your board's pinout for which to avoid (on classic ESP32 modules, for example, GPIO 6–11). A pin already held by an open channel reports `busy` for a different frequency, and plain `gpio.write` / `gpio.mode` on it also report `busy` (a digital write would silently detach the pin from its PWM signal). An exact repeat of the same `pwm.open` returns the existing handle. Hardware channels are limited — four concurrent channels on current boards — so `pwm.open` beyond that returns an error until one is closed. ## Handle Lifetime `pwm.open` returns a volatile `Handle`. Close it before reusing the pin or saving the project, then replace every top-level handle binding with a durable value such as `false` or `nil`. ```frothy channel is false to dimmer.open [ set channel to pwm.open: 4, 1000 ] to dimmer.close [ pwm.close: channel set channel to false ] boot is fn [ dimmer.open: ] ``` For precisely timed, finite high/low spans rather than a repeating duty cycle, use [Digital signals](/reference/modules/signals/). The [Fade an LED tutorial](/tutorials/fade-an-led/) develops a complete PWM project. --- Source: https://frothy.dev/reference/modules/wifi/ # Wi-Fi, HTTP & TCP The network surface has four paths: connect with stored credentials, host an access point, exchange bounded HTTP bodies, or use TCP handles. Net-enabled profiles provide every word on this page. ## Connect And Request Run `wifi.save` once when credentials change, then connect and copy the transient response into Text: ```frothy wifi.save: "network-name", "network-password" wifi.connect: body is text.pack: (http.get: "http://example.com/") print: body reply is text.pack: (http.post: "http://example.com/readings", "reading=42") print: reply ``` Credentials are stored in the target's dedicated `frothy_wifi` NVS namespace, separate from the saved Frothy project overlay. `wifi.connect` waits up to the platform connection budget and remains interruptible with Ctrl-C. ## Word Table | Word | Result | Use | | --- | --- | --- | | [`wifi.save`](/reference/words/#wifi-save) | `nil` | Store SSID and password | | [`wifi.connect`](/reference/words/#wifi-connect) | `nil` | Connect with stored credentials | | [`wifi.host`](/reference/words/#wifi-host) | `nil` | Start or reconfigure an access point with captive DNS | | [`wifi.ready?`](/reference/words/#wifi-ready) | `Bool` | Test station or access-point readiness | | [`wifi.ip`](/reference/words/#wifi-ip) | `Text` | Get the active interface address | | [`http.get`](/reference/words/#http-get) | `Bytes` | Fetch one successful response body | | [`http.post`](/reference/words/#http-post) | `Bytes` | Post Text, Bytes or a buffer and return the response body | | [`tcp.open`](/reference/words/#tcp-open) | `Handle` | Open a host and port | | [`tcp.listen`](/reference/words/#tcp-listen) | `Handle` | Open the single listener slot | | [`tcp.accept`](/reference/words/#tcp-accept) | `Handle|nil` | Accept one waiting client without blocking | | [`tcp.available`](/reference/words/#tcp-available) | `Int` | Count immediately readable bytes | | [`tcp.read`](/reference/words/#tcp-read) | `Bytes` | Read up to a requested count | | [`tcp.write`](/reference/words/#tcp-write) | `nil` | Send Text, Bytes or a buffer | | [`tcp.close`](/reference/words/#tcp-close) | `nil` | Close and release a connection or listener handle | ## Wi-Fi State And Events ```frothy to watch-wifi-down [ on wifi.disconnected [ print: "offline\n" ] ] to watch-wifi-up [ on wifi.reconnected [ print: "online\n" ] ] watch-wifi-down: watch-wifi-up: wifi.connect: wifi.ready?: ``` `wifi.ready?` returns true for a connected station or a hosted access point. The ESP32 station automatically retries after a real disconnect. The lifecycle events describe loss and recovery after an initial successful connection. Initial connection does not emit `wifi.reconnected`. Hosting adds no lifecycle event. Any TCP connection open across a disconnect becomes failed and must be closed and reopened. SSIDs may contain 1–32 bytes and passwords up to 64 bytes on the current ESP32 target. An empty password is valid for an open network. ## Host A Captive Page `wifi.host` replaces station mode with an access point and a captive DNS responder. The responder answers every lookup with the device's own address. After a phone joins, its operating system can open the portal sheet against a program that serves port 80. `wifi.ip` returns the address to use in a browser. An empty hosting password creates an open network. A nonempty password must contain 8 through 63 bytes for WPA2. A password outside that range returns `bad value`. Another `wifi.host` call reconfigures the access point. A later `wifi.connect` stops the access point and returns to station mode. `tcp.listen` opens the single listener slot. Ports must be in `1..65535`; other values return `bad value`. Repeating the same port returns the same handle. A different port returns `busy` until the listener closes. `tcp.accept` returns immediately with `nil` or an ordinary TCP connection handle. Poll it inside an `every` loop: ```frothy wifi.host: "frothy", "" server is tcp.listen: 80 to serve [ c is tcp.accept: server; if c [ tcp.read: c, 1024; tcp.write: c, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n

hello

"; tcp.close: c ] ] to start-serving [ every 150 [ serve: ] ] start-serving: ``` The `tcp.read: c, 1024` call reads the complete small request before `tcp.close`. Closing a connection with unread request bytes can send a TCP reset. The reset can arrive before the response, which makes the browser show an empty page. The prompt has a per-line input budget. Put a long response in more than one Text value. Write the values back-to-back, such as `tcp.write: c, p1; tcp.write: c, p2`. Portal browsers often request a page twice for one interaction. Use `millis` to debounce each URL that has a side effect. The bundled `examples/14-captive-portal.fr` example uses this pattern for a button that toggles the LED. Open it from the browser editor's **Examples** list or with `Frothy: Open Example` in VS Code. ## Bounded HTTP GET And POST `http.get` and `http.post` use plain `http://` URLs. Each word requires a 2xx response and returns the complete body as transient Bytes. `http.post` accepts Text, Bytes or a buffer and sends it with `Content-Type: text/plain`. To keep the latest body, see [Keep The Latest HTTP Body In A Buffer](/reference/modules/text-bytes-pad/#keep-the-latest-http-body-in-a-buffer). The current `FR_HTTP_MAX_BODY` response cap is 4096 bytes. Both words use the same timeout and network-error behavior. An oversized response returns an error and no partial result. ```frothy to body-length with url [ here body is http.get: url bytes.length: body ] body-length: "http://example.com/" http.post: "http://example.com/readings", "reading=42" ``` Pack the result when it must leave the current evaluation. Transport, DNS, timeout, non-2xx, protocol, and oversize failures are network errors. `attempt ... rescue ...` can provide an application fallback. ## TCP Client Stream ```frothy socket is tcp.open: "example.com", 80 tcp.write: socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" when (tcp.available: socket) > 0 [ print: (tcp.read: socket, 256) ] tcp.close: socket set socket to nil ``` `tcp.read` requests a positive count up to 4096 and may return fewer bytes. Its Bytes result can be printed or processed immediately. A zero ready count means an immediate read would have no data; it is not itself an end-of-stream signal. Ports must be in `1..65535`. `tcp.write` sends every byte of its Text, Bytes or buffer input or reports an error. ## TCP Capacity And Cleanup `tcp.open` and clients returned by `tcp.accept` share the connection capacity. ESP32 profiles carry four simultaneous TCP connection handles. The Nano RP2040 Connect carries two. The listener uses a separate single slot. When the connection handles are full, `tcp.open` or `tcp.accept` returns `capacity exceeded`. `tcp.close` accepts a connection or listener handle. `close-handles` also reaches both kinds. ## Persistence Pattern TCP connection and listener handles are volatile. Close them and replace top-level handle values before `save`; reconnect from `boot` when appropriate. Credentials do not need to be saved in the overlay: ```frothy socket is false boot is fn [ wifi.connect: set socket to tcp.open: "example.com", 80 ] ``` See [Events](/reference/modules/events/) for handler identity and [Text, Bytes & PAD](/reference/modules/text-bytes-pad/) for response lifetimes. --- Source: https://frothy.dev/reference/modules/power/ # Power Power words cover two distinct jobs: reset a program that stops making progress, or put the chip into deep sleep and cold-boot it later. ## Arm And Feed The Watchdog ```frothy watchdog.arm: 5000 forever [ -- do one bounded unit of useful work watchdog.feed: wait: 1000 ] ``` | Word | Result | Use | | --- | --- | --- | | [`watchdog.arm`](/reference/words/#watchdog-arm) | `nil` | Arm or re-arm with a 1000–60000 ms timeout | | [`watchdog.feed`](/reference/words/#watchdog-feed) | `nil` | Restart the window of an armed watchdog | | [`sleep.wake-on-gpio`](/reference/words/#sleep-wake-on-gpio) | `nil` | Configure an ext0 wake level for the next sleep | | [`sleep.deep`](/reference/words/#sleep-deep) | `nil` | Enter timed deep sleep and cold-boot on wake | Feeding before `watchdog.arm` is an error. Re-arming replaces the timeout and starts a new window immediately. Put feeds after demonstrated progress, not in a loop that could continue while the real work is stuck. ## Timed Deep Sleep ```frothy sleep.deep: 60000 ``` The duration is a nonnegative number of milliseconds. The call normally does not return on hardware: wake restarts the chip, restores the saved image, and runs `boot` as a cold boot. Save required durable state before sleeping. ## GPIO Wake Configure wake for the next `sleep.deep` call: ```frothy sleep.wake-on-gpio: $boot_button, 0 sleep.deep: 3600000 ``` The level must be `0` or `1`, and the selected pin must support the target's ext0 deep-sleep wake mechanism. The wake configuration is pending state for the next sleep, not a persistent event handler. The same sleep can wake because the timer expires or the configured pin reaches the requested level. Deep sleep is not `wait`: it tears down live handles and runtime state. Reopen peripherals in `boot`, and design the boot path so a permanently asserted wake pin does not create a rapid sleep/wake loop. --- Source: https://frothy.dev/reference/modules/signals/ # Digital Signals The signal modules sit below human-scale events. `trace.*` records edge timing on up to three pins; `pulse.*` transmits a finite high/low waveform. Both use live handles and 100-nanosecond time quantization. ## Capture Edges ```frothy capture is trace.open: sda-channel is trace.watch: capture, $sda scl-channel is trace.watch: capture, $scl trace.arm: capture trace.wait: capture, 1000 trace.dump: capture trace.close: capture set capture to nil ``` One trace capture can be open at a time. Add one to three pins while it is in the configuring state, then arm it. The capture completes when stopped, when its storage of 256 edges fills, or when its one-second maximum span is reached. ### Trace Words | Word | Result | Use | | --- | --- | --- | | [`trace.open`](/reference/words/#trace-open) | `Handle` | Open the single capture | | [`trace.watch`](/reference/words/#trace-watch) | `Int` | Add a pin; receive channel `0..2` | | [`trace.arm`](/reference/words/#trace-arm) | `nil` | Clear old edges and start capture | | [`trace.wait`](/reference/words/#trace-wait) | `Bool` | Wait interruptibly up to 60000 ms | | [`trace.stop`](/reference/words/#trace-stop) | `nil` | Finish an armed capture now | | [`trace.complete?`](/reference/words/#trace-complete) | `Bool` | Test for completed state | | [`trace.count`](/reference/words/#trace-count) | `Int` | Number of recorded edges | | [`trace.channel`](/reference/words/#trace-channel) | `Int` | Channel for one edge | | [`trace.level`](/reference/words/#trace-level) | `Int` | Level after one edge | | [`trace.delta-ns`](/reference/words/#trace-delta-ns) | `Int` | Time since the previous edge | | [`trace.dump`](/reference/words/#trace-dump) | `nil` | Print configuration and edges | | [`trace.close`](/reference/words/#trace-close) | `nil` | Release the capture | Inspect edges programmatically only after completion: ```frothy when trace.complete?: capture [ repeat (trace.count: capture) as i [ print: (trace.channel: capture, i) print: ":" print: (trace.delta-ns: capture, i) print: "\n" ] ] ``` The first edge's delta is relative to the arm point; later deltas are relative to the previous captured edge. `trace.wait` returns false on timeout without automatically discarding the capture; use `trace.stop` to make partial results readable. ## Transmit A Waveform ```frothy wave is pulse.open: 4, 0 pulse.add: wave, 1, 800 pulse.add: wave, 0, 450 pulse.add: wave, 1, 800 pulse.dump: wave pulse.play: wave pulse.close: wave set wave to nil ``` One pulse output can be open at a time. `idle_level` and every segment level must be `0` or `1`. A requested duration is quantized to 100-nanosecond ticks; `pulse.add` returns the actual duration so code can observe the rounding. ### Pulse Words | Word | Result | Use | | --- | --- | --- | | [`pulse.open`](/reference/words/#pulse-open) | `Handle` | Open a pin with idle level `0` or `1` | | [`pulse.add`](/reference/words/#pulse-add) | `Int` | Append a span and return actual nanoseconds | | [`pulse.clear`](/reference/words/#pulse-clear) | `nil` | Remove all spans, keep output open | | [`pulse.count`](/reference/words/#pulse-count) | `Int` | Number of spans | | [`pulse.level`](/reference/words/#pulse-level) | `Int` | Level of one span | | [`pulse.duration-ns`](/reference/words/#pulse-duration-ns) | `Int` | Actual duration of one span | | [`pulse.dump`](/reference/words/#pulse-dump) | `nil` | Print the waveform | | [`pulse.play`](/reference/words/#pulse-play) | `nil` | Transmit once and wait for completion | | [`pulse.close`](/reference/words/#pulse-close) | `nil` | Return to idle and release the output | A waveform holds up to 256 spans and at most one second of total duration. Playback is one-shot and interruptible; use a loop for repetition. Use PWM for an ongoing periodic duty cycle and GPIO events for human-scale edges. Trace and pulse handles are volatile. Close them and replace any top-level handle binding before `save`. --- Source: https://frothy.dev/reference/modules/console/ # Console Input & Routing The console is the transport carrying prompt input, replies, asynchronous event output, and recovery. Console routing moves that whole conversation; it does not open an auxiliary application UART handle. ## Read Input As Data `console.read-line:` blocks a running form until one line arrives on the active console. It removes the line ending and returns the printable characters as volatile `Bytes` instead of interpreting them as source: ```frothy to echo-once [ here line is console.read-line: print: line print: "\n" ] ``` A blank line returns empty Bytes. Ctrl-C interrupts the read. Consume the value in the current evaluation or copy it into persistent Text: ```frothy answer is text.pack: console.read-line: ``` In the browser editor, run the form and submit data through **Input for console.read-line** below Output. The editor cannot detect which word a running form is waiting in, so use that field only while the form is blocked at `console.read-line:`. ## Inspect And Move The Console ```frothy console.info: console.uart: 17, 16, 115200 ``` After `console.uart` succeeds, the next reply and all later input/output use the selected route. Reconnect your terminal to those pins at that baud rate. The baud argument is an integer: `115200` and `$baud_115200` are the same value. | Word | Result | Use | | --- | --- | --- | | [`console.read-line`](/reference/words/#console-read-line) | `Bytes` | Read one printable console line as data | | [`console.info`](/reference/words/#console-info) | `nil` | Print the current host, USB, or UART route | | [`console.uart`](/reference/words/#console-uart) | `nil` | Move to explicit TX, RX, and baud | | [`console.default`](/reference/words/#console-default) | `nil` | Restore the board's boot/recovery console | `console.info` prints a line such as: ```text console uart tx=17 rx=16 baud=115200 ``` ## Return To The Default From the currently active route: ```frothy console.default: ``` The reply moves with the route change. The board's default console is the route used at boot and for recovery; its physical transport depends on the board. ## Console Versus Auxiliary UART - Use `console.read-line` for printable input from the human/host REPL route. - Use the routing words when a human or host tool should move with the REPL. - Use [`uart.*`](/reference/modules/uart/) for application bytes while the human console stays where it is. Do not assign the active console's port or pins to an auxiliary UART at the same time. A bad route can make a healthy device look silent, so verify the pins, voltage level, baud, and terminal connection before moving it. If the new route is unreachable, reboot into the board's safe/default recovery path and avoid persisting the bad routing call in `boot` until it has been tested. --- Source: https://frothy.dev/reference/modules/bluetooth/ # Bluetooth Low Energy Frothy can sit on either side of a small Bluetooth Low Energy conversation. It can find a nearby sensor and use one of its characteristics, or expose a small service that a phone or computer can use. The interface keeps the useful parts of BLE visible: scan reports, peer addresses, connection handles, UUIDs, ATT handles, queue pressure, and raw errors are all inspectable. The common path is still short. ## Availability This page applies to BLE-enabled `esp32_plain` firmware. The current hardware proof target is the classic `esp32_devkit_v1` with its original ESP32 chip. The XIAO ESP32-S3 and other targets have not been hardware-validated for this BLE tranche. This is Bluetooth Low Energy only. It does not provide Bluetooth Classic. The current profile supports all four BLE roles, with one live connection in total: - observer: scan for advertisements - broadcaster: advertise - central: connect to another peripheral - peripheral: accept a connection to Frothy's local GATT server If your firmware does not include `ble.on`, it was built without this profile feature. ## Start With The Radio ```frothy ble.on: ble.info: ``` `ble.on` initializes the compiled BLE roles and waits for the radio to become ready. `ble.info` prints the radio state, enabled roles, scan and advertising state, connection count, queue pressure, and the last platform or protocol reason. ```frothy ble.off: ``` `ble.off` closes links, invalidates BLE connection handles, clears scan, advertising, subscription, queue, and remote-client state, and releases the active stack memory. An installed local GATT table can be reused by a later `ble.on` until the project is cleared or restored. ## Find And Connect To A Peripheral Start an active scan: ```frothy ble.scan.start: 100, 50, 1, 0, -90 ``` The arguments are `(interval_ms, window_ms, active, repeats, minimum_rssi)`. Here Frothy scans for 50 ms in every 100 ms, requests scan responses, filters duplicate reports, and ignores signals below -90 dBm. Use `1` for `repeats` when every repeated advertisement matters. Move through the bounded report queue until a report is available: ```frothy ble.scan.next?: ble.scan.rssi: ble.scan.flags: ble.scan.data: ``` The current profile keeps eight reports with up to 31 advertisement bytes each. `ble.info` shows reports received, accepted, filtered, dequeued, or dropped. `ble.scan.peer` and `ble.scan.data` return transient Bytes. Pack the selected peer before stopping the scan, because stopping clears the current report: ```frothy peer is text.pack: (ble.scan.peer:) ble.scan.stop: link is ble.connect: (bytes.from-text: peer), 5000 ble.connection.info: link ``` The packed `peer` contains seven bytes: one address-type byte followed by the six canonical address bytes. Pass it back through `bytes.from-text` when connecting. `link` is a Handle owned by the runtime. Use it for connection and GATT words: ```frothy ble.connection.ready?: link ble.connection.rssi: link ble.connection.mtu: link, 128, 5000 ``` Connection tuning is available when a device needs it: ```frothy ble.connection.params: link, 15, 30, 0, 4000 ``` Those arguments are `(connection, minimum_interval_ms, maximum_interval_ms, latency, supervision_timeout_ms)`. Leave them alone unless the peripheral or power budget gives you a reason to change them. ## Use A Remote GATT Characteristic Find a characteristic by its service and characteristic UUIDs: ```frothy battery is ble.gatt.find: link, "180F", "2A19", 5000 ``` Frothy accepts four-hex-digit 16-bit UUIDs and canonical hyphenated 128-bit UUIDs. `ble.gatt.find` returns the remote characteristic's ATT handle. That integer belongs to this connection; find it again after reconnecting instead of saving and reusing it. Read and write short values: ```frothy ble.gatt.read: link, battery, 5000 ble.gatt.write: link, battery, (bytes.from-byte: 42), 1, 5000 ``` Reads return Bytes. The fourth write argument is `1` for a write with response or `0` for a write without response. Client values are currently limited to 20 bytes. Subscribe to notifications: ```frothy ble.gatt.subscribe: link, battery, $ble.gatt.notifications, 5000 ``` Poll the notification queue from ordinary Frothy code: ```frothy when ble.gatt.next-notification: [ ble.gatt.notification-attribute: ble.gatt.notification-data: ] ``` `ble.gatt.next-notification` returns the owning connection Handle, or `nil` when the queue is empty. The attribute and data words read the notification it just selected. Use `$ble.gatt.indications` instead when the remote characteristic supports indications. ```frothy ble.gatt.unsubscribe: link, battery, 5000 ``` The client keeps four discovered characteristics and four queued notifications. When the notification queue is full, the oldest entries stay available and a new arrival is dropped. `ble.gatt.info` shows the cache, subscriptions, queue high-water mark, dropped or stale arrivals, active procedure, and raw ATT and platform errors. Only one foreground GATT procedure runs at a time. Discovery, reads, writes with response, subscriptions, and unsubscriptions wait interruptibly for their bounded timeout. This keeps serial use predictable and avoids a second hidden BLE scheduler. ## Expose A Small GATT Server A local GATT table is ordinary Frothy data. Each row has one visible shape: ```frothy record GattRow [ kind, uuid, flags, size ] service is GattRow: $ble.gatt.service, "FFF0", $ble.gatt.primary, 0 value_flags is 0 set value_flags to $ble.gatt.read + $ble.gatt.write + $ble.gatt.notify value is GattRow: $ble.gatt.characteristic, "FFF1", value_flags, 20 gatt_rows is cells: 2 set gatt_rows[0] to service set gatt_rows[1] to value ``` Characteristic rows belong to the service row immediately before them. Their source row number is also the attribute ID used by Frothy's server words. In this example the characteristic is attribute `1`. Install the complete table while the radio is off, seed its value, then start advertising: ```frothy ble.gatt.install: gatt_rows ble.gatt.set: 1, "ready" ble.on: ble.advertise.start: "\x02\x01\x06", "\x07\x09Frothy", 100, 1 ``` Advertising and scan-response arguments are raw BLE AD payloads, each limited to 31 bytes. The final argument is `1` for connectable advertising and `0` for non-connectable advertising. ```frothy ble.advertise.stop: ``` Server rows use `$ble.gatt.service` or `$ble.gatt.characteristic` for `kind`. Services use `$ble.gatt.primary` or `$ble.gatt.secondary`. Add the supported characteristic properties as needed: `$ble.gatt.read`, `$ble.gatt.write`, `$ble.gatt.write-command`, `$ble.gatt.notify`, and `$ble.gatt.indicate`. When a phone or computer connects, accept the pending link: ```frothy link is ble.accept: ``` `ble.accept` returns a Handle, or `nil` when no connection is waiting. Remote writes enter a four-entry queue: ```frothy when ble.gatt.next-write: [ ble.gatt.write-attribute: ble.gatt.write-data: ] ``` Read or replace the local value directly: ```frothy ble.gatt.get: 1 ble.gatt.set: 1, "awake" ``` Send an update after the remote side subscribes: ```frothy ble.gatt.notify: link, 1, "awake" ble.gatt.indicate: link, 1, "awake", 5000 ``` Notifications return after enqueueing. Indications wait for the peer's acknowledgement or the timeout. `ble.gatt.info` reports table capacity, value bytes, subscriptions, write-queue pressure, and indication state. The current server budget is two services, six characteristics, 256 value bytes in total, four queued remote writes of up to 64 bytes, and two subscribed CCCD entries. ## Close And Save Cleanly Connection Handles and the radio are volatile. Close a link and remove the Handle from persistent project state before saving: ```frothy ble.connection.close: link set link to false save ``` Saving shuts down the BLE radio and drops live connections, subscriptions, queues, and discovered remote handles. Restoring or clearing the project also clears the installed platform GATT table. Put GATT installation and radio startup in `boot` when the device should resume BLE behavior after restore. ## Current Limits The restrained surface is deliberate: - one connection total - one GATT procedure at a time - short client values up to 20 bytes - no long or prepared GATT values - no arbitrary descriptor API - no pairing, bonding, encryption, or authenticated characteristics - no Bluetooth Classic - no user-visible multicore or parallel-procedure machinery These limits cover the common Arduino-style BLE peripheral and central paths without hiding BLE's useful diagnostics or committing Frothy to a large framework. ## Diagnose Before Guessing Use the built-in reports first: ```frothy ble.info: ble.gatt.info: ble.connection.info: link ``` They expose requested and actual timings, queue pressure, connection state, ATT errors, stack return codes, and disconnect reasons. That is the escape hatch when the friendly path is not enough. The [BLE entries in the word catalog](/reference/words/#ble-on) give the exact signature for every radio, scan, advertising, connection, GATT, and constant word.