diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 65414ad7..1c94bf7b 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -215,12 +215,16 @@ enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_APPS apps/app_menu.c ) enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_BEAM) +enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_INFO) 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() if(ENABLE_FEAT_F4HWN_OVERLAY_BEAM AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS) message(FATAL_ERROR "Overlay Beam service requires ENABLE_FEAT_F4HWN_OVERLAY_APPS.") endif() +if(ENABLE_FEAT_F4HWN_OVERLAY_INFO AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS) + message(FATAL_ERROR "Overlay system information requires ENABLE_FEAT_F4HWN_OVERLAY_APPS.") +endif() enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER k5viewer.c ) diff --git a/App/apps/app_api.h b/App/apps/app_api.h index 7e74e069..4531ca77 100644 --- a/App/apps/app_api.h +++ b/App/apps/app_api.h @@ -42,7 +42,7 @@ * ships is frozen; services added before the next release join its level): * 1 v6.0.0 baseline: every service up to and including beam_draw * 2 ticks_ms, rand32, asset_read (+ app_header_t asset_size / asset_crc), - * idivmod, uidivmod (the resident division helpers) */ + * idivmod, uidivmod (the resident division helpers), Labs system info */ #define APP_ABI_MAJOR 1u #define APP_API_LEVEL 2u @@ -257,8 +257,28 @@ typedef struct app_api { * the AEABI helpers return them; x / 0 gives 0, remainder x. */ uint64_t (*idivmod)(int32_t n, int32_t d); uint64_t (*uidivmod)(uint32_t n, uint32_t d); + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO + /* ---- API level 2: zero-code Labs system information ---- */ + const char *sys_edition; + const char *sys_version; + const char *sys_build_date; + const char *sys_build_time; + const char *sys_build_commit; + const void *sys_flash_end; + const void *sys_ram_end; + const uint16_t *sys_battery_voltage; + const void *sys_battery_type; + unsigned int (*sys_battery_percent)(unsigned int voltage_10mV); + void (*sys_storage_read)(uint32_t address, void *buffer, uint32_t size); +#endif } app_api_t; +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO +_Static_assert(sizeof(app_api_t) == 320u, + "Labs system information must add exactly 44 API bytes"); +#endif + /* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */ enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 }; diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c index 4114638e..5ce861dd 100644 --- a/App/apps/app_overlay.c +++ b/App/apps/app_overlay.c @@ -54,6 +54,9 @@ #include "helper/battery.h" #include "settings.h" #include "misc.h" /* dBmCorrTable */ +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO +#include "version.h" +#endif _Static_assert(sizeof(app_header_t) == 64u && _Alignof(app_header_t) == 4u, "app_header_t must stay 64 bytes with word alignment"); @@ -86,6 +89,9 @@ enum { #ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM | APP_CAP_BEAM #endif +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO + | APP_CAP_SYSINFO +#endif }; /* Keep the small zero-initialized state together so callbacks can address it @@ -183,6 +189,11 @@ static int8_t app_nav_dir(uint8_t key) return gEeprom.SET_NAV ? direction : -direction; } + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO +extern uint8_t _eflash_used; +extern uint8_t _ebss; +#endif static void app_led(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, on); } static void app_play_tone(uint16_t tone, uint16_t ms) @@ -784,6 +795,19 @@ static const app_api_t app_api = { .asset_read = app_asset_read, .idivmod = __aeabi_idivmod, .uidivmod = __aeabi_uidivmod, +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_INFO + .sys_edition = Edition, + .sys_version = DisplayVersion, + .sys_build_date = BuildDate, + .sys_build_time = BuildTime, + .sys_build_commit = BuildCommit, + .sys_flash_end = &_eflash_used, + .sys_ram_end = &_ebss, + .sys_battery_voltage = &gBatteryVoltageAverage, + .sys_battery_type = &gEeprom.BATTERY_TYPE, + .sys_battery_percent = BATTERY_VoltsToPercent, + .sys_storage_read = PY25Q16_ReadBuffer, +#endif }; uint8_t APP_LaunchOverlay(uint8_t slot) diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h index e90d7f75..4dd2fdc1 100644 --- a/App/apps/app_overlay.h +++ b/App/apps/app_overlay.h @@ -97,6 +97,7 @@ * previously reserved header bytes, so app_header_t remains 64 bytes. */ #define APP_CAP_FM 0x00000001u #define APP_CAP_BEAM 0x00000004u +#define APP_CAP_SYSINFO 0x00000008u /* Aligned RAM objects; app_overlay.c pins every serialized field offset. */ typedef struct { diff --git a/App/apps/pack_app.py b/App/apps/pack_app.py index c655f428..b6879bf7 100644 --- a/App/apps/pack_app.py +++ b/App/apps/pack_app.py @@ -51,6 +51,7 @@ SHORTCUTS = { CAPABILITIES = { "fm": cdefine("app_overlay.h", "APP_CAP_FM"), "beam": cdefine("app_overlay.h", "APP_CAP_BEAM"), + "sysinfo": cdefine("app_overlay.h", "APP_CAP_SYSINFO"), } def field(s: str, n: int) -> bytes: diff --git a/App/apps/sysinfo/app.ld b/App/apps/sysinfo/app.ld new file mode 100644 index 00000000..0131fd36 --- /dev/null +++ b/App/apps/sysinfo/app.ld @@ -0,0 +1,29 @@ +APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280; +APP_LENGTH = 0x1000; + +ENTRY(app_main) + +MEMORY { + APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH +} + +SECTIONS { + .app APP_VMA : { + KEEP(*(.text.entry)) + *(.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, + "System Info overlay app overflows the 4 KiB overlay") + + /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) } +} diff --git a/App/apps/sysinfo/build.sh b/App/apps/sysinfo/build.sh new file mode 100755 index 00000000..8777fa38 --- /dev/null +++ b/App/apps/sysinfo/build.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP="$(basename "$PWD")" +APP_NAME="SystemInfo" +APP_VER="1.0" +APP_API_MIN=2 +APP_VMA=${APP_VMA:-0x20000280} +OUT="${APP_NAME// /}" + +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 \ + -DENABLE_FEAT_F4HWN_OVERLAY_INFO" +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 + +step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } +trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR + +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" \ + --require sysinfo >/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" diff --git a/App/apps/sysinfo/gen_assets.py b/App/apps/sysinfo/gen_assets.py new file mode 100755 index 00000000..cb7649fe --- /dev/null +++ b/App/apps/sysinfo/gen_assets.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +a = Assets("SYSINFO") +a.text("T_TITLE", "SYSTEM INFO") +a.table("T_SECTION", ["FIRMWARE", "SYSTEM", "BATTERY", "MEMORY", "IDENTITY", + "CODE", "WIKI"]) +a.table("T_KEY", ["EDITION", "VERSION", "BUILD", "TIME", "COMMIT", + "FLASH", "RAM", "FREQ", "OVERLAY", "LEVEL", "TYPE", + "CHANNELS", "LISTS", "HF", "VHF", "UHF", "AERO", "OTHER", + "UID1", "UID2", "UID3", "UPTIME", "AM/FM", + "WIDE/NARROW", "CTCSS/DCS", "CPUID", "MCU TEMP", "CPU", + "RESET", "SILICON REV", "CLOCK", "RDP", "VDD", "BOR", + "HW FLASH", "MEM MAP", "IWDG", "FLASH WS", "WWDG", + "NRST", "IWDG STOP", "SPI FLASH"]) +a.table("T_BATTERY", ["1600 mAh", "2200 mAh", "3500 mAh", "1500 mAh", + "2500 mAh", "UNKNOWN"]) +a.text("T_CPU", "PY32F071") +a.table("T_RESET", ["POWER", "PIN", "SOFTWARE", "IWDG", "WWDG", "OPTION", + "UNKNOWN"]) +a.table("T_CLOCK", ["HSI", "HSE", "PLL", "LSI", "LSE", "UNKNOWN"]) +a.table("T_RDP", ["OPEN", "LOCKED", "UNKNOWN"]) +a.table("T_BOR", ["OFF", "1.7-1.8V", "1.9-2.0V", "2.1-2.2V", "2.3-2.4V", + "2.5-2.6V", "2.7-2.8V", "2.9-3.0V", "3.1-3.2V"]) +a.table("T_BOOT", ["FLASH", "SYSTEM", "SRAM", "UNKNOWN"]) +a.table("T_WDG", ["HARDWARE", "SOFTWARE"]) +a.table("T_NRST", ["RESET", "GPIO"]) +a.table("T_IWDG_STOP", ["FREEZE", "ACTIVE"]) +a.text("T_HW_FLASH", "128K") +a.text("T_SPI_FLASH", "PY25Q16 2M") +a.raw("QR_CODE", bytes([ + 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0xB8, 0xCB, 0xA0, 0x6D, 0x07, 0xCB, 0x1F, 0xD2, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, + 0x87, 0xA3, 0x69, 0xC3, 0x19, 0x0E, 0x55, 0x1F, 0x43, 0x11, 0x16, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22, + 0xE0, 0x0B, 0x1D, 0x28, 0xF5, 0x87, 0x55, 0xEB, 0xA8, 0x11, 0xA3, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22, + 0xFD, 0x04, 0x75, 0x75, 0x75, 0x04, 0xFD, 0x01, 0xF7, 0xE0, 0xD6, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x2C, 0x2B, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, +])) +a.raw("QR_WIKI", bytes([ + 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0x0F, 0x74, 0x0E, 0xD2, 0xB0, 0x74, 0xB1, 0x6D, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, + 0xCD, 0x5D, 0x83, 0x65, 0xE7, 0xC6, 0x55, 0xBD, 0x6B, 0x3F, 0xA9, 0x1C, 0xA5, 0xE0, 0x69, 0xE3, 0x4A, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22, + 0xFF, 0x25, 0xAE, 0xB6, 0x30, 0xF8, 0x55, 0xDD, 0x07, 0xB6, 0xC2, 0x1C, 0xA5, 0xE0, 0x69, 0xC4, 0x66, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22, + 0xFD, 0x04, 0x74, 0x74, 0x74, 0x05, 0xFD, 0x01, 0xF7, 0xAE, 0x81, 0x1C, 0xA5, 0xE0, 0x69, 0x54, 0xFE, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x24, 0x33, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, +])) +a.main() diff --git a/App/apps/sysinfo/sysinfo_app.c b/App/apps/sysinfo/sysinfo_app.c new file mode 100644 index 00000000..63850e79 --- /dev/null +++ b/App/apps/sysinfo/sysinfo_app.c @@ -0,0 +1,810 @@ +/* Copyright 2026 Armel F4HWN + * Licensed under the Apache License, Version 2.0. + * + * Labs system information overlay. Static UI strings live in the app assets; + * enumeration and aggregation stay in the overlay to preserve firmware flash. + */ + +#include +#include +#include "../app_api.h" +#include "sysinfo_assets.h" + +#define VISIBLE_ROWS 7u +#define ROW_HEIGHT 8u +#define SCROLL_STEP 8u +#define SCROLL_FRAME_STEP 2u +#define INPUT_TICK_MS 15u +#define NAV_REPEAT_DELAY_MS 300u +#define NAV_REPEAT_MS 120u +#define VALUE_X 54u +#define CHANNEL_ATTR_BASE 0x00008000u +#define UID_ADDRESS 0x1FFF3000u +#define CPUID_ADDRESS 0xE000ED00u +#define ADC1_ADDRESS 0x40012400u +#define DBGMCU_ID_ADDRESS 0x40015800u +#define RCC_CFGR_ADDRESS 0x40021008u +#define RCC_CSR_ADDRESS 0x40021060u +#define SYSCFG_CFGR1_ADDRESS 0x40010000u +#define FLASH_ACR_ADDRESS 0x40022000u +#define FLASH_OPTR_ADDRESS 0x40022020u +#define FLASH_BORCR_ADDRESS 0x40022024u +#define ADC_SR_EOC (1u << 1) +#define ADC_CR2_EXTTRIG (1u << 20) +#define ADC_CR2_SWSTART (1u << 22) +#define ADC_CR2_TSVREFE (1u << 23) +#define ADC_CHANNEL_MASK 0x1Fu +#define ADC_CHANNEL_TEMP 16u +#define ADC_CHANNEL_VREF 17u +#define ADC_SAMPLE_16_17_MASK (0x3Fu << 18) +#define ADC_SAMPLE_16_17_LONG (0x3Fu << 18) +#define RESET_OPTION_FLAG (1u << 25) +#define RESET_PIN_FLAG (1u << 26) +#define RESET_POWER_FLAG (1u << 27) +#define RESET_SOFT_FLAG (1u << 28) +#define RESET_IWDG_FLAG (1u << 29) +#define RESET_WWDG_FLAG (1u << 30) +#define ROW_BLANK 0x7Fu +#define ROW_SECTION(n) (0x80u | (n)) +#define ROW_QR(wiki, page) (0x40u | ((wiki) << 3) | (page)) +#define QR_WIDTH 33u +#define QR_X 47u +#define UPTIME_ROW 14u +#define BATTERY_LEVEL_ROW 36u + +static const uint8_t rows[] = { + ROW_BLANK, ROW_SECTION(0), ROW_BLANK, 0, 1, 2, 3, 4, ROW_BLANK, + ROW_SECTION(1), ROW_BLANK, 27, 7, 8, 21, 25, 26, 5, 6, 28, 29, 30, 31, + 32, 33, 34, 41, 35, 36, 37, 38, 39, 40, + ROW_BLANK, + ROW_SECTION(2), ROW_BLANK, 9, 10, ROW_BLANK, + ROW_SECTION(3), ROW_BLANK, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, + ROW_BLANK, + ROW_SECTION(4), ROW_BLANK, 18, 19, 20, ROW_BLANK, + ROW_SECTION(5), ROW_BLANK, + ROW_QR(0, 0), ROW_QR(0, 1), ROW_QR(0, 2), ROW_QR(0, 3), ROW_QR(0, 4), + ROW_BLANK, + ROW_SECTION(6), ROW_BLANK, + ROW_QR(1, 0), ROW_QR(1, 1), ROW_QR(1, 2), ROW_QR(1, 3), ROW_QR(1, 4), +}; +#define ROW_COUNT ((uint16_t)sizeof(rows)) + +struct system_core { + uint32_t flash_used; + uint32_t flash_total; + uint32_t ram_used; + uint32_t ram_total; + uint32_t uid[3]; + uint32_t cpuid; + uint32_t silicon_rev; + const char *edition; + const char *version; + const char *build_date; + const char *build_time; + const char *build_commit; + uint16_t battery_voltage; + uint16_t vdd_mv; + int16_t temperature; + bool temperature_valid; + bool vdd_valid; + uint8_t battery_percent; + uint8_t battery_type; + uint8_t cpu_mhz; + uint8_t reset_source; + uint8_t clock_source; + uint8_t rdp_state; + uint8_t bor_state; + uint8_t boot_mode; + uint8_t option_modes; + uint8_t flash_wait_states; +}; + +struct globals { + const app_api_t *api; + struct system_core core; + uint16_t channels_used; + uint16_t channels_hf; + uint16_t channels_vhf; + uint16_t channels_uhf; + uint16_t channels_air; + uint16_t channels_other; + uint16_t channels_am; + uint16_t channels_fm; + uint16_t channels_wide; + uint16_t channels_narrow; + uint16_t channels_ctcss; + uint16_t channels_dcs; + uint8_t lists_used; + uint16_t scroll; + uint16_t target; + uint16_t row_count; + bool running; + bool dirty; + bool suspended; +}; +static struct globals g; +#define A (g.api) + +static uint64_t divide(uint32_t n, uint32_t d) +{ + return A->uidivmod(n, d); +} + +static char *put_u32(char *out, uint32_t value) +{ + char reverse[10]; + uint8_t count = 0; + do { + const uint64_t qr = divide(value, 10u); + reverse[count++] = (char)('0' + (uint32_t)(qr >> 32)); + value = (uint32_t)qr; + } while (value != 0u); + while (count != 0u) + *out++ = reverse[--count]; + *out = '\0'; + return out; +} + +static char *put_char(char *out, char value) +{ + *out++ = value; + *out = '\0'; + return out; +} + +static char *put_2digits(char *out, uint32_t value) +{ + if (value < 10u) + out = put_char(out, '0'); + return put_u32(out, value); +} + +static char *put_text(char *out, const char *text) +{ + uint8_t length = 0u; + while (*text && length++ < 10u) + *out++ = *text++; + *out = '\0'; + return out; +} + +static char *put_hex32(char *out, uint32_t value) +{ + for (int8_t shift = 28; shift >= 0; shift -= 4) { + const uint8_t digit = (uint8_t)((value >> shift) & 0x0Fu); + *out++ = (char)(digit < 10u ? '0' + digit : 'A' + digit - 10); + } + *out = '\0'; + return out; +} + +static uint8_t text_length(const char *text) +{ + uint8_t length = 0; + while (text[length]) + length++; + return length; +} + +static void refresh_power(void) +{ + A->battery_sample(); + g.core.battery_voltage = *A->sys_battery_voltage; + g.core.battery_percent = (uint8_t)A->sys_battery_percent(g.core.battery_voltage); + g.core.battery_type = *(const uint8_t *)A->sys_battery_type; +} + +static bool adc_read_channel(volatile uint32_t *adc, uint8_t channel, + uint16_t *sample) +{ + adc[14] = (adc[14] & ~ADC_CHANNEL_MASK) | channel; + adc[0] = ~ADC_SR_EOC; + adc[2] |= ADC_CR2_SWSTART | ADC_CR2_EXTTRIG; + + uint16_t timeout = 0xFFFFu; + while ((adc[0] & ADC_SR_EOC) == 0u && --timeout != 0u) + ; + if (timeout == 0u) + return false; + + *sample = (uint16_t)(adc[20] & 0x0FFFu); + return true; +} + +static void refresh_temperature(void) +{ + volatile uint32_t *const adc = (volatile uint32_t *)ADC1_ADDRESS; + const uint32_t saved_cr2 = adc[2]; + const uint32_t saved_smpr2 = adc[4]; + const uint32_t saved_sqr3 = adc[14]; + uint16_t temp_sample = 0u; + uint16_t vref_sample = 0u; + + adc[2] = saved_cr2 | ADC_CR2_TSVREFE; + adc[4] = (saved_smpr2 & ~ADC_SAMPLE_16_17_MASK) | + ADC_SAMPLE_16_17_LONG; + A->delay_ms(1u); + + const bool temp_ok = adc_read_channel(adc, ADC_CHANNEL_TEMP, &temp_sample); + const bool vref_ok = adc_read_channel(adc, ADC_CHANNEL_VREF, &vref_sample); + + adc[14] = saved_sqr3; + adc[4] = saved_smpr2; + adc[2] = saved_cr2; + + if (!vref_ok || vref_sample == 0u) { + g.core.vdd_valid = false; + g.core.temperature_valid = false; + return; + } + + g.core.vdd_mv = (uint16_t)divide(4095u * 1200u, vref_sample); + g.core.vdd_valid = true; + if (!temp_ok) { + g.core.temperature_valid = false; + return; + } + + const uint32_t sensor_mv = + (uint32_t)divide((uint32_t)temp_sample * 1200u, vref_sample); + if (sensor_mv >= 760u) + g.core.temperature = (int16_t)(30u + + (uint32_t)divide((sensor_mv - 760u) * 10u, 25u)); + else + g.core.temperature = (int16_t)(30 - (int32_t) + (uint32_t)divide((760u - sensor_mv) * 10u, 25u)); + g.core.temperature_valid = true; +} + +static void load_system_info(void) +{ + uint32_t list_mask = 0u; + const uint32_t reset_flags = + *(const volatile uint32_t *)RCC_CSR_ADDRESS; + + g.core.flash_used = (uint32_t)(uintptr_t)A->sys_flash_end - 0x08002800u; + g.core.flash_total = 118u * 1024u; + g.core.ram_used = (uint32_t)(uintptr_t)A->sys_ram_end - 0x20000000u; + g.core.ram_total = 16u * 1024u; + g.core.uid[0] = *(const uint32_t *)(UID_ADDRESS + 0u); + g.core.uid[1] = *(const uint32_t *)(UID_ADDRESS + 4u); + g.core.uid[2] = *(const uint32_t *)(UID_ADDRESS + 8u); + g.core.cpuid = *(const volatile uint32_t *)CPUID_ADDRESS; + g.core.silicon_rev = *(const volatile uint32_t *)DBGMCU_ID_ADDRESS; + g.core.clock_source = (uint8_t) + ((*(const volatile uint32_t *)RCC_CFGR_ADDRESS >> 3) & 7u); + if (g.core.clock_source > 4u) + g.core.clock_source = 5u; + const uint32_t optr = + *(const volatile uint32_t *)FLASH_OPTR_ADDRESS; + const uint8_t rdp = (uint8_t)optr; + g.core.rdp_state = rdp == 0xAAu ? 0u : rdp == 0x55u ? 1u : 2u; + g.core.option_modes = (uint8_t)(optr >> 11); + g.core.boot_mode = (uint8_t) + (*(const volatile uint32_t *)SYSCFG_CFGR1_ADDRESS & 3u); + if (g.core.boot_mode == 2u) + g.core.boot_mode = 3u; + else if (g.core.boot_mode == 3u) + g.core.boot_mode = 2u; + g.core.flash_wait_states = (uint8_t) + (*(const volatile uint32_t *)FLASH_ACR_ADDRESS & 3u); + const uint32_t bor = *(const volatile uint32_t *)FLASH_BORCR_ADDRESS; + g.core.bor_state = (bor & (1u << 5)) != 0u + ? (uint8_t)(((bor >> 13) & 7u) + 1u) : 0u; + if (reset_flags & RESET_IWDG_FLAG) + g.core.reset_source = 3u; + else if (reset_flags & RESET_WWDG_FLAG) + g.core.reset_source = 4u; + else if (reset_flags & RESET_SOFT_FLAG) + g.core.reset_source = 2u; + else if (reset_flags & RESET_POWER_FLAG) + g.core.reset_source = 0u; + else if (reset_flags & RESET_OPTION_FLAG) + g.core.reset_source = 5u; + else if (reset_flags & RESET_PIN_FLAG) + g.core.reset_source = 1u; + else + g.core.reset_source = 6u; + g.core.edition = A->sys_edition; + g.core.version = A->sys_version; + g.core.build_date = A->sys_build_date; + g.core.build_time = A->sys_build_time; + g.core.build_commit = A->sys_build_commit; + g.core.cpu_mhz = 48u; + refresh_power(); + refresh_temperature(); + + for (uint16_t index = 0u; index < 1024u; index++) { + uint16_t attributes; + A->sys_storage_read(CHANNEL_ATTR_BASE + (uint32_t)index * 2u, + &attributes, sizeof(attributes)); + const uint8_t band = (uint8_t)(attributes & 0x0007u); + if (attributes == 0xFFFFu || band > 6u) + continue; + + struct { + uint32_t frequency; + uint32_t offset; + uint8_t data[8]; + } channel; + A->sys_storage_read((uint32_t)index * 16u, &channel, sizeof(channel)); + if (channel.frequency == 0u || channel.frequency == 0xFFFFFFFFu) + continue; + + g.channels_used++; + if (band == 1u) { + g.channels_air++; + } else if (band == 0u) { + if (channel.frequency >= 300000u && channel.frequency < 3000000u) + g.channels_hf++; + else if (channel.frequency >= 3000000u) + g.channels_vhf++; + else + g.channels_other++; + } else if (band <= 2u || + (band == 3u && channel.frequency < 30000000u)) { + g.channels_vhf++; + } else if (channel.frequency < 300000000u) { + g.channels_uhf++; + } else { + g.channels_other++; + } + + const uint8_t modulation = channel.data[3] >> 4; + if (modulation == 1u) + g.channels_am++; + else if (modulation == 0u || modulation >= 3u) + g.channels_fm++; + + const uint8_t shape = channel.data[4]; + if (shape == 0xFFu) { + g.channels_wide++; + } else { + if (shape & 0x02u) + g.channels_narrow++; + else + g.channels_wide++; + } + + const uint8_t rx_code = channel.data[2] & 0x0Fu; + const uint8_t tx_code = channel.data[2] >> 4; + if (rx_code == 1u || tx_code == 1u) + g.channels_ctcss++; + if (rx_code == 2u || rx_code == 3u || + tx_code == 2u || tx_code == 3u) + g.channels_dcs++; + + if (attributes & 0x0080u) + continue; + const uint8_t scanlist = (uint8_t)(attributes >> 8); + if (scanlist >= 1u && scanlist <= 24u) + list_mask |= 1u << (scanlist - 1u); + else if (scanlist == 25u) + list_mask = 0x00FFFFFFu; + } + + while (list_mask != 0u) { + g.lists_used += (uint8_t)(list_mask & 1u); + list_mask >>= 1; + } +} + +static char *format_size(char *out, uint32_t bytes, uint32_t total) +{ + uint64_t qr = divide(bytes, 1024u); + out = put_u32(out, (uint32_t)qr); + out = put_char(out, '.'); + out = put_u32(out, (uint32_t)divide((uint32_t)(qr >> 32) * 10u, 1024u)); + out = put_char(out, '/'); + out = put_u32(out, (uint32_t)divide(total, 1024u)); + return put_char(out, 'K'); +} + +static char *format_pair(char *out, uint16_t value, uint16_t total) +{ + out = put_u32(out, value); + out = put_char(out, '/'); + return put_u32(out, total); +} + +static void format_voltage(char *out, uint16_t millivolts) +{ + const uint64_t qr = divide(millivolts, 1000u); + out = put_u32(out, (uint32_t)qr); + out = put_char(out, '.'); + const uint32_t hundredths = + (uint32_t)divide((uint32_t)(qr >> 32), 10u); + if (hundredths < 10u) + out = put_char(out, '0'); + out = put_u32(out, hundredths); + put_char(out, 'V'); +} + +static void format_uptime(char *out) +{ + uint32_t seconds = (uint32_t)divide(A->ticks_ms(), 1000u); + uint64_t qr = divide(seconds, 60u); + const uint32_t second = (uint32_t)(qr >> 32); + qr = divide((uint32_t)qr, 60u); + const uint32_t minute = (uint32_t)(qr >> 32); + const uint32_t hour = (uint32_t)qr; + + out = put_u32(out, hour); + out = put_char(out, ':'); + out = put_2digits(out, minute); + out = put_char(out, ':'); + put_2digits(out, second); +} + +static char *put_build_date(char *out, const char *date) +{ + *out++ = date[4] == ' ' ? '0' : date[4]; + *out++ = date[5]; + *out++ = ' '; + *out++ = date[0]; + *out++ = date[1]; + *out++ = date[2]; + *out++ = ' '; + *out++ = date[9]; + *out++ = date[10]; + *out = '\0'; + return out; +} + +static void format_item_value(uint8_t item, char *out) +{ + switch (item) { + case 0: put_text(out, g.core.edition); break; + case 1: { + const char *version = g.core.version; + while (*version && *version != ' ') + version++; + if (*version == ' ') + version++; + put_text(out, version); + break; + } + case 2: put_build_date(out, g.core.build_date); break; + case 3: put_text(out, g.core.build_time); break; + case 4: put_text(out, g.core.build_commit); break; + case 5: format_size(out, g.core.flash_used, g.core.flash_total); break; + case 6: format_size(out, g.core.ram_used, g.core.ram_total); break; + case 7: + out = put_u32(out, g.core.cpu_mhz); + out = put_char(out, 'M'); + out = put_char(out, 'H'); + put_char(out, 'z'); + break; + case 8: + out = put_char(out, 'A'); + out = put_char(out, 'B'); + out = put_char(out, 'I'); + out = put_u32(out, A->abi_major); + out = put_char(out, '/'); + out = put_char(out, 'A'); + out = put_char(out, 'P'); + out = put_char(out, 'I'); + put_u32(out, A->api_level); + break; + case 9: { + const uint64_t qr = divide(g.core.battery_voltage, 100u); + out = put_u32(out, (uint32_t)qr); + out = put_char(out, '.'); + const uint8_t decimals = (uint8_t)(qr >> 32); + if (decimals < 10u) out = put_char(out, '0'); + out = put_u32(out, decimals); + out = put_char(out, 'V'); + out = put_char(out, ' '); + out = put_u32(out, g.core.battery_percent); + put_char(out, '%'); + break; + } + case 10: { + const uint8_t type = g.core.battery_type < 6u ? g.core.battery_type : 5u; + A->asset_read(T_BATTERY + type * T_BATTERY_STRIDE, out, + T_BATTERY_STRIDE); + break; + } + case 11: format_pair(out, g.channels_used, 1024u); break; + case 12: format_pair(out, g.lists_used, 24u); break; + case 13: put_u32(out, g.channels_hf); break; + case 14: put_u32(out, g.channels_vhf); break; + case 15: put_u32(out, g.channels_uhf); break; + case 16: put_u32(out, g.channels_air); break; + case 17: put_u32(out, g.channels_other); break; + case 18: put_hex32(out, g.core.uid[0]); break; + case 19: put_hex32(out, g.core.uid[1]); break; + case 20: put_hex32(out, g.core.uid[2]); break; + case 21: format_uptime(out); break; + case 22: format_pair(out, g.channels_am, g.channels_fm); break; + case 23: format_pair(out, g.channels_wide, g.channels_narrow); break; + case 24: format_pair(out, g.channels_ctcss, g.channels_dcs); break; + case 25: put_hex32(out, g.core.cpuid); break; + case 26: + if (!g.core.temperature_valid) { + out = put_char(out, '-'); + out = put_char(out, '-'); + } else if (g.core.temperature < 0) { + out = put_char(out, '-'); + out = put_u32(out, (uint32_t)-g.core.temperature); + } else { + out = put_u32(out, (uint32_t)g.core.temperature); + } + put_char(out, 'C'); + break; + case 27: A->asset_read(T_CPU, out, T_CPU_LEN); break; + case 28: + A->asset_read(T_RESET + g.core.reset_source * T_RESET_STRIDE, out, + T_RESET_STRIDE); + break; + case 29: put_hex32(out, g.core.silicon_rev); break; + case 30: + A->asset_read(T_CLOCK + g.core.clock_source * T_CLOCK_STRIDE, out, + T_CLOCK_STRIDE); + break; + case 31: + A->asset_read(T_RDP + g.core.rdp_state * T_RDP_STRIDE, out, + T_RDP_STRIDE); + break; + case 32: + if (g.core.vdd_valid) + format_voltage(out, g.core.vdd_mv); + else { + out = put_char(out, '-'); + put_char(out, '-'); + } + break; + case 33: + A->asset_read(T_BOR + g.core.bor_state * T_BOR_STRIDE, out, + T_BOR_STRIDE); + break; + case 34: A->asset_read(T_HW_FLASH, out, T_HW_FLASH_LEN); break; + case 35: + A->asset_read(T_BOOT + g.core.boot_mode * T_BOOT_STRIDE, out, + T_BOOT_STRIDE); + break; + case 36: + A->asset_read(T_WDG + (g.core.option_modes & 1u) * T_WDG_STRIDE, out, + T_WDG_STRIDE); + break; + case 37: + if (g.core.flash_wait_states <= 2u) { + out = put_u32(out, g.core.flash_wait_states); + out = put_char(out, ' '); + out = put_char(out, 'W'); + put_char(out, 'S'); + } else { + out = put_char(out, '-'); + put_char(out, '-'); + } + break; + case 38: + A->asset_read(T_WDG + ((g.core.option_modes >> 1) & 1u) * T_WDG_STRIDE, + out, T_WDG_STRIDE); + break; + case 39: + A->asset_read(T_NRST + ((g.core.option_modes >> 2) & 1u) * T_NRST_STRIDE, + out, T_NRST_STRIDE); + break; + case 40: + A->asset_read(T_IWDG_STOP + ((g.core.option_modes >> 4) & 1u) * + T_IWDG_STOP_STRIDE, out, T_IWDG_STOP_STRIDE); + break; + case 41: A->asset_read(T_SPI_FLASH, out, T_SPI_FLASH_LEN); break; + default: *out = '\0'; break; + } + out[10] = '\0'; +} + +static void draw_tiny_capsule(const char *text, uint8_t line, bool status) +{ + const uint8_t end = (uint8_t)(2u + text_length(text) * 4u); + A->print_inverse(text, 2u, line, status, true, end); +} + +static void draw_section_capsule(const char *text, uint8_t line, bool close_top) +{ + const uint8_t start = 2u; + const uint8_t end = (uint8_t)(start + text_length(text) * 7u + 1u); + + A->print_normal(text, start, 0u, line); + A->fb[line][start - 1u] ^= 0x7Fu; + for (uint8_t x = start; x < end; x++) + A->fb[line][x] ^= 0xFFu; + + if (line != 0u) { + for (uint8_t x = 1u; x < 127u; x += 2u) + A->fb[line - 1u][x] |= 0x08u; + for (uint8_t x = start; x < end; x++) + A->fb[line - 1u][x] ^= 0x80u; + } + A->fb[line][end] ^= 0x7Fu; + + if (line == 0u && close_top) { + /* Move the complete nine-pixel capsule into the visible area. Every + * section is followed by a blank row, which receives its last pixel. */ + for (uint8_t x = start - 1u; x <= end; x++) { + const uint8_t body = A->fb[0][x]; + A->fb[0][x] = (uint8_t)(body << 1); + A->fb[1][x] |= body >> 7; + } + for (uint8_t x = start; x < end; x++) + A->fb[0][x] |= 0x01u; + } +} + +static void draw_row(uint16_t row, uint8_t line, char *text, bool close_top) +{ + const uint8_t definition = rows[row]; + if (definition == ROW_BLANK) + return; + + if (definition & 0x80u) { + const uint8_t section = definition & 0x7Fu; + A->asset_read(T_SECTION + section * T_SECTION_STRIDE, text, + T_SECTION_STRIDE); + draw_section_capsule(text, line, close_top); + return; + } + + if ((definition & 0xF0u) == 0x40u) { + const uint16_t qr = (definition & 0x08u) ? QR_WIKI : QR_CODE; + const uint8_t page = definition & 0x07u; + A->asset_read(qr + (uint16_t)page * QR_WIDTH, + &A->fb[line][QR_X], QR_WIDTH); + return; + } + + char *value = text + 12u; + A->asset_read(T_KEY + definition * T_KEY_STRIDE, text, T_KEY_STRIDE); + format_item_value(definition, value); + draw_tiny_capsule(text, line, false); + A->print_normal(value, VALUE_X, 0u, line); +} + +static void shift_frame(uint8_t pixels, const uint8_t *next_row) +{ + for (uint8_t line = 0u; line < VISIBLE_ROWS; line++) { + for (uint8_t x = 0u; x < 128u; x++) { + const uint8_t next = line + 1u < VISIBLE_ROWS + ? A->fb[line + 1u][x] : next_row[x]; + A->fb[line][x] = (uint8_t)((A->fb[line][x] >> pixels) | + (next << (ROW_HEIGHT - pixels))); + } + } +} + +static void draw_status(void) +{ + char title[T_TITLE_LEN]; + + A->status_clear(); + A->asset_read(T_TITLE, title, T_TITLE_LEN); + draw_tiny_capsule(title, 0u, true); + A->draw_battery(); + A->blit_status(); +} + +static void draw(void) +{ + char text[32]; + uint8_t next_row[128]; + const uint16_t first = g.scroll / ROW_HEIGHT; + const uint8_t phase = (uint8_t)(g.scroll & (ROW_HEIGHT - 1u)); + + draw_status(); + + if (phase != 0u) { + A->display_clear(); + const uint16_t next = first + VISIBLE_ROWS; + if (next < g.row_count) + draw_row(next, 0u, text, false); + for (uint8_t x = 0u; x < 128u; x++) + next_row[x] = A->fb[0][x]; + } + + A->display_clear(); + for (uint8_t visible = 0; visible < VISIBLE_ROWS; visible++) { + const uint16_t row = first + visible; + if (row >= g.row_count) + break; + draw_row(row, visible, text, phase == 0u && visible == 0u); + } + if (phase != 0u) + shift_frame(phase, next_row); + A->blit_full(); +} + +static void navigate(int8_t direction) +{ + const int32_t maximum = (int32_t)(g.row_count - VISIBLE_ROWS) * ROW_HEIGHT; + int32_t target = (int32_t)g.target + (int32_t)direction * SCROLL_STEP; + if (target < 0) + target = 0; + if (target > maximum) + target = maximum; + g.target = (uint16_t)target; +} + +__attribute__((section(".text.entry"), used)) +void app_main(const app_api_t *api) +{ + A = api; + load_system_info(); + g.row_count = ROW_COUNT; + g.running = true; + g.dirty = true; + A->backlight_on(); + + uint8_t previous = APP_KEY_INVALID; + uint8_t battery_ticks = 0; + uint16_t repeat_ms = 0u; + while (g.running) { + const uint8_t key = A->get_key(); + if (key == APP_KEY_SAVER) { + previous = APP_KEY_INVALID; + repeat_ms = 0u; + g.suspended = true; + } else if (key == APP_KEY_WAKE) { + previous = APP_KEY_INVALID; + repeat_ms = 0u; + g.suspended = false; + refresh_power(); + refresh_temperature(); + battery_ticks = 0u; + g.dirty = true; + } else if (key == APP_KEY_INVALID) { + previous = key; + repeat_ms = 0u; + } else if (key != previous) { + const int8_t direction = A->nav_dir(key); + A->backlight_on(); + if (key == APP_KEY_EXIT) + g.running = false; + else if (direction != 0) + navigate(direction); + else if (key != APP_KEY_PTT) + A->play_tone(500u, 60u); + previous = key; + repeat_ms = direction != 0 ? NAV_REPEAT_DELAY_MS : 0u; + } else { + const int8_t direction = A->nav_dir(key); + if (direction != 0) { + if (repeat_ms > INPUT_TICK_MS) { + repeat_ms -= INPUT_TICK_MS; + } else { + A->backlight_on(); + navigate(direction); + repeat_ms = NAV_REPEAT_MS; + } + } + } + + if (!g.suspended) { + if (g.scroll < g.target) { + g.scroll += SCROLL_FRAME_STEP; + g.dirty = true; + } + if (g.scroll > g.target) { + g.scroll -= SCROLL_FRAME_STEP; + g.dirty = true; + } + if (g.dirty) { draw(); g.dirty = false; } + } + + A->delay_ms(INPUT_TICK_MS); + if (!g.suspended && ++battery_ticks >= 67u) { + const uint16_t first = g.scroll / ROW_HEIGHT; + battery_ticks = 0u; + refresh_power(); + if ((first <= UPTIME_ROW && + first + VISIBLE_ROWS > UPTIME_ROW) || + (first <= BATTERY_LEVEL_ROW && + first + VISIBLE_ROWS > BATTERY_LEVEL_ROW)) + g.dirty = true; + else + draw_status(); + } + A->backlight_update(); + } +} diff --git a/CMakePresets.json b/CMakePresets.json index dbd8cdbd..a577b9a4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -193,6 +193,7 @@ "ENABLE_FEAT_F4HWN_GAME": false, "ENABLE_FEAT_F4HWN_OVERLAY_APPS": true, "ENABLE_FEAT_F4HWN_OVERLAY_BEAM": true, + "ENABLE_FEAT_F4HWN_OVERLAY_INFO": true, "ENABLE_FEAT_F4HWN_EXT_FLASH_RW": true, "EDITION_STRING": "Labs", "TARGET": "f4hwn.labs"