mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add external-flash overlay app platform
This commit is contained in:
1 parent
3f958f637d
commit
f35408317e
35 files changed
+3253
-293
No files matched your search
@@ -14,11 +14,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Compile firmware
|
- name: Compile firmware
|
||||||
run: |
|
run: |
|
||||||
chmod +x compile-with-docker.sh
|
chmod +x compile-firmware.sh compile-app.sh
|
||||||
./compile-with-docker.sh
|
./compile-firmware.sh All
|
||||||
|
./compile-app.sh All
|
||||||
|
|
||||||
- name: Upload firmware artifact
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: firmware-artifact
|
name: firmware-artifact
|
||||||
path: compiled-firmware/f4hwn.packed.bin
|
path: |
|
||||||
|
build/*/*.bin
|
||||||
|
build/Apps/*.app
|
||||||
@@ -17,3 +17,11 @@ __pycache__/
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|
||||||
|
# Overlay-app build artifacts (generated by build.sh / compile-app.sh)
|
||||||
|
App/apps/*/*.elf
|
||||||
|
App/apps/*/*.bin
|
||||||
|
App/apps/*/*.map
|
||||||
|
App/apps/*/*.app
|
||||||
|
App/apps/*/*_app_blob.h
|
||||||
|
build/apps/
|
||||||
+22
-3
@@ -117,11 +117,18 @@ endif()
|
|||||||
|
|
||||||
# ---- STOCK QUANSHENG FEATURES ----
|
# ---- STOCK QUANSHENG FEATURES ----
|
||||||
|
|
||||||
|
# ENABLE_FMRADIO compiles the BK1080 driver and the FM logic/helpers (fm.c).
|
||||||
|
# The resident FM *screen* (ui/fmradio.c) is redundant with the FM overlay app,
|
||||||
|
# so it is dropped when OVERLAY_APPS is on - the overlay app reaches the driver
|
||||||
|
# and the fm.c helpers through the ABI instead, and ACTION_FM's launcher body is
|
||||||
|
# neutralised in action.c so the resident screen can never open.
|
||||||
enable_feature(ENABLE_FMRADIO
|
enable_feature(ENABLE_FMRADIO
|
||||||
driver/bk1080.c
|
driver/bk1080.c
|
||||||
app/fm.c
|
app/fm.c
|
||||||
ui/fmradio.c
|
|
||||||
)
|
)
|
||||||
|
if(ENABLE_FMRADIO AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
|
target_sources(App INTERFACE ui/fmradio.c)
|
||||||
|
endif()
|
||||||
enable_feature(ENABLE_AIRCOPY
|
enable_feature(ENABLE_AIRCOPY
|
||||||
app/aircopy.c
|
app/aircopy.c
|
||||||
ui/aircopy.c
|
ui/aircopy.c
|
||||||
@@ -182,9 +189,21 @@ if(NOT ENABLE_UART AND NOT ENABLE_USB)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_feature(ENABLE_FEAT_F4HWN)
|
enable_feature(ENABLE_FEAT_F4HWN)
|
||||||
enable_feature(ENABLE_FEAT_F4HWN_GAME
|
# GAME keeps the F+7 hook; the resident breakout.c is compiled only when the
|
||||||
app/breakout.c
|
# overlay does NOT provide Breakout as an app (that is the flash we recover).
|
||||||
|
if(ENABLE_FEAT_F4HWN_GAME)
|
||||||
|
target_compile_definitions(App INTERFACE ENABLE_FEAT_F4HWN_GAME)
|
||||||
|
if(NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
|
target_sources(App INTERFACE app/breakout.c)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
apps/app_overlay.c
|
||||||
|
apps/app_menu.c
|
||||||
)
|
)
|
||||||
|
if(ENABLE_FEAT_F4HWN_OVERLAY_APPS AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
|
||||||
|
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_OVERLAY_APPS requires ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY (the 4 KiB overlay workspace).")
|
||||||
|
endif()
|
||||||
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
|
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
|
||||||
k5viewer.c
|
k5viewer.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -467,6 +467,16 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
|||||||
#ifdef ENABLE_FMRADIO
|
#ifdef ENABLE_FMRADIO
|
||||||
void ACTION_FM(void)
|
void ACTION_FM(void)
|
||||||
{
|
{
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
/* Overlay apps replace the resident FM screen with the FM overlay app, so
|
||||||
|
this launcher is inert. Every external entry point - the assignable
|
||||||
|
side-key, the F+0 key, the boot-state resume - funnels through here, and
|
||||||
|
gFmRadioMode is only ever set from inside this function, so returning
|
||||||
|
early keeps the resident screen from ever opening. fm.c still links
|
||||||
|
against ACTION_FM; its own EXIT-path calls are unreachable because the
|
||||||
|
resident screen never opens. */
|
||||||
|
return;
|
||||||
|
#else
|
||||||
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_MONITOR)
|
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_MONITOR)
|
||||||
{
|
{
|
||||||
gInputBoxIndex = 0;
|
gInputBoxIndex = 0;
|
||||||
@@ -505,6 +515,7 @@ void ACTION_FM(void)
|
|||||||
|
|
||||||
gRequestDisplayScreen = DISPLAY_FM;
|
gRequestDisplayScreen = DISPLAY_FM;
|
||||||
}
|
}
|
||||||
|
#endif /* !ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ACTION_Scan_FM(bool bRestart)
|
static void ACTION_Scan_FM(bool bRestart)
|
||||||
|
|||||||
+14
-5
@@ -31,8 +31,11 @@
|
|||||||
#include "app/spectrum.h"
|
#include "app/spectrum.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
#if defined(ENABLE_FEAT_F4HWN_GAME) && !defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
#include "app/breakout.h"
|
#include "app/breakout.h" // resident game only; the overlay path uses app_menu.h
|
||||||
|
#endif
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
#include "apps/app_menu.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
@@ -269,9 +272,15 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KEY_7:
|
case KEY_7:
|
||||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
// F + 7 opens the overlay-apps menu when that support is built;
|
||||||
|
// otherwise it launches the resident game (GAME); otherwise VOX.
|
||||||
|
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
|
||||||
if (!beep) {
|
if (!beep) {
|
||||||
APP_RunBreakout();
|
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
|
APP_MenuOpen(); // overlay-apps selector
|
||||||
|
#else
|
||||||
|
APP_RunBreakout(); // resident game (no overlay support)
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
#endif
|
#endif
|
||||||
#ifdef ENABLE_VOX
|
#ifdef ENABLE_VOX
|
||||||
@@ -279,7 +288,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
|||||||
//#else
|
//#else
|
||||||
// toggle_chan_scanlist();
|
// toggle_chan_scanlist();
|
||||||
#endif
|
#endif
|
||||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
+115
@@ -49,6 +49,10 @@
|
|||||||
#include "driver/mb_flash.h"
|
#include "driver/mb_flash.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
#include "apps/app_overlay.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(ENABLE_OVERLAY)
|
#if defined(ENABLE_OVERLAY)
|
||||||
#include "sram-overlay.h"
|
#include "sram-overlay.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -898,6 +902,7 @@ void UART_HandleCommand(uint32_t Port)
|
|||||||
// ---- M4 slot management ("Firmware Slots") ------------------------
|
// ---- M4 slot management ("Firmware Slots") ------------------------
|
||||||
case 0x0720: // slot info: read the 64-byte header only (fast, no CRC)
|
case 0x0720: // slot info: read the 64-byte header only (fast, no CRC)
|
||||||
{
|
{
|
||||||
|
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
|
||||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
uint8_t slot = pUART_Command->Data[0];
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
mb_slot_header_t hdr;
|
mb_slot_header_t hdr;
|
||||||
@@ -920,6 +925,7 @@ void UART_HandleCommand(uint32_t Port)
|
|||||||
|
|
||||||
case 0x0722: // slot erase: wipe the whole 128 KiB slot region
|
case 0x0722: // slot erase: wipe the whole 128 KiB slot region
|
||||||
{
|
{
|
||||||
|
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] slot + Data[2..5] timestamp
|
||||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
uint8_t slot = pUART_Command->Data[0];
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
uint32_t ts = (uint32_t)pUART_Command->Data[2]
|
uint32_t ts = (uint32_t)pUART_Command->Data[2]
|
||||||
@@ -979,6 +985,7 @@ void UART_HandleCommand(uint32_t Port)
|
|||||||
|
|
||||||
case 0x0726: // slot validate: full image CRC-32, no reflash
|
case 0x0726: // slot validate: full image CRC-32, no reflash
|
||||||
{
|
{
|
||||||
|
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
|
||||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
uint8_t slot = pUART_Command->Data[0];
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
uint32_t crc = 0;
|
uint32_t crc = 0;
|
||||||
@@ -1000,6 +1007,7 @@ void UART_HandleCommand(uint32_t Port)
|
|||||||
|
|
||||||
case 0x0728: // config reset: wipe the 64 KiB of a config bank (1..4)
|
case 0x0728: // config reset: wipe the 64 KiB of a config bank (1..4)
|
||||||
{
|
{
|
||||||
|
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] bank + Data[2..5] timestamp
|
||||||
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
uint8_t bank = pUART_Command->Data[0];
|
uint8_t bank = pUART_Command->Data[0];
|
||||||
uint32_t ts = (uint32_t)pUART_Command->Data[2]
|
uint32_t ts = (uint32_t)pUART_Command->Data[2]
|
||||||
@@ -1022,6 +1030,113 @@ void UART_HandleCommand(uint32_t Port)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
// ---- overlay-app slot management ("Apps") -------------------------
|
||||||
|
// Parallels the firmware-slot family (0x072x); targets the external-flash
|
||||||
|
// Apps region. External flash only, never brick-critical.
|
||||||
|
case 0x0730: // app slot info: read the 64-byte header only
|
||||||
|
{
|
||||||
|
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
|
||||||
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
|
app_header_t hdr;
|
||||||
|
memset(&hdr, 0, sizeof(hdr));
|
||||||
|
uint8_t status = APP_SlotInfo(slot, &hdr);
|
||||||
|
struct __attribute__((packed)) {
|
||||||
|
Header_t Header;
|
||||||
|
uint8_t Slot;
|
||||||
|
uint8_t Status;
|
||||||
|
uint8_t Hdr[sizeof(app_header_t)];
|
||||||
|
} Reply;
|
||||||
|
Reply.Header.ID = 0x0731;
|
||||||
|
Reply.Header.Size = 2 + sizeof(app_header_t);
|
||||||
|
Reply.Slot = slot;
|
||||||
|
Reply.Status = status;
|
||||||
|
memcpy(Reply.Hdr, &hdr, sizeof(hdr));
|
||||||
|
SendReply(Port, &Reply, sizeof(Reply));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0732: // app slot erase: wipe the whole 8 KiB slot region
|
||||||
|
{
|
||||||
|
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] slot + Data[2..5] timestamp
|
||||||
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
|
uint32_t ts = (uint32_t)pUART_Command->Data[2]
|
||||||
|
| ((uint32_t)pUART_Command->Data[3] << 8)
|
||||||
|
| ((uint32_t)pUART_Command->Data[4] << 16)
|
||||||
|
| ((uint32_t)pUART_Command->Data[5] << 24);
|
||||||
|
uint8_t status = (ts != mb_port_timestamp(Port))
|
||||||
|
? APP_ERR_AUTH : APP_SlotErase(slot);
|
||||||
|
struct __attribute__((packed)) {
|
||||||
|
Header_t Header;
|
||||||
|
uint8_t Slot;
|
||||||
|
uint8_t Status;
|
||||||
|
} Reply;
|
||||||
|
Reply.Header.ID = 0x0733;
|
||||||
|
Reply.Header.Size = 2;
|
||||||
|
Reply.Slot = slot;
|
||||||
|
Reply.Status = status;
|
||||||
|
SendReply(Port, &Reply, sizeof(Reply));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0734: // app slot write: program bytes at slot+offset (pre-erased)
|
||||||
|
{
|
||||||
|
if (pUART_Command->Header.Size < 12u)
|
||||||
|
break;
|
||||||
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
|
uint32_t offset = (uint32_t)pUART_Command->Data[2]
|
||||||
|
| ((uint32_t)pUART_Command->Data[3] << 8)
|
||||||
|
| ((uint32_t)pUART_Command->Data[4] << 16)
|
||||||
|
| ((uint32_t)pUART_Command->Data[5] << 24);
|
||||||
|
uint16_t len = (uint16_t)(pUART_Command->Data[6]
|
||||||
|
| ((uint16_t)pUART_Command->Data[7] << 8));
|
||||||
|
uint32_t ts = (uint32_t)pUART_Command->Data[8]
|
||||||
|
| ((uint32_t)pUART_Command->Data[9] << 8)
|
||||||
|
| ((uint32_t)pUART_Command->Data[10] << 16)
|
||||||
|
| ((uint32_t)pUART_Command->Data[11] << 24);
|
||||||
|
uint8_t status;
|
||||||
|
if (ts != mb_port_timestamp(Port))
|
||||||
|
status = APP_ERR_AUTH;
|
||||||
|
else if (len > pUART_Command->Header.Size - 12u)
|
||||||
|
status = APP_ERR_SIZE;
|
||||||
|
else
|
||||||
|
status = APP_SlotWrite(slot, offset, &pUART_Command->Data[12], len);
|
||||||
|
struct __attribute__((packed)) {
|
||||||
|
Header_t Header;
|
||||||
|
uint8_t Slot;
|
||||||
|
uint8_t Status;
|
||||||
|
} Reply;
|
||||||
|
Reply.Header.ID = 0x0735;
|
||||||
|
Reply.Header.Size = 2;
|
||||||
|
Reply.Slot = slot;
|
||||||
|
Reply.Status = status;
|
||||||
|
SendReply(Port, &Reply, sizeof(Reply));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x0736: // app slot validate: header only (code CRC is checked at launch)
|
||||||
|
{
|
||||||
|
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
|
||||||
|
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
|
||||||
|
uint8_t slot = pUART_Command->Data[0];
|
||||||
|
uint8_t status = APP_ValidateSlot(slot, NULL);
|
||||||
|
struct __attribute__((packed)) {
|
||||||
|
Header_t Header;
|
||||||
|
uint8_t Slot;
|
||||||
|
uint8_t Status;
|
||||||
|
} Reply;
|
||||||
|
Reply.Header.ID = 0x0737;
|
||||||
|
Reply.Header.Size = 2;
|
||||||
|
Reply.Slot = slot;
|
||||||
|
Reply.Status = status;
|
||||||
|
SendReply(Port, &Reply, sizeof(Reply));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_UART_RW_BK_REGS
|
#ifdef ENABLE_UART_RW_BK_REGS
|
||||||
case 0x0601:
|
case 0x0601:
|
||||||
CMD_0601_ReadBK4819Reg(Port, pUART_Command->Buffer);
|
CMD_0601_ReadBK4819Reg(Port, pUART_Command->Buffer);
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Overlay-app ABI (POC).
|
||||||
|
*
|
||||||
|
* A leaf, modal "overlay app" is a self-contained code blob stored in the
|
||||||
|
* external SPI flash and copied into the 4 KiB overlay RAM (the PY25Q16 sector
|
||||||
|
* cache) to run, then discarded. It is linked with NOCROSSREFS and must not
|
||||||
|
* reference resident firmware symbols: every service it needs is reached through
|
||||||
|
* this table, which the loader fills and passes to the entry point.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef APPS_APP_API_H
|
||||||
|
#define APPS_APP_API_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* ABI 2: the table below (v1 core + the fields once labelled "v2 additions") 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 ABI_VERSION in pack_app.py, which
|
||||||
|
* stamps the blob the loader checks against. */
|
||||||
|
#define APP_ABI_VERSION 2u
|
||||||
|
|
||||||
|
/* 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. */
|
||||||
|
enum {
|
||||||
|
APP_KEY_0 = 0,
|
||||||
|
APP_KEY_1 = 1,
|
||||||
|
APP_KEY_2 = 2,
|
||||||
|
APP_KEY_3 = 3,
|
||||||
|
APP_KEY_4 = 4,
|
||||||
|
APP_KEY_5 = 5,
|
||||||
|
APP_KEY_6 = 6,
|
||||||
|
APP_KEY_7 = 7,
|
||||||
|
APP_KEY_8 = 8,
|
||||||
|
APP_KEY_9 = 9,
|
||||||
|
APP_KEY_MENU = 10,
|
||||||
|
APP_KEY_UP = 11,
|
||||||
|
APP_KEY_DOWN = 12,
|
||||||
|
APP_KEY_EXIT = 13,
|
||||||
|
APP_KEY_STAR = 14,
|
||||||
|
APP_KEY_F = 15,
|
||||||
|
APP_KEY_INVALID = 19,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* The framebuffer is the resident gFrameBuffer[FRAME_LINES][128]; the app draws
|
||||||
|
* into it and calls a blit_* to push it to the LCD. */
|
||||||
|
typedef uint8_t (*app_fb_t)[128];
|
||||||
|
|
||||||
|
/* Broadcast FM shared state (mirrors gEeprom.FM_*), read/written via fm_state. */
|
||||||
|
#define APP_FM_CH_MAX 48
|
||||||
|
typedef struct {
|
||||||
|
uint16_t freq_playing; /* current tuned frequency (0.1 MHz) */
|
||||||
|
uint16_t sel_freq; /* last VFO frequency */
|
||||||
|
uint8_t band; /* 0..3 */
|
||||||
|
uint8_t is_mr; /* memory mode */
|
||||||
|
uint8_t sel_ch; /* selected memory channel 0..47 */
|
||||||
|
} app_fm_state_t;
|
||||||
|
|
||||||
|
typedef struct app_api {
|
||||||
|
uint8_t abi_version; /* == APP_ABI_VERSION */
|
||||||
|
|
||||||
|
app_fb_t fb; /* -> gFrameBuffer */
|
||||||
|
|
||||||
|
/* ---- display ---- */
|
||||||
|
void (*display_clear)(void); /* UI_DisplayClear */
|
||||||
|
void (*status_clear)(void); /* UI_StatusClear */
|
||||||
|
void (*draw_line)(app_fb_t fb, int16_t x1, int16_t y1,
|
||||||
|
int16_t x2, int16_t y2, bool black); /* UI_DrawLineBuffer */
|
||||||
|
void (*draw_rect)(app_fb_t fb, int16_t x1, int16_t y1,
|
||||||
|
int16_t x2, int16_t y2, bool black); /* UI_DrawRectangleBuffer */
|
||||||
|
void (*print_bold)(const char *s, uint8_t start,
|
||||||
|
uint8_t end, uint8_t line); /* UI_PrintStringSmallBold */
|
||||||
|
void (*print_tiny)(const char *s, uint8_t x, uint8_t y,
|
||||||
|
bool statusbar, bool fill); /* GUI_DisplaySmallest */
|
||||||
|
void (*blit_full)(void); /* ST7565_BlitFullScreen */
|
||||||
|
void (*blit_line)(unsigned line); /* ST7565_BlitLine */
|
||||||
|
void (*blit_status)(void); /* ST7565_BlitStatusLine */
|
||||||
|
|
||||||
|
/* ---- input / system ---- */
|
||||||
|
uint8_t (*get_key)(void); /* KEYBOARD_GetKey, returns an APP_KEY_* code */
|
||||||
|
void (*delay_ms)(uint32_t ms); /* SYSTEM_DelayMs */
|
||||||
|
void (*backlight_tick)(void); /* BACKLIGHT_UpdateTickless */
|
||||||
|
|
||||||
|
/* ---- audio / indicator ---- */
|
||||||
|
void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */
|
||||||
|
void (*led)(bool on); /* green GPIO indicator */
|
||||||
|
|
||||||
|
/* ---- data provided at launch ---- */
|
||||||
|
uint32_t seed; /* resident-computed PRNG seed */
|
||||||
|
|
||||||
|
/* ==== 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,
|
||||||
|
bool statusbar, bool fill, uint8_t endX); /* GUI_DisplaySmallestInverse */
|
||||||
|
void (*display_freq)(const char *s, uint8_t x, uint8_t y, bool statusbar); /* UI_DisplayFrequency (big font) */
|
||||||
|
|
||||||
|
/* ---- BK4819 radio access ---- */
|
||||||
|
int16_t (*rssi_dbm)(void); /* corrected RSSI of the RX VFO, dBm */
|
||||||
|
uint16_t (*bk_read)(uint8_t reg); /* BK4819_ReadRegister */
|
||||||
|
void (*bk_write)(uint8_t reg, uint16_t v);/* BK4819_WriteRegister */
|
||||||
|
void (*set_agc)(bool on); /* BK4819_SetAGC */
|
||||||
|
void (*set_af)(uint8_t mode); /* BK4819_SetAF (APP_AF_* below) */
|
||||||
|
void (*audio_path)(bool on); /* AUDIO_AudioPathOn/Off */
|
||||||
|
void (*prepare_tone)(void); /* BK4819_PrepareToPlayTone(true) */
|
||||||
|
void (*play_tone_raw)(uint16_t hz, uint16_t ms); /* BK4819_PlayToneRaw */
|
||||||
|
void (*tones_off_rx)(void); /* BK4819_TurnsOffTones_TurnsOnRX */
|
||||||
|
uint32_t (*rx_freq)(void); /* current RX VFO frequency (x10 Hz) */
|
||||||
|
|
||||||
|
/* ---- config persistence (deferred: staged now, committed on exit) ---- */
|
||||||
|
void (*cfg_load)(uint8_t *buf, uint8_t len); /* read the app's saved config bytes */
|
||||||
|
void (*cfg_save)(const uint8_t *buf, uint8_t len); /* stage bytes; resident commits after the app returns */
|
||||||
|
|
||||||
|
/* ---- battery + backlight ---- */
|
||||||
|
void (*draw_battery)(void); /* UI_DrawStatusBattery into the status line */
|
||||||
|
void (*battery_sample)(void); /* periodic ADC sample so the level stays live */
|
||||||
|
void (*backlight_on)(void); /* BACKLIGHT_TurnOn */
|
||||||
|
void (*backlight_update)(void); /* BACKLIGHT_Update (fade step) */
|
||||||
|
|
||||||
|
uint8_t *status_line; /* -> gStatusLine (for status-bar icons) */
|
||||||
|
|
||||||
|
/* ---- TX (beacon) ---- */
|
||||||
|
uint8_t (*tx_state)(void); /* 0 = OK to transmit, else a denial code */
|
||||||
|
void (*tx_set_params)(void); /* RADIO_SetTxParameters (key up: carrier+PA) */
|
||||||
|
void (*tx_tone)(uint16_t hz);/* BK4819_TransmitTone prime (MCW tone) */
|
||||||
|
void (*tx_mute)(bool on); /* key the tone on(false)/off(true) via TxMute */
|
||||||
|
void (*tx_end)(void); /* PA off + RADIO_SetupRegisters (back to RX) */
|
||||||
|
uint32_t (*tx_freq)(void); /* current TX VFO frequency (x10 Hz) */
|
||||||
|
void (*boot_callsign)(char *buf, uint8_t len); /* sanitised callsign from the boot message */
|
||||||
|
void (*print_string)(const char *s, uint8_t start, uint8_t end,
|
||||||
|
uint8_t line, uint8_t width); /* UI_PrintString (big font) */
|
||||||
|
|
||||||
|
/* ---- broadcast FM (BK1080), sovereign: no BK4819 dual-watch ---- */
|
||||||
|
void (*fm_enter)(uint16_t freq, uint8_t band); /* BK1080_Init + antenna filter + audio on */
|
||||||
|
void (*fm_exit)(void); /* audio off + BK1080_Init0 + restore filter */
|
||||||
|
void (*fm_set_freq)(uint16_t freq, uint8_t band);/* BK1080_SetFrequency (freq in 0.1 MHz) */
|
||||||
|
uint16_t (*fm_read)(uint8_t reg); /* BK1080_ReadRegister (RSSI / valid) */
|
||||||
|
uint16_t (*fm_lo)(uint8_t band); /* band low limit (0.1 MHz) */
|
||||||
|
uint16_t (*fm_hi)(uint8_t band); /* band high limit (0.1 MHz) */
|
||||||
|
void (*fm_mute)(bool mute); /* BK1080_Mute */
|
||||||
|
int8_t (*fm_valid)(uint16_t freq, uint16_t lo); /* FM_CheckFrequencyLock: 0 = station */
|
||||||
|
uint16_t *fm_channels; /* -> gFM_Channels[APP_FM_CH_MAX], shared RAM r/w */
|
||||||
|
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) */
|
||||||
|
} app_api_t;
|
||||||
|
|
||||||
|
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
||||||
|
enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 };
|
||||||
|
|
||||||
|
/* Register ids the apps use (mirror driver/bk4819-regs.h). */
|
||||||
|
enum { APP_BK_REG_13 = 0x13 };
|
||||||
|
|
||||||
|
/* Entry point every app blob exports, placed at blob offset 0. */
|
||||||
|
typedef void (*app_entry_t)(const app_api_t *api);
|
||||||
|
|
||||||
|
#endif /* APPS_APP_API_H */
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "apps/app_overlay.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "apps/app_menu.h"
|
||||||
|
#include "driver/st7565.h"
|
||||||
|
#include "driver/keyboard.h"
|
||||||
|
#include "driver/system.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "ui/helper.h"
|
||||||
|
|
||||||
|
/* Highlight a row (same rounded-invert look as the multiboot selector). */
|
||||||
|
static void app_invert_row(uint8_t line)
|
||||||
|
{
|
||||||
|
gFrameBuffer[line][0] ^= 0x7Fu;
|
||||||
|
for (uint8_t x = 1u; x < LCD_WIDTH - 1u; x++)
|
||||||
|
{
|
||||||
|
gFrameBuffer[line][x] ^= 0xFFu;
|
||||||
|
gFrameBuffer[line - 1u][x] ^= 0x80u;
|
||||||
|
}
|
||||||
|
gFrameBuffer[line][LCD_WIDTH - 1u] ^= 0x7Fu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Debounced blocking key read, then wait for release (mirrors mb_get_key). */
|
||||||
|
static KEY_Code_t app_get_key(void)
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
KEY_Code_t key = KEYBOARD_Poll();
|
||||||
|
if (key != KEY_INVALID)
|
||||||
|
{
|
||||||
|
SYSTEM_DelayMs(30);
|
||||||
|
if (KEYBOARD_Poll() == key)
|
||||||
|
{
|
||||||
|
while (KEYBOARD_Poll() != KEY_INVALID)
|
||||||
|
SYSTEM_DelayMs(10);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SYSTEM_DelayMs(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_wait_release(void)
|
||||||
|
{
|
||||||
|
uint8_t stable = 0;
|
||||||
|
while (stable < 10u)
|
||||||
|
{
|
||||||
|
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
|
||||||
|
stable++;
|
||||||
|
else
|
||||||
|
stable = 0;
|
||||||
|
SYSTEM_DelayMs(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_copy(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
|
||||||
|
{
|
||||||
|
uint8_t n = 0;
|
||||||
|
while (n + 1u < cap && n < src_cap && src[n])
|
||||||
|
{
|
||||||
|
dst[n] = src[n];
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
dst[n] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Human-readable reason for an APP_LaunchOverlay / APP_ValidateSlot failure. */
|
||||||
|
static const char *app_err_text(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_AUTH: return "AUTH";
|
||||||
|
default: return "ERROR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A launch failed: name the app and the reason, 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)
|
||||||
|
{
|
||||||
|
char nm[19];
|
||||||
|
app_copy(nm, sizeof(nm), 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("Press any key", 2, 0, 6);
|
||||||
|
ST7565_BlitStatusLine();
|
||||||
|
ST7565_BlitFullScreen();
|
||||||
|
|
||||||
|
app_get_key(); /* blocking: dismiss on any key */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visible app rows (framebuffer lines 1..APP_MENU_ROWS; line 0 is the header). */
|
||||||
|
#define APP_MENU_ROWS 6u
|
||||||
|
|
||||||
|
void APP_MenuOpen(void)
|
||||||
|
{
|
||||||
|
/* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1
|
||||||
|
* (shown to the user as 1..N there). Scan them all; the list is empty until
|
||||||
|
* the user pushes one ("No apps installed"). */
|
||||||
|
app_header_t hdr[APP_SLOT_COUNT];
|
||||||
|
uint8_t list[APP_SLOT_COUNT]; /* slot indices of the committed apps */
|
||||||
|
uint8_t count = 0;
|
||||||
|
|
||||||
|
for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++)
|
||||||
|
{
|
||||||
|
if (APP_SlotInfo(slot, &hdr[slot]) == APP_OK &&
|
||||||
|
(hdr[slot].flags & APP_FLAG_COMMITTED))
|
||||||
|
list[count++] = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remember the cursor across open/close of the Apps menu. Clamp in case the
|
||||||
|
* installed-app set changed since we were last here. */
|
||||||
|
static uint8_t sel = 0;
|
||||||
|
static uint8_t top = 0; /* first visible row of the scrolling window */
|
||||||
|
if (count == 0u || sel >= count)
|
||||||
|
sel = top = 0u;
|
||||||
|
app_wait_release();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
UI_DisplayClear();
|
||||||
|
UI_StatusClear(); /* wipe the VFO status line (DW, battery, ...) first */
|
||||||
|
/* 10 glyphs x ~4 px = 40 px wide, centred: x=(128-40)/2=44, endX=x+40. */
|
||||||
|
GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84);
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
UI_PrintStringSmallNormal("No apps installed", 2, 126, 3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Scrolling window: slide [top, top+APP_MENU_ROWS) so it always holds
|
||||||
|
* the selection, keeping all APP_SLOT_COUNT apps reachable - not just
|
||||||
|
* the first APP_MENU_ROWS. */
|
||||||
|
if (sel < top)
|
||||||
|
top = sel;
|
||||||
|
else if (sel >= (uint8_t)(top + APP_MENU_ROWS))
|
||||||
|
top = (uint8_t)(sel - APP_MENU_ROWS + 1u);
|
||||||
|
|
||||||
|
for (uint8_t i = top; i < count && (uint8_t)(i - top) < APP_MENU_ROWS; i++)
|
||||||
|
{
|
||||||
|
char line[19];
|
||||||
|
app_copy(line, sizeof(line), hdr[list[i]].name, APP_NAME_LEN);
|
||||||
|
const uint8_t fbLine = (uint8_t)(i - top + 1u);
|
||||||
|
UI_PrintStringSmallNormal(line, 2, 0, fbLine);
|
||||||
|
if (i == sel)
|
||||||
|
app_invert_row(fbLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ST7565_BlitStatusLine();
|
||||||
|
ST7565_BlitFullScreen();
|
||||||
|
|
||||||
|
const KEY_Code_t key = app_get_key();
|
||||||
|
if (key == KEY_EXIT)
|
||||||
|
return;
|
||||||
|
if (count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case KEY_UP:
|
||||||
|
sel = (sel == 0u) ? (uint8_t)(count - 1u) : (uint8_t)(sel - 1u);
|
||||||
|
break;
|
||||||
|
case KEY_DOWN:
|
||||||
|
sel = (uint8_t)((sel + 1u) % count);
|
||||||
|
break;
|
||||||
|
case KEY_MENU:
|
||||||
|
{
|
||||||
|
const uint8_t rc = APP_LaunchOverlay(list[sel]); /* runs until the app exits */
|
||||||
|
if (rc != APP_OK)
|
||||||
|
app_show_error(hdr[list[sel]].name, rc); /* no longer silent */
|
||||||
|
app_wait_release();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef APPS_APP_MENU_H
|
||||||
|
#define APPS_APP_MENU_H
|
||||||
|
|
||||||
|
/* Blocking "Apps" selector: scans the overlay-app slots, lists the committed
|
||||||
|
* ones by name, and launches the chosen one. UP/DOWN move, MENU launches, EXIT
|
||||||
|
* closes. Returns when the user leaves. */
|
||||||
|
void APP_MenuOpen(void);
|
||||||
|
|
||||||
|
#endif /* APPS_APP_MENU_H */
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "apps/app_overlay.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "py32f0xx.h"
|
||||||
|
|
||||||
|
#include "driver/bk4819.h"
|
||||||
|
#include "driver/bk4819-regs.h"
|
||||||
|
#ifdef ENABLE_FMRADIO
|
||||||
|
#include "driver/bk1080.h"
|
||||||
|
#include "app/fm.h"
|
||||||
|
#endif
|
||||||
|
#include "driver/keyboard.h"
|
||||||
|
#include "driver/py25q16.h"
|
||||||
|
#include "driver/st7565.h"
|
||||||
|
#include "driver/system.h"
|
||||||
|
#include "driver/backlight.h"
|
||||||
|
#include "ui/helper.h"
|
||||||
|
#include "ui/status.h"
|
||||||
|
#include "board.h"
|
||||||
|
#include "audio.h"
|
||||||
|
#include "radio.h"
|
||||||
|
#include "helper/battery.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "misc.h" /* dBmCorrTable */
|
||||||
|
|
||||||
|
_Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes");
|
||||||
|
|
||||||
|
/* CRC-32 (zlib) over RAM bytes - matches App/apps/pack_app.py and the firmware's
|
||||||
|
* mb_ext_image_crc32 (init 0xFFFFFFFF, poly 0xEDB88320, final XOR). */
|
||||||
|
static uint32_t app_crc32(const uint8_t *p, uint32_t len)
|
||||||
|
{
|
||||||
|
uint32_t crc = 0xFFFFFFFFu;
|
||||||
|
while (len--) {
|
||||||
|
crc ^= *p++;
|
||||||
|
for (int k = 0; k < 8; k++)
|
||||||
|
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
|
||||||
|
}
|
||||||
|
return crc ^ 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */
|
||||||
|
static void app_display_clear(void) { UI_DisplayClear(); }
|
||||||
|
static void app_status_clear(void) { UI_StatusClear(); }
|
||||||
|
static uint8_t app_get_key(void) { return (uint8_t)KEYBOARD_GetKey(); }
|
||||||
|
static void app_led(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, on); }
|
||||||
|
|
||||||
|
static void app_play_tone(uint16_t tone, uint16_t ms)
|
||||||
|
{
|
||||||
|
BK4819_PrepareToPlayTone(true);
|
||||||
|
AUDIO_AudioPathOn();
|
||||||
|
BK4819_PlayToneRaw(tone, ms);
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t app_make_seed(void)
|
||||||
|
{
|
||||||
|
/* Reproduces the original Breakout seed source. */
|
||||||
|
return BK4819_ReadRegister(BK4819_REG_67) & 0x01FF
|
||||||
|
* gBatteryVoltageAverage
|
||||||
|
* gEeprom.VfoInfo[0].pRX->Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- v2 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); }
|
||||||
|
static void app_set_af(uint8_t m) { BK4819_SetAF((BK4819_AF_Type_t)m); }
|
||||||
|
static void app_audio_path(bool on){ if (on) AUDIO_AudioPathOn(); else AUDIO_AudioPathOff(); }
|
||||||
|
static void app_prepare_tone(void) { BK4819_PrepareToPlayTone(true); }
|
||||||
|
static void app_play_tone_raw(uint16_t hz, uint16_t ms) { BK4819_PlayToneRaw(hz, ms); }
|
||||||
|
static void app_tones_off_rx(void) { BK4819_TurnsOffTones_TurnsOnRX(); }
|
||||||
|
static uint32_t app_rx_freq(void) { return gRxVfo->pRX->Frequency; }
|
||||||
|
|
||||||
|
/* ---- v2 config (deferred, flash-backed) ----
|
||||||
|
* Stored per app slot in the header sector, just after the 64-byte header. cfg_load
|
||||||
|
* reads flash at launch (ReadBuffer bypasses the overlay cache). cfg_save only stages
|
||||||
|
* into RAM - the app runs from the sector cache, so it cannot write flash itself; the
|
||||||
|
* loader commits the staged bytes to flash after the app returns (RMW preserves the
|
||||||
|
* slot header). Erasing/reinstalling a slot resets its config, which is intended. */
|
||||||
|
#define APP_CFG_OFFSET 0x40u /* config area within the header sector */
|
||||||
|
static uint8_t app_cfg_buf[16];
|
||||||
|
static uint8_t app_cfg_len; /* staged length; 0 = nothing to commit */
|
||||||
|
static uint8_t app_run_slot; /* slot of the app currently running */
|
||||||
|
|
||||||
|
static void app_cfg_load(uint8_t *buf, uint8_t len)
|
||||||
|
{
|
||||||
|
if (len > sizeof(app_cfg_buf)) len = sizeof(app_cfg_buf);
|
||||||
|
PY25Q16_ReadBuffer(APP_SLOT_BASE(app_run_slot) + APP_CFG_OFFSET, buf, len);
|
||||||
|
}
|
||||||
|
static void app_cfg_save(const uint8_t *buf, uint8_t len)
|
||||||
|
{
|
||||||
|
if (len > sizeof(app_cfg_buf)) len = sizeof(app_cfg_buf);
|
||||||
|
memcpy(app_cfg_buf, buf, len);
|
||||||
|
app_cfg_len = len; /* mark dirty; the loader commits after the app returns */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- v2 battery / backlight ---- */
|
||||||
|
static void app_draw_battery(void)
|
||||||
|
{
|
||||||
|
char t[8];
|
||||||
|
UI_DrawStatusBattery(gStatusLine, t);
|
||||||
|
}
|
||||||
|
static void app_battery_sample(void)
|
||||||
|
{
|
||||||
|
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
||||||
|
if (gBatteryVoltageIndex > 3)
|
||||||
|
gBatteryVoltageIndex = 0;
|
||||||
|
BATTERY_GetReadings(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- v2 TX (beacon) ---- */
|
||||||
|
static uint8_t app_tx_state(void)
|
||||||
|
{
|
||||||
|
if (TX_freq_check(gTxVfo->pTX->Frequency) != 0 && gTxVfo->TX_LOCK) return 1; /* TX disable */
|
||||||
|
if (gBatteryDisplayLevel == 0) return 2; /* battery low */
|
||||||
|
if (gBatteryDisplayLevel > 6) return 3; /* voltage high */
|
||||||
|
if (gTxVfo->Modulation != MODULATION_FM) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static void app_tx_set_params(void) { RADIO_SetTxParameters(); }
|
||||||
|
static void app_tx_tone(uint16_t hz) { BK4819_TransmitTone(false, hz); }
|
||||||
|
static void app_tx_mute(bool on) { if (on) BK4819_EnterTxMute(); else BK4819_ExitTxMute(); }
|
||||||
|
static void app_tx_end(void) { BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); RADIO_SetupRegisters(true); }
|
||||||
|
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 */
|
||||||
|
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;
|
||||||
|
if (c >= 'a' && c <= 'z') c -= 32;
|
||||||
|
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') buf[n++] = c;
|
||||||
|
}
|
||||||
|
buf[n] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_FMRADIO
|
||||||
|
/* ---- v2 broadcast FM (BK1080), sovereign (no BK4819 dual-watch) ---- */
|
||||||
|
static void app_fm_enter(uint16_t f, uint8_t b)
|
||||||
|
{
|
||||||
|
BK1080_Init(f, b);
|
||||||
|
BK4819_PickRXFilterPathBasedOnFrequency(10320000); /* FM band antenna filter */
|
||||||
|
AUDIO_AudioPathOn();
|
||||||
|
gEnableSpeaker = true;
|
||||||
|
}
|
||||||
|
static void app_fm_exit(void)
|
||||||
|
{
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
gEnableSpeaker = false;
|
||||||
|
BK1080_Init0();
|
||||||
|
BK4819_PickRXFilterPathBasedOnFrequency(gRxVfo->pRX->Frequency); /* restore RX filter */
|
||||||
|
}
|
||||||
|
static void app_fm_set_freq(uint16_t f, uint8_t b) { BK1080_SetFrequency(f, b); }
|
||||||
|
static uint16_t app_fm_read(uint8_t r) { return BK1080_ReadRegister((BK1080_Register_t)r); }
|
||||||
|
static uint16_t app_fm_lo(uint8_t b) { return BK1080_GetFreqLoLimit(b); }
|
||||||
|
static uint16_t app_fm_hi(uint8_t b) { return BK1080_GetFreqHiLimit(b); }
|
||||||
|
static void app_fm_mute(bool m) { BK1080_Mute(m); }
|
||||||
|
static int8_t app_fm_valid(uint16_t f, uint16_t lo) { return (int8_t)FM_CheckFrequencyLock(f, lo); }
|
||||||
|
|
||||||
|
static bool app_fm_dirty; /* deferred: SETTINGS_SaveFM committed after the app returns */
|
||||||
|
static void app_fm_state(app_fm_state_t *s, bool write)
|
||||||
|
{
|
||||||
|
if (write) {
|
||||||
|
gEeprom.FM_FrequencyPlaying = s->freq_playing;
|
||||||
|
gEeprom.FM_SelectedFrequency = s->sel_freq;
|
||||||
|
gEeprom.FM_Band = s->band & 3u;
|
||||||
|
gEeprom.FM_IsMrMode = s->is_mr ? true : false;
|
||||||
|
gEeprom.FM_SelectedChannel = s->sel_ch;
|
||||||
|
} else {
|
||||||
|
s->freq_playing = gEeprom.FM_FrequencyPlaying;
|
||||||
|
s->sel_freq = gEeprom.FM_SelectedFrequency;
|
||||||
|
s->band = gEeprom.FM_Band;
|
||||||
|
s->is_mr = gEeprom.FM_IsMrMode;
|
||||||
|
s->sel_ch = gEeprom.FM_SelectedChannel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void app_fm_commit(void) { app_fm_dirty = true; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header)
|
||||||
|
{
|
||||||
|
if (slot >= APP_SLOT_COUNT)
|
||||||
|
return APP_ERR_SLOT;
|
||||||
|
|
||||||
|
app_header_t h;
|
||||||
|
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), &h, sizeof(h));
|
||||||
|
|
||||||
|
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.flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED;
|
||||||
|
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) */
|
||||||
|
return APP_ERR_SIZE;
|
||||||
|
|
||||||
|
if (out_header)
|
||||||
|
*out_header = h;
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header)
|
||||||
|
{
|
||||||
|
if (slot >= APP_SLOT_COUNT)
|
||||||
|
return APP_ERR_SLOT;
|
||||||
|
app_header_t h;
|
||||||
|
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), &h, sizeof(h));
|
||||||
|
if (out_header)
|
||||||
|
*out_header = h;
|
||||||
|
return (h.magic == APP_MAGIC) ? APP_OK : APP_ERR_MAGIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t APP_LaunchOverlay(uint8_t slot)
|
||||||
|
{
|
||||||
|
app_header_t h;
|
||||||
|
uint8_t rc = APP_ValidateSlot(slot, &h);
|
||||||
|
if (rc != APP_OK)
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
/* The app's absolute data references only resolve if it runs at the exact
|
||||||
|
* VMA it was linked for. The overlay VMA varies with the firmware's RAM
|
||||||
|
* layout (per preset/features), so the app records its link VMA and we
|
||||||
|
* refuse a mismatch cleanly instead of jumping into misaddressed code. */
|
||||||
|
uint8_t *ws = PY25Q16_OverlayBuffer();
|
||||||
|
if (h.link_vma != (uint32_t)ws)
|
||||||
|
return APP_ERR_VMA;
|
||||||
|
|
||||||
|
/* Repurpose the sector cache: drop any cached config sector, load the code
|
||||||
|
* straight in (ReadBuffer bypasses the cache), and verify it in RAM before
|
||||||
|
* trusting it. Zeroing first leaves the app's .bss clean. */
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
memset(ws, 0, APP_OVERLAY_MAX);
|
||||||
|
PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_CODE_OFFSET, ws, h.code_size);
|
||||||
|
|
||||||
|
if (app_crc32(ws, h.code_size) != h.code_crc32) {
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
return APP_ERR_CRC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure every store to the overlay is visible before we branch into it. */
|
||||||
|
__DSB();
|
||||||
|
__ISB();
|
||||||
|
|
||||||
|
const app_api_t api = {
|
||||||
|
.abi_version = APP_ABI_VERSION,
|
||||||
|
.fb = gFrameBuffer,
|
||||||
|
.display_clear = app_display_clear,
|
||||||
|
.status_clear = app_status_clear,
|
||||||
|
.draw_line = UI_DrawLineBuffer,
|
||||||
|
.draw_rect = UI_DrawRectangleBuffer,
|
||||||
|
.print_bold = UI_PrintStringSmallBold,
|
||||||
|
.print_tiny = GUI_DisplaySmallest,
|
||||||
|
.blit_full = ST7565_BlitFullScreen,
|
||||||
|
.blit_line = ST7565_BlitLine,
|
||||||
|
.blit_status = ST7565_BlitStatusLine,
|
||||||
|
.get_key = app_get_key,
|
||||||
|
.delay_ms = SYSTEM_DelayMs,
|
||||||
|
.backlight_tick = BACKLIGHT_UpdateTickless,
|
||||||
|
.play_tone = app_play_tone,
|
||||||
|
.led = app_led,
|
||||||
|
.seed = app_make_seed(),
|
||||||
|
/* v2 */
|
||||||
|
.print_normal = UI_PrintStringSmallNormal,
|
||||||
|
.print_inverse = GUI_DisplaySmallestInverse,
|
||||||
|
.display_freq = UI_DisplayFrequency,
|
||||||
|
.rssi_dbm = app_rssi_dbm,
|
||||||
|
.bk_read = app_bk_read,
|
||||||
|
.bk_write = app_bk_write,
|
||||||
|
.set_agc = BK4819_SetAGC,
|
||||||
|
.set_af = app_set_af,
|
||||||
|
.audio_path = app_audio_path,
|
||||||
|
.prepare_tone = app_prepare_tone,
|
||||||
|
.play_tone_raw = app_play_tone_raw,
|
||||||
|
.tones_off_rx = app_tones_off_rx,
|
||||||
|
.rx_freq = app_rx_freq,
|
||||||
|
.cfg_load = app_cfg_load,
|
||||||
|
.cfg_save = app_cfg_save,
|
||||||
|
.draw_battery = app_draw_battery,
|
||||||
|
.battery_sample = app_battery_sample,
|
||||||
|
.backlight_on = BACKLIGHT_TurnOn,
|
||||||
|
.backlight_update = BACKLIGHT_Update,
|
||||||
|
.status_line = gStatusLine,
|
||||||
|
.tx_state = app_tx_state,
|
||||||
|
.tx_set_params = app_tx_set_params,
|
||||||
|
.tx_tone = app_tx_tone,
|
||||||
|
.tx_mute = app_tx_mute,
|
||||||
|
.tx_end = app_tx_end,
|
||||||
|
.tx_freq = app_tx_freq,
|
||||||
|
.boot_callsign = app_boot_callsign,
|
||||||
|
.print_string = UI_PrintString,
|
||||||
|
#ifdef ENABLE_FMRADIO
|
||||||
|
.fm_enter = app_fm_enter,
|
||||||
|
.fm_exit = app_fm_exit,
|
||||||
|
.fm_set_freq = app_fm_set_freq,
|
||||||
|
.fm_read = app_fm_read,
|
||||||
|
.fm_lo = app_fm_lo,
|
||||||
|
.fm_hi = app_fm_hi,
|
||||||
|
.fm_mute = app_fm_mute,
|
||||||
|
.fm_valid = app_fm_valid,
|
||||||
|
.fm_channels = gFM_Channels,
|
||||||
|
.fm_state = app_fm_state,
|
||||||
|
.fm_commit = app_fm_commit,
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
app_run_slot = slot; /* for cfg_load / cfg_save */
|
||||||
|
app_cfg_len = 0;
|
||||||
|
#ifdef ENABLE_FMRADIO
|
||||||
|
app_fm_dirty = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Pin RX to the user-selected VFO before the app runs. Under dual watch
|
||||||
|
* gRxVfo is whichever VFO the receiver was parked on when F+7 was pressed,
|
||||||
|
* so an RF app (FoxHunt, a future S-meter, ...) would measure and display a
|
||||||
|
* VFO the user did not pick - sometimes A, sometimes B. Point RX at the
|
||||||
|
* selected (TX) VFO and retune so rx_freq(), rssi_dbm() and the tuned
|
||||||
|
* hardware all agree on the selected channel. gTxVfo is left untouched, so
|
||||||
|
* Beacon's tx_freq() stays correct too. State is saved and restored on
|
||||||
|
* return so the resident dual watch resumes cleanly. */
|
||||||
|
const uint8_t saved_rx_vfo = gEeprom.RX_VFO;
|
||||||
|
VFO_Info_t *const saved_rx = gRxVfo;
|
||||||
|
gEeprom.RX_VFO = gEeprom.TX_VFO;
|
||||||
|
gRxVfo = gTxVfo;
|
||||||
|
RADIO_SetupRegisters(true);
|
||||||
|
|
||||||
|
app_entry_t entry = (app_entry_t)(((uint32_t)ws + h.entry_off) | 1u);
|
||||||
|
entry(&api);
|
||||||
|
|
||||||
|
/* Restore the resident RX/dual-watch tuning the app ran on top of. */
|
||||||
|
gEeprom.RX_VFO = saved_rx_vfo;
|
||||||
|
gRxVfo = saved_rx;
|
||||||
|
RADIO_SetupRegisters(true);
|
||||||
|
|
||||||
|
/* The overlay held app code, not a valid config sector. */
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
|
||||||
|
/* Commit any deferred config the app staged (RMW keeps the slot header). */
|
||||||
|
if (app_cfg_len) {
|
||||||
|
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + APP_CFG_OFFSET, app_cfg_buf, app_cfg_len, false);
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
}
|
||||||
|
#ifdef ENABLE_FMRADIO
|
||||||
|
/* Commit the FM config + 48 channels the app edited (shared with resident FM). */
|
||||||
|
if (app_fm_dirty) {
|
||||||
|
app_fm_dirty = false;
|
||||||
|
SETTINGS_SaveFM();
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t APP_SlotErase(uint8_t slot)
|
||||||
|
{
|
||||||
|
if (slot >= APP_SLOT_COUNT)
|
||||||
|
return APP_ERR_SLOT;
|
||||||
|
uint32_t base = APP_SLOT_BASE(slot);
|
||||||
|
for (uint32_t off = 0; off < APP_SLOT_STRIDE; off += APP_SECTOR_SIZE)
|
||||||
|
PY25Q16_SectorErase(base + off);
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len)
|
||||||
|
{
|
||||||
|
if (slot >= APP_SLOT_COUNT)
|
||||||
|
return APP_ERR_SLOT;
|
||||||
|
if (offset > APP_SLOT_STRIDE || len > APP_SLOT_STRIDE - offset)
|
||||||
|
return APP_ERR_SIZE;
|
||||||
|
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false);
|
||||||
|
PY25Q16_InvalidateCache();
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Overlay-app loader (POC).
|
||||||
|
*
|
||||||
|
* A leaf, modal app is a self-contained blob held in the external SPI flash. To
|
||||||
|
* run it the loader validates the header + code CRC, copies the code into the
|
||||||
|
* 4 KiB overlay (the PY25Q16 sector cache, shared VMA with the multiboot RAM
|
||||||
|
* stub), and calls its entry point. The blob runs from RAM and reaches every
|
||||||
|
* resident service through app_api_t. The loader never touches the internal
|
||||||
|
* application flash, so a corrupt or incompatible blob is only refused (header/CRC
|
||||||
|
* mismatch) or crashes into a watchdog reset.
|
||||||
|
*
|
||||||
|
* This is NOT a security sandbox. The CRC is an integrity check, not authentication,
|
||||||
|
* and a launched app is TRUSTED NATIVE CODE: it runs privileged with full access to
|
||||||
|
* memory and peripherals (no MPU, no crypto auth), so a malicious or buggy app CAN
|
||||||
|
* brick the radio - e.g. by driving the internal-flash controller itself. Only
|
||||||
|
* install apps you trust.
|
||||||
|
*
|
||||||
|
* External-flash "Apps" region (carved from the free space in the PY25Q16 map,
|
||||||
|
* after the multiboot markers, before the RX/TX log):
|
||||||
|
*
|
||||||
|
* 0x102000 slot 0 ] 16 slots x 8 KiB = 128 KiB
|
||||||
|
* 0x104000 slot 1 ] each slot: 4 KiB header sector + 4 KiB code
|
||||||
|
* ... ]
|
||||||
|
* 0x120000 slot 15 ]
|
||||||
|
*
|
||||||
|
* Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is
|
||||||
|
* never brick-critical; a bad slot is simply refused at launch by the CRC check.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef APPS_APP_OVERLAY_H
|
||||||
|
#define APPS_APP_OVERLAY_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "app_api.h"
|
||||||
|
|
||||||
|
/* ---- external-flash Apps region ---- */
|
||||||
|
#define APP_REGION_BASE 0x00102000u /* first app slot */
|
||||||
|
#define APP_SLOT_STRIDE 0x00002000u /* 8 KiB per slot */
|
||||||
|
#define APP_CODE_OFFSET 0x00001000u /* code starts after the 4 KiB header sector */
|
||||||
|
#define APP_SECTOR_SIZE 0x00001000u /* external NOR erase granularity */
|
||||||
|
#define APP_SLOT_COUNT 16u
|
||||||
|
#define APP_SLOT_BASE(s) (APP_REGION_BASE + (uint32_t)(s) * APP_SLOT_STRIDE)
|
||||||
|
|
||||||
|
/* ---- overlay RAM budget: the PY25Q16 4 KiB sector cache reused as workspace.
|
||||||
|
* The link VMA itself is pinned in Core/py32f071xb.ld (ORIGIN+0x280) and passed
|
||||||
|
* via compile-app.sh; the loader checks each blob's link_vma against it. ---- */
|
||||||
|
#define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */
|
||||||
|
|
||||||
|
/* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */
|
||||||
|
#define APP_MAGIC 0x31504146u /* "FAP1" */
|
||||||
|
#define APP_HDR_VERSION 1u
|
||||||
|
#define APP_FLAG_COMMITTED (1u << 0)
|
||||||
|
#define APP_NAME_LEN 16
|
||||||
|
#define APP_VERSION_LEN 16
|
||||||
|
|
||||||
|
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 */
|
||||||
|
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) */
|
||||||
|
uint16_t flags; /* APP_FLAG_COMMITTED, ... */
|
||||||
|
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 */
|
||||||
|
} 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_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 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Read + validate a slot header (no CRC of the code). */
|
||||||
|
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header);
|
||||||
|
|
||||||
|
/* Validate, load into the overlay, verify the code CRC, and run the app.
|
||||||
|
* Returns when the app exits; the internal flash is never touched. */
|
||||||
|
uint8_t APP_LaunchOverlay(uint8_t slot);
|
||||||
|
|
||||||
|
/* Host-tool slot management (external flash only, never brick-critical). */
|
||||||
|
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header);
|
||||||
|
uint8_t APP_SlotErase(uint8_t slot);
|
||||||
|
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* APPS_APP_OVERLAY_H */
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Overlay-app link script (POC).
|
||||||
|
*
|
||||||
|
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||||
|
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||||
|
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||||
|
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||||
|
*
|
||||||
|
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||||
|
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||||
|
* mismatch. */
|
||||||
|
|
||||||
|
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||||
|
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||||
|
* The default is only a fallback for a standalone size check. */
|
||||||
|
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||||
|
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||||
|
|
||||||
|
ENTRY(app_main)
|
||||||
|
|
||||||
|
MEMORY {
|
||||||
|
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS {
|
||||||
|
.app APP_VMA : {
|
||||||
|
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||||
|
*(.text .text.*)
|
||||||
|
*(.rodata .rodata.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.data .data.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_start = .;
|
||||||
|
*(.bss .bss.* COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_end = .;
|
||||||
|
} > APP
|
||||||
|
|
||||||
|
__app_end = .;
|
||||||
|
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||||
|
"Beacon overlay app overflows the 4 KiB overlay")
|
||||||
|
|
||||||
|
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Beacon (fox) — overlay app. Ported from App/app/foxhunt.c, beacon sub-mode only.
|
||||||
|
* Turns the radio into a hidden ARDF transmitter: keys up on the TX VFO and repeats
|
||||||
|
* a CW fox identifier (MOE..MO5 / MO / "<call> MOE") as MCW for the TX window, then
|
||||||
|
* stays silent for the idle gap. The carrier stays up during the window; only the
|
||||||
|
* tone is keyed per Morse element.
|
||||||
|
*
|
||||||
|
* Keys: 1 TX window · 2 idle gap · 3 fox id (F reverses) · MENU restart idle
|
||||||
|
* long F keypad lock · EXIT quit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "../app_api.h"
|
||||||
|
|
||||||
|
#define LCD_WIDTH 128
|
||||||
|
#define TICK_MS 50
|
||||||
|
#define LOCK_HOLD_MS 500
|
||||||
|
#define MORSE_UNIT 100
|
||||||
|
#define TONE_HZ 1000
|
||||||
|
#define CALL_MAX 12
|
||||||
|
|
||||||
|
#define IDLE_DEF 30
|
||||||
|
#define IDLE_MIN 5
|
||||||
|
#define IDLE_MAX 240
|
||||||
|
#define IDLE_STEP 5
|
||||||
|
#define TX_DEF 30
|
||||||
|
#define TX_MIN 5
|
||||||
|
#define TX_MAX 60
|
||||||
|
#define TX_STEP 5
|
||||||
|
|
||||||
|
#define FOX_MO 5
|
||||||
|
#define FOX_CALL 6
|
||||||
|
#define FOX_COUNT 7
|
||||||
|
#define MSG_CALL 1
|
||||||
|
#define MSG_ID 3
|
||||||
|
#define MSG_ONE 2
|
||||||
|
|
||||||
|
#define CFG_MAGIC 0xF4
|
||||||
|
|
||||||
|
/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */
|
||||||
|
static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C};
|
||||||
|
static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E};
|
||||||
|
static const char FOX_TAIL[5] = {'E','I','S','H','5'};
|
||||||
|
static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||||
|
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||||
|
|
||||||
|
static const app_api_t *A;
|
||||||
|
|
||||||
|
static bool foxLocked, fArm, fLongDone, running, phaseTx;
|
||||||
|
static uint16_t fHoldMs;
|
||||||
|
static uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
|
||||||
|
static uint16_t txMsLeft;
|
||||||
|
static char foxCall[CALL_MAX+1];
|
||||||
|
static char msg[17];
|
||||||
|
static uint8_t prevKey;
|
||||||
|
static char str[16];
|
||||||
|
|
||||||
|
/* ---- tiny formatting ---- */
|
||||||
|
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||||
|
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||||
|
static char *putu(char *o,uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
|
||||||
|
static void put2(char **o,uint8_t v){ if(v<10)*(*o)++='0'; *o=putu(*o,v); }
|
||||||
|
static void cpy(uint8_t *d,const uint8_t *s,uint8_t n){ while(n--)*d++=*s++; }
|
||||||
|
static const char *findsp(const char *s){ while(*s){ if(*s==' ')return s; s++; } return NULL; }
|
||||||
|
static uint8_t mmin(uint8_t a,uint8_t b){ return a<b?a:b; }
|
||||||
|
|
||||||
|
/* ---- settings cycles ---- */
|
||||||
|
static uint8_t rangeStep(uint8_t v,uint8_t lo,uint8_t hi,uint8_t step,int8_t dir){
|
||||||
|
if(dir>0) return (v>=hi)?lo:(uint8_t)(v+step);
|
||||||
|
return (v<=lo)?hi:(uint8_t)(v-step);
|
||||||
|
}
|
||||||
|
static bool settingKey(uint8_t key,int8_t dir){
|
||||||
|
switch(key){
|
||||||
|
case APP_KEY_1: beaconTx =rangeStep(beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
|
||||||
|
case APP_KEY_2: beaconIdle=rangeStep(beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
|
||||||
|
case APP_KEY_3: foxFox=(uint8_t)((foxFox+(dir>0?1:FOX_COUNT-1))%FOX_COUNT); return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- message ---- */
|
||||||
|
static void buildMsg(void){
|
||||||
|
char *o=msg;
|
||||||
|
if(foxFox==FOX_CALL){
|
||||||
|
if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); }
|
||||||
|
else o=put(o,"MOE");
|
||||||
|
} else if(foxFox==FOX_MO){ o=put(o,"MO"); }
|
||||||
|
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||||
|
*o='\0';
|
||||||
|
}
|
||||||
|
static void foxLabel(char *out){
|
||||||
|
char *o=put(out,"FOX ");
|
||||||
|
if(foxFox==FOX_CALL) o=put(o,"CALL");
|
||||||
|
else if(foxFox==FOX_MO) o=put(o,"MO");
|
||||||
|
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||||
|
*o='\0';
|
||||||
|
}
|
||||||
|
static uint8_t morseByte(char c){
|
||||||
|
if(c>='a'&&c<='z') c-=32;
|
||||||
|
if(c>='A'&&c<='Z') return M_LET[c-'A'];
|
||||||
|
if(c>='0'&&c<='9') return M_DIG[c-'0'];
|
||||||
|
if(c=='/') return 0x32;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- drawing ---- */
|
||||||
|
static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4)); }
|
||||||
|
|
||||||
|
static void chrome(void){
|
||||||
|
A->display_clear();
|
||||||
|
A->status_clear();
|
||||||
|
A->print_inverse("BEACON",2,0,true,true,26);
|
||||||
|
A->draw_battery();
|
||||||
|
if(foxLocked) cpy(A->status_line+69,FONT_LOCK,9);
|
||||||
|
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u;
|
||||||
|
for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
|
||||||
|
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
|
||||||
|
}
|
||||||
|
static void drawTxSeconds(void){
|
||||||
|
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
|
||||||
|
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
|
||||||
|
A->print_normal("TX",1,0,0);
|
||||||
|
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
|
||||||
|
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
|
||||||
|
secShown=sec;
|
||||||
|
}
|
||||||
|
static void drawMessage(uint8_t vis){
|
||||||
|
const char *sp=findsp(msg);
|
||||||
|
if(!sp){
|
||||||
|
uint8_t idLen=slen(msg), v=mmin(vis,idLen);
|
||||||
|
char id[17]; cpy((uint8_t*)id,(const uint8_t*)msg,v); id[v]='\0';
|
||||||
|
A->print_string(id,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ONE,8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t callLen=(uint8_t)(sp-msg), vc=mmin(vis,callLen);
|
||||||
|
char call[CALL_MAX+1]; cpy((uint8_t*)call,(const uint8_t*)msg,vc); call[vc]='\0';
|
||||||
|
A->print_string(call,(uint8_t)((LCD_WIDTH-callLen*8u)/2u),0,MSG_CALL,8);
|
||||||
|
if(vis>callLen+1){
|
||||||
|
const char *id=sp+1; uint8_t idLen=slen(id), vi=mmin((uint8_t)(vis-callLen-1),idLen);
|
||||||
|
char idb[8]; cpy((uint8_t*)idb,(const uint8_t*)id,vi); idb[vi]='\0';
|
||||||
|
A->print_string(idb,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ID,8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void updateProgress(uint8_t vis){
|
||||||
|
const char *sp=findsp(msg); uint8_t top;
|
||||||
|
charsSent=vis;
|
||||||
|
if(!sp) top=MSG_ONE;
|
||||||
|
else { uint8_t callLen=(uint8_t)(sp-msg); if(vis==callLen+1) return; top=(vis<=callLen)?MSG_CALL:MSG_ID; }
|
||||||
|
for(uint8_t x=0;x<LCD_WIDTH;x++){ A->fb[top][x]=0; A->fb[top+1][x]=0; }
|
||||||
|
drawMessage(charsSent);
|
||||||
|
A->blit_line(top); A->blit_line(top+1);
|
||||||
|
}
|
||||||
|
static void beaconDraw(bool txNow,uint8_t il){
|
||||||
|
chrome();
|
||||||
|
if(txNow){
|
||||||
|
cpy(A->status_line+48,BMP_TX,16);
|
||||||
|
drawTxSeconds();
|
||||||
|
drawMessage(charsSent);
|
||||||
|
} else {
|
||||||
|
A->print_string("IDLE",0,127,1,10);
|
||||||
|
char *o=str; put2(&o,il); *o++='s'; *o='\0';
|
||||||
|
A->print_string(str,0,127,3,10);
|
||||||
|
}
|
||||||
|
char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5);
|
||||||
|
o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6);
|
||||||
|
foxLabel(str); tag(str,(uint8_t)(125-slen(str)*4),5);
|
||||||
|
}
|
||||||
|
static void blit(void){ A->blit_status(); A->blit_full(); }
|
||||||
|
|
||||||
|
/* ---- keypad-lock long-press ---- */
|
||||||
|
static void lockTrack(uint8_t key,uint16_t ms){
|
||||||
|
if(key!=APP_KEY_F){ fHoldMs=0; fLongDone=false; return; }
|
||||||
|
if(fLongDone) return;
|
||||||
|
fHoldMs+=ms;
|
||||||
|
if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- interactive TX delay: drains the window, keys, live seconds. true = abort ---- */
|
||||||
|
static bool txDelay(uint16_t ms){
|
||||||
|
while(ms){
|
||||||
|
uint16_t slice=(ms>10)?10:ms;
|
||||||
|
A->delay_ms(slice); ms-=slice;
|
||||||
|
txMsLeft=(txMsLeft>slice)?(uint16_t)(txMsLeft-slice):0;
|
||||||
|
A->backlight_update();
|
||||||
|
if((uint8_t)((txMsLeft+999u)/1000u)!=secShown){ drawTxSeconds(); A->blit_line(0); }
|
||||||
|
|
||||||
|
uint8_t key=A->get_key();
|
||||||
|
lockTrack(key,slice);
|
||||||
|
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; continue; }
|
||||||
|
prevKey=key;
|
||||||
|
A->backlight_on();
|
||||||
|
if(foxLocked) continue;
|
||||||
|
if(key==APP_KEY_EXIT){ running=false; return true; }
|
||||||
|
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
|
||||||
|
if(key==APP_KEY_F){ fArm=!fArm; beaconDraw(true,0); blit(); continue; }
|
||||||
|
if(settingKey(key,fArm?-1:1)){ fArm=false; beaconDraw(true,0); blit(); }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static bool morseChar(char c,uint8_t vis){
|
||||||
|
uint8_t code=morseByte(c);
|
||||||
|
if(code==0){ if(txDelay(MORSE_UNIT*4)) return true; updateProgress(vis); return false; }
|
||||||
|
uint8_t bit=0x80; while(!(code&bit))bit>>=1;
|
||||||
|
for(bit>>=1;bit;bit>>=1){
|
||||||
|
uint16_t on=(code&bit)?(MORSE_UNIT*3):MORSE_UNIT;
|
||||||
|
A->tx_mute(false);
|
||||||
|
if(txDelay(on)){ A->tx_mute(true); return true; }
|
||||||
|
A->tx_mute(true);
|
||||||
|
if(txDelay(MORSE_UNIT)) return true;
|
||||||
|
}
|
||||||
|
updateProgress(vis);
|
||||||
|
return txDelay(MORSE_UNIT*2);
|
||||||
|
}
|
||||||
|
static void transmit(void){
|
||||||
|
A->tx_set_params();
|
||||||
|
A->tx_tone(TONE_HZ);
|
||||||
|
A->tx_mute(true);
|
||||||
|
txMsLeft=(uint16_t)beaconTx*1000u;
|
||||||
|
bool stop=false;
|
||||||
|
while(!stop && txMsLeft>0){
|
||||||
|
buildMsg();
|
||||||
|
charsSent=0;
|
||||||
|
beaconDraw(true,0); blit();
|
||||||
|
for(uint8_t i=0;!stop && msg[i];i++) stop=morseChar(msg[i],(uint8_t)(i+1));
|
||||||
|
if(!stop && txMsLeft>0) stop=txDelay(MORSE_UNIT*7);
|
||||||
|
}
|
||||||
|
A->tx_mute(true);
|
||||||
|
A->tx_end(); /* PA off + restore RX */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- idle-phase keys ---- */
|
||||||
|
static void idleKeys(void){
|
||||||
|
uint8_t key=A->get_key();
|
||||||
|
lockTrack(key,TICK_MS);
|
||||||
|
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
|
||||||
|
prevKey=key;
|
||||||
|
A->backlight_on();
|
||||||
|
if(foxLocked) return;
|
||||||
|
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||||
|
switch(key){
|
||||||
|
case APP_KEY_EXIT: running=false; break;
|
||||||
|
case APP_KEY_MENU: idleLeft=beaconIdle; idleTick=0; break;
|
||||||
|
default: settingKey(key,fArm?-1:1); break;
|
||||||
|
}
|
||||||
|
fArm=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tickDelay(void){ for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); } }
|
||||||
|
|
||||||
|
/* ---- config (deferred) ---- */
|
||||||
|
static void loadConfig(void){
|
||||||
|
uint8_t c[4]; A->cfg_load(c,4);
|
||||||
|
if(c[0]==CFG_MAGIC){
|
||||||
|
if(c[1]>=IDLE_MIN&&c[1]<=IDLE_MAX&&(c[1]%IDLE_STEP)==0) beaconIdle=c[1];
|
||||||
|
if(c[2]<FOX_COUNT) foxFox=c[2];
|
||||||
|
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&(c[3]%TX_STEP)==0) beaconTx=c[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void saveConfig(void){ uint8_t c[4]={CFG_MAGIC,beaconIdle,foxFox,beaconTx}; A->cfg_save(c,4); }
|
||||||
|
|
||||||
|
static void txDenied(void){
|
||||||
|
chrome();
|
||||||
|
A->print_string("TX OFF",0,127,1,8);
|
||||||
|
blit();
|
||||||
|
for(uint8_t i=0;i<20;i++) tickDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((section(".text.entry"),used))
|
||||||
|
void app_main(const app_api_t *api){
|
||||||
|
A=api;
|
||||||
|
foxLocked=fArm=fLongDone=false; fHoldMs=0;
|
||||||
|
beaconIdle=IDLE_DEF; beaconTx=TX_DEF; foxFox=FOX_CALL;
|
||||||
|
prevKey=APP_KEY_INVALID;
|
||||||
|
A->boot_callsign(foxCall,sizeof(foxCall));
|
||||||
|
loadConfig();
|
||||||
|
A->backlight_on();
|
||||||
|
|
||||||
|
phaseTx=true; idleLeft=beaconIdle; idleTick=0;
|
||||||
|
running=true;
|
||||||
|
while(running){
|
||||||
|
if(phaseTx){
|
||||||
|
if(A->tx_state()!=0){ txDenied(); phaseTx=false; idleLeft=beaconIdle; idleTick=0; continue; }
|
||||||
|
transmit();
|
||||||
|
phaseTx=false; idleLeft=beaconIdle; idleTick=0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
idleKeys();
|
||||||
|
if(!running) break;
|
||||||
|
beaconDraw(false,idleLeft); blit();
|
||||||
|
if(++idleTick>=(1000/TICK_MS)){ idleTick=0; if(idleLeft>0)idleLeft--; if(idleLeft==0)phaseTx=true; }
|
||||||
|
A->battery_sample();
|
||||||
|
tickDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
A->tx_end(); /* safety: PA off + RX restored */
|
||||||
|
A->audio_path(false);
|
||||||
|
}
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||||
|
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||||
|
# derived from the directory name; only APP_NAME - the human label baked into
|
||||||
|
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||||
|
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||||
|
APP_NAME="Beacon" # <-- the only per-app line
|
||||||
|
APP_VER="1.0"
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||||
|
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||||
|
|
||||||
|
CC=/opt/toolchain/bin/arm-none-eabi-gcc
|
||||||
|
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
|
||||||
|
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
|
||||||
|
|
||||||
|
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
|
||||||
|
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||||
|
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||||
|
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
|
||||||
|
|
||||||
|
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||||
|
|
||||||
|
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||||
|
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
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
BYTES=$(wc -c < "${APP}.bin")
|
||||||
|
if [ "$BYTES" -gt 4096 ]; then
|
||||||
|
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||||
|
fi
|
||||||
|
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||||
|
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Overlay-app link script (POC).
|
||||||
|
*
|
||||||
|
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||||
|
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||||
|
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||||
|
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||||
|
*
|
||||||
|
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||||
|
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||||
|
* mismatch. */
|
||||||
|
|
||||||
|
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||||
|
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||||
|
* The default is only a fallback for a standalone size check. */
|
||||||
|
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||||
|
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||||
|
|
||||||
|
ENTRY(app_main)
|
||||||
|
|
||||||
|
MEMORY {
|
||||||
|
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS {
|
||||||
|
.app APP_VMA : {
|
||||||
|
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||||
|
*(.text .text.*)
|
||||||
|
*(.rodata .rodata.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.data .data.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_start = .;
|
||||||
|
*(.bss .bss.* COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_end = .;
|
||||||
|
} > APP
|
||||||
|
|
||||||
|
__app_end = .;
|
||||||
|
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||||
|
"Breakout overlay app overflows the 4 KiB overlay")
|
||||||
|
|
||||||
|
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
/* Copyright 2025 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Breakout — overlay-app port (POC).
|
||||||
|
*
|
||||||
|
* Self-contained: no firmware headers, no libc. Every resident service is
|
||||||
|
* reached through the app_api_t table; the entry point app_main() is forced to
|
||||||
|
* blob offset 0 via section ".text.entry". The loader zeroes the 4 KiB overlay,
|
||||||
|
* copies this blob in, and calls app_main(&api).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "../app_api.h"
|
||||||
|
|
||||||
|
#define LCD_WIDTH 128
|
||||||
|
#define BRICK_NUMBER 18
|
||||||
|
#define BALL_NUMBER 5
|
||||||
|
|
||||||
|
#define BRICK_WIDTH 14
|
||||||
|
#define BRICK_HEIGHT 5
|
||||||
|
#define BALL_WIDTH 3
|
||||||
|
#define BALL_HEIGHT 3
|
||||||
|
#define RACKET_WIDTH 24
|
||||||
|
#define RACKET_HEIGHT 2
|
||||||
|
#define RACKET_Y 50
|
||||||
|
|
||||||
|
typedef struct { uint8_t x; uint8_t y; bool destroy; } Brick;
|
||||||
|
typedef struct { int8_t x; uint8_t p; } Racket;
|
||||||
|
typedef struct { int16_t x; int8_t y; int8_t dx; int8_t dy; } Ball;
|
||||||
|
|
||||||
|
/* ---- tiny freestanding helpers (replace libc / libgcc-free where cheap) ---- */
|
||||||
|
void *memset(void *d, int c, size_t n) {
|
||||||
|
uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d;
|
||||||
|
}
|
||||||
|
void *memcpy(void *d, const void *s, size_t n) {
|
||||||
|
uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d;
|
||||||
|
}
|
||||||
|
static int iabs(int v) { return v < 0 ? -v : v; }
|
||||||
|
static int imin(int a, int b) { return a < b ? a : b; }
|
||||||
|
|
||||||
|
/* zero-padded unsigned -> string, width w (<= 5). Replaces sprintf. */
|
||||||
|
static void u2str(char *out, const char *label, uint16_t v, uint8_t w) {
|
||||||
|
char *o = out;
|
||||||
|
while (*label) *o++ = *label++;
|
||||||
|
char tmp[6]; int8_t n = 0;
|
||||||
|
do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v && n < 6);
|
||||||
|
while (n < w) tmp[n++] = '0';
|
||||||
|
while (n--) *o++ = tmp[n];
|
||||||
|
*o = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- app-wide handle to the resident ABI, set once at entry ---- */
|
||||||
|
static const app_api_t *A;
|
||||||
|
|
||||||
|
/* ---- game state (lives in the overlay; re-initialised at entry) ---- */
|
||||||
|
static uint32_t randSeed;
|
||||||
|
static uint8_t blockAnim;
|
||||||
|
static bool isInitialized;
|
||||||
|
static bool isPaused;
|
||||||
|
static uint8_t levelCountBreakout;
|
||||||
|
static uint16_t tone;
|
||||||
|
static uint16_t score;
|
||||||
|
static int16_t ballCount;
|
||||||
|
static char str[12];
|
||||||
|
static uint8_t kbdPrev, kbdCur;
|
||||||
|
|
||||||
|
static Brick brick[BRICK_NUMBER];
|
||||||
|
static Racket racket;
|
||||||
|
static Ball ball;
|
||||||
|
|
||||||
|
static const uint8_t BRICK_ANIM_PATTERNS[4] =
|
||||||
|
{0b00110001, 0b00101001, 0b00100101, 0b00100011};
|
||||||
|
|
||||||
|
static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; }
|
||||||
|
static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; }
|
||||||
|
static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); }
|
||||||
|
|
||||||
|
static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; }
|
||||||
|
|
||||||
|
static void playBeep(uint16_t t) { A->play_tone(t, 100); }
|
||||||
|
|
||||||
|
static void drawScore(void) {
|
||||||
|
A->status_clear();
|
||||||
|
u2str(str, "Level ", levelCountBreakout, 2); A->print_tiny(str, 0, 1, true, true);
|
||||||
|
u2str(str, "Ball ", (ballCount < 0) ? 0 : ballCount, 2); A->print_tiny(str, 45, 1, true, true);
|
||||||
|
u2str(str, "Score ", score, 4); A->print_tiny(str, 88, 1, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderBall(bool state) {
|
||||||
|
A->draw_rect(A->fb, ball.x, ball.y, ball.x + BALL_WIDTH - 1, ball.y + BALL_HEIGHT - 1, state);
|
||||||
|
A->draw_line(A->fb, ball.x - 1, ball.y + 1, ball.x + BALL_WIDTH, ball.y + 1, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initBall(void) { ball.x = 62; ball.y = 30; ball.dx = 0; ball.dy = 1; renderBall(true); }
|
||||||
|
|
||||||
|
static void directionBall(int16_t x, uint8_t w, int8_t num) {
|
||||||
|
ball.dx = (int16_t)(x + w - ball.x) * (-num - num) / w + num;
|
||||||
|
ball.dy *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initWall(void) {
|
||||||
|
Brick *current = brick;
|
||||||
|
for (uint8_t y = 0; y < 24; y += 8)
|
||||||
|
for (uint8_t x = 6; x < 126; x += 20) {
|
||||||
|
current->x = x; current->y = y; current->destroy = false; current++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initRacket(void);
|
||||||
|
|
||||||
|
static void drawBall(void) {
|
||||||
|
renderBall(false);
|
||||||
|
ball.x += ball.dx; ball.y += ball.dy;
|
||||||
|
|
||||||
|
if (ball.y <= 0) { ball.dx = randInt(-3, 3); ball.dy = 1; }
|
||||||
|
else if (ball.x <= 2) { ball.dx = iabs(ball.dx); }
|
||||||
|
else if (ball.x >= 124) { ball.dx = -iabs(ball.dx); }
|
||||||
|
|
||||||
|
if (ball.y == 47) {
|
||||||
|
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
|
||||||
|
directionBall(racket.x, RACKET_WIDTH, 3); tone = 400;
|
||||||
|
}
|
||||||
|
} else if (ball.y > 49) {
|
||||||
|
ballCount--;
|
||||||
|
A->display_clear();
|
||||||
|
drawScore();
|
||||||
|
tone = 800;
|
||||||
|
if (ballCount < 0) {
|
||||||
|
reset(); initWall(); /* drawWall drawn by main loop */
|
||||||
|
isPaused = true;
|
||||||
|
A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4);
|
||||||
|
}
|
||||||
|
initRacket(); initBall();
|
||||||
|
}
|
||||||
|
renderBall(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawWall(void) {
|
||||||
|
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
|
||||||
|
if (brick[i].destroy) continue;
|
||||||
|
uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x;
|
||||||
|
fb_ptr[0] = 0b00011110;
|
||||||
|
fb_ptr[14] = 0b00011110;
|
||||||
|
|
||||||
|
if ((ball.x + 1 >= brick[i].x && ball.x - 1 <= brick[i].x + BRICK_WIDTH) &&
|
||||||
|
(ball.y + 1 >= brick[i].y && ball.y - 1 <= brick[i].y + BRICK_HEIGHT)) {
|
||||||
|
brick[i].destroy = true; score++;
|
||||||
|
directionBall(brick[i].x, BRICK_WIDTH, 2);
|
||||||
|
|
||||||
|
A->led(true);
|
||||||
|
memset(fb_ptr + 1, 0b00111111, 13);
|
||||||
|
A->blit_line(brick[i].y / 8);
|
||||||
|
playBeep(600);
|
||||||
|
memset(fb_ptr + 0, 0b00000000, 15);
|
||||||
|
A->blit_line(brick[i].y / 8);
|
||||||
|
A->led(false);
|
||||||
|
|
||||||
|
if (score % BRICK_NUMBER == 0) {
|
||||||
|
levelCountBreakout++; ballCount++; initWall(); return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (uint8_t k = 0; k < 13; k++)
|
||||||
|
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderRacket(int x, bool state) {
|
||||||
|
A->draw_rect(A->fb, x + 1, RACKET_Y, x + RACKET_WIDTH - 2, RACKET_Y + RACKET_HEIGHT, state);
|
||||||
|
A->draw_line(A->fb, x, RACKET_Y + 1, x + RACKET_WIDTH - 1, RACKET_Y + 1, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initRacket(void) { racket.x = 64 - (RACKET_WIDTH / 2); racket.p = racket.x; renderRacket(racket.x, true); }
|
||||||
|
|
||||||
|
static void drawRacket(void) {
|
||||||
|
if (racket.p != racket.x) {
|
||||||
|
renderRacket(racket.p, false);
|
||||||
|
racket.p = racket.x;
|
||||||
|
renderRacket(racket.x, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnKeyDown(uint8_t key) {
|
||||||
|
bool wasPaused = isPaused;
|
||||||
|
switch (key) {
|
||||||
|
case APP_KEY_4:
|
||||||
|
case APP_KEY_UP: if (!isPaused && racket.x > 0) racket.x -= 2; isPaused = false; break;
|
||||||
|
case APP_KEY_0:
|
||||||
|
case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break;
|
||||||
|
case APP_KEY_MENU:
|
||||||
|
isPaused = !isPaused;
|
||||||
|
if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4);
|
||||||
|
break;
|
||||||
|
case APP_KEY_EXIT: isPaused = false; isInitialized = false; break;
|
||||||
|
}
|
||||||
|
if (wasPaused && !isPaused)
|
||||||
|
for (uint8_t i = 0; i < 8; i++)
|
||||||
|
A->draw_line(A->fb, 32, 32 + i, 96, 32 + i, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleInput(void) {
|
||||||
|
kbdPrev = kbdCur;
|
||||||
|
kbdCur = A->get_key();
|
||||||
|
if (kbdCur == APP_KEY_INVALID) return;
|
||||||
|
if (kbdCur == APP_KEY_UP || kbdCur == APP_KEY_DOWN ||
|
||||||
|
kbdCur == APP_KEY_4 || kbdCur == APP_KEY_0 || kbdCur != kbdPrev)
|
||||||
|
OnKeyDown(kbdCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- entry point, pinned to blob offset 0 ---- */
|
||||||
|
__attribute__((section(".text.entry"), used))
|
||||||
|
void app_main(const app_api_t *api) {
|
||||||
|
A = api;
|
||||||
|
|
||||||
|
#ifdef APP_POC_HELLO
|
||||||
|
/* Minimal bisection test: prove jump + ABI + return work, no game body. */
|
||||||
|
A->display_clear();
|
||||||
|
A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3);
|
||||||
|
A->blit_full();
|
||||||
|
A->delay_ms(2000);
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* reset all state (the overlay is not persistent across launches) */
|
||||||
|
blockAnim = 0; isPaused = false; tone = 0; score = 0;
|
||||||
|
kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID;
|
||||||
|
uint8_t swap = 0;
|
||||||
|
|
||||||
|
srand_custom(api->seed);
|
||||||
|
A->led(false);
|
||||||
|
A->backlight_tick();
|
||||||
|
|
||||||
|
A->display_clear();
|
||||||
|
reset(); initWall(); initRacket(); initBall();
|
||||||
|
A->status_clear();
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
while (isInitialized) {
|
||||||
|
handleInput();
|
||||||
|
if (!isPaused) {
|
||||||
|
if (swap == 0) blockAnim = (blockAnim + 1) % 4;
|
||||||
|
swap = (swap + 1) % 4;
|
||||||
|
drawScore(); drawWall(); drawRacket(); drawBall();
|
||||||
|
if (tone != 0) { playBeep(tone); tone = 0; }
|
||||||
|
else A->delay_ms(40 - imin(levelCountBreakout - 1, 20));
|
||||||
|
}
|
||||||
|
A->blit_status();
|
||||||
|
A->blit_full();
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||||
|
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||||
|
# derived from the directory name; only APP_NAME - the human label baked into
|
||||||
|
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||||
|
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||||
|
APP_NAME="Breakout" # <-- the only per-app line
|
||||||
|
APP_VER="1.0"
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||||
|
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||||
|
|
||||||
|
CC=/opt/toolchain/bin/arm-none-eabi-gcc
|
||||||
|
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
|
||||||
|
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
|
||||||
|
|
||||||
|
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
|
||||||
|
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||||
|
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||||
|
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
|
||||||
|
|
||||||
|
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||||
|
|
||||||
|
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||||
|
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
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
BYTES=$(wc -c < "${APP}.bin")
|
||||||
|
if [ "$BYTES" -gt 4096 ]; then
|
||||||
|
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||||
|
fi
|
||||||
|
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||||
|
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Overlay-app link script (POC).
|
||||||
|
*
|
||||||
|
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||||
|
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||||
|
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||||
|
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||||
|
*
|
||||||
|
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||||
|
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||||
|
* mismatch. */
|
||||||
|
|
||||||
|
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||||
|
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||||
|
* The default is only a fallback for a standalone size check. */
|
||||||
|
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||||
|
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||||
|
|
||||||
|
ENTRY(app_main)
|
||||||
|
|
||||||
|
MEMORY {
|
||||||
|
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS {
|
||||||
|
.app APP_VMA : {
|
||||||
|
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||||
|
*(.text .text.*)
|
||||||
|
*(.rodata .rodata.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.data .data.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_start = .;
|
||||||
|
*(.bss .bss.* COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_end = .;
|
||||||
|
} > APP
|
||||||
|
|
||||||
|
__app_end = .;
|
||||||
|
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||||
|
"Broadcast FM overlay app overflows the 4 KiB overlay")
|
||||||
|
|
||||||
|
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||||
|
}
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||||
|
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||||
|
# derived from the directory name; only APP_NAME - the human label baked into
|
||||||
|
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||||
|
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||||
|
APP_NAME="Broadcast FM" # <-- the only per-app line
|
||||||
|
APP_VER="1.0"
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||||
|
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||||
|
|
||||||
|
CC=/opt/toolchain/bin/arm-none-eabi-gcc
|
||||||
|
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
|
||||||
|
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
|
||||||
|
|
||||||
|
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
|
||||||
|
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||||
|
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||||
|
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
|
||||||
|
|
||||||
|
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||||
|
|
||||||
|
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||||
|
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
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
BYTES=$(wc -c < "${APP}.bin")
|
||||||
|
if [ "$BYTES" -gt 4096 ]; then
|
||||||
|
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||||
|
fi
|
||||||
|
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||||
|
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Broadcast FM — overlay app. Sovereign: drives the BK1080 directly and owns the
|
||||||
|
* radio while running (NO BK4819 dual-watch, unlike the resident FM). Reimplements
|
||||||
|
* the full resident FM (fm.c / UI_DisplayFM) - VFO + 48 memories - sharing the
|
||||||
|
* resident channel table (gFM_Channels) and config (gEeprom.FM_*), committed to
|
||||||
|
* EEPROM on exit. The BK1080 audio is a hardware path, so playback needs no CPU loop.
|
||||||
|
*
|
||||||
|
* Keys (iso legacy): 0-9 = frequency (VFO) / channel (MR) entry · UP/DOWN = tune /
|
||||||
|
* channel step · STAR = manual scan · F+STAR = auto-scan (stores 48) · MENU = save
|
||||||
|
* (VFO) / delete (MR) · F+1 = band · F+3 = VFO<->MR · F+0 / EXIT = quit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "../app_api.h"
|
||||||
|
|
||||||
|
#define STEP 1
|
||||||
|
#define SCAN_SETTLE 100
|
||||||
|
#define CHMAX APP_FM_CH_MAX
|
||||||
|
|
||||||
|
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
|
||||||
|
static const uint8_t FONT_F[8] = {0x7f,0x00,0x76,0x76,0x76,0x76,0x7e,0x7f};
|
||||||
|
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||||
|
|
||||||
|
static const app_api_t *A;
|
||||||
|
static uint16_t *ch; /* shared gFM_Channels[48] */
|
||||||
|
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
|
||||||
|
static uint16_t lo, hi;
|
||||||
|
static int8_t scanState; /* 0 = off, +1 up, -1 down (non-blocking) */
|
||||||
|
static bool autoScan;
|
||||||
|
static uint8_t chPos;
|
||||||
|
static uint16_t scanTimer; /* ms until the next scan step */
|
||||||
|
static bool foundFreq;
|
||||||
|
static bool askSave, askDelete;
|
||||||
|
static uint8_t savePos;
|
||||||
|
static bool fArm;
|
||||||
|
static uint8_t inBox[4], inIdx;
|
||||||
|
static char str[16];
|
||||||
|
|
||||||
|
static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
|
||||||
|
static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
|
||||||
|
|
||||||
|
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
|
||||||
|
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
|
||||||
|
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
|
||||||
|
static void dirty(void){ /* nothing yet; committed on exit */ }
|
||||||
|
|
||||||
|
static bool validCh(uint8_t c){ return c<CHMAX && ch[c]>=lo && ch[c]<hi; }
|
||||||
|
static uint8_t findNext(uint8_t c, int8_t dir)
|
||||||
|
{
|
||||||
|
for(uint8_t i=0;i<CHMAX;i++){
|
||||||
|
if(c==0xFF) c=CHMAX-1;
|
||||||
|
else if(c>=CHMAX) c=0;
|
||||||
|
if(validCh(c)) return c;
|
||||||
|
c=(uint8_t)(c+dir);
|
||||||
|
}
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
static int configChannel(void)
|
||||||
|
{
|
||||||
|
st.freq_playing=st.sel_freq;
|
||||||
|
if(st.is_mr){
|
||||||
|
uint8_t c=findNext(st.sel_ch,+1);
|
||||||
|
if(c==0xFF){ st.is_mr=false; return -1; }
|
||||||
|
st.sel_ch=c; st.freq_playing=ch[c];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- drawing (mirror UI_DisplayFM) ---- */
|
||||||
|
static void freqBig(uint16_t f)
|
||||||
|
{
|
||||||
|
uint16_t mhz=f/10u; char *o=str;
|
||||||
|
if(mhz<100u)*o++=' ';
|
||||||
|
o=putu(o,mhz); *o++='.'; *o++=(char)('0'+f%10u); *o='\0';
|
||||||
|
A->display_freq(str,36,1,true);
|
||||||
|
}
|
||||||
|
/* append a zero-padded 2-digit number (1..99), return the end pointer */
|
||||||
|
static char *num2(char *o, uint8_t n){ if(n<10)*o++='0'; return putu(o,n); }
|
||||||
|
|
||||||
|
static void draw(void)
|
||||||
|
{
|
||||||
|
char b[12], *o;
|
||||||
|
|
||||||
|
A->display_clear();
|
||||||
|
A->status_clear();
|
||||||
|
A->draw_battery();
|
||||||
|
if(fArm) cpy(A->status_line+69,FONT_F,8); /* F armed indicator */
|
||||||
|
A->print_string("FM",2,0,0,8);
|
||||||
|
A->print_normal(BAND_NAME[st.band],1,0,6);
|
||||||
|
|
||||||
|
/* ---- status (line 3) ---- */
|
||||||
|
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
|
||||||
|
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
|
||||||
|
else if(scanState) {
|
||||||
|
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
|
||||||
|
else o=put(b,"M-SCAN");
|
||||||
|
*o='\0';
|
||||||
|
}
|
||||||
|
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
|
||||||
|
else {
|
||||||
|
o=put(b,"VFO"); *o='\0';
|
||||||
|
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
|
||||||
|
}
|
||||||
|
A->print_string(b,0,127,3,10);
|
||||||
|
|
||||||
|
/* ---- line 1: input in progress / channel prompt / frequency ---- */
|
||||||
|
if(inIdx>0){
|
||||||
|
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
|
||||||
|
o=put(b,"CH-");
|
||||||
|
*o++=(char)('0'+inBox[0]);
|
||||||
|
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
|
||||||
|
*o='\0';
|
||||||
|
A->print_string(b,0,127,1,10);
|
||||||
|
} else { /* VFO frequency: "ddd.d" */
|
||||||
|
b[0]=(char)('0'+inBox[0]);
|
||||||
|
b[1]=(inIdx>1)?(char)('0'+inBox[1]):'-';
|
||||||
|
b[2]=(inIdx>2)?(char)('0'+inBox[2]):'-';
|
||||||
|
b[3]='.';
|
||||||
|
b[4]=(inIdx>3)?(char)('0'+inBox[3]):'-';
|
||||||
|
b[5]='\0';
|
||||||
|
A->display_freq(b,36,1,false);
|
||||||
|
}
|
||||||
|
} else if(askSave){ /* pick a save slot */
|
||||||
|
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
|
||||||
|
A->print_string(b,0,127,1,10);
|
||||||
|
} else if(askDelete){
|
||||||
|
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
|
||||||
|
A->print_string(b,0,127,1,10);
|
||||||
|
} else {
|
||||||
|
freqBig(st.freq_playing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void show(void){ draw(); A->blit_status(); A->blit_full(); }
|
||||||
|
|
||||||
|
/* ---- non-blocking scan state machine (mirrors ACTION_Scan_FM / FM_Play / FM_Tune) ---- */
|
||||||
|
static void scanTuneNext(void) /* FM_Tune(freq, scanState, false): step + mute + settle */
|
||||||
|
{
|
||||||
|
A->fm_mute(true);
|
||||||
|
foundFreq=false;
|
||||||
|
st.freq_playing=wrap((uint16_t)(st.freq_playing+scanState));
|
||||||
|
tune();
|
||||||
|
scanTimer=SCAN_SETTLE;
|
||||||
|
}
|
||||||
|
static void stopScan(void) /* FM_PlayAndUpdate */
|
||||||
|
{
|
||||||
|
if(autoScan){ st.is_mr=true; st.sel_ch=0; }
|
||||||
|
scanState=0; autoScan=false;
|
||||||
|
configChannel();
|
||||||
|
tune();
|
||||||
|
A->fm_mute(false);
|
||||||
|
}
|
||||||
|
static void scanStep(void) /* FM_Play: one step when the settle timer expires */
|
||||||
|
{
|
||||||
|
if(A->fm_valid(st.freq_playing,lo)==0){ /* station locked */
|
||||||
|
if(!autoScan){
|
||||||
|
scanState=0; foundFreq=true;
|
||||||
|
if(!st.is_mr) st.sel_freq=st.freq_playing;
|
||||||
|
A->fm_mute(false); /* audio on */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(chPos<CHMAX) ch[chPos++]=st.freq_playing;
|
||||||
|
if(chPos>=CHMAX){ stopScan(); return; }
|
||||||
|
}
|
||||||
|
if(autoScan && st.freq_playing>=A->fm_hi(1)){ stopScan(); return; }
|
||||||
|
scanTuneNext();
|
||||||
|
}
|
||||||
|
static void startManual(int8_t dir)
|
||||||
|
{
|
||||||
|
if(scanState){ stopScan(); return; } /* STAR toggles */
|
||||||
|
autoScan=false; chPos=0; scanState=dir;
|
||||||
|
scanTuneNext();
|
||||||
|
}
|
||||||
|
static void startAuto(void)
|
||||||
|
{
|
||||||
|
if(scanState){ stopScan(); return; }
|
||||||
|
autoScan=true; chPos=0; scanState=+1;
|
||||||
|
for(uint8_t i=0;i<CHMAX;i++) ch[i]=0xFFFF;
|
||||||
|
st.freq_playing=lo;
|
||||||
|
A->fm_mute(true);
|
||||||
|
tune(); /* FM_Tune(lo,1,true): no step */
|
||||||
|
scanTimer=SCAN_SETTLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- digit entry ---- */
|
||||||
|
static void digit(uint8_t d)
|
||||||
|
{
|
||||||
|
if(askDelete||scanState) return;
|
||||||
|
if(askSave || st.is_mr){ /* 2-digit channel */
|
||||||
|
if(inIdx<2) inBox[inIdx++]=d;
|
||||||
|
if(inIdx>=2){
|
||||||
|
uint8_t c=(uint8_t)(inBox[0]*10+inBox[1]-1); inIdx=0;
|
||||||
|
if(askSave){ if(c<CHMAX) savePos=c; }
|
||||||
|
else if(validCh(c)){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* VFO frequency: 4 digits, with the leading-digit>1 zero-pad trick */
|
||||||
|
inBox[inIdx++]=d;
|
||||||
|
if(inIdx==1 && inBox[0]>1){ inBox[1]=inBox[0]; inBox[0]=0; inIdx=2; }
|
||||||
|
else if(inIdx>3){
|
||||||
|
uint16_t f=(uint16_t)(inBox[0]*1000+inBox[1]*100+inBox[2]*10+inBox[3]); inIdx=0;
|
||||||
|
if(f>=lo && f<=hi){ st.sel_freq=f; st.freq_playing=f; tune(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- MENU: save (VFO) / delete (MR) ---- */
|
||||||
|
static void menu(void)
|
||||||
|
{
|
||||||
|
inIdx=0;
|
||||||
|
if(!st.is_mr){ /* VFO: save */
|
||||||
|
if(askSave){ ch[savePos]=st.freq_playing; dirty(); }
|
||||||
|
askSave=!askSave;
|
||||||
|
} else { /* MR: delete */
|
||||||
|
if(askDelete){ ch[st.sel_ch]=0xFFFF; configChannel(); tune(); dirty(); }
|
||||||
|
askDelete=!askDelete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- UP/DOWN: tune (VFO) or channel step (MR) ---- */
|
||||||
|
static void upDown(int8_t step)
|
||||||
|
{
|
||||||
|
if(scanState){ scanState=step; scanTuneNext(); return; } /* continue scan, new direction */
|
||||||
|
if(askSave){ savePos=(uint8_t)((savePos+step+CHMAX)%CHMAX); return; }
|
||||||
|
if(st.is_mr){
|
||||||
|
uint8_t c=findNext((uint8_t)(st.sel_ch+step),step);
|
||||||
|
if(c!=0xFF && c!=st.sel_ch){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
|
||||||
|
} else {
|
||||||
|
st.sel_freq=wrap((uint16_t)(st.sel_freq+step));
|
||||||
|
st.freq_playing=st.sel_freq; tune();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void toggleMr(void)
|
||||||
|
{
|
||||||
|
st.is_mr=!st.is_mr;
|
||||||
|
if(configChannel()!=0){ /* no valid channel: stayed VFO */ }
|
||||||
|
tune();
|
||||||
|
askSave=askDelete=false; inIdx=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool running;
|
||||||
|
|
||||||
|
static void cycleBand(void)
|
||||||
|
{
|
||||||
|
st.band=(uint8_t)((st.band+1)&3u); bandLimits();
|
||||||
|
if(st.freq_playing<lo||st.freq_playing>hi){ st.freq_playing=lo; st.sel_freq=lo; }
|
||||||
|
tune();
|
||||||
|
}
|
||||||
|
/* F+key and long-press functions (band / VFO<->MR / auto-scan / quit). */
|
||||||
|
static void func(uint8_t key)
|
||||||
|
{
|
||||||
|
switch(key){
|
||||||
|
case APP_KEY_0: running=false; break;
|
||||||
|
case APP_KEY_1: cycleBand(); break;
|
||||||
|
case APP_KEY_3: toggleMr(); break;
|
||||||
|
case APP_KEY_STAR: startAuto(); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void onShort(uint8_t key)
|
||||||
|
{
|
||||||
|
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||||
|
if(fArm){ fArm=false; func(key); return; } /* F + key */
|
||||||
|
if(key<=APP_KEY_9){ digit(key); return; }
|
||||||
|
switch(key){
|
||||||
|
case APP_KEY_UP: upDown(+1); break;
|
||||||
|
case APP_KEY_DOWN: upDown(-1); break;
|
||||||
|
case APP_KEY_STAR: startManual(+1); break;
|
||||||
|
case APP_KEY_MENU: menu(); break;
|
||||||
|
case APP_KEY_EXIT:
|
||||||
|
if(inIdx) inIdx--;
|
||||||
|
else if(askSave||askDelete) askSave=askDelete=false;
|
||||||
|
else running=false;
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void onLong(uint8_t key){ fArm=false; func(key); } /* long band / MR / scan / quit */
|
||||||
|
|
||||||
|
__attribute__((section(".text.entry"),used))
|
||||||
|
void app_main(const app_api_t *api)
|
||||||
|
{
|
||||||
|
A=api;
|
||||||
|
ch=A->fm_channels;
|
||||||
|
A->fm_state(&st,false); /* inherit the resident FM state */
|
||||||
|
if(st.band>3) st.band=0;
|
||||||
|
bandLimits();
|
||||||
|
if(st.freq_playing<lo||st.freq_playing>hi) st.freq_playing=lo;
|
||||||
|
if(st.sel_freq<lo||st.sel_freq>hi) st.sel_freq=st.freq_playing;
|
||||||
|
if(st.is_mr) configChannel();
|
||||||
|
askSave=askDelete=fArm=false; inIdx=0; savePos=0;
|
||||||
|
|
||||||
|
A->fm_enter(st.freq_playing,st.band);
|
||||||
|
|
||||||
|
/* short = on release (held < ~0.5 s) · long = at the 0.5 s threshold ·
|
||||||
|
UP/DOWN = immediate step on press, then auto-repeat while held. */
|
||||||
|
running=true;
|
||||||
|
uint8_t held=APP_KEY_INVALID;
|
||||||
|
uint16_t heldMs=0;
|
||||||
|
bool firedLong=false;
|
||||||
|
bool dirty=true; /* redraw only on change */
|
||||||
|
|
||||||
|
while(running){
|
||||||
|
uint8_t key=A->get_key();
|
||||||
|
bool rep=(key==APP_KEY_UP||key==APP_KEY_DOWN);
|
||||||
|
|
||||||
|
if(key==APP_KEY_INVALID){
|
||||||
|
if(held!=APP_KEY_INVALID && !firedLong &&
|
||||||
|
held!=APP_KEY_UP && held!=APP_KEY_DOWN){
|
||||||
|
onShort(held); dirty=true;
|
||||||
|
}
|
||||||
|
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
|
||||||
|
} else if(key!=held){
|
||||||
|
held=key; heldMs=0; firedLong=false;
|
||||||
|
if(rep){ onShort(key); dirty=true; } /* immediate first step */
|
||||||
|
} else {
|
||||||
|
heldMs=(uint16_t)(heldMs+50u);
|
||||||
|
if(rep){
|
||||||
|
if(heldMs>=300){ onShort(held); dirty=true; } /* auto-repeat */
|
||||||
|
} else if(!firedLong && heldMs>=400){
|
||||||
|
firedLong=true; onLong(held); dirty=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* non-blocking scan: one step each time the settle timer expires */
|
||||||
|
if(scanState){
|
||||||
|
if(scanTimer<=50) scanStep();
|
||||||
|
else scanTimer=(uint16_t)(scanTimer-50);
|
||||||
|
dirty=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirty){ show(); dirty=false; }
|
||||||
|
A->delay_ms(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* push our state back to the resident FM and persist (config + 48 channels) */
|
||||||
|
if(!st.is_mr) st.sel_freq=st.freq_playing;
|
||||||
|
A->fm_state(&st,true);
|
||||||
|
A->fm_commit();
|
||||||
|
A->fm_exit();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Overlay-app link script (POC).
|
||||||
|
*
|
||||||
|
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
|
||||||
|
* the multiboot RAM stub — never both active at once). Code + rodata + data +
|
||||||
|
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
|
||||||
|
* data only; the loader zeroes the overlay before copying, so bss starts clean.
|
||||||
|
*
|
||||||
|
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
|
||||||
|
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
|
||||||
|
* mismatch. */
|
||||||
|
|
||||||
|
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
|
||||||
|
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
|
||||||
|
* The default is only a fallback for a standalone size check. */
|
||||||
|
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||||
|
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
|
||||||
|
|
||||||
|
ENTRY(app_main)
|
||||||
|
|
||||||
|
MEMORY {
|
||||||
|
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS {
|
||||||
|
.app APP_VMA : {
|
||||||
|
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
|
||||||
|
*(.text .text.*)
|
||||||
|
*(.rodata .rodata.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.data .data.*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_start = .;
|
||||||
|
*(.bss .bss.* COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__app_bss_end = .;
|
||||||
|
} > APP
|
||||||
|
|
||||||
|
__app_end = .;
|
||||||
|
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
|
||||||
|
"FoxHunt overlay app overflows the 4 KiB overlay")
|
||||||
|
|
||||||
|
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||||
|
}
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
|
||||||
|
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
|
||||||
|
# derived from the directory name; only APP_NAME - the human label baked into
|
||||||
|
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
|
||||||
|
# stripped), so "Broadcast FM" -> BroadcastFM.app.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
|
||||||
|
APP_NAME="FoxHunt" # <-- the only per-app line
|
||||||
|
APP_VER="1.0"
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||||
|
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
|
||||||
|
|
||||||
|
CC=/opt/toolchain/bin/arm-none-eabi-gcc
|
||||||
|
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
|
||||||
|
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
|
||||||
|
|
||||||
|
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
|
||||||
|
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||||
|
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||||
|
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
|
||||||
|
|
||||||
|
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
|
||||||
|
|
||||||
|
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
|
||||||
|
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
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
BYTES=$(wc -c < "${APP}.bin")
|
||||||
|
if [ "$BYTES" -gt 4096 ]; then
|
||||||
|
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
|
||||||
|
fi
|
||||||
|
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
|
||||||
|
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* https://github.com/armel
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FoxHunt (RX) — overlay app. Ported from App/app/foxhunt.c, hunt sub-mode only
|
||||||
|
* (the beacon becomes a separate app). Signal-strength hunter: corrected dBm with
|
||||||
|
* a big number, an IARU S-meter staircase or a scrolling history, peak/min hold,
|
||||||
|
* a 1 s trend, a front-end attenuator ladder, and Geiger/station audio.
|
||||||
|
*
|
||||||
|
* Keys: 1 gauge (bar/history) · 2 audio (off/beep/station) · 3 + UP/DOWN attenuator
|
||||||
|
* MENU reset holds · long F keypad lock · EXIT quit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "../app_api.h"
|
||||||
|
|
||||||
|
#define LCD_WIDTH 128
|
||||||
|
#define BK_REG_13 0x13
|
||||||
|
|
||||||
|
/* --- tuning (from foxhunt.c) --- */
|
||||||
|
#define DBM_FLOOR (-141)
|
||||||
|
#define DBM_CEIL (-53)
|
||||||
|
#define TICK_MS 50
|
||||||
|
#define TREND_TICKS 20
|
||||||
|
#define LOCK_HOLD_MS 500
|
||||||
|
#define SILENCE_DBM (-120)
|
||||||
|
#define TONE_MIN 400
|
||||||
|
#define TONE_MAX 2400
|
||||||
|
#define RATE_SLOW 20
|
||||||
|
#define RATE_FAST 2
|
||||||
|
#define BLIP_MS 50
|
||||||
|
#define AUDIO_SETTLE 60
|
||||||
|
#define ATT_SETTLE 40
|
||||||
|
|
||||||
|
#define ATT_COUNT 6
|
||||||
|
#define ATT_BYP0 4
|
||||||
|
#define SEG_COUNT 13
|
||||||
|
#define BAR_X0 6
|
||||||
|
#define SEG_PITCH 9
|
||||||
|
#define SEG_W 8
|
||||||
|
#define SEG_BOTTOM 37
|
||||||
|
|
||||||
|
#define HIST_LEN 120
|
||||||
|
#define HIST_DECIM 3
|
||||||
|
#define GRAPH_X0 4
|
||||||
|
#define GRAPH_TOP 27
|
||||||
|
#define GRAPH_BOT 45
|
||||||
|
#define GRAPH_FLOOR 2
|
||||||
|
|
||||||
|
#define AUDIO_OFF 0
|
||||||
|
#define AUDIO_BEEP 1
|
||||||
|
#define AUDIO_STATION 2
|
||||||
|
#define GRAPH_BAR 0
|
||||||
|
#define GRAPH_HIST 1
|
||||||
|
|
||||||
|
#define CFG_MAGIC 0xF1 /* config validity marker */
|
||||||
|
|
||||||
|
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
|
||||||
|
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
|
||||||
|
|
||||||
|
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
|
||||||
|
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
|
||||||
|
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
|
||||||
|
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||||
|
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||||
|
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
|
||||||
|
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
|
||||||
|
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
|
||||||
|
static const uint8_t FONT_F[8] = {0x7f,0x00,0x76,0x76,0x76,0x76,0x7e,0x7f};
|
||||||
|
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||||
|
|
||||||
|
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||||
|
|
||||||
|
static const app_api_t *A;
|
||||||
|
|
||||||
|
static bool foxLocked, fArm, fLongDone;
|
||||||
|
static uint16_t fHoldMs;
|
||||||
|
static uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
|
||||||
|
static int16_t curDbm, peakDbm, minDbm, trendRef, trendDelta;
|
||||||
|
static uint8_t trendTick;
|
||||||
|
static uint8_t histBuf[HIST_LEN];
|
||||||
|
static uint8_t histHead, histTick;
|
||||||
|
static int16_t histEma;
|
||||||
|
static uint8_t prevKey;
|
||||||
|
static bool running;
|
||||||
|
static char str[16];
|
||||||
|
|
||||||
|
/* ---- tiny formatting ---- */
|
||||||
|
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||||
|
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||||
|
static char *puti(char *o,int v){
|
||||||
|
if(v<0){*o++='-';v=-v;}
|
||||||
|
char t[6]; int8_t n=0;
|
||||||
|
do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6);
|
||||||
|
while(n--)*o++=t[n];
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
|
||||||
|
|
||||||
|
/* ---- radio helpers ---- */
|
||||||
|
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
|
||||||
|
|
||||||
|
static void applyAtt(void){
|
||||||
|
uint16_t reg = A->bk_read(BK_REG_13);
|
||||||
|
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
|
||||||
|
A->bk_write(BK_REG_13, reg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setAudio(void){
|
||||||
|
if(foxAudioMode==AUDIO_OFF){ A->audio_path(false); return; }
|
||||||
|
A->audio_path(true);
|
||||||
|
A->delay_ms(AUDIO_SETTLE);
|
||||||
|
A->set_af(APP_AF_MUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t lerp(int16_t dbm,int32_t lo,int32_t hi){
|
||||||
|
if(dbm<=DBM_FLOOR) return lo;
|
||||||
|
if(dbm>=DBM_CEIL) return hi;
|
||||||
|
return lo + ((int32_t)(dbm-DBM_FLOOR)*(hi-lo))/(DBM_CEIL-DBM_FLOOR);
|
||||||
|
}
|
||||||
|
static void blip(uint16_t freq){
|
||||||
|
A->prepare_tone();
|
||||||
|
A->play_tone_raw(freq, BLIP_MS);
|
||||||
|
A->tones_off_rx();
|
||||||
|
A->set_agc(false);
|
||||||
|
applyAtt();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t fillCount(int16_t dbm){
|
||||||
|
int16_t n;
|
||||||
|
if(dbm<-141) return 0;
|
||||||
|
if(dbm<=-93) n=1+(dbm+141)/6;
|
||||||
|
else n=9+(dbm+93)/10;
|
||||||
|
if(n>SEG_COUNT) n=SEG_COUNT;
|
||||||
|
return (uint8_t)n;
|
||||||
|
}
|
||||||
|
static void buildS(char *out,int16_t dbm){
|
||||||
|
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
|
||||||
|
else if(dbm<-141) put(out,"S0")[0]='\0';
|
||||||
|
else { char *p=put(out,"S"); i2str(p,(dbm+147)/6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void histSample(void){
|
||||||
|
histEma += ((int16_t)curDbm*8 - histEma)/4;
|
||||||
|
if(++histTick<HIST_DECIM) return;
|
||||||
|
histTick=0;
|
||||||
|
histBuf[histHead]=fillCount((int16_t)(histEma/8));
|
||||||
|
histHead=(uint8_t)((histHead+1)%HIST_LEN);
|
||||||
|
}
|
||||||
|
static void rebase(void){
|
||||||
|
curDbm=A->rssi_dbm();
|
||||||
|
peakDbm=minDbm=trendRef=curDbm;
|
||||||
|
trendDelta=0; trendTick=0;
|
||||||
|
uint8_t lvl=fillCount(curDbm);
|
||||||
|
for(uint8_t i=0;i<HIST_LEN;i++) histBuf[i]=lvl;
|
||||||
|
histHead=0; histTick=0; histEma=(int16_t)(curDbm*8);
|
||||||
|
}
|
||||||
|
static void attCycle(int8_t dir){
|
||||||
|
attStep=(uint8_t)((attStep + (dir>0?1:ATT_COUNT-1))%ATT_COUNT);
|
||||||
|
applyAtt();
|
||||||
|
A->delay_ms(ATT_SETTLE);
|
||||||
|
rebase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- drawing ---- */
|
||||||
|
static void fillRect(int16_t x0,int16_t y0,int16_t x1,int16_t y1){
|
||||||
|
for(int16_t x=x0;x<=x1;x++) A->draw_line(A->fb,x,y0,x,y1,true);
|
||||||
|
}
|
||||||
|
static void drawBar(void){
|
||||||
|
uint8_t n=fillCount(curDbm);
|
||||||
|
for(uint8_t i=0;i<n;i++){
|
||||||
|
int16_t sx=BAR_X0+i*SEG_PITCH;
|
||||||
|
fillRect(sx,SEG_BOTTOM-i,sx+SEG_W-1,SEG_BOTTOM);
|
||||||
|
}
|
||||||
|
A->draw_line(A->fb,BAR_X0,SEG_BOTTOM+2,BAR_X0+(SEG_COUNT-1)*SEG_PITCH+SEG_W-1,SEG_BOTTOM+2,true);
|
||||||
|
}
|
||||||
|
static void drawHist(void){
|
||||||
|
const uint8_t floorY=GRAPH_BOT-GRAPH_FLOOR;
|
||||||
|
const uint8_t span=floorY-GRAPH_TOP;
|
||||||
|
A->draw_line(A->fb,GRAPH_X0,GRAPH_BOT,GRAPH_X0+HIST_LEN-1,GRAPH_BOT,true);
|
||||||
|
uint8_t prevY=0, idx=histHead;
|
||||||
|
for(uint8_t c=0;c<HIST_LEN;c++){
|
||||||
|
uint8_t lvl=histBuf[idx]; if(++idx>=HIST_LEN)idx=0;
|
||||||
|
if(lvl>SEG_COUNT)lvl=SEG_COUNT;
|
||||||
|
uint8_t x=GRAPH_X0+c;
|
||||||
|
uint8_t y=(uint8_t)(floorY-(lvl*span)/SEG_COUNT);
|
||||||
|
for(uint8_t yy=y+1;yy<=floorY;yy++) if(((x+yy)&1)==0) A->draw_line(A->fb,x,yy,x,yy,true);
|
||||||
|
if(c==0){ A->draw_line(A->fb,x,y,x,y,true); }
|
||||||
|
else { uint8_t lo=(y<prevY)?y:prevY, hi=(y<prevY)?prevY:y; A->draw_line(A->fb,x,lo,x,hi,true); }
|
||||||
|
prevY=y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void tag(const char *s,uint8_t x,uint8_t line){
|
||||||
|
A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4));
|
||||||
|
}
|
||||||
|
static void draw(void){
|
||||||
|
char big[8], sMeter[8];
|
||||||
|
|
||||||
|
A->display_clear();
|
||||||
|
A->status_clear();
|
||||||
|
A->print_inverse("FOX HUNT",2,0,true,true,34);
|
||||||
|
A->draw_battery();
|
||||||
|
|
||||||
|
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
|
||||||
|
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
|
||||||
|
else cpy(A->status_line+38,BMP_BARS,11);
|
||||||
|
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
|
||||||
|
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
|
||||||
|
if(foxLocked) cpy(A->status_line+69,FONT_LOCK,9);
|
||||||
|
else if(fArm) cpy(A->status_line+69,FONT_F,8);
|
||||||
|
|
||||||
|
i2str(big,curDbm);
|
||||||
|
A->display_freq(big,2,0,false);
|
||||||
|
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
|
||||||
|
|
||||||
|
/* trend arrow (line 0, right) + signed delta (line 1) */
|
||||||
|
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
|
||||||
|
if(trendDelta!=0){
|
||||||
|
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
|
||||||
|
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
|
||||||
|
A->print_normal("dBm",127-3*7,0,1);
|
||||||
|
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
i2str(put(str,"PK "),peakDbm);
|
||||||
|
tag(str,4,2);
|
||||||
|
i2str(put(str,"MN "),minDbm);
|
||||||
|
tag(str,(uint8_t)((LCD_WIDTH-slen(str)*4)/2),2);
|
||||||
|
buildS(sMeter,curDbm);
|
||||||
|
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
|
||||||
|
|
||||||
|
if(foxGraphMode==GRAPH_HIST) drawHist();
|
||||||
|
else drawBar();
|
||||||
|
|
||||||
|
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
|
||||||
|
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
|
||||||
|
tag(str,4,6);
|
||||||
|
|
||||||
|
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
|
||||||
|
uint32_t fr=f%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
|
||||||
|
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- config (deferred) ---- */
|
||||||
|
static void loadConfig(void){
|
||||||
|
uint8_t c[4];
|
||||||
|
A->cfg_load(c,4);
|
||||||
|
if(c[0]==CFG_MAGIC){
|
||||||
|
if(c[1]<ATT_COUNT) attStep=c[1];
|
||||||
|
if(c[2]<=GRAPH_HIST) foxGraphMode=c[2];
|
||||||
|
if(c[3]<=AUDIO_STATION) foxAudioMode=c[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void saveConfig(void){
|
||||||
|
uint8_t c[4]={CFG_MAGIC,attStep,foxGraphMode,foxAudioMode};
|
||||||
|
A->cfg_save(c,4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- input ---- */
|
||||||
|
static void handleKeys(void){
|
||||||
|
uint8_t key=A->get_key();
|
||||||
|
|
||||||
|
/* long-press F -> keypad lock */
|
||||||
|
if(key==APP_KEY_F){
|
||||||
|
if(!fLongDone){ fHoldMs+=TICK_MS; if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); } }
|
||||||
|
} else { fHoldMs=0; fLongDone=false; }
|
||||||
|
|
||||||
|
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
|
||||||
|
prevKey=key;
|
||||||
|
A->backlight_on();
|
||||||
|
|
||||||
|
if(foxLocked){
|
||||||
|
if(key==APP_KEY_UP) attCycle(+1);
|
||||||
|
if(key==APP_KEY_DOWN) attCycle(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||||
|
int8_t dir=fArm?-1:1;
|
||||||
|
|
||||||
|
switch(key){
|
||||||
|
case APP_KEY_EXIT: running=false; break;
|
||||||
|
case APP_KEY_1: foxGraphMode^=1; break;
|
||||||
|
case APP_KEY_2:
|
||||||
|
foxAudioMode=(uint8_t)((foxAudioMode+(dir>0?1:2))%3); setAudio();
|
||||||
|
if(foxAudioMode==AUDIO_BEEP){ audioTick=RATE_SLOW; }
|
||||||
|
break;
|
||||||
|
case APP_KEY_3: attCycle(dir); break;
|
||||||
|
case APP_KEY_UP: attCycle(+1); break;
|
||||||
|
case APP_KEY_DOWN: attCycle(-1); break;
|
||||||
|
case APP_KEY_MENU: peakDbm=minDbm=trendRef=curDbm; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
fArm=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tickDelay(void){
|
||||||
|
for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((section(".text.entry"),used))
|
||||||
|
void app_main(const app_api_t *api){
|
||||||
|
A=api;
|
||||||
|
foxLocked=fArm=fLongDone=false; fHoldMs=0;
|
||||||
|
attStep=0; foxGraphMode=GRAPH_BAR; foxAudioMode=AUDIO_OFF;
|
||||||
|
prevKey=APP_KEY_INVALID;
|
||||||
|
|
||||||
|
loadConfig();
|
||||||
|
A->backlight_on();
|
||||||
|
A->set_agc(false);
|
||||||
|
applyAtt();
|
||||||
|
setAudio();
|
||||||
|
A->delay_ms(ATT_SETTLE);
|
||||||
|
rebase();
|
||||||
|
|
||||||
|
running=true;
|
||||||
|
while(running){
|
||||||
|
handleKeys();
|
||||||
|
if(!running) break;
|
||||||
|
|
||||||
|
curDbm=A->rssi_dbm();
|
||||||
|
if(curDbm>peakDbm) peakDbm=curDbm;
|
||||||
|
if(curDbm<minDbm) minDbm=curDbm;
|
||||||
|
if(++trendTick>=TREND_TICKS){ trendDelta=curDbm-trendRef; trendRef=curDbm; trendTick=0; }
|
||||||
|
histSample();
|
||||||
|
|
||||||
|
draw();
|
||||||
|
A->blit_status();
|
||||||
|
A->blit_full();
|
||||||
|
|
||||||
|
if(foxAudioMode==AUDIO_BEEP && curDbm>=SILENCE_DBM){
|
||||||
|
if(++audioTick>=(uint8_t)lerp(curDbm,RATE_SLOW,RATE_FAST)){ audioTick=0; blip((uint16_t)lerp(curDbm,TONE_MIN,TONE_MAX)); }
|
||||||
|
} else if(foxAudioMode==AUDIO_STATION){
|
||||||
|
A->set_af(curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
A->battery_sample();
|
||||||
|
tickDelay();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig();
|
||||||
|
A->audio_path(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/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
|
||||||
|
# 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
|
||||||
|
import argparse, os, re, struct, zlib, sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
def cdefine(header: str, name: str) -> int:
|
||||||
|
"""Value of a `#define <name> <int-literal>` in a sibling C header.
|
||||||
|
Accepts a decimal or 0x-hex literal with optional u/U/l/L suffixes. Keeps the
|
||||||
|
blob format tied to the firmware's own headers (single source of truth)."""
|
||||||
|
pat = re.compile(r"^\s*#define\s+" + re.escape(name) +
|
||||||
|
r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b")
|
||||||
|
with open(os.path.join(HERE, header)) as f:
|
||||||
|
for line in f:
|
||||||
|
m = pat.match(line)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1), 0)
|
||||||
|
sys.exit(f"{header}: #define {name} not found")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
|
||||||
|
FLAG_COMMITTED = 1 # APP_FLAG_COMMITTED, bit 0
|
||||||
|
|
||||||
|
def field(s: str, n: int) -> bytes:
|
||||||
|
b = s.encode("ascii", "strict")[: n - 1]
|
||||||
|
return b + b"\x00" * (n - len(b))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("infile")
|
||||||
|
ap.add_argument("outfile")
|
||||||
|
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("--vma", type=lambda x: int(x, 0), required=True,
|
||||||
|
help="RAM VMA the app was linked at (must match the firmware overlay)")
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
code = open(a.infile, "rb").read()
|
||||||
|
if len(code) == 0:
|
||||||
|
sys.exit("empty input")
|
||||||
|
if len(code) > OVERLAY_MAX:
|
||||||
|
sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B")
|
||||||
|
|
||||||
|
crc = zlib.crc32(code) & 0xFFFFFFFF
|
||||||
|
header = struct.pack(
|
||||||
|
"<4sHHIIHH16s16sI8s",
|
||||||
|
MAGIC, HDR_VERSION, ABI_VERSION,
|
||||||
|
len(code), crc, a.entry, FLAG_COMMITTED,
|
||||||
|
field(a.name, 16), field(a.ver, 16), a.vma, b"\x00" * 8,
|
||||||
|
)
|
||||||
|
assert len(header) == 64, len(header)
|
||||||
|
|
||||||
|
with open(a.outfile, "wb") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(code)
|
||||||
|
|
||||||
|
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} vma=0x{a.vma:08x} "
|
||||||
|
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -416,6 +416,16 @@ void PY25Q16_InvalidateCache(void)
|
|||||||
SectorCacheAddr = 0x1000000;
|
SectorCacheAddr = 0x1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
/* Expose the 4 KiB sector cache as the overlay-app workspace. It lives in
|
||||||
|
* .bss.mb_workspace (the overlay VMA), so an app blob linked there runs in
|
||||||
|
* place once copied in. The caller InvalidateCache()s around its use. */
|
||||||
|
uint8_t *PY25Q16_OverlayBuffer(void)
|
||||||
|
{
|
||||||
|
return SectorCache;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static inline void WriteAddr(uint32_t Addr)
|
static inline void WriteAddr(uint32_t Addr)
|
||||||
{
|
{
|
||||||
SPI_WriteByte(0xff & (Addr >> 16));
|
SPI_WriteByte(0xff & (Addr >> 16));
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ void PY25Q16_SectorErase(uint32_t Address);
|
|||||||
* is also called before multiboot reuses the cache storage as a RAM overlay. */
|
* is also called before multiboot reuses the cache storage as a RAM overlay. */
|
||||||
void PY25Q16_InvalidateCache(void);
|
void PY25Q16_InvalidateCache(void);
|
||||||
|
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||||
|
/* The 4 KiB sector cache, reused as the overlay-app execution workspace. */
|
||||||
|
uint8_t *PY25Q16_OverlayBuffer(void);
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||||
/*
|
/*
|
||||||
* Multiboot per-bank config banking.
|
* Multiboot per-bank config banking.
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ static void convertTime(uint8_t *line, uint8_t type)
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
void UI_DrawStatusBattery(uint8_t *line, char *str)
|
void UI_DrawStatusBattery(uint8_t *line, char *str)
|
||||||
#else
|
#else
|
||||||
static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str)
|
static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str)
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
void UI_DrawStatusBattery(uint8_t *line, char *str);
|
void UI_DrawStatusBattery(uint8_t *line, char *str);
|
||||||
#endif
|
#endif
|
||||||
void UI_DisplayStatus();
|
void UI_DisplayStatus();
|
||||||
|
|||||||
+7
-1
@@ -53,8 +53,14 @@ void (*const UI_DisplayFunctions[])(void) = {
|
|||||||
[DISPLAY_MENU] = &UI_DisplayMenu,
|
[DISPLAY_MENU] = &UI_DisplayMenu,
|
||||||
[DISPLAY_SCANNER] = &UI_DisplayScanner,
|
[DISPLAY_SCANNER] = &UI_DisplayScanner,
|
||||||
|
|
||||||
#ifdef ENABLE_FMRADIO
|
#if defined(ENABLE_FMRADIO) && !defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||||
[DISPLAY_FM] = &UI_DisplayFM,
|
[DISPLAY_FM] = &UI_DisplayFM,
|
||||||
|
#elif defined(ENABLE_FMRADIO)
|
||||||
|
/* The FM overlay app replaces the resident FM screen (ui/fmradio.c is not
|
||||||
|
compiled). DISPLAY_FM still exists in the enum, so the slot must stay
|
||||||
|
initialised to keep ARRAY_SIZE == DISPLAY_N_ELEM; point it at a
|
||||||
|
never-reached stub - the resident FM screen can no longer open. */
|
||||||
|
[DISPLAY_FM] = &UI_DisplayMain,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ENABLE_AIRCOPY
|
#ifdef ENABLE_AIRCOPY
|
||||||
|
|||||||
+10
-7
@@ -91,6 +91,7 @@
|
|||||||
"ENABLE_FEAT_F4HWN_MENU_CAT": false,
|
"ENABLE_FEAT_F4HWN_MENU_CAT": false,
|
||||||
"ENABLE_FEAT_F4HWN_MULTIBOOT": false,
|
"ENABLE_FEAT_F4HWN_MULTIBOOT": false,
|
||||||
"ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": false,
|
"ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": false,
|
||||||
|
"ENABLE_FEAT_F4HWN_OVERLAY_APPS": false,
|
||||||
"ENABLE_AGC_SHOW_DATA": false,
|
"ENABLE_AGC_SHOW_DATA": false,
|
||||||
"ENABLE_UART_RW_BK_REGS": false,
|
"ENABLE_UART_RW_BK_REGS": false,
|
||||||
"ENABLE_SWD": false,
|
"ENABLE_SWD": false,
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
"ENABLE_FEAT_F4HWN_ACTION_PICKER": true,
|
"ENABLE_FEAT_F4HWN_ACTION_PICKER": true,
|
||||||
"ENABLE_FEAT_F4HWN_MULTIBOOT": true,
|
"ENABLE_FEAT_F4HWN_MULTIBOOT": true,
|
||||||
"ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": true,
|
"ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": true,
|
||||||
|
"ENABLE_FEAT_F4HWN_OVERLAY_APPS": false,
|
||||||
"ENABLE_SWD": true,
|
"ENABLE_SWD": true,
|
||||||
"EDITION_STRING": "Fusion",
|
"EDITION_STRING": "Fusion",
|
||||||
"TARGET": "f4hwn.fusion"
|
"TARGET": "f4hwn.fusion"
|
||||||
@@ -168,16 +170,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Max",
|
"name": "Labs",
|
||||||
"inherits": "Fusion",
|
"inherits": "Fusion",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
|
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
|
||||||
"ENABLE_FEAT_F4HWN_FOXHUNT": true,
|
"ENABLE_FEAT_F4HWN_FOXHUNT": false,
|
||||||
"ENABLE_AIRCOPY": true,
|
"ENABLE_AIRCOPY": true,
|
||||||
"ENABLE_FEAT_F4HWN_BEAM": true,
|
"ENABLE_FEAT_F4HWN_BEAM": true,
|
||||||
"ENABLE_FEAT_F4HWN_GAME": true,
|
"ENABLE_FEAT_F4HWN_GAME": false,
|
||||||
"EDITION_STRING": "Max",
|
"ENABLE_FEAT_F4HWN_OVERLAY_APPS": true,
|
||||||
"TARGET": "f4hwn.max"
|
"EDITION_STRING": "Labs",
|
||||||
|
"TARGET": "f4hwn.labs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -199,8 +202,8 @@
|
|||||||
"configurePreset": "FieldOps"
|
"configurePreset": "FieldOps"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Max",
|
"name": "Labs",
|
||||||
"configurePreset": "Max"
|
"configurePreset": "Labs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+11
-2
@@ -162,8 +162,17 @@ SECTIONS
|
|||||||
|
|
||||||
/* Optional multiboot overlay. The restore stub is linked at the same RAM
|
/* Optional multiboot overlay. The restore stub is linked at the same RAM
|
||||||
address as the 4 KiB PY25Q16 sector cache, but its load image remains in
|
address as the 4 KiB PY25Q16 sector cache, but its load image remains in
|
||||||
FLASH and is copied into the cache only immediately before reflashing. */
|
FLASH and is copied into the cache only immediately before reflashing.
|
||||||
OVERLAY : NOCROSSREFS AT (LOADADDR(.noncacheable) + SIZEOF(.noncacheable))
|
|
||||||
|
The VMA is PINNED to a fixed RAM address (ORIGIN + 0x280) so a single
|
||||||
|
overlay-app blob runs on EVERY preset: without pinning, the sector-cache
|
||||||
|
VMA shifts with the per-preset .data size (Fusion 0x1dc, Transfer/Max 0x1e0,
|
||||||
|
...), and a blob linked for one preset is refused by the loader's VMA guard
|
||||||
|
on another. 0x280 clears the ~0x1ec of RAM used before it; the assert fails
|
||||||
|
loudly (raise the anchor) if a preset ever needs more. */
|
||||||
|
ASSERT((ADDR(.noncacheable) + SIZEOF(.noncacheable)) <= (ORIGIN(RAM) + 0x280),
|
||||||
|
"RAM before the overlay exceeds the fixed 0x280 anchor - raise MB_OVERLAY_VMA")
|
||||||
|
OVERLAY (ORIGIN(RAM) + 0x280) : NOCROSSREFS AT (LOADADDR(.noncacheable) + SIZEOF(.noncacheable))
|
||||||
{
|
{
|
||||||
.mb_ramfunc
|
.mb_ramfunc
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,9 +73,11 @@ Specialized presets extend Fusion for specific uses:
|
|||||||
|
|
||||||
- **Transfer** adds AirCopy and BEAM wireless channel transfer.
|
- **Transfer** adds AirCopy and BEAM wireless channel transfer.
|
||||||
- **FieldOps** adds first-responder controls, Fox Hunt and Morse Beacon support.
|
- **FieldOps** adds first-responder controls, Fox Hunt and Morse Beacon support.
|
||||||
- **Max** packs the broadest practical selection of features that fits safely within
|
- **Labs** is the experimental edition. It carries the broad feature selection of the
|
||||||
the radio's Flash and RAM limits. It is not a strict superset of every other edition:
|
other releases and adds the overlay-apps platform (apps loaded from external Flash and
|
||||||
features may be exchanged between releases to preserve stability and memory headroom.
|
run in a 4 KiB RAM overlay) — the newest, least-settled work. Expect rough edges. It is
|
||||||
|
not a strict superset of every other edition: features may be exchanged between releases
|
||||||
|
to preserve stability and memory headroom.
|
||||||
- **Custom** remains a manually configured build based directly on the hidden technical default.
|
- **Custom** remains a manually configured build based directly on the hidden technical default.
|
||||||
|
|
||||||
### Radio and signal handling
|
### Radio and signal handling
|
||||||
@@ -330,7 +332,7 @@ But, they are nice toys for the price, fun to play with.
|
|||||||
## Compiling and Building from Docker
|
## Compiling and Building from Docker
|
||||||
|
|
||||||
This project provides a Docker-based build system for the UV-K1 and UV-K5 V3.
|
This project provides a Docker-based build system for the UV-K1 and UV-K5 V3.
|
||||||
Everything is handled through the `compile-with-docker.sh` helper script. Fusion is
|
Everything is handled through the `compile-firmware.sh` helper script. Fusion is
|
||||||
the default generic preset, while specialized builds are generated in their own
|
the default generic preset, while specialized builds are generated in their own
|
||||||
`build/<Preset>` directories.
|
`build/<Preset>` directories.
|
||||||
|
|
||||||
@@ -341,7 +343,7 @@ the default generic preset, while specialized builds are generated in their own
|
|||||||
|
|
||||||
### Build Script Overview
|
### Build Script Overview
|
||||||
|
|
||||||
The script `compile-with-docker.sh`:
|
The script `compile-firmware.sh`:
|
||||||
|
|
||||||
1. Builds the Docker image (`uvk1-uvk5v3`) if it does not already exist.
|
1. Builds the Docker image (`uvk1-uvk5v3`) if it does not already exist.
|
||||||
2. Configures the selected preset with `cmake --fresh`.
|
2. Configures the selected preset with `cmake --fresh`.
|
||||||
@@ -351,7 +353,7 @@ The script `compile-with-docker.sh`:
|
|||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./compile-with-docker.sh [Preset] [extra CMake options]
|
./compile-firmware.sh [Preset] [extra CMake options]
|
||||||
```
|
```
|
||||||
|
|
||||||
The default preset is **Fusion**. Available presets are:
|
The default preset is **Fusion**. Available presets are:
|
||||||
@@ -360,18 +362,18 @@ The default preset is **Fusion**. Available presets are:
|
|||||||
- **Fusion**
|
- **Fusion**
|
||||||
- **Transfer**
|
- **Transfer**
|
||||||
- **FieldOps**
|
- **FieldOps**
|
||||||
- **Max**
|
- **Labs**
|
||||||
- **All** (Fusion, Transfer, FieldOps and Max)
|
- **All** (Fusion, Transfer, FieldOps and Labs)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./compile-with-docker.sh
|
./compile-firmware.sh
|
||||||
./compile-with-docker.sh Fusion
|
./compile-firmware.sh Fusion
|
||||||
./compile-with-docker.sh Transfer
|
./compile-firmware.sh Transfer
|
||||||
./compile-with-docker.sh FieldOps
|
./compile-firmware.sh FieldOps
|
||||||
./compile-with-docker.sh Max
|
./compile-firmware.sh Labs
|
||||||
./compile-with-docker.sh All
|
./compile-firmware.sh All
|
||||||
```
|
```
|
||||||
|
|
||||||
### Passing Additional CMake Options
|
### Passing Additional CMake Options
|
||||||
@@ -382,15 +384,15 @@ These are forwarded directly to `cmake --preset` inside the container.
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./compile-with-docker.sh FieldOps -DENABLE_VOX=OFF
|
./compile-firmware.sh FieldOps -DENABLE_VOX=OFF
|
||||||
./compile-with-docker.sh Fusion -DENABLE_FEAT_F4HWN_GAME=ON
|
./compile-firmware.sh Fusion -DENABLE_FEAT_F4HWN_GAME=ON
|
||||||
./compile-with-docker.sh Fusion -DSQL_TONE=600
|
./compile-firmware.sh Fusion -DSQL_TONE=600
|
||||||
```
|
```
|
||||||
|
|
||||||
To prepare the rolling development firmware:
|
To prepare the rolling development firmware:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./compile-with-docker.sh Fusion -DDEV=ON
|
./compile-firmware.sh Fusion -DDEV=ON
|
||||||
```
|
```
|
||||||
|
|
||||||
This keeps the regular build output in `build/Fusion` and also updates
|
This keeps the regular build output in `build/Fusion` and also updates
|
||||||
|
|||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Build overlay-app blobs (.app), the counterpart of compile-firmware.sh.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./compile-app.sh # build every app
|
||||||
|
# ./compile-app.sh All # build every app
|
||||||
|
# ./compile-app.sh breakout # build one app
|
||||||
|
# ./compile-app.sh fm foxhunt # build several apps
|
||||||
|
#
|
||||||
|
# Each app is compiled in the uvk1-uvk5v3 Docker image, linked at the pinned
|
||||||
|
# overlay VMA, packed into build/Apps/<Name>.app (named after the app's human
|
||||||
|
# label, spaces stripped), and reported against the 4 KiB overlay budget. Apps
|
||||||
|
# are auto-discovered from App/apps/<name>/build.sh.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE=uvk1-uvk5v3
|
||||||
|
APPS_DIR=App/apps
|
||||||
|
OUT_DIR=build/Apps
|
||||||
|
OVERLAY_MAX=4096
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# --- discover apps (any App/apps/<name>/build.sh) ---
|
||||||
|
ALL_APPS=()
|
||||||
|
for d in "$APPS_DIR"/*/; do
|
||||||
|
[ -f "${d}build.sh" ] && ALL_APPS+=("$(basename "$d")")
|
||||||
|
done
|
||||||
|
if [ "${#ALL_APPS[@]}" -eq 0 ]; then
|
||||||
|
echo "❌ No apps found under $APPS_DIR/*/build.sh"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- select targets ---
|
||||||
|
if [ "$#" -eq 0 ] || [ "${1,,}" = "all" ]; then
|
||||||
|
TARGETS=("${ALL_APPS[@]}")
|
||||||
|
else
|
||||||
|
TARGETS=()
|
||||||
|
for want in "$@"; do
|
||||||
|
ok=0
|
||||||
|
for a in "${ALL_APPS[@]}"; do [ "$a" = "$want" ] && ok=1; done
|
||||||
|
if [ "$ok" -eq 0 ]; then
|
||||||
|
echo "❌ Unknown app '$want'. Available: ${ALL_APPS[*]} (or All)"; exit 1
|
||||||
|
fi
|
||||||
|
TARGETS+=("$want")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; }
|
||||||
|
if [[ "$(docker images -q "$IMAGE")" == "" ]]; then
|
||||||
|
echo "🐳 Building Docker image..."; docker build -q -t "$IMAGE" . >/dev/null
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
# Give the container a pseudo-TTY when we are interactive, so build.sh's
|
||||||
|
# single-line [k/3] progress refreshes in place (same trick as the firmware
|
||||||
|
# build gives Ninja). Batch/redirected runs stay plain and line-oriented.
|
||||||
|
TTY_ARGS=(); [[ -t 1 ]] && TTY_ARGS=(-t)
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "🚀 Building overlay apps"
|
||||||
|
printf ' VMA %s · budget %d B / %d.00 KiB · out %s/\n\n' \
|
||||||
|
"$APP_VMA" "$OVERLAY_MAX" "$((OVERLAY_MAX / 1024))" "$OUT_DIR"
|
||||||
|
|
||||||
|
files=(); sizes=(); vmas=(); states=()
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk '{printf "0x%s%s%s%s",$4,$3,$2,$1}'; }
|
||||||
|
|
||||||
|
for app in "${TARGETS[@]}"; do
|
||||||
|
if docker run --rm "${TTY_ARGS[@]}" -u "$(id -u):$(id -g)" \
|
||||||
|
-v "$PWD":/work -w "/work/$APPS_DIR/$app" \
|
||||||
|
-e PATH="/opt/toolchain/bin:/usr/bin:/bin" -e APP_VMA="$APP_VMA" \
|
||||||
|
"$IMAGE" bash ./build.sh; then
|
||||||
|
appfile=$(ls -1 "$APPS_DIR/$app"/*.app 2>/dev/null | head -1)
|
||||||
|
if [ -n "$appfile" ] && [ -f "$appfile" ]; then
|
||||||
|
cp -f "$appfile" "$OUT_DIR/"
|
||||||
|
base=$(basename "$appfile")
|
||||||
|
code=$(( $(wc -c < "$appfile") - 64 ))
|
||||||
|
vma=$(read_u32le "$appfile" 52)
|
||||||
|
state="✅ OK"; [ "$code" -gt "$OVERLAY_MAX" ] && { state="🚨 OVERFLOW"; fail=1; }
|
||||||
|
else
|
||||||
|
base="$app.app"; code=-1; vma="--"; state="❌ NO BLOB"; fail=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
base="$app.app"; code=-1; vma="--"; state="❌ BUILD FAIL"; fail=1
|
||||||
|
fi
|
||||||
|
files+=("$base"); sizes+=("$code"); vmas+=("$vma"); states+=("$state")
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- memory report (styled like the firmware Flash/RAM tables) ---
|
||||||
|
echo
|
||||||
|
printf '📦 Overlay apps (budget: %d B / %d.00 KiB)\n' "$OVERLAY_MAX" "$((OVERLAY_MAX / 1024))"
|
||||||
|
printf '%-16s | %9s | %9s | %9s | %9s | %7s | %s\n' \
|
||||||
|
"File" "Code (B)" "Code(KiB)" "Free (B)" "Free(KiB)" "Usage" "Status"
|
||||||
|
printf '%-16s-+-%9s-+-%9s-+-%9s-+-%9s-+-%7s-+-%s\n' \
|
||||||
|
"----------------" "---------" "---------" "---------" "---------" "-------" "----------"
|
||||||
|
for i in "${!files[@]}"; do
|
||||||
|
c=${sizes[$i]}
|
||||||
|
if [ "$c" -ge 0 ]; then
|
||||||
|
free=$(( OVERLAY_MAX - c ))
|
||||||
|
bp=$(( (c * 10000 + OVERLAY_MAX / 2) / OVERLAY_MAX ))
|
||||||
|
ck100=$(( (c * 100 + 512) / 1024 )); fk100=$(( (free * 100 + 512) / 1024 ))
|
||||||
|
printf -v pct '%d.%02d%%' "$((bp/100))" "$((bp%100))"
|
||||||
|
printf -v ckib '%d.%02d' "$((ck100/100))" "$((ck100%100))"
|
||||||
|
printf -v fkib '%d.%02d' "$((fk100/100))" "$((fk100%100))"
|
||||||
|
printf '%-16s | %9d | %9s | %9d | %9s | %7s | %s\n' \
|
||||||
|
"${files[$i]}" "$c" "$ckib" "$free" "$fkib" "$pct" "${states[$i]}"
|
||||||
|
else
|
||||||
|
printf '%-16s | %9s | %9s | %9s | %9s | %7s | %s\n' \
|
||||||
|
"${files[$i]}" "-" "-" "-" "-" "-" "${states[$i]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$fail" -eq 0 ]; then
|
||||||
|
if [ "${#files[@]}" -eq 1 ]; then
|
||||||
|
echo "🎉 App ${files[0]} built successfully -> $OUT_DIR/"
|
||||||
|
else
|
||||||
|
echo "🎉 All ${#files[@]} apps built successfully -> $OUT_DIR/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Some apps failed or overflowed — see the table above."
|
||||||
|
fi
|
||||||
|
exit $fail
|
||||||
Executable
+252
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Usage:
|
||||||
|
# ./compile-firmware.sh [Preset] [CMake options...]
|
||||||
|
# Examples:
|
||||||
|
# ./compile-firmware.sh Fusion
|
||||||
|
# ./compile-firmware.sh Transfer
|
||||||
|
# ./compile-firmware.sh FieldOps
|
||||||
|
# ./compile-firmware.sh Labs
|
||||||
|
# ./compile-firmware.sh Fusion -DDEV=ON
|
||||||
|
# ./compile-firmware.sh FieldOps -DENABLE_VOX=OFF
|
||||||
|
# ./compile-firmware.sh All
|
||||||
|
# Default preset: "Fusion"
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
IMAGE=uvk1-uvk5v3
|
||||||
|
RELEASE_PRESETS=(Fusion Transfer FieldOps Labs)
|
||||||
|
FLASH_LIMIT=$((118 * 1024))
|
||||||
|
RAM_LIMIT=$((16 * 1024))
|
||||||
|
|
||||||
|
PRESET=${1:-Fusion}
|
||||||
|
shift || true # remove preset from arguments if present
|
||||||
|
|
||||||
|
# Any remaining args will be treated as CMake cache variables
|
||||||
|
EXTRA_ARGS=("$@")
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Validate preset name
|
||||||
|
# ---------------------------------------------
|
||||||
|
if [[ ! "$PRESET" =~ ^(Custom|Fusion|Transfer|FieldOps|Labs|All)$ ]]; then
|
||||||
|
echo "❌ Unknown preset: '$PRESET'"
|
||||||
|
echo "Valid presets are: Custom, Fusion, Transfer, FieldOps, Labs, All"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
QUIET=0
|
||||||
|
if [[ "$PRESET" == "All" ]]; then
|
||||||
|
QUIET=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remember whether the script itself was started from an interactive terminal.
|
||||||
|
# The build output is piped through tee below, so checking stdout later from
|
||||||
|
# run_preset_build() would always report a non-terminal.
|
||||||
|
INTERACTIVE=0
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
INTERACTIVE=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Build the Docker image (only needed once)
|
||||||
|
# ---------------------------------------------
|
||||||
|
if [[ "$(docker images -q "$IMAGE")" == "" ]]; then
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -q -t "$IMAGE" . >/dev/null
|
||||||
|
fi
|
||||||
|
export MSYS_NO_PATHCONV=1
|
||||||
|
|
||||||
|
RESULT_PRESETS=()
|
||||||
|
RESULT_FLASH_SIZES=()
|
||||||
|
RESULT_RAM_SIZES=()
|
||||||
|
|
||||||
|
run_preset_build() {
|
||||||
|
local preset="$1"
|
||||||
|
local -a docker_tty_args=()
|
||||||
|
|
||||||
|
# Give Ninja a pseudo-terminal for an interactive single-preset build. This
|
||||||
|
# lets it refresh its [current/total] progress on one line. Batch/redirected
|
||||||
|
# builds keep plain line-oriented output suitable for logs and CI.
|
||||||
|
if (( INTERACTIVE && ! QUIET )); then
|
||||||
|
docker_tty_args=(-t)
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm "${docker_tty_args[@]}" \
|
||||||
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-v "$PWD":/src -w /src "$IMAGE" \
|
||||||
|
bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version &&
|
||||||
|
cmake --fresh --preset "$1" "${@:2}" &&
|
||||||
|
cmake --build --preset "$1" -j' \
|
||||||
|
bash "$preset" "${EXTRA_ARGS[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_preset() {
|
||||||
|
local preset="$1"
|
||||||
|
local preset_slug log_file bin_file flash_size ram_size status
|
||||||
|
|
||||||
|
preset_slug="${preset,,}"
|
||||||
|
log_file="$(mktemp)"
|
||||||
|
bin_file=""
|
||||||
|
|
||||||
|
find "build/${preset}" -maxdepth 1 -type f -name 'f4hwn.*' -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
if (( QUIET )); then
|
||||||
|
printf "Building %-10s ... " "$preset"
|
||||||
|
if run_preset_build "$preset" >"$log_file" 2>&1; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "=== 🚀 Building preset: ${preset} ==="
|
||||||
|
echo "---------------------------------------------"
|
||||||
|
if (( INTERACTIVE )); then
|
||||||
|
# Do not put a line-oriented filter after tee here: it would buffer
|
||||||
|
# Ninja's carriage-return progress updates and defeat the TTY display.
|
||||||
|
if run_preset_build "$preset" 2>&1 | tee "$log_file"; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if run_preset_build "$preset" 2>&1 | tee "$log_file" | awk '
|
||||||
|
/^Memory region[[:space:]]+Used Size[[:space:]]+Region Size/ { next }
|
||||||
|
/^[[:space:]]+RAM:[[:space:]]/ { next }
|
||||||
|
/^[[:space:]]+FLASH:[[:space:]]/ { next }
|
||||||
|
{ print; fflush() }
|
||||||
|
'; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( status != 0 )); then
|
||||||
|
if (( QUIET )); then
|
||||||
|
printf "FAILED\n\n"
|
||||||
|
cat "$log_file"
|
||||||
|
else
|
||||||
|
echo "Failed: ${preset}"
|
||||||
|
fi
|
||||||
|
rm -f -- "$log_file"
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
bin_file="build/${preset}/f4hwn.${preset_slug}.bin"
|
||||||
|
if [[ -z "$bin_file" || ! -f "$bin_file" ]]; then
|
||||||
|
if (( QUIET )); then
|
||||||
|
printf "FAILED\n"
|
||||||
|
fi
|
||||||
|
echo "Expected binary not found: $bin_file"
|
||||||
|
if (( QUIET )); then
|
||||||
|
cat "$log_file"
|
||||||
|
fi
|
||||||
|
rm -f -- "$log_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
flash_size="$(wc -c < "$bin_file")"
|
||||||
|
ram_size="$(awk '$1 == "RAM:" {
|
||||||
|
value = $2
|
||||||
|
if ($3 == "KB") value *= 1024
|
||||||
|
else if ($3 == "MB") value *= 1024 * 1024
|
||||||
|
printf "%.0f\n", value
|
||||||
|
}' "$log_file" | tail -n 1)"
|
||||||
|
|
||||||
|
if [[ ! "$ram_size" =~ ^[0-9]+$ ]]; then
|
||||||
|
if (( QUIET )); then
|
||||||
|
printf "FAILED\n"
|
||||||
|
fi
|
||||||
|
echo "Could not read RAM usage from linker output"
|
||||||
|
if (( QUIET )); then
|
||||||
|
cat "$log_file"
|
||||||
|
fi
|
||||||
|
rm -f -- "$log_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESULT_PRESETS+=("$preset")
|
||||||
|
RESULT_FLASH_SIZES+=("$flash_size")
|
||||||
|
RESULT_RAM_SIZES+=("$ram_size")
|
||||||
|
|
||||||
|
if (( QUIET )); then
|
||||||
|
printf "OK\n"
|
||||||
|
else
|
||||||
|
echo "✅ Done: ${preset}"
|
||||||
|
fi
|
||||||
|
rm -f -- "$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
local i
|
||||||
|
local flash_used flash_free flash_bp flash_pct
|
||||||
|
local flash_used_kib100 flash_free_kib100 flash_used_kib flash_free_kib
|
||||||
|
local ram_used ram_free ram_bp ram_pct
|
||||||
|
local ram_used_kib100 ram_free_kib100 ram_used_kib ram_free_kib
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "Flash (limit: %d B / %d.00 KiB)\n" "$FLASH_LIMIT" "$((FLASH_LIMIT / 1024))"
|
||||||
|
printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \
|
||||||
|
"Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage"
|
||||||
|
printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \
|
||||||
|
"----------" "----------" "----------" "----------" "----------" "-------"
|
||||||
|
|
||||||
|
for i in "${!RESULT_PRESETS[@]}"; do
|
||||||
|
flash_used="${RESULT_FLASH_SIZES[$i]}"
|
||||||
|
flash_free=$((FLASH_LIMIT - flash_used))
|
||||||
|
flash_bp=$(((flash_used * 10000 + FLASH_LIMIT / 2) / FLASH_LIMIT))
|
||||||
|
flash_used_kib100=$(((flash_used * 100 + 512) / 1024))
|
||||||
|
flash_free_kib100=$(((flash_free * 100 + 512) / 1024))
|
||||||
|
printf -v flash_pct "%d.%02d%%" "$((flash_bp / 100))" "$((flash_bp % 100))"
|
||||||
|
printf -v flash_used_kib "%d.%02d" "$((flash_used_kib100 / 100))" "$((flash_used_kib100 % 100))"
|
||||||
|
printf -v flash_free_kib "%d.%02d" "$((flash_free_kib100 / 100))" "$((flash_free_kib100 % 100))"
|
||||||
|
|
||||||
|
printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \
|
||||||
|
"${RESULT_PRESETS[$i]}" "$flash_used" "$flash_used_kib" \
|
||||||
|
"$flash_free" "$flash_free_kib" "$flash_pct"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "RAM (limit: %d B / %d.00 KiB)\n" "$RAM_LIMIT" "$((RAM_LIMIT / 1024))"
|
||||||
|
printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \
|
||||||
|
"Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage"
|
||||||
|
printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \
|
||||||
|
"----------" "----------" "----------" "----------" "----------" "-------"
|
||||||
|
|
||||||
|
for i in "${!RESULT_PRESETS[@]}"; do
|
||||||
|
ram_used="${RESULT_RAM_SIZES[$i]}"
|
||||||
|
ram_free=$((RAM_LIMIT - ram_used))
|
||||||
|
ram_bp=$(((ram_used * 10000 + RAM_LIMIT / 2) / RAM_LIMIT))
|
||||||
|
ram_used_kib100=$(((ram_used * 100 + 512) / 1024))
|
||||||
|
ram_free_kib100=$(((ram_free * 100 + 512) / 1024))
|
||||||
|
printf -v ram_pct "%d.%02d%%" "$((ram_bp / 100))" "$((ram_bp % 100))"
|
||||||
|
printf -v ram_used_kib "%d.%02d" "$((ram_used_kib100 / 100))" "$((ram_used_kib100 % 100))"
|
||||||
|
printf -v ram_free_kib "%d.%02d" "$((ram_free_kib100 / 100))" "$((ram_free_kib100 % 100))"
|
||||||
|
|
||||||
|
printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \
|
||||||
|
"${RESULT_PRESETS[$i]}" "$ram_used" "$ram_used_kib" \
|
||||||
|
"$ram_free" "$ram_free_kib" "$ram_pct"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if (( ${#RESULT_PRESETS[@]} == 1 )); then
|
||||||
|
echo "🎉 Preset ${RESULT_PRESETS[0]} built successfully!"
|
||||||
|
else
|
||||||
|
echo "🎉 All presets built successfully!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Handle 'All' preset
|
||||||
|
# ---------------------------------------------
|
||||||
|
if [[ "$PRESET" == "All" ]]; then
|
||||||
|
for p in "${RELEASE_PRESETS[@]}"; do
|
||||||
|
build_preset "$p"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
build_preset "$PRESET"
|
||||||
|
fi
|
||||||
|
print_summary
|
||||||
+7
-251
@@ -1,252 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
# ---------------------------------------------------------------------------
|
||||||
|
# DEPRECATED: this script was renamed to compile-firmware.sh (paired with
|
||||||
# ---------------------------------------------
|
# compile-app.sh). This thin wrapper forwards to it for backward compatibility.
|
||||||
# Usage:
|
# Please update your scripts, docs and habits to call compile-firmware.sh.
|
||||||
# ./compile-with-docker.sh [Preset] [CMake options...]
|
# ---------------------------------------------------------------------------
|
||||||
# Examples:
|
echo "note: 'compile-with-docker.sh' was renamed to 'compile-firmware.sh' - forwarding." >&2
|
||||||
# ./compile-with-docker.sh Fusion
|
exec "$(dirname "$0")/compile-firmware.sh" "$@"
|
||||||
# ./compile-with-docker.sh Transfer
|
|
||||||
# ./compile-with-docker.sh FieldOps
|
|
||||||
# ./compile-with-docker.sh Max
|
|
||||||
# ./compile-with-docker.sh Fusion -DDEV=ON
|
|
||||||
# ./compile-with-docker.sh FieldOps -DENABLE_VOX=OFF
|
|
||||||
# ./compile-with-docker.sh All
|
|
||||||
# Default preset: "Fusion"
|
|
||||||
# ---------------------------------------------
|
|
||||||
|
|
||||||
IMAGE=uvk1-uvk5v3
|
|
||||||
RELEASE_PRESETS=(Fusion Transfer FieldOps Max)
|
|
||||||
FLASH_LIMIT=$((118 * 1024))
|
|
||||||
RAM_LIMIT=$((16 * 1024))
|
|
||||||
|
|
||||||
PRESET=${1:-Fusion}
|
|
||||||
shift || true # remove preset from arguments if present
|
|
||||||
|
|
||||||
# Any remaining args will be treated as CMake cache variables
|
|
||||||
EXTRA_ARGS=("$@")
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
|
||||||
# Validate preset name
|
|
||||||
# ---------------------------------------------
|
|
||||||
if [[ ! "$PRESET" =~ ^(Custom|Fusion|Transfer|FieldOps|Max|All)$ ]]; then
|
|
||||||
echo "❌ Unknown preset: '$PRESET'"
|
|
||||||
echo "Valid presets are: Custom, Fusion, Transfer, FieldOps, Max, All"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
QUIET=0
|
|
||||||
if [[ "$PRESET" == "All" ]]; then
|
|
||||||
QUIET=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remember whether the script itself was started from an interactive terminal.
|
|
||||||
# The build output is piped through tee below, so checking stdout later from
|
|
||||||
# run_preset_build() would always report a non-terminal.
|
|
||||||
INTERACTIVE=0
|
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
INTERACTIVE=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
|
||||||
# Build the Docker image (only needed once)
|
|
||||||
# ---------------------------------------------
|
|
||||||
if [[ "$(docker images -q "$IMAGE")" == "" ]]; then
|
|
||||||
echo "Building Docker image..."
|
|
||||||
docker build -q -t "$IMAGE" . >/dev/null
|
|
||||||
fi
|
|
||||||
export MSYS_NO_PATHCONV=1
|
|
||||||
|
|
||||||
RESULT_PRESETS=()
|
|
||||||
RESULT_FLASH_SIZES=()
|
|
||||||
RESULT_RAM_SIZES=()
|
|
||||||
|
|
||||||
run_preset_build() {
|
|
||||||
local preset="$1"
|
|
||||||
local -a docker_tty_args=()
|
|
||||||
|
|
||||||
# Give Ninja a pseudo-terminal for an interactive single-preset build. This
|
|
||||||
# lets it refresh its [current/total] progress on one line. Batch/redirected
|
|
||||||
# builds keep plain line-oriented output suitable for logs and CI.
|
|
||||||
if (( INTERACTIVE && ! QUIET )); then
|
|
||||||
docker_tty_args=(-t)
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker run --rm "${docker_tty_args[@]}" \
|
|
||||||
-u "$(id -u):$(id -g)" \
|
|
||||||
-v "$PWD":/src -w /src "$IMAGE" \
|
|
||||||
bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version &&
|
|
||||||
cmake --fresh --preset "$1" "${@:2}" &&
|
|
||||||
cmake --build --preset "$1" -j' \
|
|
||||||
bash "$preset" "${EXTRA_ARGS[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_preset() {
|
|
||||||
local preset="$1"
|
|
||||||
local preset_slug log_file bin_file flash_size ram_size status
|
|
||||||
|
|
||||||
preset_slug="${preset,,}"
|
|
||||||
log_file="$(mktemp)"
|
|
||||||
bin_file=""
|
|
||||||
|
|
||||||
find "build/${preset}" -maxdepth 1 -type f -name 'f4hwn.*' -delete 2>/dev/null || true
|
|
||||||
|
|
||||||
if (( QUIET )); then
|
|
||||||
printf "Building %-10s ... " "$preset"
|
|
||||||
if run_preset_build "$preset" >"$log_file" 2>&1; then
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "=== 🚀 Building preset: ${preset} ==="
|
|
||||||
echo "---------------------------------------------"
|
|
||||||
if (( INTERACTIVE )); then
|
|
||||||
# Do not put a line-oriented filter after tee here: it would buffer
|
|
||||||
# Ninja's carriage-return progress updates and defeat the TTY display.
|
|
||||||
if run_preset_build "$preset" 2>&1 | tee "$log_file"; then
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if run_preset_build "$preset" 2>&1 | tee "$log_file" | awk '
|
|
||||||
/^Memory region[[:space:]]+Used Size[[:space:]]+Region Size/ { next }
|
|
||||||
/^[[:space:]]+RAM:[[:space:]]/ { next }
|
|
||||||
/^[[:space:]]+FLASH:[[:space:]]/ { next }
|
|
||||||
{ print; fflush() }
|
|
||||||
'; then
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( status != 0 )); then
|
|
||||||
if (( QUIET )); then
|
|
||||||
printf "FAILED\n\n"
|
|
||||||
cat "$log_file"
|
|
||||||
else
|
|
||||||
echo "Failed: ${preset}"
|
|
||||||
fi
|
|
||||||
rm -f -- "$log_file"
|
|
||||||
return "$status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
bin_file="build/${preset}/f4hwn.${preset_slug}.bin"
|
|
||||||
if [[ -z "$bin_file" || ! -f "$bin_file" ]]; then
|
|
||||||
if (( QUIET )); then
|
|
||||||
printf "FAILED\n"
|
|
||||||
fi
|
|
||||||
echo "Expected binary not found: $bin_file"
|
|
||||||
if (( QUIET )); then
|
|
||||||
cat "$log_file"
|
|
||||||
fi
|
|
||||||
rm -f -- "$log_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
flash_size="$(wc -c < "$bin_file")"
|
|
||||||
ram_size="$(awk '$1 == "RAM:" {
|
|
||||||
value = $2
|
|
||||||
if ($3 == "KB") value *= 1024
|
|
||||||
else if ($3 == "MB") value *= 1024 * 1024
|
|
||||||
printf "%.0f\n", value
|
|
||||||
}' "$log_file" | tail -n 1)"
|
|
||||||
|
|
||||||
if [[ ! "$ram_size" =~ ^[0-9]+$ ]]; then
|
|
||||||
if (( QUIET )); then
|
|
||||||
printf "FAILED\n"
|
|
||||||
fi
|
|
||||||
echo "Could not read RAM usage from linker output"
|
|
||||||
if (( QUIET )); then
|
|
||||||
cat "$log_file"
|
|
||||||
fi
|
|
||||||
rm -f -- "$log_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
RESULT_PRESETS+=("$preset")
|
|
||||||
RESULT_FLASH_SIZES+=("$flash_size")
|
|
||||||
RESULT_RAM_SIZES+=("$ram_size")
|
|
||||||
|
|
||||||
if (( QUIET )); then
|
|
||||||
printf "OK\n"
|
|
||||||
else
|
|
||||||
echo "✅ Done: ${preset}"
|
|
||||||
fi
|
|
||||||
rm -f -- "$log_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_summary() {
|
|
||||||
local i
|
|
||||||
local flash_used flash_free flash_bp flash_pct
|
|
||||||
local flash_used_kib100 flash_free_kib100 flash_used_kib flash_free_kib
|
|
||||||
local ram_used ram_free ram_bp ram_pct
|
|
||||||
local ram_used_kib100 ram_free_kib100 ram_used_kib ram_free_kib
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "Flash (limit: %d B / %d.00 KiB)\n" "$FLASH_LIMIT" "$((FLASH_LIMIT / 1024))"
|
|
||||||
printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \
|
|
||||||
"Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage"
|
|
||||||
printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \
|
|
||||||
"----------" "----------" "----------" "----------" "----------" "-------"
|
|
||||||
|
|
||||||
for i in "${!RESULT_PRESETS[@]}"; do
|
|
||||||
flash_used="${RESULT_FLASH_SIZES[$i]}"
|
|
||||||
flash_free=$((FLASH_LIMIT - flash_used))
|
|
||||||
flash_bp=$(((flash_used * 10000 + FLASH_LIMIT / 2) / FLASH_LIMIT))
|
|
||||||
flash_used_kib100=$(((flash_used * 100 + 512) / 1024))
|
|
||||||
flash_free_kib100=$(((flash_free * 100 + 512) / 1024))
|
|
||||||
printf -v flash_pct "%d.%02d%%" "$((flash_bp / 100))" "$((flash_bp % 100))"
|
|
||||||
printf -v flash_used_kib "%d.%02d" "$((flash_used_kib100 / 100))" "$((flash_used_kib100 % 100))"
|
|
||||||
printf -v flash_free_kib "%d.%02d" "$((flash_free_kib100 / 100))" "$((flash_free_kib100 % 100))"
|
|
||||||
|
|
||||||
printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \
|
|
||||||
"${RESULT_PRESETS[$i]}" "$flash_used" "$flash_used_kib" \
|
|
||||||
"$flash_free" "$flash_free_kib" "$flash_pct"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "RAM (limit: %d B / %d.00 KiB)\n" "$RAM_LIMIT" "$((RAM_LIMIT / 1024))"
|
|
||||||
printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \
|
|
||||||
"Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage"
|
|
||||||
printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \
|
|
||||||
"----------" "----------" "----------" "----------" "----------" "-------"
|
|
||||||
|
|
||||||
for i in "${!RESULT_PRESETS[@]}"; do
|
|
||||||
ram_used="${RESULT_RAM_SIZES[$i]}"
|
|
||||||
ram_free=$((RAM_LIMIT - ram_used))
|
|
||||||
ram_bp=$(((ram_used * 10000 + RAM_LIMIT / 2) / RAM_LIMIT))
|
|
||||||
ram_used_kib100=$(((ram_used * 100 + 512) / 1024))
|
|
||||||
ram_free_kib100=$(((ram_free * 100 + 512) / 1024))
|
|
||||||
printf -v ram_pct "%d.%02d%%" "$((ram_bp / 100))" "$((ram_bp % 100))"
|
|
||||||
printf -v ram_used_kib "%d.%02d" "$((ram_used_kib100 / 100))" "$((ram_used_kib100 % 100))"
|
|
||||||
printf -v ram_free_kib "%d.%02d" "$((ram_free_kib100 / 100))" "$((ram_free_kib100 % 100))"
|
|
||||||
|
|
||||||
printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \
|
|
||||||
"${RESULT_PRESETS[$i]}" "$ram_used" "$ram_used_kib" \
|
|
||||||
"$ram_free" "$ram_free_kib" "$ram_pct"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if (( ${#RESULT_PRESETS[@]} == 1 )); then
|
|
||||||
echo "🎉 Preset ${RESULT_PRESETS[0]} built successfully!"
|
|
||||||
else
|
|
||||||
echo "🎉 All presets built successfully!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
|
||||||
# Handle 'All' preset
|
|
||||||
# ---------------------------------------------
|
|
||||||
if [[ "$PRESET" == "All" ]]; then
|
|
||||||
for p in "${RELEASE_PRESETS[@]}"; do
|
|
||||||
build_preset "$p"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
build_preset "$PRESET"
|
|
||||||
fi
|
|
||||||
print_summary
|
|
||||||
Reference in new issue
Block a user