diff --git a/App/app/action.c b/App/app/action.c index 65affa67..3e2f25a4 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -58,6 +58,10 @@ #include "ui/menu.h" #endif +#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM) +static void ACTION_Beam(void); +#endif + #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); #endif @@ -113,7 +117,7 @@ void (*const action_opt_table[ACTION_OPT_LEN])(void) = { [ACTION_OPT_REMOVE_OFFSET] = &ACTION_Remove_Offset, #endif #endif -#ifdef ENABLE_FEAT_F4HWN_BEAM +#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) [ACTION_OPT_BEAM] = &ACTION_Beam, #endif #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG @@ -153,6 +157,10 @@ bool ACTION_IsAvailable(uint8_t action) #ifndef ENABLE_FEAT_F4HWN_BEACON case ACTION_OPT_BEACON: return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEACON) != 0; +#endif +#ifndef ENABLE_FEAT_F4HWN_BEAM + case ACTION_OPT_BEAM: + return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEAM) != 0; #endif default: break; @@ -338,7 +346,7 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action) case ACTION_OPT_REMOVE_OFFSET: #endif #endif -#ifdef ENABLE_FEAT_F4HWN_BEAM +#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) case ACTION_OPT_BEAM: #endif #if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) @@ -495,6 +503,14 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) ACTION_Execute(func); } +#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM) +static void ACTION_Beam(void) +{ + if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEAM) != APP_OK) + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; +} +#endif + #if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void ACTION_FoxHunt(void) { diff --git a/App/apps/app_api.h b/App/apps/app_api.h index e3a1899b..94827c13 100644 --- a/App/apps/app_api.h +++ b/App/apps/app_api.h @@ -40,7 +40,7 @@ * reorder, a removal, or an append - MUST bump this. Keep in sync with the * value read by pack_app.py, which * stamps the blob the loader checks against. */ -#define APP_ABI_VERSION 6u +#define APP_ABI_VERSION 8u /* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by * value so the app stays independent of the firmware headers. */ @@ -120,6 +120,41 @@ enum { APP_TRIVFO_TX_STATE = 3, }; +/* Pointer-free BEAM channel description. The resident bridge translates this + * stable ABI type to/from feature-dependent VFO_Info_t. */ +typedef struct { + uint32_t rx_frequency; + uint32_t tx_offset_frequency; + uint8_t rx_code; + uint8_t tx_code; + uint8_t rx_codetype; + uint8_t tx_codetype; + uint8_t modulation; + uint8_t tx_offset_direction; + uint8_t tx_lock; + uint8_t busy_channel_lock; + uint8_t output_power; + uint8_t channel_bandwidth; + uint8_t frequency_reverse; + uint8_t dtmf_ptt_id_mode; + uint8_t dtmf_decoding_enable; + uint8_t step_setting; + uint8_t scrambling_type; + uint8_t band; + uint8_t scanlist; + uint8_t compander; + char name[16]; +} app_beam_channel_t; + +_Static_assert(sizeof(app_beam_channel_t) == 44u, + "BEAM ABI/wire channel layout changed"); + +enum { + APP_BEAM_RX_WAIT = 0, + APP_BEAM_RX_READY = 1, + APP_BEAM_RX_ERROR = 2, +}; + typedef struct app_api { uint8_t abi_version; /* == APP_ABI_VERSION */ @@ -223,6 +258,16 @@ typedef struct app_api { uint16_t (*trivfo_step)(uint8_t vfo, int8_t direction); uint8_t (*trivfo_tick)(void); uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */ + + /* ---- BEAM channel transfer (ABI 8) ---- */ + void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */ + void (*beam_leave)(void); /* defensively stop FSK before app return */ + void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */ + uint16_t (*beam_save)(const app_beam_channel_t *channel); /* first free MR, or 0xffff */ + void (*beam_send)(uint16_t *packet); /* transmit one 36-word FSK packet */ + void (*beam_rx)(bool start); /* arm or stop FSK reception */ + uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */ + void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */ } app_api_t; /* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */ diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c index 3b19c5e1..c5cc843a 100644 --- a/App/apps/app_overlay.c +++ b/App/apps/app_overlay.c @@ -525,6 +525,200 @@ static uint8_t app_trivfo_ptt(bool pressed) return 0; } +/* ---- ABI 8: BEAM radio/channel bridge ------------------------------------ + * The modal app owns the packet format, CRC, UI and state machine. Resident + * code only translates the stable ABI channel structure and performs the FSK + * operations which depend on VFO_Info_t and the BK4819 driver. */ +static VFO_Info_t app_beam_vfo; +static uint8_t app_beam_fsk_index; +static uint16_t app_beam_pending_channel; +static bool app_beam_dirty; + +static void app_beam_prepare(void) +{ + const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz; + RADIO_InitInfo(&app_beam_vfo, channel, DEFAULT_FREQ); + app_beam_vfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW; + app_beam_vfo.OUTPUT_POWER = OUTPUT_POWER_LOW1; + RADIO_ConfigureSquelchAndOutputPower(&app_beam_vfo); + + gRxVfo = &app_beam_vfo; + gTxVfo = &app_beam_vfo; + gCurrentVfo = &app_beam_vfo; + RADIO_SetupRegisters(true); + BK4819_SetupAircopy(); + BK4819_ResetFSK(); + app_beam_fsk_index = 0; +} + +static void app_beam_leave(void) +{ + BK4819_ResetFSK(); +} + +static void app_beam_get(app_beam_channel_t *out) +{ + if (out == NULL) + return; + memset(out, 0, sizeof(*out)); + const VFO_Info_t *vfo = &gEeprom.VfoInfo[gEeprom.TX_VFO]; + out->rx_frequency = vfo->freq_config_RX.Frequency; + out->tx_offset_frequency = vfo->TX_OFFSET_FREQUENCY; + out->rx_code = vfo->freq_config_RX.Code; + out->tx_code = vfo->freq_config_TX.Code; + out->rx_codetype = vfo->freq_config_RX.CodeType; + out->tx_codetype = vfo->freq_config_TX.CodeType; + out->modulation = vfo->Modulation; + out->tx_offset_direction = vfo->TX_OFFSET_FREQUENCY_DIRECTION; + out->tx_lock = vfo->TX_LOCK; + out->busy_channel_lock = vfo->BUSY_CHANNEL_LOCK; + out->output_power = vfo->OUTPUT_POWER; + out->channel_bandwidth = vfo->CHANNEL_BANDWIDTH; + out->frequency_reverse = vfo->FrequencyReverse; + out->dtmf_ptt_id_mode = vfo->DTMF_PTT_ID_TX_MODE; +#ifdef ENABLE_DTMF_CALLING + out->dtmf_decoding_enable = vfo->DTMF_DECODING_ENABLE; +#endif + out->step_setting = vfo->STEP_SETTING; + out->band = vfo->Band; + out->scanlist = vfo->SCANLIST_PARTICIPATION; + out->compander = vfo->Compander; + if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) + SETTINGS_FetchChannelName(out->name, vfo->CHANNEL_SAVE); + else + memcpy(out->name, vfo->Name, sizeof(out->name)); +} + +static uint16_t app_beam_save(const app_beam_channel_t *in) +{ + if (in == NULL) + return 0xFFFFu; + + uint16_t channel = MR_CHANNEL_FIRST; + while (IS_MR_CHANNEL(channel) && RADIO_CheckValidChannel(channel, false, 0)) + channel++; + if (!IS_MR_CHANNEL(channel)) + return 0xFFFFu; + + /* Reuse the temporary BEAM VFO as resident staging RAM. No external-flash + * write is allowed while the overlay still executes from that flash's + * 4 KiB sector cache. */ + RADIO_InitInfo(&app_beam_vfo, channel, in->rx_frequency); + app_beam_vfo.TX_OFFSET_FREQUENCY = in->tx_offset_frequency; + app_beam_vfo.freq_config_RX.Code = in->rx_code; + app_beam_vfo.freq_config_TX.Code = in->tx_code; + app_beam_vfo.freq_config_RX.CodeType = in->rx_codetype; + app_beam_vfo.freq_config_TX.CodeType = in->tx_codetype; + app_beam_vfo.TX_OFFSET_FREQUENCY_DIRECTION = in->tx_offset_direction; + app_beam_vfo.Modulation = in->modulation; + app_beam_vfo.TX_LOCK = in->tx_lock; + app_beam_vfo.BUSY_CHANNEL_LOCK = in->busy_channel_lock; + app_beam_vfo.OUTPUT_POWER = in->output_power; + app_beam_vfo.CHANNEL_BANDWIDTH = in->channel_bandwidth; + app_beam_vfo.FrequencyReverse = in->frequency_reverse; + app_beam_vfo.DTMF_PTT_ID_TX_MODE = in->dtmf_ptt_id_mode; +#ifdef ENABLE_DTMF_CALLING + app_beam_vfo.DTMF_DECODING_ENABLE = in->dtmf_decoding_enable; +#endif + app_beam_vfo.STEP_SETTING = in->step_setting < STEP_N_ELEM ? in->step_setting : STEP_12_5kHz; + app_beam_vfo.StepFrequency = gStepFrequencyTable[app_beam_vfo.STEP_SETTING]; + app_beam_vfo.SCANLIST_PARTICIPATION = in->scanlist; + app_beam_vfo.Compander = in->compander; + memcpy(app_beam_vfo.Name, in->name, sizeof(app_beam_vfo.Name)); + app_beam_vfo.Name[sizeof(app_beam_vfo.Name) - 1u] = '\0'; + + app_beam_pending_channel = channel; + app_beam_dirty = true; + return channel; +} + +/* Called only after the overlay has returned and its code no longer executes + * from the PY25Q16 sector cache. */ +static void app_beam_commit(void) +{ + if (!app_beam_dirty) + return; + app_beam_dirty = false; + + const uint16_t channel = app_beam_pending_channel; + + SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &app_beam_vfo, 3); +#ifndef ENABLE_KEEP_MEM_NAME + SETTINGS_SaveChannelName(channel, app_beam_vfo.Name); +#endif + + gEeprom.MrChannel[gEeprom.TX_VFO] = channel; + gEeprom.ScreenChannel[gEeprom.TX_VFO] = channel; + RADIO_ConfigureChannel(gEeprom.TX_VFO, VFO_CONFIGURE_RELOAD); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + PY25Q16_InvalidateCache(); +} + +static void app_beam_send(uint16_t *packet) +{ + if (packet == NULL) + return; + RADIO_SetTxParameters(); + BK4819_SendFSKData(packet); + BK4819_SetupPowerAmplifier(0, 0); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); +} + +static void app_beam_rx(bool start) +{ + app_beam_fsk_index = 0; + if (start) + BK4819_PrepareFSKReceive(); + else + BK4819_ResetFSK(); +} + +static uint8_t app_beam_rx_poll(uint16_t *packet) +{ + if (packet == NULL) + return APP_BEAM_RX_ERROR; + + while (BK4819_ReadRegister(BK4819_REG_0C) & 1u) { + BK4819_WriteRegister(BK4819_REG_02, 0); + const uint16_t irq = BK4819_ReadRegister(BK4819_REG_02); + if (irq & (BK4819_REG_02_FSK_FIFO_ALMOST_FULL | BK4819_REG_02_FSK_RX_FINISHED)) { + const unsigned words = (irq & BK4819_REG_02_FSK_RX_FINISHED) + ? (app_beam_fsk_index < 36u ? 36u - app_beam_fsk_index : 0u) + : 4u; + for (unsigned i = 0; i < words; i++) { + const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F); + if (app_beam_fsk_index < 36u) + packet[app_beam_fsk_index++] = word; + } + } + } + + if (app_beam_fsk_index < 36u) + return APP_BEAM_RX_WAIT; + + app_beam_fsk_index = 0; + const uint16_t status = BK4819_ReadRegister(BK4819_REG_0B); + BK4819_PrepareFSKReceive(); + return (status & 0x0010u) ? APP_BEAM_RX_ERROR : APP_BEAM_RX_READY; +} + +static void app_beam_draw(const char *status) +{ + UI_DisplayStatus(); + UI_DisplayMain(); +#ifdef ENABLE_FEAT_F4HWN + const uint8_t line = (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF && + gEeprom.CROSS_BAND_RX_TX == CROSS_BAND_OFF) ? 5u : 3u; +#else + const uint8_t line = 3u; +#endif + memset(gFrameBuffer[line], 0, LCD_WIDTH); + UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line); +} + /* ---- v2 radio wrappers ---- */ static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; } static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); } @@ -669,13 +863,14 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header) static bool app_shortcuts_cached; static uint8_t app_shortcut_mask; -static uint8_t app_shortcut_slots[3]; +static uint8_t app_shortcut_slots[4]; static int8_t app_shortcut_index(uint8_t shortcut) { if (shortcut == APP_SHORTCUT_FM) return 0; if (shortcut == APP_SHORTCUT_FOXHUNT) return 1; if (shortcut == APP_SHORTCUT_BEACON) return 2; + if (shortcut == APP_SHORTCUT_BEAM) return 3; return -1; } @@ -807,6 +1002,14 @@ static const app_api_t app_api = { .trivfo_step = app_trivfo_step, .trivfo_tick = app_trivfo_tick, .trivfo_ptt = app_trivfo_ptt, + .beam_prepare = app_beam_prepare, + .beam_leave = app_beam_leave, + .beam_get = app_beam_get, + .beam_save = app_beam_save, + .beam_send = app_beam_send, + .beam_rx = app_beam_rx, + .beam_rx_poll = app_beam_rx_poll, + .beam_draw = app_beam_draw, }; uint8_t APP_LaunchOverlay(uint8_t slot) @@ -845,6 +1048,7 @@ uint8_t APP_LaunchOverlay(uint8_t slot) #ifdef ENABLE_FMRADIO app_fm_dirty = false; #endif + app_beam_dirty = false; app_allow_screen_saver = (h.flags & APP_FLAG_SCREEN_SAVER) != 0; app_screen_saver_wake = false; @@ -891,6 +1095,8 @@ uint8_t APP_LaunchOverlay(uint8_t slot) /* The overlay held app code, not a valid config sector. */ PY25Q16_InvalidateCache(); + app_beam_commit(); + if (app_trivfo_ab_dirty) { SETTINGS_SaveVfoIndices(); app_trivfo_ab_dirty = false; diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h index 93e5174c..c5c7a73c 100644 --- a/App/apps/app_overlay.h +++ b/App/apps/app_overlay.h @@ -69,13 +69,14 @@ #define APP_FLAG_COMMITTED 0x0001u #define APP_FLAG_SCREEN_SAVER 0x0002u #define APP_FLAG_SHORTCUT_SHIFT 8u -#define APP_FLAG_SHORTCUT_MASK 0x0700u +#define APP_FLAG_SHORTCUT_MASK 0x0F00u #define APP_NAME_LEN 16 #define APP_VERSION_LEN 16 #define APP_SHORTCUT_FM 0x01u #define APP_SHORTCUT_FOXHUNT 0x02u #define APP_SHORTCUT_BEACON 0x04u +#define APP_SHORTCUT_BEAM 0x08u typedef struct __attribute__((packed)) { uint32_t magic; /* APP_MAGIC */ diff --git a/App/apps/beam/app.ld b/App/apps/beam/app.ld new file mode 100644 index 00000000..ab4f428b --- /dev/null +++ b/App/apps/beam/app.ld @@ -0,0 +1,30 @@ +/* Overlay-app link script. */ +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, + "BEAM overlay app overflows the 4 KiB overlay") + + /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) } +} diff --git a/App/apps/beam/beam_app.c b/App/apps/beam/beam_app.c new file mode 100644 index 00000000..a49f4a25 --- /dev/null +++ b/App/apps/beam/beam_app.c @@ -0,0 +1,241 @@ +/* Copyright 2026 Armel F4HWN + * Licensed under the Apache License, Version 2.0. + * + * BEAM overlay app. Packet version 2 remains wire-compatible with the + * resident implementation: one selected VFO is sent and a received VFO is + * stored in the first free memory channel. + */ + +#include +#include +#include +#include "../app_api.h" + +#define PACKET_MAGIC 0xBEA5u +#define PACKET_VERSION 2u +#define PACKET_START 0xABCDu +#define PACKET_END 0xDCBAu +#define PACKET_WORDS 36u + +enum { + STATUS_READY = 0, + STATUS_TX_WAIT, + STATUS_TX_DONE, + STATUS_RX_WAIT, + STATUS_RX_SAVED, + STATUS_RX_FULL, + STATUS_ERROR, +}; + +typedef struct { + uint16_t magic; + uint8_t version; + uint8_t pad; + app_beam_channel_t channel; +} beam_payload_t; + +_Static_assert(sizeof(beam_payload_t) == 48u, + "BEAM v2 wire payload layout changed"); + +static const uint16_t OBFUSCATION[8] = { + 0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9 +}; + +static const app_api_t *A; +static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */ +static uint16_t copiedChannel; +static uint8_t mode, status; +static bool receiving, running, dirty; + +static uint16_t *packet(void) { return (uint16_t *)packet_store; } +static beam_payload_t *payload(void) { return (beam_payload_t *)&packet()[2]; } + +static void clear_bytes(void *ptr, uint8_t count) +{ + uint8_t *p = (uint8_t *)ptr; + while (count--) *p++ = 0; +} + +static uint16_t crc16(const void *buffer, uint16_t size) +{ + const uint8_t *data = (const uint8_t *)buffer; + uint16_t crc = 0; + while (size--) { + crc ^= (uint16_t)*data++ << 8; + for (uint8_t bit = 0; bit < 8u; bit++) + crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) + : (uint16_t)(crc << 1); + } + return crc; +} + +static void obfuscate(void) +{ + uint16_t *p = packet(); + for (uint8_t i = 0; i < 32u; i++) + p[i + 1u] ^= OBFUSCATION[i & 7u]; +} + +static void draw(void) +{ + const char *state; + switch (status) { + case STATUS_TX_WAIT: state = "SENDING"; break; + case STATUS_TX_DONE: state = "SENT"; break; + case STATUS_RX_WAIT: state = "WAITING"; break; + case STATUS_RX_SAVED: state = "RECEIVED"; break; + case STATUS_RX_FULL: state = "MEM FULL"; break; + case STATUS_ERROR: state = "ERROR"; break; + default: state = mode ? "BEAM RX" : "BEAM TX"; break; + } + A->beam_draw(state); + A->blit_status(); + A->blit_full(); +} + +static void stop_rx(void) +{ + if (receiving) { + A->beam_rx(false); + receiving = false; + } +} + +static void send_packet(void) +{ + uint16_t *p = packet(); + clear_bytes(packet_store, sizeof(packet_store)); + p[0] = PACKET_START; + payload()->magic = PACKET_MAGIC; + payload()->version = PACKET_VERSION; + A->beam_get(&payload()->channel); + p[34] = crc16(&p[1], 2u + 64u); + p[35] = PACKET_END; + obfuscate(); + + status = STATUS_TX_WAIT; + draw(); + A->beam_send(p); + status = STATUS_TX_DONE; + for (uint8_t i = 0; i < 3u; i++) { + A->play_tone(880, 60); + if (i < 2u) A->delay_ms(20); + } + dirty = true; +} + +static void start(void) +{ + stop_rx(); + A->beam_prepare(); + if (!mode) { + send_packet(); + return; + } + copiedChannel = 0xFFFFu; + A->beam_rx(true); + receiving = true; + status = STATUS_RX_WAIT; + dirty = true; +} + +static bool valid_packet(void) +{ + uint16_t *p = packet(); + if (p[0] != PACKET_START || p[35] != PACKET_END) + return false; + obfuscate(); + if (p[34] != crc16(&p[1], 2u + 64u)) + return false; + return payload()->magic == PACKET_MAGIC && payload()->version == PACKET_VERSION; +} + +static void poll_rx(void) +{ + if (!receiving) + return; + const uint8_t result = A->beam_rx_poll(packet()); + if (result == APP_BEAM_RX_WAIT) + return; + if (result == APP_BEAM_RX_ERROR || !valid_packet()) { + status = STATUS_ERROR; + A->backlight_on(); + dirty = true; + return; + } + + copiedChannel = A->beam_save(&payload()->channel); + A->beam_rx(false); + receiving = false; + status = copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED; + A->backlight_on(); + dirty = true; +} + +static void key_press(uint8_t key) +{ + switch (key) { + case APP_KEY_UP: + case APP_KEY_DOWN: + stop_rx(); + mode ^= 1u; + status = STATUS_READY; + dirty = true; + break; + case APP_KEY_MENU: + start(); + break; + case APP_KEY_EXIT: + running = false; + break; + case APP_KEY_PTT: + break; + default: + A->play_tone(500, 60); + break; + } +} + +__attribute__((section(".text.entry"), used)) +void app_main(const app_api_t *api) +{ + A = api; + mode = 0; + status = STATUS_READY; + copiedChannel = 0xFFFFu; + receiving = false; + running = true; + dirty = true; + A->backlight_on(); + + uint8_t previous = APP_KEY_INVALID; + uint8_t batteryTicks = 0; + while (running) { + const uint8_t key = A->get_key(); + if (key == APP_KEY_SAVER) { + previous = APP_KEY_INVALID; + } else if (key == APP_KEY_WAKE) { + previous = APP_KEY_INVALID; + dirty = true; + } else { + if (key != previous && key != APP_KEY_INVALID) { + A->backlight_on(); + key_press(key); + } + previous = key; + } + + poll_rx(); + if (dirty) { draw(); dirty = false; } + A->delay_ms(10); + A->backlight_update(); + if (++batteryTicks >= 50u) { + batteryTicks = 0; + A->battery_sample(); + dirty = true; + } + } + + stop_rx(); + A->beam_leave(); +} diff --git a/App/apps/beam/build.sh b/App/apps/beam/build.sh new file mode 100755 index 00000000..ad2fcd6e --- /dev/null +++ b/App/apps/beam/build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP="$(basename "$PWD")" +APP_NAME="BEAM" +APP_VER="1.0" +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" +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/3] %-8s' "$APP_NAME" "$1" "$2"; } +trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR + +step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" \ + --shortcut beam >/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/pack_app.py b/App/apps/pack_app.py index eaa4a10c..1f0c26cc 100644 --- a/App/apps/pack_app.py +++ b/App/apps/pack_app.py @@ -38,6 +38,7 @@ SHORTCUTS = { "fm": cdefine("app_overlay.h", "APP_SHORTCUT_FM"), "foxhunt": cdefine("app_overlay.h", "APP_SHORTCUT_FOXHUNT"), "beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"), + "beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"), } def field(s: str, n: int) -> bytes: diff --git a/App/driver/bk4819.c b/App/driver/bk4819.c index 168adf45..2e15dc4e 100644 --- a/App/driver/bk4819.c +++ b/App/driver/bk4819.c @@ -1095,7 +1095,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void) BK4819_REG_30_ENABLE_RX_DSP); } -#ifdef ENABLE_AIRCOPY +#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void BK4819_SetupAircopy(void) { BK4819_WriteRegister(BK4819_REG_70, 0x00E0); // Enable Tone2, tuning gain 48 diff --git a/App/driver/bk4819.h b/App/driver/bk4819.h index 9bbab159..79870bff 100644 --- a/App/driver/bk4819.h +++ b/App/driver/bk4819.h @@ -114,7 +114,7 @@ void BK4819_EnterTxMute(void); void BK4819_ExitTxMute(void); void BK4819_Sleep(void); void BK4819_TurnsOffTones_TurnsOnRX(void); -#ifdef ENABLE_AIRCOPY +#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void BK4819_SetupAircopy(void); #endif void BK4819_ResetFSK(void); diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index 78c38c9e..27e99d6f 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -1133,7 +1133,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void) BK4819_REG_30_ENABLE_RX_DSP); } -#ifdef ENABLE_AIRCOPY +#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) void BK4819_SetupAircopy(void) { BK4819_WriteRegister(BK4819_REG_70, 0x00C3); // Enable Tone2, tuning gain 48 diff --git a/CMakePresets.json b/CMakePresets.json index 9ce2fd38..f3d571a4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -180,7 +180,7 @@ "ENABLE_FEAT_F4HWN_FOXHUNT": false, "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_AIRCOPY": true, - "ENABLE_FEAT_F4HWN_BEAM": true, + "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_GAME": false, "ENABLE_FEAT_F4HWN_OVERLAY_APPS": true, "EDITION_STRING": "Labs",