Level B

Level B is where FALL turns a target description into a running application. This page teaches only the current application API, Makefile commands, Linux and ESP32 builds, sockets, socket chains, filesystem handles, and filesystem chains.

b::application

Application code starts at fall_main(). The function receives b::application&, which is the target-specific application object for the selected build.

#include "b.hpp"

[[noreturn]] void fall_main(b::application& app) noexcept {
    app.debug().write("boot\n");

    b::run_forever([&app]() noexcept {
        app.time().delay_for(b::seconds(1));
        app.debug().write("tick\n");
    });
}

The application does not call _start(), configure UART registers, or choose a Linux syscall number. src/start.cpp provides the platform entry point, constructs b::application, and then calls fall_main().

Today b::application exposes two services:

Makefile Commands

The root Makefile is the normal entry point for this repository.

make build                 Linux + RP2350 + optional ESP32 targets
make all                   same as make build
make strict-all            require Linux, RP2350, ESP32, and ESP32 display
make firmware              firmware targets only
make linux                 Linux applications, tests, and host tools
make ttir                  host font asset compiler only
make noto                  generate assets/noto.hpp
make noto-regular          generate assets/noto-regular.hpp
make rp2350                build Pico 2 / RP2350 firmware
make esp32                 build generic ESP32 firmware
make esp32-display         build the T-Display firmware
make run                   run the Linux smoke sequence
make smoke                 run Linux smoke sequence and clangd checks
make clangd-check          check source files with clangd
make size                  print artifact sizes
make tools                 show detected tools
make clean                 clean configured build trees
make distclean             remove build trees
make distclean-tools       remove FALL-managed ESP32 tools

Useful variables:

make linux BUILD_TYPE=RelBareSafe
make rp2350 APP=fall_hello
make esp32 ESP32_AUTO_INSTALL=0
make flash-esp32 PORT=/dev/ttyUSB1

Build For Linux

Linux x86-64 is the host target with working debug, time, sockets, and filesystem backends.

make linux
make run

Direct CMake form:

cmake -S . -B build-relbare -G Ninja \
    -DCMAKE_BUILD_TYPE=RelBare \
    -DFALL_EXAMPLE_TARGET=linux_x86_64 \
    -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

cmake --build build-relbare

make run runs the current Linux smoke sequence. fall_hello is an endless embedded-style application, so the Makefile runs it with a timeout.

Build For Embedded ESP32

The supported embedded build path is classic ESP32 / Xtensa LX6. The generic target id is esp32_xtensa; the T-Display board target id is esp32_t_display.

make esp32
make flash-esp32 PORT=/dev/ttyUSB0

Build and flash the Tenstar/TTGO T-Display example:

make esp32-display
make flash-esp32-display PORT=/dev/ttyUSB0

By default ESP32_AUTO_INSTALL=1, so the Makefile may prepare local ESP32 tools under .fall-tools/esp32. Disable that when the Xtensa compiler and esptool.py are already managed by your environment.

make esp32 ESP32_AUTO_INSTALL=0

RP2350 has a firmware build target in this repository, but it is currently a bring-up scaffold. This documentation only teaches Linux and embedded ESP32 as supported workflows.

Sockets

b::net::socket is a move-only RAII capability. When the object is destroyed, an open native socket is closed. Errors are reported as b::net::error.

The current public socket API supports IPv4 TCP and UDP on Linux. Embedded targets currently expose an unavailable network provider.

b::net::socket server{};

b::net::error status = server
    .open(b::net::tcp)
    .bind(b::net::loopback_ipv4(b::net::service(43191)))
    .listen(b::net::backlog(1));

if (b::net::failed(status)) {
    return;
}

Common operations:

Socket Chains

socket_chain lets several socket operations read like one operation. Each step runs only while the previous status is success. The final chain value converts to b::net::error.

b::net::socket client{};

b::net::error status = client
    .open(b::net::tcp)
    .connect(b::net::loopback_ipv4(b::net::service(43191)))
    .send_all("hello");

A chain preserves the first failure. If open() fails, connect() and send_all() are not executed.

For fixed-capacity resumable socket work, use socket.flow<Capacity>(). The flow stores its operation list inside the flow object and can report that it is waiting for readable or writable readiness.

b::net::socket client{};
a::u8 response[8]{};

auto flow = client.flow<4>();
flow.open(b::net::tcp, b::net::blocking_mode::nonblocking)
    .connect(b::net::loopback_ipv4(b::net::service(43191)))
    .send_all("fall-net")
    .recv_exact(a::view<a::u8>{response});

b::net::operation_poll poll = flow.poll();

if (poll.state == b::net::operation_state::pending) {
    auto events = b::net::events(poll.wait_for);
    poll = flow.resume({events.readable, events.writable});
}

The flow state machine exists, but FALL does not yet provide a portable poller or reactor.

Filesystem

b::fs::file and b::fs::dir are move-only RAII handles. Errors are reported as b::fs::error. The current working backend is Linux.

Directory-relative operations use b::fs::entry_name. entry_name rejects empty names, ., .., names containing /, and names with embedded zeroes.

b::fs::dir root{};
b::fs::dir tmp{};

b::fs::error status = root
    .open("/")
    .open_dir(tmp, "tmp")
    .close();

if (b::fs::failed(status)) {
    return;
}

After this chain, root is closed by the cleanup step and tmp remains open as the child directory capability.

Filesystem Chains

file_chain and dir_chain use the same sticky status idea as socket chains. They skip later normal steps after a failure, but close() is cleanup and still runs.

static constexpr const char name[] = "settings.bin";
static constexpr const char payload[] = "fall-fs";

b::fs::file file{};

status = tmp
    .open_file(file, name, b::fs::open_mode::create_or_replace)
    .write_all(payload)
    .commit(b::fs::durability::data_and_metadata)
    .close();

Read the file back:

a::u8 buffer[32]{};
a::usize count = 0;

status = tmp
    .open_file(file, name, b::fs::open_mode::read_only)
    .read_to_end(a::view<a::u8>{buffer}, &count)
    .close();

Remove the file and commit the containing directory:

status = tmp.remove(name);
if (b::fs::ok(status)) {
    status = tmp.commit();
}

Durability levels describe intent: