mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add SysInfo overlay App
This commit is contained in:
1 parent
096b768ff6
commit
a65944ed33
10 files changed
+979
-1
No files matched your search
@@ -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
|
||||
)
|
||||
|
||||
+21
-1
@@ -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 };
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+40
@@ -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"
|
||||
Executable
+48
@@ -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()
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user