Refine multiboot presets, UI and build output

This commit is contained in:
Armel FAUVEAU committed 2026-08-12 03:25:53 +02:00
1 parent a2551d97dc
commit 919a687936
4 files changed
+318 -204

No files matched your search

+53 -36
View File
@@ -41,52 +41,32 @@ static void mb_copy_label(char *dst, uint8_t cap, const char *src, uint8_t src_c
dst[n] = 0;
}
static void mb_format_slot_label(char *dst, uint8_t cap, const mb_slot_header_t *header)
static uint8_t mb_copy_slot_version(char *dst, uint8_t cap, const mb_slot_header_t *header)
{
const char *version = NULL;
uint8_t version_cap = 0;
uint8_t version_len = 0;
uint8_t name_limit = cap - 1u;
uint8_t n = 0;
if (!header->name[0])
{
mb_copy_label(dst, cap, header->fw_version, MB_VERSION_LEN);
return;
}
dst[0] = 0;
for (uint8_t i = 0; i + 1u < MB_VERSION_LEN && header->fw_version[i]; i++)
{
if (header->fw_version[i] == 'v' &&
header->fw_version[i + 1u] >= '0' &&
header->fw_version[i + 1u] <= '9')
{
version = &header->fw_version[i + 1u];
version_cap = MB_VERSION_LEN - i - 1u;
i++;
while (n + 1u < cap && i < MB_VERSION_LEN)
{
const char c = header->fw_version[i];
if ((c < '0' || c > '9') && c != '.')
break;
dst[n++] = c;
i++;
}
break;
}
}
if (version)
{
while (version_len < version_cap && version[version_len])
version_len++;
if (version_len + 1u < cap)
name_limit = cap - version_len - 2u;
}
while (n < name_limit && n < MB_NAME_LEN && header->name[n])
{
dst[n] = header->name[n];
n++;
}
if (version && n + version_len + 1u < cap)
{
dst[n++] = ' ';
for (uint8_t i = 0; i < version_len; i++)
dst[n++] = version[i];
}
dst[n] = 0;
return n;
}
/* "MULTIBOOT" mode banner in the top status bar, shown on every screen - the
@@ -116,6 +96,17 @@ static void mb_key_hints(const char *act_menu, const char *act_exit)
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
}
static void mb_invert_rounded_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;
}
static void mb_show_message(const char *line1, const char *line2, const char *line3)
{
UI_DisplayClear();
@@ -171,6 +162,7 @@ static void mb_render_slots(uint8_t selected,
const uint8_t status[MB_SLOT_COUNT])
{
char line[19]; /* 18 glyphs max: 18 * 7 px fits from x=2 to x=126. */
char version[MB_VERSION_LEN];
UI_DisplayClear();
mb_status_bar();
@@ -178,23 +170,48 @@ static void mb_render_slots(uint8_t selected,
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
const uint8_t fbLine = (uint8_t)(slot + 1u); /* page 1 stays blank */
uint8_t version_len = 0;
uint8_t version_x = 0;
memset(line, 0, sizeof(line));
memset(version, 0, sizeof(version));
line[0] = (char)('0' + slot);
line[1] = ' ';
line[2] = ' ';
if (status[slot] == MB_OK)
mb_format_slot_label(&line[3], sizeof(line) - 3u, &headers[slot]);
{
uint8_t name_cap = sizeof(line) - 3u;
version_len = mb_copy_slot_version(version, sizeof(version), &headers[slot]);
if (version_len)
{
const uint8_t name_x = 2u + 3u * 7u;
uint8_t available;
version_x = (uint8_t)(LCD_WIDTH - 2u - version_len * 7u);
available = version_x > name_x
? (uint8_t)((version_x - name_x) / 7u)
: 0u;
if (available + 1u < name_cap)
name_cap = available + 1u;
}
if (headers[slot].name[0])
mb_copy_label(&line[3], name_cap, headers[slot].name, MB_NAME_LEN);
else if (!version_len)
mb_copy_label(&line[3], name_cap, headers[slot].fw_version, MB_VERSION_LEN);
}
else
mb_copy_label(&line[3], sizeof(line) - 3u, mb_error_text(status[slot]), 20u);
UI_PrintStringSmallNormal(line, 2, 0, fbLine);
if (version_len)
UI_PrintStringSmallNormal(version, version_x, 0, fbLine);
/* Selected row: full-width inverse bar, like the firmware menu list. */
/* Selected row: full-width rounded inverse capsule. */
if (slot == selected)
for (uint8_t x = 0; x < LCD_WIDTH; x++)
gFrameBuffer[fbLine][x] ^= 0xFFu;
mb_invert_rounded_row(fbLine);
}
mb_key_hints("SELECT", "QUIT");
+52 -119
View File
@@ -99,99 +99,6 @@
"TARGET": "f4hwn.custom"
}
},
{
"name": "Bandscope",
"inherits": "default",
"cacheVariables": {
"ENABLE_SPECTRUM": true,
"ENABLE_FMRADIO": false,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": false,
"EDITION_STRING": "Bandscope",
"TARGET": "f4hwn.bandscope"
}
},
{
"name": "Broadcast",
"inherits": "default",
"cacheVariables": {
"ENABLE_SPECTRUM": false,
"ENABLE_FMRADIO": true,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": false,
"EDITION_STRING": "Broadcast",
"TARGET": "f4hwn.broadcast"
}
},
{
"name": "Basic",
"inherits": "default",
"cacheVariables": {
"ENABLE_SPECTRUM": true,
"ENABLE_FMRADIO": true,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": false,
"ENABLE_FEAT_F4HWN_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_SPECTRUM": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_AUDIO_BAR": false,
"ENABLE_FEAT_F4HWN_RESUME_STATE": false,
"ENABLE_FEAT_F4HWN_CHARGING_C": false,
"ENABLE_FEAT_F4HWN_INV": true,
"ENABLE_FEAT_F4HWN_CTR": false,
"ENABLE_FEAT_F4HWN_NARROWER": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": false,
"EDITION_STRING": "Basic",
"TARGET": "f4hwn.basic"
}
},
{
"name": "RescueOps",
"inherits": "default",
"cacheVariables": {
"ENABLE_SPECTRUM": false,
"ENABLE_FMRADIO": false,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_NOAA": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"EDITION_STRING": "RescueOps",
"TARGET": "f4hwn.rescueops"
}
},
{
"name": "Game",
"inherits": "default",
"cacheVariables": {
"ENABLE_SPECTRUM": false,
"ENABLE_FMRADIO": true,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": true,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": false,
"EDITION_STRING": "Game",
"TARGET": "f4hwn.game"
}
},
{
"name": "Fusion",
"inherits": "default",
@@ -199,12 +106,12 @@
"ENABLE_SPECTRUM": true,
"ENABLE_FMRADIO": true,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_AIRCOPY": false,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": false,
"ENABLE_FEAT_F4HWN_VOL": true,
"ENABLE_FEAT_F4HWN_AUDIO": true,
"ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true,
@@ -212,9 +119,8 @@
"ENABLE_FEAT_F4HWN_SCAN_FASTER": true,
"ENABLE_FEAT_F4HWN_SCAN_RSSI": true,
"ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true,
"ENABLE_FEAT_F4HWN_BEAM": true,
"ENABLE_FEAT_F4HWN_FOXHUNT": true,
"ENABLE_FEAT_F4HWN_ACTION_PICKER": true,
"ENABLE_FEAT_F4HWN_BEAM": false,
"ENABLE_FEAT_F4HWN_FOXHUNT": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG": true,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true,
@@ -223,10 +129,45 @@
"ENABLE_FEAT_F4HWN_LOGO": true,
"ENABLE_FEAT_F4HWN_LOGO_SAV": true,
"ENABLE_FEAT_F4HWN_MENU_CAT": true,
"ENABLE_FEAT_F4HWN_ACTION_PICKER": true,
"ENABLE_FEAT_F4HWN_MULTIBOOT": true,
"ENABLE_SWD": true,
"EDITION_STRING": "Fusion",
"TARGET": "f4hwn.fusion"
}
},
{
"name": "Transfer",
"inherits": "Fusion",
"cacheVariables": {
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_BEAM": true,
"EDITION_STRING": "Transfer",
"TARGET": "f4hwn.transfer"
}
},
{
"name": "Field",
"inherits": "Fusion",
"cacheVariables": {
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"ENABLE_FEAT_F4HWN_FOXHUNT": true,
"EDITION_STRING": "Field",
"TARGET": "f4hwn.field"
}
},
{
"name": "Extended",
"inherits": "Fusion",
"cacheVariables": {
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_GAME": true,
"ENABLE_FEAT_F4HWN_RESCUE_OPS": true,
"ENABLE_FEAT_F4HWN_BEAM": true,
"ENABLE_FEAT_F4HWN_FOXHUNT": true,
"EDITION_STRING": "Extended",
"TARGET": "f4hwn.extended"
}
}
],
"buildPresets": [
@@ -234,29 +175,21 @@
"name": "Custom",
"configurePreset": "Custom"
},
{
"name": "Bandscope",
"configurePreset": "Bandscope"
},
{
"name": "Broadcast",
"configurePreset": "Broadcast"
},
{
"name": "Basic",
"configurePreset": "Basic"
},
{
"name": "RescueOps",
"configurePreset": "RescueOps"
},
{
"name": "Game",
"configurePreset": "Game"
},
{
"name": "Fusion",
"configurePreset": "Fusion"
},
{
"name": "Transfer",
"configurePreset": "Transfer"
},
{
"name": "Field",
"configurePreset": "Field"
},
{
"name": "Extended",
"configurePreset": "Extended"
}
]
}
+32 -18
View File
@@ -59,18 +59,23 @@ Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Ol
### Fusion edition
Fusion is the reference edition of the project. It provides an all-in-one firmware for the UV-K1 and UV-K5 V3, including:
Fusion is the generic reference edition for the UV-K1 and UV-K5 V3. It is intended
for everyday use and is the base inherited by the specialized editions. It includes:
- Fagci's spectrum analyzer,
- broadcast FM radio,
- VOX and AirCopy,
- BEAM wireless channel transfer,
- broadcast FM radio and VOX,
- [UV Studio](https://armel.github.io/uvstudio/) with integrated K5Viewer screen mirroring, screenshots and remote keyboard control,
- advanced RX audio profiles and Audio Scope,
- first-responder-oriented controls,
- the Breakout game,
- automatic RX/TX activity logging with RF Log,
- full Fox Hunt and Morse Beacon support.
- multiboot support.
Specialized presets extend Fusion for specific uses:
- **Transfer** adds AirCopy and BEAM wireless channel transfer.
- **Field** adds first-responder controls, Fox Hunt and Morse Beacon support.
- **Extended** combines the Transfer and Field feature sets without claiming to include every optional feature.
- **Custom** remains a manually configured build based directly on the hidden technical default.
### Radio and signal handling
@@ -324,9 +329,10 @@ But, they are nice toys for the price, fun to play with.
## Compiling and Building from Docker
This project provides a Docker-based build system to compile the Fusion firmware for the UV-K1 and UV-K5 V3. Everything is handled through the `compile-with-docker.sh` helper script.
The documented build output is generated inside `build/Fusion`, using the CMake presets defined in `CMakePresets.json`.
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
the default generic preset, while specialized builds are generated in their own
`build/<Preset>` directories.
### Prerequisites
@@ -335,29 +341,37 @@ The documented build output is generated inside `build/Fusion`, using the CMake
### Build Script Overview
The script `compile-with-docker.sh` performs the following actions:
The script `compile-with-docker.sh`:
1. Builds the Docker image (`uvk1-uvk5v3`) if it does not already exist.
2. Removes any previous `build` directory to ensure a clean configuration.
3. Runs CMake using the `Fusion` preset inside the Docker container.
4. Builds the firmware and outputs `.elf`, `.bin` and `.hex` files.
2. Configures the selected preset with `cmake --fresh`.
3. Builds the firmware and outputs matching `.elf`, `.bin` and `.hex` files.
4. Displays Flash and RAM usage; `All` keeps the individual build logs quiet.
### Usage
```bash
./compile-with-docker.sh Fusion [extra CMake options]
./compile-with-docker.sh [Preset] [extra CMake options]
```
### Documented Preset
The default preset is **Fusion**. Available presets are:
- **Custom**
- **Fusion**
- **Transfer**
- **Field**
- **Extended**
- **All** (Fusion, Transfer, Field and Extended)
### Examples
Build Fusion:
Examples:
```bash
./compile-with-docker.sh
./compile-with-docker.sh Fusion
./compile-with-docker.sh Transfer
./compile-with-docker.sh Field
./compile-with-docker.sh Extended
./compile-with-docker.sh All
```
### Passing Additional CMake Options
+181 -31
View File
@@ -5,16 +5,21 @@ set -euo pipefail
# Usage:
# ./compile-with-docker.sh [Preset] [CMake options...]
# Examples:
# ./compile-with-docker.sh Custom
# ./compile-with-docker.sh Bandscope -DENABLE_SPECTRUM=ON
# ./compile-with-docker.sh Broadcast -DENABLE_FEAT_F4HWN_GAME=ON -DENABLE_NOAA=ON
# ./compile-with-docker.sh Fusion
# ./compile-with-docker.sh Transfer
# ./compile-with-docker.sh Field -DENABLE_NOAA=ON
# ./compile-with-docker.sh Custom -DENABLE_SPECTRUM=ON
# ./compile-with-docker.sh Fusion -DDEV=ON
# ./compile-with-docker.sh All
# Default preset: "Custom"
# Default preset: "Fusion"
# ---------------------------------------------
IMAGE=uvk1-uvk5v3
PRESET=${1:-Custom}
RELEASE_PRESETS=(Fusion Transfer Field Extended)
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
@@ -23,53 +28,198 @@ EXTRA_ARGS=("$@")
# ---------------------------------------------
# Validate preset name
# ---------------------------------------------
if [[ ! "$PRESET" =~ ^(Custom|Bandscope|Broadcast|Basic|RescueOps|Game|Fusion|All)$ ]]; then
if [[ ! "$PRESET" =~ ^(Custom|Fusion|Transfer|Field|Extended|All)$ ]]; then
echo "❌ Unknown preset: '$PRESET'"
echo "Valid presets are: Custom, Bandscope, Broadcast, Basic, RescueOps, Game, Fusion, All"
echo "Valid presets are: Custom, Fusion, Transfer, Field, Extended, All"
exit 1
fi
QUIET=0
if [[ "$PRESET" == "All" ]]; then
QUIET=1
fi
# ---------------------------------------------
# Build the Docker image (only needed once)
# ---------------------------------------------
if [[ "$(docker images -q $IMAGE)" == "" ]]; then
if [[ "$(docker images -q "$IMAGE")" == "" ]]; then
echo "Building Docker image..."
docker build -t "$IMAGE" .
docker build -q -t "$IMAGE" . >/dev/null
fi
# ---------------------------------------------
# Clean existing CMake cache to ensure toolchain reload
# ---------------------------------------------
rm -rf build
export MSYS_NO_PATHCONV=1
# ---------------------------------------------
# Function to build one preset
# ---------------------------------------------
RESULT_PRESETS=()
RESULT_FLASH_SIZES=()
RESULT_RAM_SIZES=()
run_preset_build() {
local preset="$1"
docker run --rm \
-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 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
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 ""
echo "=== 🚀 Building preset: ${preset} ==="
echo "---------------------------------------------"
docker run --rm \
-u $(id -u):$(id -g) \
-it -v "$PWD":/src -w /src "$IMAGE" \
bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version &&
cmake --preset "$1" "${@:2}" &&
cmake --build --preset "$1" -j' \
bash "${preset}" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}
echo "✅ Done: ${preset}"
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
PRESETS=(Bandscope Broadcast Basic RescueOps Game Fusion)
for p in "${PRESETS[@]}"; do
for p in "${RELEASE_PRESETS[@]}"; do
build_preset "$p"
done
echo ""
echo "🎉 All presets built successfully!"
else
build_preset "$PRESET"
fi
print_summary