Compiled Air 10 patches are linked into known erased CDX ranges. The build system measures every payload, allocates non-overlapping addresses, links final binaries at those addresses, and generates one layout per CDX version.
The payload build currently defines layouts for:
- SX567-0302
- SX567-0305
- SX567-0306
- SX567-0401
- SX567-0402
The Makefile PAYLOADS_<version> lists are the source of truth for which
payloads are built for each version. A payload may be built and assigned space
without being enabled in a particular output image.
patches/s10_code_caves.tsv lists available half-open ranges:
cdx_version region storage_start storage_end image_base runtime_base priority
0402 cdx 0x080FCF98 0x080FFFFE 0x08000000 0x08000000 0
The format supports multiple sorted ranges per version. Ranges must start on a
4-byte boundary and must not overlap. image_base and runtime_base describe
how stored payload bytes map into the execution address space. They are equal
for the current CDX regions, so storage and runtime addresses are identical.
priority is a non-negative integer; lower values are preferred. Keep rows
sorted by storage address, regardless of priority. Equal priorities are tried
in address order.
Reclaimed ranges must be freed by their preparation patch before payload injection. Disabling that patch does not change the build's layout; injection fails if a selected payload needs a range that is still occupied.
The payload layout is generated in two link passes:
- compile versioned source with
CDX_VER_<version> - probe-link each payload at
0x08000000 - convert each probe ELF to a binary and record its measured size
- allocate payloads by cave priority, then address
- align each allocation to 4 bytes
- write
build/payload_layout_<version>.tsv - link final ELFs at their assigned addresses
- convert final ELFs to the binaries consumed by the patchers
The allocator processes payloads in the order emitted by
payload_sizes_<version>.tsv. Each payload uses the preferred cave with enough
remaining space, advancing upward within that cave. Later payloads can still
use space left in a preferred cave after a larger payload fell back to another.
Allocation fails if no cave has enough remaining space.
Example generated layout:
payload runtime size runtime_end storage storage_end
mop_callback_dispatcher 0x080FCF98 ... ... 0x080FCF98 ...
vid_spoof ... ... ... ... ...
Inspect a layout with:
make binaries
cat build/payload_layout_0402.tsvThe Python patcher consumes the generated TSV layout. Before injection it verifies:
- the payload exists in the selected CDX layout
- the binary size matches the measured layout size
- the final ELF is linked at the assigned runtime address
- flattening the final ELF reproduces the supplied binary exactly
- the complete destination range contains erased
0xFFbytes - the storage range lies inside the selected firmware region
Patch code resolves entry points and ABI objects from the final ELF symbol table. Source code and patcher functions do not maintain duplicate payload addresses.
Compiled payloads use two versioned symbol families.
patches/s10_vars.h selects one generated header:
#if defined(CDX_VER_0402)
#include "s10_vars_0402.h"
#endifThe selected header defines VAR_ID_<UART_NAME> values for that CDX version.
The values come from the firmware globals[23] UART-name table. Payload source
uses these macros instead of embedding numeric IDs that can move between CDX
versions.
Reclaimed custom-setting assignments are not firmware-version constants. Their resolved IDs are written to payload ABI slots by the patcher.
patches/s10_<version>_stubs.S defines firmware function entry points and known
static RAM symbols for one CDX version. Payloads call those symbols through the
normal linker. New firmware entry points belong in the versioned stub file, not
in Makefile argument lists or generic C source.
An ABI slot carries data that is known only while patching the selected image. Current uses include:
- claimed custom-setting var IDs
- the original Square Wave handler pointer
- the generated custom menu registry address
- the custom settings globals[16] group index
An erased var-ID slot uses 0xFFFF. Payloads must define an explicit fallback
for that state so they can run without custom_settings. Pointer slots use
0xFFFFFFFF when no target has been installed.
Firmware-stable var IDs belong in s10_vars. Firmware-stable function entry
points belong in versioned stubs. Neither should be passed through a new ABI
slot.
variable_callback_register(payload, symbol) registers a function from an
injected payload and returns a callback ID for variable descriptors. Repeated
registration of the same function returns the same ID. New IDs follow the stock
entries and must fit the descriptor's one-byte callback field; ID zero means
no callback.
variable_callback_add(var, payload, symbol) appends a handler after a g[4] or
g[8] variable's existing callback. With callback ID zero, it registers and
assigns a new callback. Otherwise it preserves the ID and chains the existing
function followed by appended handlers in registration order. Registrations
are deduplicated by callback ID and function pointer, so variables sharing an
ID also share the appended handlers.
Finalization copies the current stock table into CCX, appends registered functions, installs requested callback chains, and redirects the firmware lookup to the resulting table. This runs after the MOP dispatcher is installed, preserving its replacement pointer. With no registrations, the stock table and lookup remain unchanged.
Chains use the standalone variable_callback_dispatcher payload. Each chained
entry points to a local adapter with its own immutable, zero-terminated handler
list. The dispatcher keeps its cursor in a callee-saved register,
allowing callbacks to invoke other chains without overwriting shared state.
Callbacks take no arguments and run under the variable-system recursive lock. They also run when firmware initializes variables at startup, not only after a setting is edited.
MOP-dependent payloads share one standalone dispatcher instead of replacing the same firmware callback independently.
The patcher resolves MOP.callback_id, preserves the original callback pointer,
and replaces that callback-table entry with the dispatcher. Its handler table
contains:
- the original stock callback
- registered feature handlers
- a
0xFFFFFFFFsentinel
The stock callback therefore updates normal mode state before custom handlers run. Current registered users are VID spoof and custom menu visibility.
button_hook_add(event, payload, symbol, hold_ms=None, consume=False, on_release=True) registers
a handler from an already injected payload. Finalization emits the registry and
installs the standalone button_hooks payload only when handlers are registered.
| Event | Trigger |
|---|---|
start_stop, home, encoder |
Stock button event |
start_stop_hold |
Stock long Start/Stop event |
clinical_menu |
Stock Home+encoder combination; observation only |
aux_8, aux_9 |
Stock events 8 and 9 on front-panel profiles providing them |
home_hold, encoder_hold |
Debounced standalone hold; on release or at the configured threshold |
hold_ms applies to home_hold and encoder_hold, defaulting to 3000 ms.
With on_release=False, the handler runs once at the first GUI poll after the
threshold is reached, without waiting for release. Pressing another button
cancels pending hold actions until all buttons are released; an action that
already ran is not undone.
Input reset discards pending gestures and ignores buttons held across the reset.
Rotary movement and UART-injected key events retain their stock paths.
Handlers use the signature declared in patches/button_hooks.h:
int handler(unsigned event, unsigned held_ms);They run in registration order in the GUI input task, with interrupts restored,
and should return promptly. held_ms contains the measured duration for the two
holds and zero for stock events. A nonzero return reports that
the action was handled. With consume=True, this suppresses the stock event or
the hold's accompanying click; other registered observers still run. A zero
return preserves stock behavior. Each event permits one consuming handler.
Home+encoder always remains available to firmware.
For example, a feature can register an action that fires after three seconds with:
self.button_hook_add('encoder_hold', payload, 'on_encoder_hold',
hold_ms=3000, consume=True, on_release=False)The controller retains stock sampling, debounce and event storage. Additional hold detection runs alongside its periodic update; feature handlers are called only by the GUI reader, not calibration readers. Like stock input, pending events are a mailbox, not a queue of every press between GUI polls.
| Family | Build behavior |
|---|---|
| common-code dependent | imports symbols from the final common_code ELF during final linking |
| standalone | links source, optional <payload>_abi.S, and versioned stubs through the generic rule |
| dispatcher | owns a shared firmware callback and calls a generated handler list |
The generated layout is shared by all families.
- add the payload name to each supported
PAYLOADS_<version>list - provide source with a
startentry point - include
s10_vars.hfor firmware variable IDs - add required firmware functions to each supported versioned stub
- add an ABI assembly file only for patch-time values
- add a generic build rule or use the standalone rule
- inject by payload name through the patcher layout helper
- resolve hook and ABI symbols from the final ELF
- verify generated layouts and all intended output images
Do not add a fixed payload address to source, patcher code, or a second Makefile
variable. Code-cave ownership belongs to s10_code_caves.tsv and the generated
layout.
See Custom settings for menu and persistence integration.