From c8da5f99cc758af9cc06d6597b21666457f8ca3b Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 31 Aug 2026 20:51:09 +0200 Subject: [PATCH] Version overlay app ABI and capabilities --- App/app/foxhunt.c | 4 ++-- App/apps/app_api.h | 42 +++++++++++++++++++++++--------------- App/apps/app_menu.c | 30 ++++++++++++++++----------- App/apps/app_overlay.c | 26 ++++++++++++++++------- App/apps/app_overlay.h | 13 +++++++++--- App/apps/beacon/build.sh | 3 ++- App/apps/beam/build.sh | 3 ++- App/apps/breakout/build.sh | 3 ++- App/apps/cube3d/build.sh | 3 ++- App/apps/fm/build.sh | 5 +++-- App/apps/foxhunt/build.sh | 3 ++- App/apps/pack_app.py | 32 ++++++++++++++++++++++------- App/apps/plasma/build.sh | 3 ++- App/apps/trivfo/build.sh | 3 ++- App/settings.c | 13 +++++++----- App/settings.h | 4 ++++ App/ui/welcome.c | 4 ++-- 17 files changed, 130 insertions(+), 64 deletions(-) diff --git a/App/app/foxhunt.c b/App/app/foxhunt.c index 17dfe0c7..2682d865 100644 --- a/App/app/foxhunt.c +++ b/App/app/foxhunt.c @@ -26,6 +26,7 @@ #include "k5viewer.h" #endif +#include "settings.h" #include "ui/status.h" // Signal window mapped onto the RSSI bar, in dBm. @@ -104,7 +105,6 @@ #define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s) #define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s) #define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s) -#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash #define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon // Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5 @@ -1366,7 +1366,7 @@ static void FOXHUNT_LoadCallsign(void) // Read the callsign from the boot message (line 1), sanitised to what Morse can // send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE). - PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX); + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, call, FOXHUNT_CALLSIGN_MAX); call[FOXHUNT_CALLSIGN_MAX] = '\0'; for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) { char c = call[i]; diff --git a/App/apps/app_api.h b/App/apps/app_api.h index 94827c13..96355154 100644 --- a/App/apps/app_api.h +++ b/App/apps/app_api.h @@ -25,9 +25,11 @@ * * void app_main(const app_api_t *api); // entry, at blob offset 0 * - * Both the firmware loader and the app include THIS header, so the struct layout - * can never disagree. Bump APP_ABI_VERSION on any incompatible change; the loader - * refuses a blob whose header abi_version does not match. + * Both the firmware loader and the app include THIS header. Within one ABI + * major, app_api_t is append-only: existing fields may never move, disappear or + * change signature. Append a service and bump APP_API_LEVEL; only apps using + * that service need the new level. A breaking layout change bumps + * APP_ABI_MAJOR and resets APP_API_LEVEL to 1. */ #ifndef APPS_APP_API_H @@ -36,11 +38,10 @@ #include #include -/* The table below is a single versioned layout. Any change to app_api_t - a - * reorder, a removal, or an append - MUST bump this. Keep in sync with the - * value read by pack_app.py, which - * stamps the blob the loader checks against. */ -#define APP_ABI_VERSION 8u +/* First unpublished/public baseline: all services currently present below are + * ABI major 1, API level 1. */ +#define APP_ABI_MAJOR 1u +#define APP_API_LEVEL 1u /* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by * value so the app stays independent of the firmware headers. */ @@ -143,7 +144,7 @@ typedef struct { uint8_t band; uint8_t scanlist; uint8_t compander; - char name[16]; + char name[16]; /* fixed-width wire field; NUL termination is not required */ } app_beam_channel_t; _Static_assert(sizeof(app_beam_channel_t) == 44u, @@ -155,8 +156,19 @@ enum { APP_BEAM_RX_ERROR = 2, }; +/* CRITICAL PERSISTENCE RULE + * + * The app executes from the RAM buffer also used as PY25Q16's sector cache. + * Therefore no API callback may erase or write external flash while app_main() + * is running: a read-modify-write would overwrite the executing app. Services + * such as cfg_save, fm_commit and beam_save must only stage resident RAM state; + * APP_LaunchOverlay commits it after app_main() returns. */ + typedef struct app_api { - uint8_t abi_version; /* == APP_ABI_VERSION */ + /* Fixed four-byte prefix; services remain naturally pointer-aligned. */ + uint8_t abi_major; /* == APP_ABI_MAJOR */ + uint8_t api_level; /* == APP_API_LEVEL */ + uint16_t api_size; /* sizeof(app_api_t), for optional probing */ app_fb_t fb; /* -> gFrameBuffer */ @@ -182,10 +194,6 @@ typedef struct app_api { void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */ void (*led)(bool on); /* green GPIO indicator */ - /* ==== appended when the ABI moved 1 -> 2; now an integral part of ABI 2. - * A firmware and an app that agree on abi_version agree on this whole layout, - * so these must never be reached through a table that does not include them. == */ - /* ---- extra text drawing ---- */ void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */ void (*print_inverse)(const char *s, uint8_t x, uint8_t line, @@ -240,14 +248,14 @@ typedef struct app_api { void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */ void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */ - /* ---- navigation (ABI 3) ---- + /* ---- navigation (API level 1 baseline) ---- * Convert a raw APP_KEY_UP/DOWN into a semantic value direction: * UV-K5 UP/DOWN -> +1/-1 * UV-K1 LEFT/RIGHT -> -1/+1 * Returns 0 for any other key. Keep get_key() raw for spatial controls. */ int8_t (*nav_dir)(uint8_t key); - /* ---- triple VFO (ABI 4) ---- + /* ---- triple VFO (API level 1 baseline) ---- * A and B are the live Main Display VFOs. C is a resident temporary VFO * loaded from c_channel (or the first valid memory after B when invalid). * tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */ @@ -259,7 +267,7 @@ typedef struct app_api { uint8_t (*trivfo_tick)(void); uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */ - /* ---- BEAM channel transfer (ABI 8) ---- */ + /* ---- BEAM channel transfer (API level 1 baseline) ---- */ void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */ void (*beam_leave)(void); /* defensively stop FSK before app return */ void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */ diff --git a/App/apps/app_menu.c b/App/apps/app_menu.c index f1b14fa7..24f86eed 100644 --- a/App/apps/app_menu.c +++ b/App/apps/app_menu.c @@ -170,35 +170,41 @@ static void app_format_size(char out[6], uint32_t bytes) out[5] = '\0'; } -/* Human-readable reason for an APP_LaunchOverlay / APP_ValidateSlot failure. */ -static const char *app_err_text(uint8_t rc) +/* Human-readable action for an APP_LaunchOverlay / APP_ValidateSlot failure. */ +static const char *app_err_text(const app_header_t *header, uint8_t rc) { switch (rc) { case APP_ERR_SLOT: return "BAD SLOT"; case APP_ERR_MAGIC: return "NO APP"; - case APP_ERR_ABI: return "ABI MISMATCH"; - case APP_ERR_NOT_COMMITTED: return "INCOMPLETE"; - case APP_ERR_SIZE: return "BAD SIZE"; - case APP_ERR_CRC: return "CRC ERROR"; - case APP_ERR_VMA: return "VMA MISMATCH"; + case APP_ERR_ABI: + /* api_min == 0 also identifies pre-reset development blobs whose + * former uint16_t ABI value occupies these two bytes. */ + if (header->api_min == 0u || header->abi_major < APP_ABI_MAJOR) + return "UPDATE APP"; + return "UPDATE FIRMWARE"; + case APP_ERR_NOT_COMMITTED: return "REINSTALL APP"; + case APP_ERR_SIZE: return "UPDATE APP"; + case APP_ERR_CRC: return "REINSTALL APP"; + case APP_ERR_VMA: return "UPDATE APP"; case APP_ERR_AUTH: return "AUTH"; + case APP_ERR_CAP: return "NOT SUPPORTED"; default: return "ERROR"; } } -/* A launch failed: name the app and the reason, then wait for a key. Without this +/* A launch failed: name the app and the action to take, then wait for a key. Without this * an incompatible app would silently "do nothing" when selected. */ -static void app_show_error(const char *name, uint8_t rc) +static void app_show_error(const app_header_t *header, uint8_t rc) { char nm[19]; - app_copy(nm, sizeof(nm), name, APP_NAME_LEN); + app_copy(nm, sizeof(nm), header->name, APP_NAME_LEN); UI_DisplayClear(); UI_StatusClear(); GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82); UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */ - UI_PrintStringSmallNormal(app_err_text(rc), 2, 0, 4); /* why */ + UI_PrintStringSmallNormal(app_err_text(header, rc), 2, 0, 4); /* action */ UI_PrintStringSmallNormal("Press any key", 2, 0, 6); ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); @@ -316,7 +322,7 @@ void APP_MenuOpen(void) const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */ if (rc != APP_OK) - app_show_error(hdr[sel].name, rc); /* no longer silent */ + app_show_error(&hdr[sel], rc); /* no longer silent */ app_wait_release(); break; } diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c index c5cc843a..1db22709 100644 --- a/App/apps/app_overlay.c +++ b/App/apps/app_overlay.c @@ -51,6 +51,13 @@ #include "misc.h" /* dBmCorrTable */ _Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes"); +_Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow"); + +#ifdef ENABLE_FMRADIO + #define APP_AVAILABLE_CAPS APP_CAP_FM +#else + #define APP_AVAILABLE_CAPS 0u +#endif /* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */ static bool app_allow_screen_saver; @@ -129,7 +136,7 @@ static void app_play_tone(uint16_t tone, uint16_t ms) AUDIO_AudioPathOff(); } -/* ---- ABI 4: resident triple-VFO engine --------------------------------- +/* ---- API level 1: resident triple-VFO engine ---------------------------- * The overlay owns the UI and key timing, while this resident engine owns all * radio details. Keeping VFO_Info_t and BK4819 sequencing on this side makes * the app independent of feature-dependent firmware layouts. */ @@ -525,7 +532,7 @@ static uint8_t app_trivfo_ptt(bool pressed) return 0; } -/* ---- ABI 8: BEAM radio/channel bridge ------------------------------------ +/* ---- API level 1: BEAM radio/channel bridge ------------------------------- * The modal app owns the packet format, CRC, UI and state machine. Resident * code only translates the stable ABI channel structure and performs the FSK * operations which depend on VFO_Info_t and the BK4819 driver. */ @@ -719,7 +726,7 @@ static void app_beam_draw(const char *status) UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line); } -/* ---- v2 radio wrappers ---- */ +/* ---- radio wrappers ---- */ static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; } static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); } static void app_bk_write(uint8_t r, uint16_t v) { BK4819_WriteRegister((BK4819_REGISTER_t)r, v); } @@ -787,7 +794,7 @@ static uint32_t app_tx_freq(void) { return gTxVfo->pTX->Frequency; } static void app_boot_callsign(char *buf, uint8_t len) { char raw[12]; uint8_t n = 0; - PY25Q16_ReadBuffer(0x00A0C8u, raw, sizeof(raw)); /* boot message line 1 */ + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, raw, sizeof(raw)); for (uint8_t i = 0; i < sizeof(raw) && (uint8_t)(n + 1) < len; i++) { char c = raw[i]; if (c == '\0' || (uint8_t)c == 0xFFu) break; @@ -849,8 +856,10 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header) if (h.magic != APP_MAGIC) return APP_ERR_MAGIC; if (h.hdr_version != APP_HDR_VERSION) return APP_ERR_MAGIC; - if (h.abi_version != APP_ABI_VERSION) return APP_ERR_ABI; + if (h.abi_major != APP_ABI_MAJOR || h.api_min == 0u || + h.api_min > APP_API_LEVEL) return APP_ERR_ABI; if (!(h.flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED; + if (h.required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP; if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX || (uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */ (h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */ @@ -932,9 +941,12 @@ uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header) /* All services are immutable. Keeping the table in flash avoids rebuilding a * roughly quarter-kilobyte automatic object on every launch and removes that - * object from the launcher's stack frame. */ + * object from the launcher's stack frame. Callbacks must also obey the ABI's + * no-external-flash-write rule while entry() is running. */ static const app_api_t app_api = { - .abi_version = APP_ABI_VERSION, + .abi_major = APP_ABI_MAJOR, + .api_level = APP_API_LEVEL, + .api_size = sizeof(app_api_t), .fb = gFrameBuffer, .display_clear = UI_DisplayClear, .status_clear = UI_StatusClear, diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h index c5c7a73c..8c07e703 100644 --- a/App/apps/app_overlay.h +++ b/App/apps/app_overlay.h @@ -78,10 +78,15 @@ #define APP_SHORTCUT_BEACON 0x04u #define APP_SHORTCUT_BEAM 0x08u +/* Optional resident facilities an app may require. Requirements live in the + * previously reserved header bytes, so app_header_t remains 64 bytes. */ +#define APP_CAP_FM 0x00000001u + typedef struct __attribute__((packed)) { uint32_t magic; /* APP_MAGIC */ uint16_t hdr_version; /* APP_HDR_VERSION */ - uint16_t abi_version; /* ABI the app was built against */ + uint8_t abi_major; /* required ABI family */ + uint8_t api_min; /* minimum append-only API level */ uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */ uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */ uint16_t entry_off; /* entry offset within the code (0) */ @@ -89,19 +94,21 @@ typedef struct __attribute__((packed)) { char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */ char version[APP_VERSION_LEN];/* app version string */ uint32_t link_vma; /* RAM VMA the code was linked at */ - uint8_t reserved[8]; /* pad to 64 bytes */ + uint32_t required_caps; /* APP_CAP_* required by this app */ + uint8_t reserved[4]; /* pad to 64 bytes */ } app_header_t; enum { APP_OK = 0, APP_ERR_SLOT, /* slot index out of range */ APP_ERR_MAGIC, /* no/invalid header */ - APP_ERR_ABI, /* ABI version mismatch */ + APP_ERR_ABI, /* ABI family/API level mismatch */ APP_ERR_NOT_COMMITTED, /* image not marked complete */ APP_ERR_SIZE, /* code_size out of range */ APP_ERR_CRC, /* code CRC-32 mismatch */ APP_ERR_VMA, /* overlay buffer not at the link VMA */ APP_ERR_AUTH, /* host write refused: timestamp mismatch */ + APP_ERR_CAP, /* required firmware capability missing */ }; /* Read + validate a slot header (no CRC of the code). */ diff --git a/App/apps/beacon/build.sh b/App/apps/beacon/build.sh index 905866b6..bcbf1b2d 100755 --- a/App/apps/beacon/build.sh +++ b/App/apps/beacon/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Beacon" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ --shortcut beacon >/dev/null trap - ERR diff --git a/App/apps/beam/build.sh b/App/apps/beam/build.sh index ad2fcd6e..9aff338d 100755 --- a/App/apps/beam/build.sh +++ b/App/apps/beam/build.sh @@ -4,6 +4,7 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="BEAM" APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -24,7 +25,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ --shortcut beam >/dev/null trap - ERR diff --git a/App/apps/breakout/build.sh b/App/apps/breakout/build.sh index 34efcbd4..a17a7159 100755 --- a/App/apps/breakout/build.sh +++ b/App/apps/breakout/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Breakout" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" >/dev/null + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/cube3d/build.sh b/App/apps/cube3d/build.sh index 3e17025a..cadc1df6 100755 --- a/App/apps/cube3d/build.sh +++ b/App/apps/cube3d/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Cube3D" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/fm/build.sh b/App/apps/fm/build.sh index 2b452893..5500c65e 100755 --- a/App/apps/fm/build.sh +++ b/App/apps/fm/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Broadcast FM" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,8 +30,8 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ - --shortcut fm --screensaver >/dev/null + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ + --shortcut fm --require fm --screensaver >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/foxhunt/build.sh b/App/apps/foxhunt/build.sh index fa4a86e8..2d77d1f0 100755 --- a/App/apps/foxhunt/build.sh +++ b/App/apps/foxhunt/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="FoxHunt" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ --shortcut foxhunt >/dev/null trap - ERR diff --git a/App/apps/pack_app.py b/App/apps/pack_app.py index 1f0c26cc..8d6a7def 100644 --- a/App/apps/pack_app.py +++ b/App/apps/pack_app.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # Pack an overlay-app raw binary into a .app blob: 64-byte header + code. # Header layout mirrors app_overlay.h : app_header_t (little-endian, packed). -# The format/version constants (magic, header + ABI version, overlay budget) are +# The format/version constants (magic, header + ABI/API levels, overlay budget) are # read straight from the C headers the firmware itself compiles, so there is a # SINGLE source of truth - the packer can never silently drift from the loader. # CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match # the firmware's mb_ext_image_crc32 / mb_crc32_bytes. # -# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout --ver 1.0 +# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \ +# --ver 1.0 --vma 0x20000280 --api-min 1 import argparse, os, re, struct, zlib, sys HERE = os.path.dirname(os.path.abspath(__file__)) @@ -28,7 +29,8 @@ def cdefine(header: str, name: str) -> int: # Single source of truth: the C headers the firmware also compiles. MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1" HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION") -ABI_VERSION = cdefine("app_api.h", "APP_ABI_VERSION") +ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR") +API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL") OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED") FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER") @@ -40,6 +42,9 @@ SHORTCUTS = { "beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"), "beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"), } +CAPABILITIES = { + "fm": cdefine("app_overlay.h", "APP_CAP_FM"), +} def field(s: str, n: int) -> bytes: b = s.encode("ascii", "strict")[: n - 1] @@ -52,14 +57,21 @@ def main(): ap.add_argument("--name", default="app") ap.add_argument("--ver", default="1.0") ap.add_argument("--entry", type=lambda x: int(x, 0), default=0) + ap.add_argument("--api-min", type=int, required=True, + help="minimum append-only firmware API level required by this app") ap.add_argument("--vma", type=lambda x: int(x, 0), required=True, help="RAM VMA the app was linked at (must match the firmware overlay)") ap.add_argument("--screensaver", action="store_true", help="allow the resident BLTime screen saver while this app is idle") ap.add_argument("--shortcut", choices=SHORTCUTS, default="none", help="resident quick action advertised by this app") + ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[], + help="resident capability required by this app (repeatable)") a = ap.parse_args() + if not 1 <= a.api_min <= API_LEVEL: + sys.exit(f"--api-min must be between 1 and current API level {API_LEVEL}") + code = open(a.infile, "rb").read() if len(code) == 0: sys.exit("empty input") @@ -70,11 +82,15 @@ def main(): flags = (FLAG_COMMITTED | (FLAG_SCREEN_SAVER if a.screensaver else 0) | (SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT)) + required_caps = 0 + for capability in a.require: + required_caps |= CAPABILITIES[capability] header = struct.pack( - "<4sHHIIHH16s16sI8s", - MAGIC, HDR_VERSION, ABI_VERSION, + "<4sHBBIIHH16s16sII4s", + MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min, len(code), crc, a.entry, flags, - field(a.name, 16), field(a.ver, 16), a.vma, b"\x00" * 8, + field(a.name, 16), field(a.ver, 16), a.vma, + required_caps, b"\x00" * 4, ) assert len(header) == 64, len(header) @@ -82,7 +98,9 @@ def main(): f.write(header) f.write(code) - print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} vma=0x{a.vma:08x} " + print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} " + f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} " + f"vma=0x{a.vma:08x} " f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B") if __name__ == "__main__": diff --git a/App/apps/plasma/build.sh b/App/apps/plasma/build.sh index 7066c42e..69959c3d 100755 --- a/App/apps/plasma/build.sh +++ b/App/apps/plasma/build.sh @@ -9,6 +9,7 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Plasma" # <-- the only per-app line APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -29,7 +30,7 @@ trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/trivfo/build.sh b/App/apps/trivfo/build.sh index 5e78a6e5..b8bafb88 100755 --- a/App/apps/trivfo/build.sh +++ b/App/apps/trivfo/build.sh @@ -3,6 +3,7 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Triple VFO" APP_VER="1.0" +APP_API_MIN=1 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" CC=/opt/toolchain/bin/arm-none-eabi-gcc @@ -13,7 +14,7 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} -Wl,- rm -f ./*.app ./*.elf ./*.bin "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" --screensaver >/dev/null +python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null BYTES=$(wc -c < "${APP}.bin") test "$BYTES" -le 4096 printf ' ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app\n' \ diff --git a/App/settings.c b/App/settings.c index f7836c87..56a051f0 100644 --- a/App/settings.c +++ b/App/settings.c @@ -90,8 +90,10 @@ void SETTINGS_InitEEPROM(void) // 4. Reset logo lines (clear to null for strlen() == 0) - char logoLines[32]; - PY25Q16_ReadBuffer(0x00A0C8, logoLines, sizeof(logoLines)); + /* The two boot-message lines are contiguous in external flash. */ + char bootMessageLines[32]; + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, + bootMessageLines, sizeof(bootMessageLines)); bool needsWrite = false; @@ -99,12 +101,12 @@ void SETTINGS_InitEEPROM(void) int offset = line * 16; for (int i = 0; i < 16; i++) { - char c = logoLines[offset + i]; + char c = bootMessageLines[offset + i]; if (c == 0) { break; } if (c < 0x20 || c > 0x7E) { - memset(logoLines + offset, 0, 16); + memset(bootMessageLines + offset, 0, 16); needsWrite = true; break; } @@ -112,7 +114,8 @@ void SETTINGS_InitEEPROM(void) } if (needsWrite) { - PY25Q16_WriteBuffer(0x00A0C8, logoLines, sizeof(logoLines), false); + PY25Q16_WriteBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, + bootMessageLines, sizeof(bootMessageLines), false); } // 5. Reset dBmCorrTable diff --git a/App/settings.h b/App/settings.h index 4673a56c..3aa2d734 100644 --- a/App/settings.h +++ b/App/settings.h @@ -25,6 +25,10 @@ #include "radio.h" #include +/* Shared PY25Q16 locations for the two configurable boot-message lines. */ +#define SETTINGS_BOOT_MESSAGE_LINE1_ADDR 0x00A0C8u +#define SETTINGS_BOOT_MESSAGE_LINE2_ADDR 0x00A0D8u + enum POWER_OnDisplayMode_t { #ifdef ENABLE_FEAT_F4HWN POWER_ON_DISPLAY_MODE_ALL, diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 37e288a8..a41f45e4 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -261,10 +261,10 @@ void UI_DisplayWelcome(void) char WelcomeString3[32]; // 0x0EB0 - PY25Q16_ReadBuffer(0x00A0C8, WelcomeString0, 16); + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, WelcomeString0, 16); WelcomeString0[16] = '\0'; // 0x0EC0 - PY25Q16_ReadBuffer(0x00A0D8, WelcomeString1, 16); + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE2_ADDR, WelcomeString1, 16); WelcomeString1[16] = '\0'; sprintf(WelcomeString2, "%u.%02uV %u%%",