Compare commits

..
Author SHA1 Message Date
Armel FAUVEAU 3bd3ebba2c Merge pull request #457 from armel/feature_update_v5
Feature update v5
2026-07-19 01:41:58 +02:00
Armel FAUVEAU 39cbbd7e90 Prepare new version v5.7.0 2026-07-19 01:40:08 +02:00
Armel FAUVEAU 0567f01786 Keep RF log indexes traffic-only 2026-07-15 04:55:06 +02:00
Armel FAUVEAU fc12a35418 Update .gitignore 2026-07-15 04:55:06 +02:00
Armel FAUVEAU 2580acfb07 Give session markers a trafficSeq and stream them to K5Viewer 2026-07-13 04:27:33 +02:00
Armel FAUVEAU 56a6083689 Stream paginated RF log history to K5Viewer 2026-07-12 05:15:57 +02:00
Armel FAUVEAU 29725ed704 Allow unlock from RF LOG display 2026-07-11 03:26:47 +02:00
Armel FAUVEAU b44df132d0 Add ACTIONS lock scope to SetLck 2026-07-11 03:07:08 +02:00
Armel FAUVEAU 676f26be34 Remove legacy Python k5viewer tool 2026-07-09 05:25:42 +02:00
Armel FAUVEAU 46034ec39f Rename screenshot module to k5viewer 2026-07-09 05:22:39 +02:00
Armel FAUVEAU e2d21c61c2 Add optional RX/TX log streaming to K5Viewer 2026-07-09 05:12:57 +02:00
Armel FAUVEAU f8e1b7a436 Increase the scanrange skip list from 32 to 64 entries 2026-07-08 03:41:27 +02:00
Armel FAUVEAU 9447eaada9 Add secure clear log confirmation flow 2026-07-07 14:15:02 +02:00
Armel FAUVEAU 43533c838b Reorder RX/TX log flash layout to shrink the view cache 2026-07-07 02:56:22 +02:00
Armel FAUVEAU 7306db5f8f Show TX power level for TX rows in RX/TX log S-meter view 2026-07-07 02:36:16 +02:00
Armel FAUVEAU fbd7b37c21 Log minimum battery voltage per session in RX/TX log 2026-07-07 02:30:15 +02:00
Armel FAUVEAU 8d3754b42f Center S-meter text in the RX/TX log detail badge 2026-07-07 02:18:58 +02:00
Armel FAUVEAU 6a556e0601 Add S-meter to RX/TX log 2026-07-07 00:13:50 +02:00
Armel FAUVEAU 9c1b1cb063 Remove unused REGA emergency alarm feature (ENABLE_REGA) 2026-07-06 15:25:29 +02:00
Armel FAUVEAU ae7bf65383 Ignore squelch-only incoming state in RX/TX log 2026-07-06 12:22:50 +02:00
Armel FAUVEAU fb1360cce2 Replace unreachable RX/TX log EEPROM mapping with documentation comment 2026-07-06 03:52:57 +02:00
Armel FAUVEAU 374e52c4d1 Resolve RX/TX log channel names at display time instead of storing them 2026-07-06 03:35:54 +02:00
Armel FAUVEAU 42aeac3469 Move left by 1 px 2026-07-06 03:18:22 +02:00
Armel FAUVEAU 98ac57b032 Keep status bar layout stable when RX/TX log filter label is shown 2026-07-06 00:11:49 +02:00
Armel FAUVEAU 3e6e7c87b5 Add F + UP/DOWN shortcuts to jump to first/last RX/TX log entry 2026-07-05 23:28:31 +02:00
Armel FAUVEAU e908eda1da Make RX/TX log wrap-around scrolling optional via ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP 2026-07-05 23:19:00 +02:00
Armel FAUVEAU 4fccdb78b9 Dedupe RX/TX log entry copies by aligning RAM and flash struct layouts 2026-07-05 23:01:27 +02:00
Armel FAUVEAU a14127867e Remove redundant name sanitization already done by SETTINGS_FetchChannelName 2026-07-05 22:48:40 +02:00
Armel FAUVEAU c02aac265a Remove unused KEY_STAR 2026-07-05 22:38:36 +02:00
Armel FAUVEAU c24592cbe8 Factor RX/TX log empty screen rendering 2026-07-05 22:33:33 +02:00
Armel FAUVEAU ff096e12e5 Simplify RX/TX log badge numbering and dedupe repeated reset/stop code 2026-07-05 21:01:31 +02:00
Armel FAUVEAU 5fcc1598dd Fix RX/TX log wrap upward before total is known 2026-07-05 20:02:35 +02:00
Armel FAUVEAU 9b8493b450 Fix RX/TX log end-of-list scrolling 2026-07-05 19:16:35 +02:00
Armel FAUVEAU f1df96f1f6 Keep RX/TX log numbering continuous across sessions 2026-07-05 18:44:32 +02:00
Armel FAUVEAU f36c524fa7 Expose 512 entries in RX/TX log view instead of 250 2026-07-05 18:39:40 +02:00
Armel FAUVEAU 9ab5542326 Fix duplicate session marker on repeated reboots without traffic 2026-07-05 18:16:49 +02:00
Armel FAUVEAU 0187c05e65 Add RX/TX history log stored in external flash 2026-07-05 17:59:58 +02:00
Armel FAUVEAU 914c354da0 Update donors 2026-07-02 00:57:02 +02:00
Armel FAUVEAU a771387edd Merge pull request #437 from armel/feature_update_v5
Feature update v5
2026-06-30 16:06:49 +02:00
Armel FAUVEAU 09bc14e64b Add NOTICE 2026-06-30 16:05:51 +02:00
Armel FAUVEAU 58c0170fb1 Fix auto key lock timer on startup (issue #431) 2026-06-29 15:09:55 +02:00
Armel FAUVEAU 5b4b3e2a71 Merge pull request #423 from armel/feature_update_v5
Feature update v5
2026-06-21 19:02:08 +02:00
Armel FAUVEAU 65a5c9f4ef Prepare new version v5.6.1 2026-06-21 19:01:14 +02:00
Armel FAUVEAU a2936402c2 Fix screenshot state updates without display redraw 2026-06-21 18:07:10 +02:00
Armel FAUVEAU f069ed935f Fix screensaver state during deep sleep 2026-06-21 04:28:58 +02:00
Armel FAUVEAU dcbbd744ac Add screenshot state flags 2026-06-21 03:21:16 +02:00
Armel FAUVEAU bca06e3382 Fix NAR+ FM demod boost 2026-06-21 00:17:50 +02:00
Armel FAUVEAU 8723863fb0 Fix F+4 verify VHF harmonic before accepting frequency 2026-06-20 13:41:14 +02:00
Armel FAUVEAU c775fa8b2f Update donors 2026-06-18 02:29:34 +02:00
Armel FAUVEAU 80e7d4b3d1 Fix F+4 frequency scan sensitivity 2026-06-17 03:25:24 +02:00
Armel FAUVEAU e6a727eeda Fix this fucking and stupid roger beep (Issue #416) 2026-06-17 02:32:23 +02:00
Armel FAUVEAU 887e18efe6 Fix reset defaults (Issue #413) 2026-06-16 16:00:56 +02:00
Armel FAUVEAU 20b87feb1d Merge pull request #407 from mrkusypl/feature_update_v5_2
Code refactoring (4 B)
2026-06-15 22:20:52 +02:00
mrkusypl e2e9c9ba60 Code refactoring (4 B) 2026-06-15 20:40:56 +02:00
Armel FAUVEAU 337eb0cb12 Update donors 2026-06-15 16:10:31 +02:00
Armel FAUVEAU 504a2b0fb0 Update .gitignore 2026-06-15 03:15:45 +02:00
Armel FAUVEAU 16b07c6374 Merge pull request #405 from armel/feature_update_v5
Feature update v5
2026-06-15 03:10:46 +02:00
Armel FAUVEAU f5b20d4f80 Prepare v5.6.0 2026-06-15 03:09:32 +02:00
Armel FAUVEAU ae14bf5df3 Hollow out backlight bulb icon when manual light is off 2026-06-13 14:33:42 +02:00
Armel FAUVEAU 9dc85852ba Widen screenshot chunk fingerprint to 16 bits to fix stale chunks 2026-06-13 13:20:10 +02:00
Armel FAUVEAU f7d29f6a14 Save 128 bytes of SRAM in backlight PWM while preserving distinct brightness steps 2026-06-13 03:58:43 +02:00
Armel FAUVEAU 1184618731 Reduce screenshot peak stack usage with on-demand chunk computation 2026-06-13 03:30:22 +02:00
Armel FAUVEAU 3d15d59ce0 Remove dead screenshot bit packing flush 2026-06-12 18:06:44 +02:00
Armel FAUVEAU 15fccc31d6 Fix AM to FM dual-watch RX reconfiguration (issue #389) 2026-06-11 16:49:06 +02:00
Armel FAUVEAU 48f753caaf Reduce screenshot RAM usage with per-chunk frame hashing 2026-06-10 13:46:18 +02:00
Armel FAUVEAU edc4673983 Add subaudible detection for scan ranges 2026-06-09 04:57:15 +02:00
Armel FAUVEAU 9e03f02296 Refine VFO lock icon placement during scan 2026-06-08 04:11:36 +02:00
Armel FAUVEAU 25ea1f827b Add scan RSSI sparkline indicator 2026-06-08 01:50:28 +02:00
Armel FAUVEAU 8f11a32c9d Merge pull request #392 from mrkusypl/feature_update_v5_2
Code refactoring (32 B)
2026-06-06 18:23:54 +02:00
mrkusypl 576b8c552f Code refactoring (32 B) 2026-06-06 17:56:33 +02:00
Armel FAUVEAU 85db5a2394 Update .gitignore 2026-06-03 04:06:57 +02:00
Armel FAUVEAU 98989d4f0a Fix bandscope 8.33 kHz frequency rounding (issue #380) 2026-05-28 00:26:46 +02:00
Armel FAUVEAU 8c976cfffd Update donors 2026-05-25 12:24:23 +02:00
Armel FAUVEAU 0ce5422db3 Fix ENABLE_FEAT_F4HWN_RX_TX_TIMER guard with ENABLE_FEAT_F4HWN_DEBUG 2026-05-25 01:41:27 +02:00
Armel FAUVEAU d939009949 Merge pull request #370 from mrkusypl/feature_update_v5_2
Code refactoring (104 B)
2026-05-25 01:35:25 +02:00
mrkusypl 50efade5c4 Code refactoring (104 B) 2026-05-25 00:46:34 +02:00
Armel FAUVEAU 682a59fc0e Merge pull request #365 from mrkusypl/feature_update_v5_2
Prevent screensaver during M-SCAN; Code refactoring (20 B)
2026-05-23 14:40:01 +02:00
mrkusypl 3d3f370b86 Minor correction for checking gFM_ChannelPosition 2026-05-23 10:48:59 +02:00
mrkusypl eebeba09db Prevent screensaver during M-SCAN; Code refactoring (20 B) 2026-05-23 02:21:11 +02:00
65 changed files with 2766 additions and 1122 deletions

No files matched your search

+2
View File
@@ -8,6 +8,7 @@ firmware
compile_commands.json
.vscode
.claude
.agents
/docs
k5_eeprom.raw
@@ -15,3 +16,4 @@ k5_eeprom.raw
__pycache__/
.DS_Store
AGENTS.md
+13 -6
View File
@@ -171,10 +171,6 @@ enable_feature(ENABLE_SCAN_RANGES)
# ---- CONTRIB MODS ----
# Thank you @markusb
enable_feature(ENABLE_REGA
app/rega.c
)
# Thank you @reppad
enable_feature(ENABLE_EXTRA_UART_CMD)
@@ -184,8 +180,8 @@ enable_feature(ENABLE_FEAT_F4HWN)
enable_feature(ENABLE_FEAT_F4HWN_GAME
app/breakout.c
)
enable_feature(ENABLE_FEAT_F4HWN_SCREENSHOT
screenshot.c
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
k5viewer.c
)
enable_feature(ENABLE_FEAT_F4HWN_SPECTRUM)
enable_feature(ENABLE_FEAT_F4HWN_RX_TX_TIMER)
@@ -197,6 +193,8 @@ enable_feature(ENABLE_FEAT_F4HWN_INV)
enable_feature(ENABLE_FEAT_F4HWN_CTR)
enable_feature(ENABLE_FEAT_F4HWN_SCAN_PROGRESS)
enable_feature(ENABLE_FEAT_F4HWN_SCAN_FASTER)
enable_feature(ENABLE_FEAT_F4HWN_SCAN_RSSI)
enable_feature(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE)
enable_feature(ENABLE_FEAT_F4HWN_RESCUE_OPS)
enable_feature(ENABLE_FEAT_F4HWN_VOL)
enable_feature(ENABLE_FEAT_F4HWN_AUDIO)
@@ -209,6 +207,15 @@ enable_feature(ENABLE_FEAT_F4HWN_MEM)
enable_feature(ENABLE_FEAT_F4HWN_BEAM
app/beam.c
)
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
app/rxtx_log.c
)
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP)
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER)
if(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER AND NOT (ENABLE_FEAT_F4HWN_RXTX_LOG AND ENABLE_FEAT_F4HWN_K5VIEWER))
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER requires ENABLE_FEAT_F4HWN_RXTX_LOG (log data) and ENABLE_FEAT_F4HWN_K5VIEWER (serial transport).")
endif()
if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).")
+7 -8
View File
@@ -42,12 +42,12 @@
#include "ui/inputbox.h"
#include "ui/main.h"
#include "ui/ui.h"
#ifdef ENABLE_REGA
#include "app/rega.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
#include "app/beam.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#if defined(ENABLE_FMRADIO)
static void ACTION_Scan_FM(bool bRestart);
@@ -131,13 +131,12 @@ void (*action_opt_table[])(void) = {
#else
[ACTION_OPT_RXMODE] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_REGA
[ACTION_OPT_REGA_ALARM] = &ACTION_RegaAlarm,
[ACTION_OPT_REGA_TEST] = &ACTION_RegaTest,
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
[ACTION_OPT_BEAM] = &ACTION_Beam,
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
[ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog,
#endif
};
static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN);
@@ -473,7 +472,7 @@ static void ACTION_Scan_FM(bool bRestart)
static void ACTION_AlarmOr1750(const bool b1750)
{
if(gEeprom.KEY_LOCK && gEeprom.KEY_LOCK_PTT)
if(gEeprom.KEY_LOCK && (gSetting_set_lck & SET_LCK_PTT))
return;
#if defined(ENABLE_ALARM)
+3
View File
@@ -50,6 +50,9 @@ void ACTION_SwitchDemodul(void);
void ACTION_Power_High(void);
void ACTION_Remove_Offset(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
void ACTION_RxTxLog(void);
#endif
#endif
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
+8 -8
View File
@@ -30,8 +30,8 @@
#include "settings.h"
#include <stddef.h>
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
static const uint16_t Obfuscation[8] = { 0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9 };
@@ -140,8 +140,8 @@ static void AIRCOPY_clear()
{
crc[i] = 0;
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(true);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(true);
#endif
}
@@ -170,8 +170,8 @@ static inline void AIRCOPY_CheckComplete(uint16_t *num)
if (done >= map->total_blocks)
{
gAircopyState = AIRCOPY_COMPLETE;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(false);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
}
}
@@ -222,8 +222,8 @@ bool AIRCOPY_SendMessage(void)
// Check if transfer is complete
if (CurrentSegmentIndex >= map->num_segments) {
gAircopyState = AIRCOPY_COMPLETE;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(false);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
return 0;
}
+49 -29
View File
@@ -39,6 +39,9 @@
#include "app/generic.h"
#include "app/main.h"
#include "app/menu.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#include "app/scanner.h"
#if defined(ENABLE_UART) || defined(ENABLE_USB)
#include "app/uart.h"
@@ -80,8 +83,8 @@
#include "ui/ui.h"
#include "ui/welcome.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
static bool flagSaveVfo;
@@ -116,14 +119,13 @@ void (*ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
#ifdef ENABLE_AIRCOPY
[DISPLAY_AIRCOPY] = &AIRCOPY_ProcessKeys,
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
[DISPLAY_RXTX_LOG] = &RXTX_LOG_ProcessKeys,
#endif
};
#ifdef ENABLE_REGA
// This is a hack for REGA as I need a special display element only for it with no key
static_assert(ARRAY_SIZE(ProcessKeysFunctions) == DISPLAY_N_ELEM-1);
#else
static_assert(ARRAY_SIZE(ProcessKeysFunctions) == DISPLAY_N_ELEM);
#endif
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
static uint8_t ScreenSaverRandom(void)
@@ -135,20 +137,13 @@ static uint8_t ScreenSaverRandom(void)
static void ScreenSaverSetPixel(uint8_t x, uint8_t y, bool fill)
{
const uint8_t pattern = 1u << (y & 7u);
uint8_t *target = (y < 8) ? &gStatusLine[x] : &gFrameBuffer[(y >> 3) - 1][x];
if (y < 8) {
if (fill)
gStatusLine[x] |= pattern;
else
gStatusLine[x] &= (uint8_t)~pattern;
return;
}
y -= 8;
if (fill)
gFrameBuffer[y >> 3][x] |= pattern;
*target |= pattern;
else
gFrameBuffer[y >> 3][x] &= (uint8_t)~pattern;
*target &= (uint8_t)~pattern;
}
static void ScreenSaverDrawMatrixGlyph(uint8_t x, int16_t y, uint8_t glyph)
@@ -235,8 +230,8 @@ static void ScreenSaverRenderLogoPlus(bool reset)
static void ScreenSaverUpdateViewer(void)
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(false);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
}
@@ -246,11 +241,14 @@ static bool ScreenSaverCanDisplay(void)
gEeprom.BACKLIGHT_TIME == 0 ||
gEeprom.BACKLIGHT_TIME >= 61 ||
gScreenSaverDisplayed ||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
gWakeUp ||
#endif
gCurrentFunction == FUNCTION_TRANSMIT ||
FUNCTION_IsRx() ||
gPttIsPressed
#ifdef ENABLE_FMRADIO
|| gFM_ScanState != FM_SCAN_OFF
|| (gFM_ScanState != FM_SCAN_OFF && !gFM_FoundFrequency)
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
|| gBeamActive
@@ -776,6 +774,10 @@ void APP_StartListening(FUNCTION_Type_t function)
FUNCTION_Select(function);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_BeginRx(gRxVfo, function);
#endif
#ifdef ENABLE_FMRADIO
if (function == FUNCTION_MONITOR || gFmRadioMode)
#else
@@ -1119,10 +1121,10 @@ static void HandleVox(void)
void APP_Update(void)
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Parse incoming packets on every tick so serial keys are never missed,
// regardless of whether the screen needs redrawing.
SCREENSHOT_ParseInput();
K5VIEWER_ParseInput();
#endif
#ifdef ENABLE_VOICE
@@ -1580,6 +1582,10 @@ void APP_TimeSlice10ms(void)
SETTINGS_SaveVfoIndicesFlush();
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_Task10ms();
#endif
BACKLIGHT_Update();
gFlashLightBlinkCounter++;
@@ -1660,13 +1666,15 @@ void APP_TimeSlice10ms(void)
UI_DisplayStatus();
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
if (gUpdateDisplayCurrent || gUpdateStatusCurrent
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|| screenSaverRendered
#endif
) {
SCREENSHOT_Update(false);
K5VIEWER_Update(false);
} else if (K5VIEWER_HasPendingStateChange()) {
K5VIEWER_Update(false);
}
#endif
@@ -1762,7 +1770,7 @@ void APP_TimeSlice10ms(void)
SCANNER_TimeSlice10ms();
#ifdef ENABLE_SCAN_RANGES
#if defined(ENABLE_SCAN_RANGES) && defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
CHFRSCANNER_UpdateCssDetection();
#endif
@@ -1808,6 +1816,10 @@ void APP_TimeSlice500ms(void)
if (--gKeypadLocked == 0)
gUpdateDisplay = true;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_Tick500ms();
#endif
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
if (gSetting_set_tmr && (gCurrentFunction == FUNCTION_TRANSMIT || FUNCTION_IsRx())) {
const uint16_t timerCountdown = (gCurrentFunction == FUNCTION_TRANSMIT)
@@ -1922,6 +1934,9 @@ void APP_TimeSlice500ms(void)
gBacklightCountdown_500ms = 0;
gPowerSave_10ms = 1;
gWakeUp = true;
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
ScreenSaverExit();
#endif
// TODO:
// PWM_PLUS0_CH0_COMP = 0;
BACKLIGHT_SetBrightness(0);
@@ -2293,7 +2308,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
#ifdef ENABLE_FEAT_F4HWN // Disable PTT if KEY_LOCK
bool lck_condition = (gEeprom.KEY_LOCK || lowBatPopup) && gCurrentFunction != FUNCTION_TRANSMIT;
if(!gSetting_set_lck)
if((gSetting_set_lck & SET_LCK_PTT) == 0)
lck_condition = lck_condition && Key != KEY_PTT;
if (lck_condition)
@@ -2310,6 +2325,12 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
return;
}
// The ACTIONS restriction only applies when the keypad is actually
// locked: this block is also entered for the low battery popup, where
// action keys must keep working as before
const bool passActionKey = ((gSetting_set_lck & SET_LCK_ACTIONS) == 0 || !gEeprom.KEY_LOCK) &&
(Key == KEY_SIDE1 || Key == KEY_SIDE2 || (Key == KEY_MENU && bKeyHeld));
if (Key == KEY_F) { // function/key-lock key
if (!bKeyPressed)
return;
@@ -2324,8 +2345,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
// KEY_MENU has a special treatment here, because we want to pass hold event to ACTION_Handle
// but we don't want it to complain when initial press happens
// we want to react on realese instead
else if (Key != KEY_SIDE1 && Key != KEY_SIDE2 && // pass side buttons
!(Key == KEY_MENU && bKeyHeld)) // pass KEY_MENU held
else if (!passActionKey)
{
if ((!bKeyPressed || bKeyHeld || (Key == KEY_MENU && bKeyPressed)) && // prevent released or held, prevent KEY_MENU pressed
!(Key == KEY_MENU && !bKeyPressed)) // pass KEY_MENU released
+6 -6
View File
@@ -16,8 +16,8 @@
#include "app/breakout.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
static uint32_t randSeed = 1;
@@ -304,10 +304,10 @@ static void OnKeyDown(uint8_t key)
// HandleUserInput
static bool HandleUserInput()
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Parse incoming packets on every tick so serial keys are never missed,
// regardless of whether the screen needs redrawing.
SCREENSHOT_ParseInput();
K5VIEWER_ParseInput();
#endif
kbd.prev = kbd.current;
@@ -388,10 +388,10 @@ void APP_RunBreakout(void) {
ST7565_BlitStatusLine(); // Blank status line
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
if(isPaused || swap == 0)
{
SCREENSHOT_Update(false);
K5VIEWER_Update(false);
}
#endif
}
+81 -2
View File
@@ -1,5 +1,6 @@
#include <stddef.h>
#include <string.h>
#include "app/app.h"
#include "app/chFrScanner.h"
@@ -20,12 +21,15 @@ bool gScanPauseMode;
#ifdef ENABLE_SCAN_RANGES
uint32_t gScanRangeStart;
uint32_t gScanRangeStop;
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
DCS_CodeType_t gScanRangeCssType = CODE_TYPE_OFF;
uint8_t gScanRangeCssCode = 0xFF;
static uint8_t scanRangeCssCandidate = 0xFF;
static uint8_t scanRangeCssHitCount = 0;
#endif
#define SCAN_RANGE_SKIP_MAX 32
#define SCAN_RANGE_SKIP_MAX 64
#if (SCAN_RANGE_SKIP_MAX & (SCAN_RANGE_SKIP_MAX - 1)) != 0
#error SCAN_RANGE_SKIP_MAX must be a power of two
#endif
@@ -187,6 +191,7 @@ bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t la
return false;
}
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
void CHFRSCANNER_UpdateCssDetection(void)
{
if (!gScanRangeStart || !FUNCTION_IsRx())
@@ -237,6 +242,7 @@ void CHFRSCANNER_UpdateCssDetection(void)
}
}
#endif
#endif
static void CHFRSCANNER_AbortActiveReception(void)
{
@@ -286,6 +292,65 @@ static uint32_t scanFastPrevFrequency;
static bool scanFastLastFullTuneCandidate;
static VFO_Info_t scanFastDisplayVfo;
static bool scanFastDisplayVfoValid;
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
static uint16_t scanRssiSparkline[CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
static uint8_t scanRssiSparklineWrite;
static uint8_t scanRssiSparklineCount;
static void ScanRssiSparklineReset(void)
{
memset(scanRssiSparkline, 0, sizeof(scanRssiSparkline));
scanRssiSparklineWrite = 0;
scanRssiSparklineCount = 0;
}
static void ScanRssiSparklinePush(uint16_t rssi)
{
scanRssiSparkline[scanRssiSparklineWrite] = rssi;
scanRssiSparklineWrite++;
if (scanRssiSparklineWrite >= CHFRSCANNER_RSSI_SPARKLINE_WIDTH)
scanRssiSparklineWrite = 0;
if (scanRssiSparklineCount < CHFRSCANNER_RSSI_SPARKLINE_WIDTH)
scanRssiSparklineCount++;
}
bool CHFRSCANNER_HasScanRssiSparkline(void)
{
return gScanStateDir != SCAN_OFF && scanRssiSparklineCount > 1;
}
uint8_t CHFRSCANNER_GetScanRssiSparklineLevel(uint8_t index)
{
uint16_t minRssi = SCAN_FAST_RSSI_MAX;
uint16_t rssi = 0;
uint8_t oldest;
if (index >= CHFRSCANNER_RSSI_SPARKLINE_WIDTH || scanRssiSparklineCount == 0)
return 0;
if (index < CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount)
return 0;
oldest = (uint8_t)((scanRssiSparklineWrite + CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount)
% CHFRSCANNER_RSSI_SPARKLINE_WIDTH);
index -= (uint8_t)(CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount);
for (uint8_t i = 0; i < scanRssiSparklineCount; i++)
{
const uint16_t sample = scanRssiSparkline[(oldest + i) % CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
if (sample != 0 && sample < minRssi)
minRssi = sample;
}
rssi = scanRssiSparkline[(oldest + index) % CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
if (rssi == 0 || minRssi == SCAN_FAST_RSSI_MAX || rssi <= minRssi + 2)
return 0;
// 48 RSSI units ~= 24 dB. This keeps quiet jitter low while strong hits pop.
return (uint8_t)MIN(((uint32_t)(rssi - minRssi) * 5u + 24u) / 48u, 5u);
}
#endif
static void ScanFastResetState(void)
{
@@ -592,6 +657,9 @@ static scan_fast_result_t ScanRangeFastPrecheck(void)
ScanFastTune(freq);
const uint16_t rssi = ScanFastReadCandidateRssi();
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
ScanRssiSparklinePush(rssi);
#endif
if (ScanFastIsCandidate(rssi))
{
ScanRangeFastRefineCandidate(rssi);
@@ -627,7 +695,12 @@ static bool MemChannelFastPrecheck(uint16_t channel)
scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC;
ScanFastTune(frequency);
if (ScanFastIsCandidate(ScanFastReadCandidateRssi()))
const uint16_t rssi = ScanFastReadCandidateRssi();
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
ScanRssiSparklinePush(rssi);
#endif
if (ScanFastIsCandidate(rssi))
{
scanFastLastFullTuneCandidate = true;
return true; // signal detected: let the full tune path follow
@@ -691,6 +764,9 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi
currentScanList = SCAN_NEXT_CHAN_SCANLIST1;
gScanStateDir = scan_direction;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
ScanRssiSparklineReset();
#endif
ScanFastResetState();
#endif
@@ -874,6 +950,9 @@ void CHFRSCANNER_Stop(void)
}
gScanStateDir = SCAN_OFF;
#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI)
ScanRssiSparklineReset();
#endif
const uint32_t chFr = gScanKeepResult ? lastFoundFrqOrChan : initialFrqOrChan;
const bool channelChanged = chFr != initialFrqOrChan;
+10
View File
@@ -15,12 +15,17 @@ extern bool gScanPauseMode;
#ifdef ENABLE_SCAN_RANGES
extern uint32_t gScanRangeStart;
extern uint32_t gScanRangeStop;
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
extern DCS_CodeType_t gScanRangeCssType;
extern uint8_t gScanRangeCssCode;
#endif
bool CHFRSCANNER_ExcludeCurrentScanRange(void);
bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t last_ordinal);
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
void CHFRSCANNER_UpdateCssDetection(void);
#endif
#endif
void CHFRSCANNER_Found(void);
void CHFRSCANNER_Stop(void);
@@ -29,6 +34,11 @@ void CHFRSCANNER_ManualResume(const int8_t scan_direction);
void CHFRSCANNER_ContinueScanning(void);
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void);
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
bool CHFRSCANNER_HasScanRssiSparkline(void);
uint8_t CHFRSCANNER_GetScanRssiSparklineLevel(uint8_t index);
#define CHFRSCANNER_RSSI_SPARKLINE_WIDTH 24u
#endif
#endif
#if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES)
+1 -1
View File
@@ -474,7 +474,7 @@ void DTMF_Reply(void)
if (pString == NULL)
return;
Delay = (gEeprom.DTMF_PRELOAD_TIME < 200) ? 200 : gEeprom.DTMF_PRELOAD_TIME;
Delay = MAX(gEeprom.DTMF_PRELOAD_TIME, 200);
if (gEeprom.DTMF_SIDE_TONE)
{ // the user will also hear the transmitted tones
+19 -13
View File
@@ -94,6 +94,11 @@ int FM_ConfigureChannelState(void)
return 0;
}
void FM_SetFrequency(void)
{
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
}
void FM_TurnOff(void)
{
gFmRadioMode = false;
@@ -163,11 +168,10 @@ void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag)
gFM_ScanState = Step;
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
}
void FM_AudioPathOn(void) {
BACKLIGHT_TurnOn();
AUDIO_AudioPathOn();
gEnableSpeaker = true;
}
@@ -182,13 +186,14 @@ void FM_PlayAndUpdate(void)
}
FM_ConfigureChannelState();
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
SETTINGS_SaveFM();
gFmPlayCountdown_10ms = 0;
gScheduleFM = false;
gAskToSave = false;
BACKLIGHT_TurnOn();
FM_AudioPathOn();
}
@@ -291,7 +296,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gEeprom.FM_FrequencyPlaying = gEeprom.FM_SelectedFrequency;
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
gRequestSaveFM = true;
return;
}
@@ -311,7 +316,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
#endif
gEeprom.FM_SelectedChannel = Channel;
gEeprom.FM_FrequencyPlaying = gFM_Channels[Channel];
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
gRequestSaveFM = true;
return;
}
@@ -366,7 +371,7 @@ static void Key_FUNC(KEY_Code_t Key, uint8_t state)
gEeprom.FM_IsMrMode = !gEeprom.FM_IsMrMode;
if (!FM_ConfigureChannelState()) {
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
gRequestSaveFM = true;
}
else
@@ -480,7 +485,7 @@ static void Key_MENU(uint8_t state)
gFM_Channels[gEeprom.FM_SelectedChannel] = 0xFFFF;
FM_ConfigureChannelState();
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
gRequestSaveFM = true;
}
@@ -558,7 +563,7 @@ static void Key_UP_DOWN(uint8_t state, int8_t Step)
gRequestSaveFM = true;
Bail:
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
FM_SetFrequency();
gRequestDisplayScreen = DISPLAY_FM;
}
@@ -612,19 +617,19 @@ void FM_Play(void)
if (!gEeprom.FM_IsMrMode)
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
BACKLIGHT_TurnOn();
FM_AudioPathOn();
GUI_SelectNextDisplay(DISPLAY_FM);
return;
goto Display;
}
if (gFM_ChannelPosition < FM_CHANNELS_MAX)
gFM_Channels[gFM_ChannelPosition++] = gEeprom.FM_FrequencyPlaying;
if (gFM_ChannelPosition >= FM_CHANNELS_MAX) {
FM_PlayAndUpdate();
GUI_SelectNextDisplay(DISPLAY_FM);
return;
goto Display;
}
}
@@ -633,6 +638,7 @@ void FM_Play(void)
else
FM_Tune(gEeprom.FM_FrequencyPlaying, gFM_ScanState, false);
Display:
GUI_SelectNextDisplay(DISPLAY_FM);
}
+1 -1
View File
@@ -426,7 +426,7 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
#endif
case MENU_SET_LCK:
//*pMin = 0;
*pMax = ARRAY_SIZE(gSubMenu_SET_LCK) - 1;
*pMax = SET_LCK_LEN - 1;
break;
case MENU_SET_MET:
case MENU_SET_GUI:
-187
View File
@@ -1,187 +0,0 @@
/*
* REGA Alam and Test
* ==================
*
* This bit of code is to implement the REGA test and alarm functions.
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes.
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
*
* There are two REGA ACTIONS: Test and Alarm.
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
*
* These actions can be assigned to a key in the settings menu.
*
* The Test/Alarm function will perform the following actions:
* - Set the radio to transmit mode
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the tramsmitter
* - Send out the ZVEI tone sequence
* - Wait 100ms to allow the radio to finish transmitting
* - Set the radio back to receive mode
*
* The ZVEI tone squence for alarm is: 21414
* The ZVEI tone squence for test is: 21301
*
* To save space the two tone sequences are stored as precalculated register values.
* This avoids the need to calculate the register values at runtime.
* They are hardcoded as array of 32bit integers in the rega.h file.
*
* Copyright 2025 Markus Bärtschi
* https://github.com/markusb
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include "rega.h"
#include "radio.h"
#include "action.h"
#include "misc.h"
#include "settings.h"
#include "functions.h"
#include "driver/bk4819.h"
#include "driver/st7565.h"
#include "driver/system.h"
#include "driver/gpio.h"
#include "bsp/dp32g030/gpio.h"
#include "ui/helper.h"
#include "ui/ui.h"
const uint16_t rega_alarm_tones[5] = {
1160, // 2 1160Hz
1060, // 1 1060Hz
1400, // 4 1400Hz
1060, // 1 1060Hz
1400, // 4 1400Hz
};
const uint16_t rega_test_tones[5] = {
1160, // 2 1160Hz
1060, // 1 1060Hz
1260, // 3 1260Hz
2400, // 0 2400Hz
1060, // 1 1060Hz
};
// Global variable to pass the message to the display
char rega_message[16];
// Transmit the ZVEI tone sequence for alarm
void ACTION_RegaAlarm()
{
const char message[16] = "REGA Alarm";
REGA_TransmitZvei(rega_alarm_tones,message);
}
// Tranmit the ZVEI tone sequence for test
void ACTION_RegaTest()
{
const char message[16] = "REGA Test";
REGA_TransmitZvei(rega_test_tones,message);
}
// Display the REGA message on the screen
void UI_DisplayREGA()
{
UI_DisplayClear();
UI_DisplayPopup(rega_message);
ST7565_BlitFullScreen();
}
// Transmit a ZVEI tone sequence on the REGA frequency
// Configures the radio on VFO A with the required parameters
// tones: array of 5 ZVEI tones
// message: message to display on the screen
void REGA_TransmitZvei(const uint16_t tones[],const char message[])
{
// Copy the message text
strncpy(rega_message,message,16);
// Trigger the display
gScreenToDisplay = DISPLAY_REGA;
// Flash the green LED twice
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
SYSTEM_DelayMs(70);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
SYSTEM_DelayMs(50);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
SYSTEM_DelayMs(70);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
// Select VFO A
gEeprom.TX_VFO = 0;
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO];
gRxVfo = gTxVfo;
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
// Set the REGA frequency, no offset
gTxVfo->freq_config_RX.Frequency = REGA_FREQUENCY;
gTxVfo->freq_config_TX = gTxVfo->freq_config_RX;
gTxVfo->TX_OFFSET_FREQUENCY = 0;
// Set the modulation to FM narrow
gTxVfo->Modulation = MODULATION_FM;
gTxVfo->CHANNEL_BANDWIDTH = BK4819_FILTER_BW_NARROW;
// Set Tx Squelch Tone
gTxVfo->freq_config_TX.CodeType = CODE_TYPE_CONTINUOUS_TONE;
gTxVfo->freq_config_TX.Code = REGA_CTCSS_FREQ_INDEX;
BK4819_SetCTCSSFrequency(CTCSS_Options[gTxVfo->freq_config_TX.Code]);
// Turn Rx squelch tone off
gTxVfo->freq_config_RX.CodeType = CODE_TYPE_OFF;
// Set the transmit power to high
gTxVfo->OUTPUT_POWER = OUTPUT_POWER_HIGH;
// Lock the keyboard
gEeprom.KEY_LOCK = true;
gRequestSaveChannel = 1;
gRequestSaveSettings = true;
// Configure the receiver
BK4819_RX_TurnOn();
// Set the radio to transmit mode
RADIO_PrepareCssTX();
FUNCTION_Select(FUNCTION_TRANSMIT);
// Wait to allow the radio to switch to transmit mode and stabilize the transmitter
SYSTEM_DelayMs(ZVEI_PRE_LENGTH_MS);
// Send out the ZVEI2 tone sequence
for (int i = 0; i < ZVEI_NUM_TONES; i++)
{
BK4819_PlaySingleTone(tones[i], ZVEI_TONE_LENGTH_MS, 100, true);
SYSTEM_DelayMs(ZVEI_PAUSE_LENGTH_MS);
}
// Wait to allow the radio to finish transmitting
SYSTEM_DelayMs(ZVEI_POST_LENGTH_MS);
// Flash the green LED twice
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
SYSTEM_DelayMs(70);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
SYSTEM_DelayMs(70);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
SYSTEM_DelayMs(70);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
// Display the normal screen
gRequestDisplayScreen = DISPLAY_MAIN;
}
-65
View File
@@ -1,65 +0,0 @@
/*
* REGA Alam and Test
* ==================
*
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes for the Alpine REGA alarm channel.
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
*
* There are two REGA ACTIONS: Test and Alarm.
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
*
* These actions can be assigned to a key in the settings menu.
*
* The Test/Alarm function will perform the following actions:
* - Set the radio configuration to the REGA frequency, CTCSS, FM, full power etc.
* - Start transmitting
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the transmitter
* - Send out the ZVEI tone sequence
* - Wait 100ms to allow the radio to finish transmitting
* - Set the radio back to receive mode
*
* The REGA frequency is 161.300 Mhz
*
* The ZVEI tone squence for alarm is: 21414
* The ZVEI tone squence for test is: 21301
*
* The two tone sequences are hardcoded in two arrays
*
* Copyright 2025 Markus Bärtschi
*
* https://github.com/markusb
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef REGA_H
#define REGA_H
#include <stdint.h>
#define ZVEI_NUM_TONES 5
#define ZVEI_TONE_LENGTH_MS 70 // 70
#define ZVEI_PAUSE_LENGTH_MS 10 // 10
#define ZVEI_PRE_LENGTH_MS 300
#define ZVEI_POST_LENGTH_MS 100
#define REGA_CTCSS_FREQ_INDEX 18 // dcs.c: CTCSS_Options[18] = 1230
#define REGA_FREQUENCY 16130000 // 43370000 16130000
void ACTION_RegaAlarm(void);
void ACTION_RegaTest(void);
void UI_DisplayREGA(void);
void REGA_TransmitZvei(const uint16_t[], const char[]);
#endif
+1358
View File
File diff suppressed because it is too large. Load diff
+86
View File
@@ -0,0 +1,86 @@
/* Copyright 2026
*
* Licensed under the Apache License, Version 2.0.
*/
#ifndef APP_RXTX_LOG_H
#define APP_RXTX_LOG_H
#include <stdbool.h>
#include <stdint.h>
#include "driver/keyboard.h"
#include "functions.h"
#include "radio.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#define RXTX_LOG_VISIBLE_COUNT 512
// Field order mirrors the leading fields of RXTX_LogFlashEntry_t
// (rxtx_log.c) so both layouts match byte-for-byte up to and including
// battVolt, copied in one pass. Scan-only fields (sequence) sit past the
// copied prefix in the flash layout and are not cached in RAM.
// The channel name is not stored in flash: it is resolved from `channel` when
// exporting the K5Viewer packet.
typedef struct {
uint32_t frequency;
uint32_t trafficSeq;
uint16_t durationSeconds;
uint16_t channel;
uint8_t flags;
uint8_t sMeter;
uint8_t battVolt;
} RXTX_LogEntry_t;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define RXTX_LOG_K5VIEWER_VERSION 2u
#define RXTX_LOG_K5VIEWER_ROW_COUNT 64u
#define RXTX_LOG_K5VIEWER_HISTORY_ROW_COUNT 64u
#define RXTX_LOG_K5VIEWER_HISTORY_START 0xFFFFFFFFu
#define RXTX_LOG_K5VIEWER_NAME_LENGTH 10u
#define RXTX_LOG_K5VIEWER_STATUS_ACTIVE (1u << 0)
#define RXTX_LOG_K5VIEWER_STATUS_HAS_TRAFFIC (1u << 1)
#define RXTX_LOG_K5VIEWER_STATUS_CLEARING (1u << 2)
typedef struct __attribute__((packed)) {
uint32_t frequency;
uint32_t trafficSeq;
uint16_t durationSeconds;
uint16_t channel;
uint8_t flags;
uint8_t meter;
uint8_t battVolt;
char channelName[RXTX_LOG_K5VIEWER_NAME_LENGTH];
} RXTX_LogK5ViewerRow_t;
// On-wire packet layout: 4-byte header (version, status, rowCount,
// reserved), one live row, then RXTX_LOG_K5VIEWER_ROW_COUNT row slots,
// zero-padded past the last valid row. The packet is streamed row by row
// and never built whole in RAM, so only its size is defined here.
#define RXTX_LOG_K5VIEWER_PACKET_SIZE (4u + ((RXTX_LOG_K5VIEWER_ROW_COUNT + 1u) * sizeof(RXTX_LogK5ViewerRow_t)))
#define RXTX_LOG_K5VIEWER_HISTORY_PACKET_SIZE (RXTX_LOG_K5VIEWER_HISTORY_ROW_COUNT * sizeof(RXTX_LogK5ViewerRow_t))
uint32_t RXTX_LOG_K5ViewerSignature(void);
void RXTX_LOG_SendK5ViewerPacket(void (*send)(const uint8_t *data, uint16_t size));
// Sends the page of history rows below `beforeSeq` (HISTORY_START begins
// a dump below the live packet's coverage) and returns the bound for the
// next page, 0 once the visible history is exhausted.
uint32_t RXTX_LOG_SendK5ViewerHistoryPage(uint32_t beforeSeq, void (*send)(const uint8_t *data, uint16_t size));
#endif
void RXTX_LOG_Init(void);
void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function);
void RXTX_LOG_BeginTx(const VFO_Info_t *vfo);
void RXTX_LOG_EndActive(void);
void RXTX_LOG_Task10ms(void);
void RXTX_LOG_Tick500ms(void);
const char *RXTX_LOG_GetFilterName(void);
void ACTION_RxTxLog(void);
void RXTX_LOG_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
void UI_DisplayRxTxLog(void);
#endif
#endif
+110 -14
View File
@@ -41,6 +41,101 @@ bool gScanUseCssResult;
STEP_Setting_t stepSetting;
uint8_t scanHitCount;
typedef enum
{
SCAN_FREQ_VERIFY_OFF,
SCAN_FREQ_VERIFY_FUNDAMENTAL,
SCAN_FREQ_VERIFY_HARMONIC
} SCAN_FrequencyVerifyState_t;
static SCAN_FrequencyVerifyState_t scanFreqVerifyState;
static uint32_t scanVerifyFundamental;
static uint32_t scanVerifyHarmonic;
static uint16_t scanVerifyFundamentalRssi;
static bool SCANNER_ShouldVerifyVhfSecondHarmonic(const uint32_t frequency)
{
const uint32_t fundamental = frequency / 2;
return frequency >= (frequencyBandTable[BAND3_137MHz].lower * 2) &&
frequency < (frequencyBandTable[BAND4_174MHz].lower * 2) &&
fundamental >= frequencyBandTable[BAND3_137MHz].lower &&
fundamental < frequencyBandTable[BAND4_174MHz].lower;
}
static void SCANNER_StartCssScanAtFrequency(const uint32_t frequency)
{
gScanFrequency = frequency;
gScanCssResultCode = 0xFF;
gScanCssResultType = 0xFF;
scanHitCount = 0;
gScanUseCssResult = false;
gScanProgressIndicator = 0;
gScanCssState = SCAN_CSS_STATE_SCANNING;
gScanDelay_10ms = scan_delay_10ms;
BK4819_SetScanFrequency(gScanFrequency);
if (!gCssBackgroundScan)
GUI_SelectNextDisplay(DISPLAY_SCANNER);
gUpdateStatus = true;
}
static void SCANNER_TuneForFrequencyVerification(const uint32_t frequency)
{
BK4819_SetFrequency(frequency);
BK4819_PickRXFilterPathBasedOnFrequency(frequency);
BK4819_RX_TurnOn();
}
static uint16_t SCANNER_ReadVerificationRssi(void)
{
(void)BK4819_GetRSSI();
return BK4819_GetRSSI();
}
static void SCANNER_StartFrequencyVerification(const uint32_t harmonic)
{
scanVerifyFundamental = harmonic / 2;
scanVerifyHarmonic = harmonic;
scanFreqVerifyState = SCAN_FREQ_VERIFY_FUNDAMENTAL;
SCANNER_TuneForFrequencyVerification(scanVerifyFundamental);
gScanDelay_10ms = 2;
}
static bool SCANNER_HandleFrequencyVerification(void)
{
switch (scanFreqVerifyState) {
case SCAN_FREQ_VERIFY_FUNDAMENTAL:
scanVerifyFundamentalRssi = SCANNER_ReadVerificationRssi();
scanFreqVerifyState = SCAN_FREQ_VERIFY_HARMONIC;
SCANNER_TuneForFrequencyVerification(scanVerifyHarmonic);
gScanDelay_10ms = 2;
return true;
case SCAN_FREQ_VERIFY_HARMONIC: {
const uint16_t harmonicRssi = SCANNER_ReadVerificationRssi();
const uint16_t margin = 4;
uint32_t verifiedFrequency = scanVerifyHarmonic;
if (scanVerifyFundamentalRssi > harmonicRssi &&
scanVerifyFundamentalRssi - harmonicRssi >= margin)
{
verifiedFrequency = scanVerifyFundamental;
}
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
SCANNER_StartCssScanAtFrequency(verifiedFrequency);
return true;
}
default:
return false;
}
}
static void SCANNER_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (!bKeyHeld && bKeyPressed)
@@ -341,7 +436,8 @@ void SCANNER_Start(bool singleFreq)
gScanCssState = SCAN_CSS_STATE_OFF;
gScanFrequency = 0xFFFFFFFF;
BK4819_PickRXFilterPathBasedOnFrequency(gScanFrequency);
// Keep the RX input path selected by RADIO_SetupRegisters().
// 0xFFFFFFFF means "unknown frequency" here and would turn both LNAs off.
BK4819_SetFrequencyScan(true);
gUpdateStatus = true;
@@ -366,6 +462,7 @@ void SCANNER_Start(bool singleFreq)
g_SquelchLost = false;
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
gScanProgressIndicator = 0;
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
}
void SCANNER_Stop(void)
@@ -377,6 +474,7 @@ void SCANNER_Stop(void)
gUpdateStatus = true;
gCssBackgroundScan = false;
gScanUseCssResult = false;
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
@@ -398,6 +496,10 @@ void SCANNER_TimeSlice10ms(void)
return;
}
if (SCANNER_HandleFrequencyVerification()) {
return;
}
switch (gScanCssState) {
case SCAN_CSS_STATE_OFF: {
// must be RF frequency scanning if we're here ?
@@ -420,22 +522,16 @@ void SCANNER_TimeSlice10ms(void)
if (scanHitCount < 3) {
BK4819_SetFrequencyScan(true);
}
else if (SCANNER_ShouldVerifyVhfSecondHarmonic(gScanFrequency)) {
SCANNER_StartFrequencyVerification(gScanFrequency);
}
else {
BK4819_SetScanFrequency(gScanFrequency);
gScanCssResultCode = 0xFF;
gScanCssResultType = 0xFF;
scanHitCount = 0;
gScanUseCssResult = false;
gScanProgressIndicator = 0;
gScanCssState = SCAN_CSS_STATE_SCANNING;
if(!gCssBackgroundScan)
GUI_SelectNextDisplay(DISPLAY_SCANNER);
gUpdateStatus = true;
SCANNER_StartCssScanAtFrequency(gScanFrequency);
}
gScanDelay_10ms = scan_delay_10ms;
if (scanFreqVerifyState == SCAN_FREQ_VERIFY_OFF) {
gScanDelay_10ms = scan_delay_10ms;
}
//gScanDelay_10ms = 1; // 10ms
break;
}
+19 -8
View File
@@ -27,8 +27,8 @@
#include "ui/helper.h"
#include "ui/main.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
@@ -433,8 +433,15 @@ static void ToggleAFDAC(bool on)
BK4819_WriteRegister(BK4819_REG_30, Reg);
}
static uint32_t NormalizeScanFrequency(uint32_t f)
{
const uint16_t step = scanStepValues[settings.scanStepIndex];
return (step == 833) ? FREQUENCY_RoundToStep(f, step) : f;
}
static void SetF(uint32_t f)
{
f = NormalizeScanFrequency(f);
fMeasure = f;
BK4819_SetFrequency(fMeasure);
@@ -451,6 +458,8 @@ static void SetF(uint32_t f)
// cutting the SPI bus activity that causes SPI-induced audio interference.
static void SetFScan(uint32_t f)
{
f = NormalizeScanFrequency(f);
// Refresh RF path only when crossing the VHF/UHF boundary (280 MHz)
if ((f < 28000000) != (fMeasure < 28000000))
BK4819_PickRXFilterPathBasedOnFrequency(f);
@@ -756,7 +765,7 @@ static void UpdateScanInfo()
if (scanInfo.rssi > scanInfo.rssiMax)
{
scanInfo.rssiMax = scanInfo.rssi;
scanInfo.fPeak = scanInfo.f;
scanInfo.fPeak = NormalizeScanFrequency(scanInfo.f);
scanInfo.iPeak = scanInfo.i;
}
@@ -1593,6 +1602,7 @@ static void ShowChannelName(uint32_t f)
{
static uint32_t channelF = 0;
static char channelName[12];
f = NormalizeScanFrequency(f);
// Channel name starts at x=43 (fixed), leaving room for the dBm
// string on the left (max ~40 px) and battery indicator at x=116.
@@ -1632,6 +1642,7 @@ static void FormatFrequency(uint32_t freq, char *buffer) {
static void DrawF(uint32_t f)
{
f = NormalizeScanFrequency(f);
FormatFrequency(f, String);
// Align frequency with channel name in status bar (both at x=43).
// Left-aligned (End == Start = 43) so it does not collide with BW at x=108.
@@ -2450,10 +2461,10 @@ static void UpdateListening()
static void Tick()
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Parse incoming packets on every tick so serial keys are never missed,
// regardless of whether the screen needs redrawing.
SCREENSHOT_ParseInput();
K5VIEWER_ParseInput();
#endif
if (gNextTimeslice)
@@ -2521,9 +2532,9 @@ static void Tick()
if (redrawScreen || ++renderTimer >= RENDER_PERIOD_TICKS)
{
Render();
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(false);
// For K5Viewer
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
redrawScreen = false;
renderTimer = 0;
+2 -2
View File
@@ -863,7 +863,7 @@ void UART_HandleCommand(uint32_t Port)
#endif
} // switch
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
gUART_LockScreenshot = 20; // lock screenshot
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
gUART_LockK5Viewer = 20; // lock the K5Viewer stream
#endif
}
+15
View File
@@ -57,6 +57,21 @@ const uint8_t gFontLight[9] =
0b00001100,
};
// Same bulb outline as gFontLight with the inner filament removed,
// used when the manual backlight is currently off
const uint8_t gFontLightOff[9] =
{
0b00001100,
0b00010010,
0b00100001,
0b01100001,
0b01100001,
0b01100001,
0b00100001,
0b00010010,
0b00001100,
};
const uint8_t gFontMute[12] =
{
0b00011100,
+1
View File
@@ -12,6 +12,7 @@ extern const uint8_t gFontS[6];
extern const uint8_t gFontKeyLock[9];
extern const uint8_t gFontLight[9];
extern const uint8_t gFontLightOff[9];
extern const uint8_t gFontMute[12];
extern const uint8_t gFontXB[2][6];
+5 -3
View File
@@ -32,7 +32,9 @@
#endif
#define PWM_FREQ 4000
#define DUTY_CYCLE_LEVELS 64
// 32 levels keep every step of the value[] table distinct after
// quantization while halving the DMA duty cycle buffer (128 bytes saved)
#define DUTY_CYCLE_LEVELS 32
#define DUTY_CYCLE_ON_VALUE GPIO_PIN_MASK(GPIO_PIN_BACKLIGHT)
#define DUTY_CYCLE_OFF_VALUE (DUTY_CYCLE_ON_VALUE << 16)
@@ -56,8 +58,8 @@ static int16_t fadeStep = 0;
const uint8_t value[] = {
0, // 0 off
8, // 1 visible in the dark
14, // 2
22, // 3
16, // 2
24, // 3
32, // 4
48, // 5
72, // 6
+21 -5
View File
@@ -411,6 +411,11 @@ void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet)
BK4819_WriteRegister(BK4819_REG_33, gBK4819_GpioOutState);
}
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin)
{
return (gBK4819_GpioOutState & (0x40u >> Pin)) != 0;
}
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord)
{
// REG_51
@@ -1719,7 +1724,7 @@ void BK4819_PrepareFSKReceive(void)
BK4819_WriteRegister(BK4819_REG_59, 0x3068);
}
static void BK4819_PlayRogerNormal(void)
static void BK4819_PlayRogerNormal(BK4819_FilterBandwidth_t Bandwidth)
{
#if 0
const uint32_t tone1_Hz = 500;
@@ -1734,12 +1739,19 @@ static void BK4819_PlayRogerNormal(void)
BK4819_EnterTxMute();
BK4819_SetAF(BK4819_AF_MUTE);
BK4819_WriteRegister(BK4819_REG_70, BK4819_REG_70_ENABLE_TONE1 | (66u << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
const uint8_t rogerToneGain = (Bandwidth == BK4819_FILTER_BW_WIDE) ? 32u : 66u;
if (Bandwidth == BK4819_FILTER_BW_WIDE)
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
BK4819_EnableTXLink();
SYSTEM_DelayMs(50);
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
BK4819_WriteRegister(BK4819_REG_70,
BK4819_REG_70_ENABLE_TONE1 |
(rogerToneGain << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
BK4819_ExitTxMute();
SYSTEM_DelayMs(80);
@@ -1752,6 +1764,10 @@ static void BK4819_PlayRogerNormal(void)
BK4819_EnterTxMute();
BK4819_WriteRegister(BK4819_REG_70, 0x0000);
if (Bandwidth == BK4819_FILTER_BW_WIDE)
BK4819_SetFilterBandwidth(Bandwidth, true);
BK4819_WriteRegister(BK4819_REG_30, 0xC1FE); // 1 1 0000 0 1 1111 1 1 1 0
}
@@ -1804,10 +1820,10 @@ void BK4819_PlayRogerMDC(void)
BK4819_WriteRegister(BK4819_REG_58, 0x0000);
}
void BK4819_PlayRoger(void)
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth)
{
if (gEeprom.ROGER == ROGER_MODE_ROGER) {
BK4819_PlayRogerNormal();
BK4819_PlayRogerNormal(Bandwidth);
} else if (gEeprom.ROGER == ROGER_MODE_MDC) {
BK4819_PlayRogerMDC();
}
+2 -1
View File
@@ -77,6 +77,7 @@ void BK4819_SetAGC(bool enable);
void BK4819_InitAGC(bool amModulation);
void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet);
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin);
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord);
void BK4819_SetCTCSSFrequency(uint32_t BaudRate);
@@ -169,7 +170,7 @@ uint8_t BK4819_GetCTCType(void);
void BK4819_SendFSKData(uint16_t *pData);
void BK4819_PrepareFSKReceive(void);
void BK4819_PlayRoger(void);
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth);
void BK4819_Enable_AfDac_DiscMode_TxDsp(void);
+30 -11
View File
@@ -441,6 +441,11 @@ void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet)
BK4819_WriteRegister(BK4819_REG_33, gBK4819_GpioOutState);
}
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin)
{
return (gBK4819_GpioOutState & (0x40u >> Pin)) != 0;
}
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord)
{
// REG_51
@@ -696,11 +701,11 @@ void BK4819_SetFilterBandwidth(const BK4819_FilterBandwidth_t Bandwidth, const b
break;
case BK4819_FILTER_BW_NARROWER: // 6.25kHz
// 0x205C = (0b010 << 12) | (0b000 << 9) | (0b001 << 6) |
// (0b01 << 4) | (1 << 3) | (1 << 2)
// 0x2058 = (0b010 << 12) | (0b000 << 9) | (0b001 << 6) |
// (0b01 << 4) | (1 << 3)
// = RF 3.0 kHz, weak-RF 2.0 kHz, AF Tx LPF2 2.5 kHz,
// 6.25k mode, FM gain +6 dB.
val = 0x205C;
// 6.25k mode, FM gain 0 dB.
val = 0x2058;
break;
case BK4819_FILTER_BW_AM: // Stock AM preset
@@ -1754,7 +1759,7 @@ void BK4819_PrepareFSKReceive(void)
BK4819_WriteRegister(BK4819_REG_59, 0x3068);
}
static void BK4819_PlayRogerNormal(void)
static void BK4819_PlayRogerNormal(BK4819_FilterBandwidth_t Bandwidth)
{
#if 0
const uint32_t tone1_Hz = 500;
@@ -1769,17 +1774,31 @@ static void BK4819_PlayRogerNormal(void)
BK4819_EnterTxMute();
BK4819_SetAF(BK4819_AF_MUTE);
BK4819_WriteRegister(BK4819_REG_70, // BK4819_REG_70_ENABLE_TONE1 | (66u << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
0xC300
);
const uint8_t rogerToneGain = (Bandwidth == BK4819_FILTER_BW_WIDE) ? 32u : 67u;
if (Bandwidth == BK4819_FILTER_BW_WIDE)
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
BK4819_EnableTXLink();
SYSTEM_DelayMs(50);
BK4819_PlayToneRaw(tone1_Hz, 80);
BK4819_WriteRegister(BK4819_REG_70,
BK4819_REG_70_ENABLE_TONE1 |
(rogerToneGain << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
BK4819_ExitTxMute();
SYSTEM_DelayMs(80);
BK4819_EnterTxMute();
BK4819_PlayToneRaw(tone2_Hz, 80);
BK4819_WriteRegister(BK4819_REG_70, 0x0000);
if (Bandwidth == BK4819_FILTER_BW_WIDE)
BK4819_SetFilterBandwidth(Bandwidth, true);
BK4819_WriteRegister(BK4819_REG_30, 0xC1FE); // 1 1 0000 0 1 1111 1 1 1 0
}
@@ -1832,10 +1851,10 @@ void BK4819_PlayRogerMDC(void)
BK4819_WriteRegister(BK4819_REG_58, 0x0000);
}
void BK4819_PlayRoger(void)
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth)
{
if (gEeprom.ROGER == ROGER_MODE_ROGER) {
BK4819_PlayRogerNormal();
BK4819_PlayRogerNormal(Bandwidth);
} else if (gEeprom.ROGER == ROGER_MODE_MDC) {
BK4819_PlayRogerMDC();
}
+7
View File
@@ -72,6 +72,13 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
// [0x00..0x07] 8-byte header (reserved)
// [0x08..0x407] 128x64 monochrome bitmap, 1024 Bytes
// ST7565-native: 8 pages * 128 columns, column-major LSB-top
// Not mapped, for documentation only (the EEPROM API uses 16-bit
// addresses and could not reach a 32 KB window anyway):
//
// 0x1E0000 -> 0x1E8000: RX/TX append-only log * 32 KB / 8 sectors
// (ENABLE_FEAT_F4HWN_RXTX_LOG, accessed directly
// by app/rxtx_log.c, not through this layer)
};
static void AddrTranslate(uint16_t EEPROM_Addr, uint16_t Size, uint32_t *PY25Q16_Addr_out, uint16_t *Size_out, bool *End_out);
+33 -5
View File
@@ -27,7 +27,7 @@ KEY_Code_t gKeyReading1 = KEY_INVALID;
uint16_t gDebounceCounter = 0;
bool gWasFKeyPressed = false;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Short press: hold key for SERIAL_KEY_SHORT_POLLS calls.
// Must exceed key_debounce_10ms (2) to trigger ProcessKey(key, true, false).
#define SERIAL_KEY_SHORT_POLLS 5
@@ -39,8 +39,14 @@ bool gWasFKeyPressed = false;
// Packet types for serial key injection (K5Viewer → radio)
#define SERIAL_KEY_TYPE 0x03
#define SERIAL_KEY_TYPE_LONG 0x04
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define SERIAL_FEATURE_TYPE 0x05
#endif
volatile KEY_Code_t gKeyFromSerial = KEY_INVALID;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
volatile uint8_t gSerialViewerFeatures = 0;
#endif
static uint8_t gSerialKeyHoldCount = 0;
static uint8_t gSerialKeyLong = 0; // 0 = short press, 1 = long press
@@ -71,13 +77,35 @@ bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b)
break;
case STATE_KA_2:
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (b == 0x00)
*state = STATE_KA_3;
else if (b == SERIAL_FEATURE_TYPE)
*state = STATE_KA_FEATURE;
else
*state = STATE_IDLE;
#else
*state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE;
#endif
break;
case STATE_KA_3:
if (b == 0x00) connected = true;
if (b == 0x00) {
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
gSerialViewerFeatures = 0;
#endif
connected = true;
}
*state = STATE_IDLE;
break;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
case STATE_KA_FEATURE:
gSerialViewerFeatures = b;
connected = true;
*state = STATE_IDLE;
break;
#endif
case STATE_KEY_1:
*state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE;
@@ -156,7 +184,7 @@ static const KEY_Code_t keyboard[5][4] = {
KEY_Code_t KEYBOARD_Poll(void)
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Serial-injected key: hold it for SHORT or LONG polls depending on press type,
// so the debounce counter in app.c reaches the right threshold:
// - Short: key_debounce_10ms (2) → ProcessKey(key, true, false)
@@ -265,4 +293,4 @@ KEY_Code_t KEYBOARD_GetKey(void)
void HideFKeyIcon(void) {
gWasFKeyPressed = false;
gUpdateStatus = true;
}
}
+14 -2
View File
@@ -50,6 +50,9 @@ typedef enum {
STATE_KA_1,
STATE_KA_2,
STATE_KA_3,
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
STATE_KA_FEATURE,
#endif
STATE_KEY_1,
STATE_KEY_2,
STATE_KEY_3,
@@ -61,10 +64,19 @@ extern KEY_Code_t gKeyReading1;
extern uint16_t gDebounceCounter;
extern bool gWasFKeyPressed;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll).
extern volatile KEY_Code_t gKeyFromSerial;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#define SERIAL_VIEWER_FEATURE_RF_LOG 0x01u
#define SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY 0x02u
#define SERIAL_VIEWER_FEATURE_RF_LOG_RESTART 0x80u
// Extension flags announced by the viewer's feature keepalive.
extern volatile uint8_t gSerialViewerFeatures;
#endif
bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b);
#endif
@@ -73,4 +85,4 @@ KEY_Code_t KEYBOARD_GetKey(void);
void HideFKeyIcon(void);
#endif
#endif
+3 -3
View File
@@ -24,7 +24,7 @@
#include "driver/st7565.h"
#include "driver/system.h"
#include "misc.h"
#include "screenshot.h"
#include "k5viewer.h"
#define SPIx SPI1
@@ -164,8 +164,8 @@ void ST7565_DrawLine(const unsigned int Column, const unsigned int Line, const u
void ST7565_BlitLine(unsigned line)
{
ST7565_BlitScreen(line + 1);
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(true); // Force immediate capture
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(true); // Force immediate capture
#endif
}
+2 -2
View File
@@ -23,7 +23,7 @@
#include "py32f071_ll_gpio.h"
#include "py32f071_ll_usart.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "driver/keyboard.h"
#endif
@@ -150,7 +150,7 @@ void UART_LogSend(const void *pBuffer, uint32_t Size)
}
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool UART_IsCableConnected(void) {
static uint8_t read_ptr = 0;
static ParseState_t state = STATE_IDLE;
+1 -1
View File
@@ -27,7 +27,7 @@ void UART_Init(void);
void UART_Send(const void *pBuffer, uint32_t Size);
void UART_LogSend(const void *pBuffer, uint32_t Size);
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool UART_IsCableConnected(void);
#endif
+15 -9
View File
@@ -18,7 +18,7 @@
#include "usb_config.h"
#include "py32f071_ll_bus.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "driver/keyboard.h"
#endif
@@ -42,19 +42,25 @@ void VCP_Init()
NVIC_EnableIRQ(USBD_IRQn);
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
bool VCP_ScreenshotPing(void)
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool VCP_K5ViewerPing(void)
{
// State machine for parsing incoming packets:
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive, no extensions
// Feature keepalive (ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER builds only):
// 0x55 0xAA 0x05 <flags> → viewer alive, extensions enabled
// flags bit 0 = RF log stream
// flags bit 1 = paged RF log history
// flags bit 7 = restart RF log synchronization
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
//
// State transitions:
// IDLE → 0x55 → KA_1
// KA_1 → 0xAA → KA_2 (else IDLE)
// KA_2 → 0x00 → KA_3 (else IDLE)
// KA_3 → 0x00 → keepalive OK, IDLE
// KA_2 → 0x00 → KA_3 (else 0x05 → KA_FEATURE, else IDLE)
// KA_3 → 0x00 → keepalive OK, clear extensions, IDLE
// KA_FEATURE → <flags> → keepalive OK, set extensions, IDLE
//
// IDLE → 0xAA → KEY_1
// KEY_1 → 0x55 → KEY_2 (else IDLE)
@@ -89,4 +95,4 @@ bool VCP_ScreenshotPing(void)
return connected;
}
#endif // ENABLE_FEAT_F4HWN_SCREENSHOT
#endif // ENABLE_FEAT_F4HWN_K5VIEWER
+2 -2
View File
@@ -29,8 +29,8 @@ extern volatile uint32_t VCP_RxBufPointer;
void VCP_Init();
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
bool VCP_ScreenshotPing(void);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool VCP_K5ViewerPing(void);
#endif
static inline void VCP_Send(const uint8_t *Buf, uint32_t Size)
+26
View File
@@ -33,6 +33,9 @@
#include "frequencies.h"
#include "functions.h"
#include "helper/battery.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#include "misc.h"
#include "radio.h"
#include "settings.h"
@@ -239,8 +242,31 @@ void FUNCTION_Select(FUNCTION_Type_t Function)
const FUNCTION_Type_t PreviousFunction = gCurrentFunction;
const bool bWasPowerSave = PreviousFunction == FUNCTION_POWER_SAVE;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
const bool previousWasActive =
PreviousFunction == FUNCTION_TRANSMIT ||
PreviousFunction == FUNCTION_MONITOR ||
PreviousFunction == FUNCTION_RECEIVE;
const bool previousWasTx = PreviousFunction == FUNCTION_TRANSMIT;
const bool nextIsActive =
Function == FUNCTION_TRANSMIT ||
Function == FUNCTION_MONITOR ||
Function == FUNCTION_RECEIVE;
const bool nextIsTx = Function == FUNCTION_TRANSMIT;
if (previousWasActive &&
(!nextIsActive ||
(previousWasTx != nextIsTx)))
RXTX_LOG_EndActive();
#endif
gCurrentFunction = Function;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
if (nextIsActive && !nextIsTx && (!previousWasActive || previousWasTx))
RXTX_LOG_BeginRx(gRxVfo, Function);
#endif
if (bWasPowerSave && Function != FUNCTION_POWER_SAVE) {
BK4819_Conditional_RX_TurnOn_and_GPIO6_Enable();
gRxIdleMode = false;
+1
View File
@@ -17,6 +17,7 @@
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include <stdbool.h>
#include <stdint.h>
enum FUNCTION_Type_t
+6 -6
View File
@@ -73,12 +73,14 @@ BOOT_Mode_t BOOT_GetMode(void)
void BOOT_ProcessMode(BOOT_Mode_t Mode)
{
GUI_DisplayType_t display = DISPLAY_MAIN;
if (Mode == BOOT_MODE_F_LOCK)
{
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
gEeprom.CURRENT_STATE = 0; // Don't resume is active...
#endif
GUI_SelectNextDisplay(DISPLAY_MENU);
display = DISPLAY_MENU;
}
#ifdef ENABLE_AIRCOPY
else
@@ -119,11 +121,9 @@ void BOOT_ProcessMode(BOOT_Mode_t Mode)
gEeprom.CURRENT_STATE = 0; // Don't resume is active...
#endif
GUI_SelectNextDisplay(DISPLAY_AIRCOPY);
display = DISPLAY_AIRCOPY;
}
#endif
else
{
GUI_SelectNextDisplay(DISPLAY_MAIN);
}
GUI_SelectNextDisplay(display);
}
+331
View File
@@ -0,0 +1,331 @@
/* Copyright 2024 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "debugging.h"
#include "driver/st7565.h"
#include "k5viewer.h"
#include "misc.h"
#include "driver/vcp.h"
#include "driver/keyboard.h"
#include "driver/bk4819.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
#include "app/rxtx_log.h"
#endif
// SRAM optimization: minimize static allocations
// - previousHash: one fingerprint per 8-byte chunk instead of a full
// 1024-byte copy of the previous frame (chunks are only compared,
// never retransmitted from history). A 16-bit fingerprint keeps the
// collision odds at ~1/65536 per chunk, so a stale chunk slipping
// through is rare; the forcedBlock rotation repairs that residual
// within at most 128 frames.
// Stack optimization: chunks are computed on demand straight from
// gStatusLine/gFrameBuffer instead of building the whole 1024-byte
// frame on the stack. Changed chunks are tracked in a 16-byte bitmap.
// Peak stack drops from ~1.3 KB to ~40 bytes, at the cost of
// transforming each transmitted chunk twice (negligible next to the
// blocking UART transfer).
static uint16_t previousHash[128];
static uint8_t previousStateFlags = 0xFF;
static uint8_t forcedBlock = 0;
static uint8_t keepAlive = 3;
static bool hasConnectionPing = false;
static bool wasConnected = false;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
static uint32_t previousRfLogSignature = 0;
static bool rfLogSent = false;
// Pagination bound of the history dump: rows with trafficSeq below this
// value remain to be sent; HISTORY_START arms a new dump, 0 means done.
static uint32_t rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
#endif
// FNV-1a over one 8-byte chunk, folded to 16 bits
static uint16_t K5VIEWER_Hash(const uint8_t *data)
{
uint32_t h = 2166136261u;
for (uint8_t i = 0; i < 8; i++) {
h = (h ^ data[i]) * 16777619u;
}
return (uint16_t)(h ^ (h >> 16));
}
void K5VIEWER_ParseInput(void)
{
if (K5VIEWER_IsLocked())
return;
if (UART_IsCableConnected()) {
keepAlive = 15;
hasConnectionPing = true;
gUSB_K5ViewerEnabled = false;
}
else if (VCP_K5ViewerPing()) {
keepAlive = 15;
hasConnectionPing = true;
gUSB_K5ViewerEnabled = true;
}
}
static void K5VIEWER_Send(const uint8_t *buf, uint16_t len)
{
if (gUSB_K5ViewerEnabled) {
cdc_acm_data_send_with_dtr(buf, len);
} else {
UART_Send(buf, len);
}
}
enum {
K5VIEWER_CHUNK_SIZE = 8,
K5VIEWER_CHUNKS_PER_LINE = 16,
K5VIEWER_HALF_LINE_COLUMNS = LCD_WIDTH / 2,
K5VIEWER_MARKER_BASE = 0xF0,
K5VIEWER_TYPE_DIFF = 0x02,
K5VIEWER_TYPE_RXTX_LOG = 0x05,
K5VIEWER_TYPE_RXTX_LOG_HISTORY = 0x06,
K5VIEWER_FLAG_DEEP_SLEEP = 1 << 0,
K5VIEWER_FLAG_LED_RED = 1 << 1,
K5VIEWER_FLAG_LED_GREEN = 1 << 2,
};
static uint8_t K5VIEWER_StateFlags(void)
{
uint8_t flags = 0;
#ifdef ENABLE_FEAT_F4HWN_SLEEP
if (gWakeUp)
flags |= K5VIEWER_FLAG_DEEP_SLEEP;
#endif
if (BK4819_IsGpioOutSet(BK4819_GPIO5_PIN1_RED))
flags |= K5VIEWER_FLAG_LED_RED;
if (BK4819_IsGpioOutSet(BK4819_GPIO6_PIN2_GREEN))
flags |= K5VIEWER_FLAG_LED_GREEN;
return flags;
}
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
static bool K5VIEWER_HasPendingRfLogUpdate(void)
{
if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_RESTART) != 0) {
gSerialViewerFeatures &= ~SERIAL_VIEWER_FEATURE_RF_LOG_RESTART;
rfLogSent = false;
rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
}
if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG) == 0)
return false;
return !rfLogSent || RXTX_LOG_K5ViewerSignature() != previousRfLogSignature;
}
static void K5VIEWER_SendRfLogFrameHeader(uint8_t type, uint16_t len)
{
uint8_t header[5] = {0xAA, 0x55, type, (uint8_t)(len >> 8), (uint8_t)len};
K5VIEWER_Send(header, sizeof(header));
}
static void K5VIEWER_SendRfLogFrameEnd(void)
{
const uint8_t end = 0x0A;
K5VIEWER_Send(&end, 1);
}
static void K5VIEWER_SendRfLog(void)
{
// Capture the signature before streaming: a state change landing
// during the blocking send still differs afterwards and triggers a
// resend on the next cycle.
previousRfLogSignature = RXTX_LOG_K5ViewerSignature();
rfLogSent = true;
K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG, RXTX_LOG_K5VIEWER_PACKET_SIZE);
RXTX_LOG_SendK5ViewerPacket(K5VIEWER_Send);
K5VIEWER_SendRfLogFrameEnd();
}
static bool K5VIEWER_HasPendingRfLogHistory(void)
{
return (gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY) != 0 &&
rfLogHistoryBefore != 0;
}
static void K5VIEWER_SendRfLogHistory(void)
{
K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG_HISTORY,
RXTX_LOG_K5VIEWER_HISTORY_PACKET_SIZE);
rfLogHistoryBefore = RXTX_LOG_SendK5ViewerHistoryPage(rfLogHistoryBefore, K5VIEWER_Send);
K5VIEWER_SendRfLogFrameEnd();
}
#endif
bool K5VIEWER_HasPendingStateChange(void)
{
if (gUART_LockK5Viewer > 0 || keepAlive == 0 || !hasConnectionPing)
return false;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (K5VIEWER_HasPendingRfLogUpdate())
return true;
if (K5VIEWER_HasPendingRfLogHistory())
return true;
#endif
return !wasConnected || K5VIEWER_StateFlags() != previousStateFlags;
}
// Compute one 8-byte output chunk directly from the display buffers.
// Frame layout: per line (status + 7 frame lines), 8 bit layers of
// 16 bytes each. Each bit layer is split into two 64-column chunks.
static void K5VIEWER_Chunk(uint8_t chunkIdx, uint8_t *dest)
{
const uint8_t chunkInLine = chunkIdx % K5VIEWER_CHUNKS_PER_LINE;
const uint8_t line = chunkIdx / K5VIEWER_CHUNKS_PER_LINE;
const uint8_t bit = chunkInLine / 2;
const uint8_t columnBase = (chunkInLine % 2) * K5VIEWER_HALF_LINE_COLUMNS;
const uint8_t *src = (line == 0 ? gStatusLine : gFrameBuffer[line - 1])
+ columnBase;
for (uint8_t j = 0; j < K5VIEWER_CHUNK_SIZE; j++) {
uint8_t acc = 0;
for (uint8_t k = 0; k < K5VIEWER_CHUNK_SIZE; k++) {
if (src[j * K5VIEWER_CHUNK_SIZE + k] & (1 << bit)) acc |= (1 << k);
}
dest[j] = gSetting_set_inv ? ~acc : acc;
}
}
void K5VIEWER_Update(bool force)
{
if (K5VIEWER_IsLocked())
return;
if (keepAlive > 0) {
if (--keepAlive == 0) {
// Connection just lost → reset state for next reconnection
wasConnected = false;
hasConnectionPing = false;
previousStateFlags = 0xFF;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
gSerialViewerFeatures = 0;
rfLogSent = false;
rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
#endif
return;
}
} else {
return;
}
// Connection is alive — detect reconnection and force full frame
if (!wasConnected) {
force = true;
}
const uint8_t stateFlags = K5VIEWER_StateFlags();
const bool stateChanged = (stateFlags != previousStateFlags);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
const bool rfLogPending = K5VIEWER_HasPendingRfLogUpdate();
const bool rfLogHistoryPending = K5VIEWER_HasPendingRfLogHistory();
#else
const bool rfLogPending = false;
const bool rfLogHistoryPending = false;
#endif
// ==== FIRST PASS: Count changed chunks ====
uint16_t deltaLen = 0;
uint8_t changedBitmap[16] = {0}; // 1 bit per chunk
uint8_t chunk[9]; // [0] = index, [1..8] = payload
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
K5VIEWER_Chunk(chunkIdx, &chunk[1]);
bool changed = K5VIEWER_Hash(&chunk[1]) != previousHash[chunkIdx];
bool isForced = (chunkIdx == forcedBlock);
if (changed || isForced || force) {
changedBitmap[chunkIdx >> 3] |= 1 << (chunkIdx & 7);
deltaLen += 9;
}
}
forcedBlock = (forcedBlock + 1) % 128;
if (deltaLen == 0 && !stateChanged && !rfLogPending && !rfLogHistoryPending)
return;
// Skip transmission if a key is currently pressed
// UART_Send is blocking - would freeze the main loop and lose keypresses
if (gKeyReading0 != KEY_INVALID)
return;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
// A pending RF log packet may be the only thing to send: skip the
// frame section when it carries nothing. Without the RF log stream
// the early return above already guarantees this condition.
if (deltaLen != 0 || stateChanged)
#endif
{
// ==== Send version marker and state flags ====
// 0xF0 keeps a resync-safe marker before the standard AA 55 header.
uint8_t versionMarker = K5VIEWER_MARKER_BASE | stateFlags;
K5VIEWER_Send(&versionMarker, 1);
// ==== Send header ====
uint8_t header[5] = {
0xAA, 0x55, K5VIEWER_TYPE_DIFF,
(uint8_t)(deltaLen >> 8),
(uint8_t)(deltaLen & 0xFF)
};
K5VIEWER_Send(header, 5);
// ==== SECOND PASS: Send only changed chunks ====
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
if (!(changedBitmap[chunkIdx >> 3] & (1 << (chunkIdx & 7))))
continue;
chunk[0] = chunkIdx;
K5VIEWER_Chunk(chunkIdx, &chunk[1]);
K5VIEWER_Send(chunk, 9);
// Update the fingerprint only once the chunk is actually sent,
// so chunks skipped by an early return stay marked as changed.
// Hashing the recomputed payload also keeps the fingerprint in
// sync if the display buffer changed between the two passes.
previousHash[chunkIdx] = K5VIEWER_Hash(&chunk[1]);
}
uint8_t end = 0x0A;
K5VIEWER_Send(&end, 1);
}
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
if (rfLogPending)
K5VIEWER_SendRfLog();
if (rfLogHistoryPending)
K5VIEWER_SendRfLogHistory();
#endif
previousStateFlags = stateFlags;
wasConnected = true;
}
+6 -5
View File
@@ -14,10 +14,11 @@
* limitations under the License.
*/
#ifndef SCREENSHOT_H
#define SCREENSHOT_H
#ifndef K5VIEWER_H
#define K5VIEWER_H
void SCREENSHOT_Update(bool force);
void SCREENSHOT_ParseInput(void);
void K5VIEWER_Update(bool force);
void K5VIEWER_ParseInput(void);
bool K5VIEWER_HasPendingStateChange(void);
#endif
#endif
+10
View File
@@ -24,6 +24,9 @@
#include "audio.h"
#include "board.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#include "misc.h"
#include "radio.h"
#include "settings.h"
@@ -99,6 +102,10 @@ void Main(void)
SETTINGS_InitEEPROM();
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_Init();
#endif
#ifdef ENABLE_FEAT_F4HWN
gDW = gEeprom.DUAL_WATCH;
gCB = gEeprom.CROSS_BAND_RX_TX;
@@ -245,6 +252,9 @@ void Main(void)
BOOT_ProcessMode(BootMode);
if (gEeprom.AUTO_KEYPAD_LOCK && !gEeprom.KEY_LOCK)
gKeyLockCountdown = gEeprom.AUTO_KEYPAD_LOCK * 30; // 15 seconds step
// GPIO_ClearBit(&GPIOA->DATA, GPIOA_PIN_VOICE_0);
gUpdateStatus = true;
+8 -8
View File
@@ -127,7 +127,7 @@ enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
uint8_t gSetting_set_ctr = 10;
bool gSetting_set_inv = false;
uint8_t gSetting_set_eot = 0;
bool gSetting_set_lck = false;
uint8_t gSetting_set_lck = SET_LCK_KEYS;
bool gSetting_set_met = 0;
bool gSetting_set_gui = 0;
#ifdef ENABLE_FEAT_F4HWN_AUDIO
@@ -210,9 +210,9 @@ volatile bool gTxTimeoutReached;
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
volatile uint16_t gRxTimerCountdown_500ms;
#endif
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
volatile uint8_t gUART_LockScreenshot = 0; // lock screenshot if Chirp is used
bool gUSB_ScreenshotEnabled = false;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
volatile uint8_t gUART_LockK5Viewer = 0; // lock the K5Viewer stream if Chirp is used
bool gUSB_K5ViewerEnabled = false;
#endif
#endif
@@ -646,11 +646,11 @@ void MR_PrintCacheStats(void)
#endif
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
bool SCREENSHOT_IsLocked(void)
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool K5VIEWER_IsLocked(void)
{
if (gUART_LockScreenshot > 0) {
gUART_LockScreenshot--;
if (gUART_LockK5Viewer > 0) {
gUART_LockK5Viewer--;
return true;
}
+18 -5
View File
@@ -181,6 +181,19 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
#endif
#ifdef ENABLE_FEAT_F4HWN
// Keypad lock scope. Values are BOTH a bitmask (ACTIONS/PTT bits tested
// individually) AND a contiguous 0..3 menu index: the order must stay
// aligned with gSubMenu_SET_LCK[] and any new entry must keep the range
// contiguous so that SET_LCK_LEN remains valid as menu bound and EEPROM
// range check.
enum SET_LCK_t {
SET_LCK_KEYS = 0u,
SET_LCK_ACTIONS = 1u,
SET_LCK_PTT = 2u,
SET_LCK_ACTIONS_PTT = SET_LCK_ACTIONS | SET_LCK_PTT,
SET_LCK_LEN
};
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
enum SET_SAV_t {
SET_SAV_OFF,
@@ -197,7 +210,7 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
extern uint8_t gSetting_set_ctr;
extern bool gSetting_set_inv;
extern uint8_t gSetting_set_eot;
extern bool gSetting_set_lck;
extern uint8_t gSetting_set_lck;
extern bool gSetting_set_met;
extern bool gSetting_set_gui;
#ifdef ENABLE_FEAT_F4HWN_AUDIO
@@ -336,11 +349,11 @@ extern volatile bool gTxTimeoutReached;
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
extern volatile uint16_t gRxTimerCountdown_500ms;
#endif
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
extern volatile uint8_t gUART_LockScreenshot; // lock screenshot if Chirp is used
extern bool gUSB_ScreenshotEnabled;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
extern volatile uint8_t gUART_LockK5Viewer; // lock the K5Viewer stream if Chirp is used
extern bool gUSB_K5ViewerEnabled;
bool SCREENSHOT_IsLocked(void);
bool K5VIEWER_IsLocked(void);
#endif
#endif
+61 -60
View File
@@ -19,6 +19,9 @@
#include "am_fix.h"
#include "app/dtmf.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
@@ -510,17 +513,20 @@ void RADIO_ConfigureSquelchAndOutputPower(VFO_Info_t *pInfo)
}
else
{ // squelch >= 1
Base += gEeprom.SQUELCH_LEVEL; // my eeprom squelch-1
// VHF UHF
PY25Q16_ReadBuffer(Base + 0x00, &pInfo->SquelchOpenRSSIThresh, 1); // 50 10
PY25Q16_ReadBuffer(Base + 0x10, &pInfo->SquelchCloseRSSIThresh, 1); // 40 5
PY25Q16_ReadBuffer(Base + 0x20, &pInfo->SquelchOpenNoiseThresh, 1); // 65 90
PY25Q16_ReadBuffer(Base + 0x30, &pInfo->SquelchCloseNoiseThresh, 1); // 70 100
PY25Q16_ReadBuffer(Base + 0x40, &pInfo->SquelchCloseGlitchThresh, 1); // 90 90
PY25Q16_ReadBuffer(Base + 0x50, &pInfo->SquelchOpenGlitchThresh, 1); // 100 100
Base += gEeprom.SQUELCH_LEVEL; // my eeprom squelch-1
// VHF UHF
uint8_t *sq_ptrs[6] = {
&pInfo->SquelchOpenRSSIThresh, // 50 10
&pInfo->SquelchCloseRSSIThresh, // 40 5
&pInfo->SquelchOpenNoiseThresh, // 65 90
&pInfo->SquelchCloseNoiseThresh, // 70 100
&pInfo->SquelchCloseGlitchThresh, // 90 90
&pInfo->SquelchOpenGlitchThresh // 100 100
};
for(uint8_t i = 0; i < 6; i++) {
PY25Q16_ReadBuffer(Base + (i * 0x10), sq_ptrs[i], 1);
}
uint16_t noise_open = pInfo->SquelchOpenNoiseThresh;
uint16_t noise_close = pInfo->SquelchCloseNoiseThresh;
@@ -544,14 +550,14 @@ void RADIO_ConfigureSquelchAndOutputPower(VFO_Info_t *pInfo)
if (glitch_close == glitch_open && glitch_close <= 253)
glitch_close += 2;
pInfo->SquelchOpenRSSIThresh = (rssi_open > 255) ? 255 : rssi_open;
pInfo->SquelchCloseRSSIThresh = (rssi_close > 255) ? 255 : rssi_close;
pInfo->SquelchOpenGlitchThresh = (glitch_open > 255) ? 255 : glitch_open;
pInfo->SquelchCloseGlitchThresh = (glitch_close > 255) ? 255 : glitch_close;
pInfo->SquelchOpenRSSIThresh = MIN(rssi_open, 255);
pInfo->SquelchCloseRSSIThresh = MIN(rssi_close, 255);
pInfo->SquelchOpenGlitchThresh = MIN(glitch_open, 255);
pInfo->SquelchCloseGlitchThresh = MIN(glitch_close, 255);
#endif
pInfo->SquelchOpenNoiseThresh = (noise_open > 127) ? 127 : noise_open;
pInfo->SquelchCloseNoiseThresh = (noise_close > 127) ? 127 : noise_close;
pInfo->SquelchOpenNoiseThresh = MIN(noise_open, 127);
pInfo->SquelchCloseNoiseThresh = MIN(noise_close, 127);
}
// *******************************
@@ -698,18 +704,11 @@ void RADIO_ApplyOffset(VFO_Info_t *pInfo)
{
uint32_t Frequency = pInfo->freq_config_RX.Frequency;
switch (pInfo->TX_OFFSET_FREQUENCY_DIRECTION)
{
case TX_OFFSET_FREQUENCY_DIRECTION_OFF:
break;
case TX_OFFSET_FREQUENCY_DIRECTION_ADD:
Frequency += pInfo->TX_OFFSET_FREQUENCY;
break;
case TX_OFFSET_FREQUENCY_DIRECTION_SUB:
Frequency -= pInfo->TX_OFFSET_FREQUENCY;
break;
}
if (pInfo->TX_OFFSET_FREQUENCY_DIRECTION == TX_OFFSET_FREQUENCY_DIRECTION_ADD)
Frequency += pInfo->TX_OFFSET_FREQUENCY;
else if (pInfo->TX_OFFSET_FREQUENCY_DIRECTION == TX_OFFSET_FREQUENCY_DIRECTION_SUB)
Frequency -= pInfo->TX_OFFSET_FREQUENCY;
pInfo->freq_config_TX.Frequency = Frequency;
}
@@ -809,6 +808,9 @@ void RADIO_SetupRegisters(bool switchToForeground)
#endif
BK4819_SetFrequency(Frequency);
// Keep the demodulator in sync when retuning without entering RX audio.
RADIO_SetModulation(gRxVfo->Modulation);
BK4819_SetupSquelch(
gRxVfo->SquelchOpenRSSIThresh, gRxVfo->SquelchCloseRSSIThresh,
gRxVfo->SquelchOpenNoiseThresh, gRxVfo->SquelchCloseNoiseThresh,
@@ -841,7 +843,7 @@ void RADIO_SetupRegisters(bool switchToForeground)
BK4819_SetCTCSSFrequency(SQL_TONE);
BK4819_SetTailDetection(SQL_TONE); // Default 550 = QS's 55Hz tone method
InterruptMask = BK4819_REG_3F_CxCSS_TAIL | BK4819_REG_3F_SQUELCH_FOUND | BK4819_REG_3F_SQUELCH_LOST;
InterruptMask |= BK4819_REG_3F_CxCSS_TAIL;
break;
case CODE_TYPE_CONTINUOUS_TONE:
@@ -853,24 +855,17 @@ void RADIO_SetupRegisters(bool switchToForeground)
// BK4819_SetTailDetection(CTCSS_Options[Code]);
//#endif
InterruptMask = 0
| BK4819_REG_3F_CxCSS_TAIL
| BK4819_REG_3F_CTCSS_FOUND
| BK4819_REG_3F_CTCSS_LOST
| BK4819_REG_3F_SQUELCH_FOUND
| BK4819_REG_3F_SQUELCH_LOST;
InterruptMask |= BK4819_REG_3F_CxCSS_TAIL
| BK4819_REG_3F_CTCSS_FOUND
| BK4819_REG_3F_CTCSS_LOST;
break;
case CODE_TYPE_DIGITAL:
case CODE_TYPE_REVERSE_DIGITAL:
BK4819_SetCDCSSCodeWord(DCS_GetGolayCodeWord(CodeType, Code));
InterruptMask = 0
| BK4819_REG_3F_CxCSS_TAIL
| BK4819_REG_3F_CDCSS_FOUND
| BK4819_REG_3F_CDCSS_LOST
| BK4819_REG_3F_SQUELCH_FOUND
| BK4819_REG_3F_SQUELCH_LOST;
InterruptMask |= BK4819_REG_3F_CxCSS_TAIL
| BK4819_REG_3F_CDCSS_FOUND
| BK4819_REG_3F_CDCSS_LOST;
break;
}
@@ -888,11 +883,8 @@ void RADIO_SetupRegisters(bool switchToForeground)
else
{
BK4819_SetCTCSSFrequency(2625);
InterruptMask = 0
| BK4819_REG_3F_CTCSS_FOUND
| BK4819_REG_3F_CTCSS_LOST
| BK4819_REG_3F_SQUELCH_FOUND
| BK4819_REG_3F_SQUELCH_LOST;
InterruptMask |= BK4819_REG_3F_CTCSS_FOUND
| BK4819_REG_3F_CTCSS_LOST;
}
#endif
@@ -1099,11 +1091,12 @@ void RADIO_SetModulation(ModulationMode_t modulation)
// So for now we just keep it as is to maintain compatibility.
//
uint16_t uVar1 = BK4819_ReadRegister(0x31);
switch (modulation)
{
case MODULATION_AM:
{
uint16_t uVar1 = BK4819_ReadRegister(0x31);
BK4819_WriteRegister(0x31, uVar1 | 1); // AM Demodulation Enable
BK4819_WriteRegister(0x42, 0x6f5c);
BK4819_WriteRegister(0x2a, 0x7434);
@@ -1115,7 +1108,7 @@ void RADIO_SetModulation(ModulationMode_t modulation)
BK4819_WriteRegister(0x55, 0x31a9);
#endif
BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(gCurrentVfo), true);
BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(gRxVfo), true);
break;
}
@@ -1123,7 +1116,6 @@ void RADIO_SetModulation(ModulationMode_t modulation)
case MODULATION_FM:
default:
{
uint16_t uVar1 = BK4819_ReadRegister(0x31);
BK4819_WriteRegister(0x31, uVar1 & 0xfffe); // AM Demodulation Disable
BK4819_WriteRegister(0x42, 0x6b5a);
BK4819_WriteRegister(0x2a, 0x7400);
@@ -1213,16 +1205,12 @@ void RADIO_PrepareTX(void)
RADIO_SelectCurrentVfo();
if(TX_freq_check(gCurrentVfo->pTX->Frequency) != 0
#ifdef ENABLE_FEAT_F4HWN
if(TX_freq_check(gCurrentVfo->pTX->Frequency) != 0 && gCurrentVfo->TX_LOCK == true
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
&& gAlarmState != ALARM_STATE_SITE_ALARM
#endif
#else
if(TX_freq_check(gCurrentVfo->pTX->Frequency) != 0
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
&& gAlarmState != ALARM_STATE_SITE_ALARM
#endif
&& gCurrentVfo->TX_LOCK == true
#endif
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
&& gAlarmState != ALARM_STATE_SITE_ALARM
#endif
){
// TX frequency not allowed
@@ -1287,6 +1275,10 @@ void RADIO_PrepareTX(void)
FUNCTION_Select(FUNCTION_TRANSMIT);
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_BeginTx(gCurrentVfo);
#endif
gTxTimerCountdown_500ms = 0; // no timeout
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
@@ -1343,7 +1335,16 @@ void RADIO_SendCssTail(void)
void RADIO_SendEndOfTransmission(void)
{
BK4819_PlayRoger();
BK4819_FilterBandwidth_t Bandwidth = gCurrentVfo->CHANNEL_BANDWIDTH;
#ifdef ENABLE_FEAT_F4HWN_NARROWER
if(Bandwidth == BK4819_FILTER_BW_NARROW && gSetting_set_nfm == 1)
{
Bandwidth = BK4819_FILTER_BW_NARROWER;
}
#endif
BK4819_PlayRoger(Bandwidth);
DTMF_SendEndOfTransmission();
// send the CTCSS/DCS tail tone - allows the receivers to mute the usual FM squelch tail/crash
-174
View File
@@ -1,174 +0,0 @@
/* Copyright 2024 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "debugging.h"
#include "driver/st7565.h"
#include "screenshot.h"
#include "misc.h"
#include "driver/vcp.h"
#include "driver/keyboard.h"
// SRAM optimization: minimize static allocations
// - previousFrame: 1024 bytes (REQUIRED - need to compare for delta)
// - No currentFrame or deltaFrame static buffers
static uint8_t previousFrame[1024] = {0};
static uint8_t forcedBlock = 0;
static uint8_t keepAlive = 3;
void SCREENSHOT_ParseInput(void)
{
if (SCREENSHOT_IsLocked())
return;
if (UART_IsCableConnected()) {
keepAlive = 15;
gUSB_ScreenshotEnabled = false;
}
else if (VCP_ScreenshotPing()) {
keepAlive = 15;
gUSB_ScreenshotEnabled = true;
}
}
static void SCREENSHOT_Send(const uint8_t *buf, uint16_t len)
{
if (gUSB_ScreenshotEnabled) {
cdc_acm_data_send_with_dtr(buf, len);
} else {
UART_Send(buf, len);
}
}
void SCREENSHOT_Line(uint8_t *src, uint8_t *dest, uint16_t *idx) {
for (uint8_t b = 0; b < 8; b++) {
for (uint8_t i = 0; i < 128; i += 8) {
uint8_t acc = 0;
for (uint8_t k = 0; k < 8; k++) {
if (src[i + k] & (1 << b)) acc |= (1 << k);
}
dest[(*idx)++] = gSetting_set_inv ? ~acc : acc;
}
}
}
void SCREENSHOT_Update(bool force)
{
// Build frame in a temporary stack buffer
// This is 1024 bytes but it's temporary and gets freed after the function
uint8_t frameBuffer[1024];
uint16_t index = 0;
uint8_t acc = 0;
uint8_t bitCount = 0;
static bool wasConnected = false;
if (SCREENSHOT_IsLocked())
return;
if (keepAlive > 0) {
if (--keepAlive == 0) {
// Connection just lost → reset state for next reconnection
wasConnected = false;
return;
}
} else {
return;
}
// Connection is alive — detect reconnection and force full frame
if (!wasConnected) {
force = true;
wasConnected = true;
}
// ==== BUILD FRAME ONCE ====
// Status line: 8 bit layers × 128 columns
SCREENSHOT_Line(gStatusLine, frameBuffer, &index);
// Frame buffer: 7 lines × 8 bit layers × 128 columns
for (uint8_t l = 0; l < 7; l++) {
SCREENSHOT_Line(gFrameBuffer[l], frameBuffer, &index);
}
if (bitCount > 0)
frameBuffer[index++] = acc;
if (index != 1024)
return;
// ==== FIRST PASS: Count changed chunks ====
uint16_t deltaLen = 0;
uint8_t changedChunks[128]; // List of changed chunk indices
uint8_t changedCount = 0;
for (uint8_t chunk = 0; chunk < 128; chunk++) {
uint8_t *cur = &frameBuffer[chunk * 8];
uint8_t *prev = &previousFrame[chunk * 8];
bool changed = memcmp(cur, prev, 8) != 0;
bool isForced = (chunk == forcedBlock);
bool fullUpdate = force;
if (changed || isForced || fullUpdate) {
changedChunks[changedCount++] = chunk;
deltaLen += 9;
}
}
forcedBlock = (forcedBlock + 1) % 128;
if (deltaLen == 0)
return;
// Skip transmission if a key is currently pressed
// UART_Send is blocking - would freeze the main loop and lose keypresses
if (gKeyReading0 != KEY_INVALID)
return;
// ==== Send version marker (for backward compatibility detection) ====
// New format: sends 0xFF before header
// Old format: doesn't exist, so viewers can differentiate
uint8_t versionMarker = 0xFF;
SCREENSHOT_Send(&versionMarker, 1);
// ==== Send header ====
uint8_t header[5] = {
0xAA, 0x55, 0x02,
(uint8_t)(deltaLen >> 8),
(uint8_t)(deltaLen & 0xFF)
};
SCREENSHOT_Send(header, 5);
// ==== SECOND PASS: Send only changed chunks ====
uint8_t chunk[9];
for (uint8_t i = 0; i < changedCount; i++) {
uint8_t chunkIdx = changedChunks[i];
uint8_t *cur = &frameBuffer[chunkIdx * 8];
uint8_t *prev = &previousFrame[chunkIdx * 8];
chunk[0] = chunkIdx;
memcpy(&chunk[1], cur, 8);
SCREENSHOT_Send(chunk, 9);
// Update previousFrame for next comparison
memcpy(prev, cur, 8);
}
uint8_t end = 0x0A;
SCREENSHOT_Send(&end, 1);
}
+6 -11
View File
@@ -455,15 +455,16 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
// TODO: address TBD
PY25Q16_ReadBuffer(0x00A158, Data, 8);
const uint8_t set_ptt_scn_sav = Data[7] & 0x0F;
const bool set_ptt_scn_sav_erased = Data[7] == 0xFF;
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
const bool set_ptt_scn_sav_valid = set_ptt_scn_sav < (SET_SAV_LEN << 2);
const bool set_ptt_scn_sav_valid = !set_ptt_scn_sav_erased && set_ptt_scn_sav < (SET_SAV_LEN << 2);
#else
const bool set_ptt_scn_sav_valid = set_ptt_scn_sav < 4;
const bool set_ptt_scn_sav_valid = !set_ptt_scn_sav_erased && set_ptt_scn_sav < 4;
#endif
gSetting_set_pwr = (((Data[7] & 0xF0) >> 4) < 7) ? ((Data[7] & 0xF0) >> 4) : 0;
gSetting_set_ptt = set_ptt_scn_sav_valid ? (set_ptt_scn_sav & 0x01) : 0;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
gSetting_set_scn = set_ptt_scn_sav_valid ? ((set_ptt_scn_sav & 0x02) == 0) : 1;
gSetting_set_scn = set_ptt_scn_sav_valid ? ((set_ptt_scn_sav & 0x02) == 0) : 0;
#endif
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
gSetting_set_sav = set_ptt_scn_sav_valid ? ((set_ptt_scn_sav >> 2) & 0x03) : SET_SAV_OFF;
@@ -476,7 +477,6 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
int tmp = ((Data[5] & 0xF0) >> 4);
gSetting_set_inv = (((tmp >> 0) & 0x01) < 2) ? ((tmp >> 0) & 0x01): 0;
gSetting_set_lck = (((tmp >> 1) & 0x01) < 2) ? ((tmp >> 1) & 0x01): 0;
gSetting_set_met = (((tmp >> 2) & 0x01) < 2) ? ((tmp >> 2) & 0x01): 0;
gSetting_set_gui = (((tmp >> 3) & 0x01) < 2) ? ((tmp >> 3) & 0x01): 0;
gSetting_set_ctr = (((Data[5] & 0x0F)) > 00 && ((Data[5] & 0x0F)) < 16) ? ((Data[5] & 0x0F)) : 10;
@@ -491,7 +491,7 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
#else
gSetting_set_inv = 0;
#endif
gSetting_set_lck = (tmp >> 1) & 0x01;
gSetting_set_lck = (Data[2] < SET_LCK_LEN) ? Data[2] : SET_LCK_KEYS;
gSetting_set_met = (tmp >> 2) & 0x01;
gSetting_set_gui = (tmp >> 3) & 0x01;
@@ -509,7 +509,6 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
// And set special session settings for actions
gSetting_set_ptt_session = gSetting_set_ptt;
gEeprom.KEY_LOCK_PTT = gSetting_set_lck;
#endif
}
@@ -1118,8 +1117,6 @@ void SETTINGS_SaveSettings(void)
if(gSetting_set_inv == 1)
tmp = tmp | (1 << 0);
if (gSetting_set_lck == 1)
tmp = tmp | (1 << 1);
if (gSetting_set_met == 1)
tmp = tmp | (1 << 2);
if (gSetting_set_gui == 1)
@@ -1133,10 +1130,10 @@ void SETTINGS_SaveSettings(void)
#endif
tmp = (gSetting_set_inv << 0) |
(gSetting_set_lck << 1) |
(gSetting_set_met << 2) |
(gSetting_set_gui << 3);
State[2] = gSetting_set_lck;
State[5] = ((tmp << 4) | (gSetting_set_ctr & 0x0F));
State[6] = ((gSetting_set_tot << 4) | (gSetting_set_eot & 0x0F));
uint8_t set_ptt_scn_sav = gSetting_set_ptt & 0x01;
@@ -1150,8 +1147,6 @@ void SETTINGS_SaveSettings(void)
State[7] = ((gSetting_set_pwr << 4) | set_ptt_scn_sav);
gEeprom.KEY_LOCK_PTT = gSetting_set_lck;
PY25Q16_WriteBuffer(0x00A158, SecBuf, 8, false);
#endif
+3 -5
View File
@@ -127,12 +127,11 @@ enum ACTION_OPT_t {
ACTION_OPT_REMOVE_OFFSET,
#endif
#endif
#ifdef ENABLE_REGA
ACTION_OPT_REGA_ALARM,
ACTION_OPT_REGA_TEST,
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
ACTION_OPT_BEAM,
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
ACTION_OPT_RXTX_LOG,
#endif
ACTION_OPT_LEN
};
@@ -200,7 +199,6 @@ typedef struct {
uint8_t TX_TIMEOUT_TIMER;
bool KEY_LOCK;
#ifdef ENABLE_FEAT_F4HWN
bool KEY_LOCK_PTT;
bool SET_NAV;
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
+17
View File
@@ -325,6 +325,23 @@ static void sort(int16_t *a, int16_t *b)
}
}
void GUI_DisplaySmallestInverse(const char *pString, uint8_t x, uint8_t Line,
bool statusbar, bool fill, uint8_t end)
{
// First draw the string normally
GUI_DisplaySmallest(pString, x, (Line * 8) + 1, statusbar, fill);
// Now invert the framebuffer/statusline bits for the rendered area
uint8_t start = (x - 2);
uint8_t *buffer = statusbar ? gStatusLine : gFrameBuffer[Line];
buffer[start] ^= 0x3E;
for (uint8_t i = start + 1; i < end; i++) {
buffer[i] ^= 0x7F;
}
buffer[end] ^= 0x3E;
}
void UI_DisplayUnlockKeyboard(uint8_t shift) {
if (gEeprom.KEY_LOCK && gKeypadLocked > 0)
{ // tell user how to unlock the keyboard
+1
View File
@@ -38,6 +38,7 @@ void UI_DrawPixelBuffer(uint8_t (*buffer)[128], uint8_t x, uint8_t y, bool black
void PutPixel(uint8_t x, uint8_t y, bool fill);
void PutPixelStatus(uint8_t x, uint8_t y, bool fill);
void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y, bool statusbar, bool fill);
void GUI_DisplaySmallestInverse(const char *pString, uint8_t x, uint8_t Line, bool statusbar, bool fill, uint8_t endX);
void UI_DisplayUnlockKeyboard(uint8_t shift);
bool IsEmptyName(const char *name, uint8_t len);
#endif
+111 -31
View File
@@ -140,6 +140,71 @@ const char *VfoStateStr[] = {
[VFO_STATE_VOLTAGE_HIGH]="VOLT HIGH"
};
#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI)
static uint8_t UI_MAIN_GetScanRssiSparklineMask(uint8_t previousLevel, uint8_t level)
{
uint8_t mask = 0;
const uint8_t bottom = 5;
if (level == 0)
return (uint8_t)(1u << bottom);
const uint8_t top = bottom + 1 - level;
uint8_t bridgeTop = top;
uint8_t bridgeBottom = top;
if (previousLevel > 0)
{
const uint8_t previousTop = bottom + 1 - previousLevel;
if (previousTop < bridgeTop)
bridgeTop = previousTop;
else if (previousTop > bridgeBottom)
bridgeBottom = previousTop;
}
// Solid crest line with a small vertical bridge to avoid broken diagonals.
for (uint8_t y = bridgeTop; y <= bridgeBottom; y++)
mask |= (uint8_t)(1u << y);
return mask;
}
static void UI_MAIN_DrawScanRssiSparkline(uint8_t line)
{
uint8_t *p_line = gFrameBuffer[line];
const uint8_t x0 = 7;
uint8_t level[CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
if (!CHFRSCANNER_HasScanRssiSparkline())
return;
for (uint8_t i = 0; i < CHFRSCANNER_RSSI_SPARKLINE_WIDTH; i++)
level[i] = CHFRSCANNER_GetScanRssiSparklineLevel(i);
for (uint8_t i = 0; i < CHFRSCANNER_RSSI_SPARKLINE_WIDTH; i++)
{
const uint8_t previousRaw = (i > 0) ? level[i - 1] : 0;
const uint8_t nextRaw = (i + 1 < CHFRSCANNER_RSSI_SPARKLINE_WIDTH) ? level[i + 1] : 0;
uint8_t displayLevel = (uint8_t)((previousRaw + 2u * level[i] + nextRaw + 2u) >> 2);
uint8_t previousLevel = previousRaw;
if (level[i] == 0 && previousRaw == 0 && nextRaw == 0)
displayLevel = 0;
else if (displayLevel == 0)
displayLevel = 1;
if (i > 0)
{
const uint8_t previousPreviousRaw = (i > 1) ? level[i - 2] : 0;
previousLevel = (uint8_t)((previousPreviousRaw + 2u * previousRaw + level[i] + 2u) >> 2);
}
const uint8_t mask = UI_MAIN_GetScanRssiSparklineMask(previousLevel, displayLevel);
p_line[x0 + i] = (p_line[x0 + i] & 0x80) | mask;
}
}
#endif
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
static void ScanProgress_ResetSession(void)
{
@@ -287,8 +352,7 @@ static void ScanProgress_DrawGaugeLine(uint8_t line, uint32_t current_index, uin
current_index = total;
head_col = (total <= 1) ? (fill_cols - 1) : ((current_index - 1) * (fill_cols - 1)) / (total - 1);
if (head_col >= fill_cols)
head_col = fill_cols - 1;
head_col = MIN(head_col, fill_cols - 1);
gFrameBuffer[line][gauge_left] = 0x0c;
gFrameBuffer[line][gauge_left + 1] = 0x12;
@@ -1169,6 +1233,26 @@ static void UI_FormatFrequency(uint32_t freq, char *buffer) {
sprintf(buffer, "%3u.%05u", freq / 100000, freq % 100000);
}
#if defined(ENABLE_SCAN_RANGES) && defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
static void UI_PrintScanRangeCss(char *String, uint8_t LabelX, uint8_t ValueX, uint8_t Line)
{
if (gScanRangeCssType == CODE_TYPE_CONTINUOUS_TONE)
{
strcpy(String, "CTCSS");
UI_PrintStringSmallNormalInverse(String, LabelX, 0, Line);
sprintf(String, "%u.%uHz", CTCSS_Options[gScanRangeCssCode] / 10, CTCSS_Options[gScanRangeCssCode] % 10);
}
else
{
strcpy(String, "DCS");
UI_PrintStringSmallNormalInverse(String, LabelX, 0, Line);
sprintf(String, "D%03o%c", DCS_Options[gScanRangeCssCode], gScanRangeCssType == CODE_TYPE_REVERSE_DIGITAL ? 'I' : 'N');
}
UI_PrintStringSmallNormal(String, ValueX, 0, Line);
}
#endif
void UI_DisplayMain(void)
{
char String[22];
@@ -1267,15 +1351,10 @@ void UI_DisplayMain(void)
UI_FormatFrequency(gScanRangeStop, String);
UI_PrintStringSmallNormal(String, 56, 0, line + shift + 1);
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
if (!isMainOnly() && gScanRangeCssCode != 0xFF)
{
if (gScanRangeCssType == CODE_TYPE_CONTINUOUS_TONE)
sprintf(String, "CTCSS %u.%uHz", CTCSS_Options[gScanRangeCssCode] / 10, CTCSS_Options[gScanRangeCssCode] % 10);
else
sprintf(String, "DCS D%03o%c", DCS_Options[gScanRangeCssCode],
gScanRangeCssType == CODE_TYPE_REVERSE_DIGITAL ? 'I' : 'N');
UI_PrintStringSmallNormal(String, 6, 0, line + 2);
}
UI_PrintScanRangeCss(String, 6, 48, line + 2);
#endif
if (!isMainOnly())
continue;
@@ -1291,15 +1370,10 @@ void UI_DisplayMain(void)
UI_FormatFrequency(gScanRangeStop, String);
UI_PrintStringSmallNormal(String, 56, 0, line + 1);
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
if (gScanRangeCssCode != 0xFF)
{
if (gScanRangeCssType == CODE_TYPE_CONTINUOUS_TONE)
sprintf(String, "CTCSS:%u.%uHz", CTCSS_Options[gScanRangeCssCode] / 10, CTCSS_Options[gScanRangeCssCode] % 10);
else
sprintf(String, "DCS:D%03o%c", DCS_Options[gScanRangeCssCode],
gScanRangeCssType == CODE_TYPE_REVERSE_DIGITAL ? 'I' : 'N');
UI_PrintStringSmallNormal(String, 2, 0, line + 2);
}
UI_PrintScanRangeCss(String, 2, 44, line + 2);
#endif
continue;
#endif
@@ -1464,10 +1538,15 @@ void UI_DisplayMain(void)
#endif
}
if(TX_freq_check(frequency) != 0 && gEeprom.VfoInfo[vfo_num].TX_LOCK == true)
#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI)
if (vfo_num == gEeprom.RX_VFO && gScanStateDir != SCAN_OFF && !FUNCTION_IsRx())
UI_MAIN_DrawScanRssiSparkline(line);
#endif
if((gScanStateDir == SCAN_OFF || vfo_num != gEeprom.RX_VFO) && TX_freq_check(frequency) != 0 && gEeprom.VfoInfo[vfo_num].TX_LOCK == true)
{
if (!FUNCTION_IsRx() || RxOnVfofrequency != frequency)
memcpy(p_line0 + 25, BITMAP_VFO_Lock, sizeof(BITMAP_VFO_Lock));
memcpy(p_line0 + 24, BITMAP_VFO_Lock, sizeof(BITMAP_VFO_Lock));
}
if (IS_MR_CHANNEL(gEeprom.ScreenChannel[vfo_num]))
@@ -1634,6 +1713,9 @@ void UI_DisplayMain(void)
xStart = 117;
}
#ifdef ENABLE_FEAT_F4HWN
GUI_DisplaySmallestInverse(displayStr, xStart + 2, line, false, true, 127);
#else
GUI_DisplaySmallest(displayStr, xStart + 2, line == 0 ? 1 : 33, false, true);
gFrameBuffer[line][xStart] ^= 0x3E;
@@ -1641,6 +1723,7 @@ void UI_DisplayMain(void)
gFrameBuffer[line][x] ^= 0x7F;
}
gFrameBuffer[line][127] ^= 0x3E;
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
{
@@ -2120,16 +2203,9 @@ void UI_DisplayMain(void)
UI_MAIN_PrintAGC(false);
#endif
#if defined(ENABLE_SCAN_RANGES) && defined(ENABLE_FEAT_F4HWN)
#if defined(ENABLE_SCAN_RANGES) && defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
if (isMainOnly() && gScanRangeStart && gScanRangeCssCode != 0xFF)
{
if (gScanRangeCssType == CODE_TYPE_CONTINUOUS_TONE)
sprintf(String, "CTCSS %u.%uHz", CTCSS_Options[gScanRangeCssCode] / 10, CTCSS_Options[gScanRangeCssCode] % 10);
else
sprintf(String, "DCS D%03o%c", DCS_Options[gScanRangeCssCode],
gScanRangeCssType == CODE_TYPE_REVERSE_DIGITAL ? 'I' : 'N');
UI_PrintStringSmallNormal(String, 2, 0, 6);
}
UI_PrintScanRangeCss(String, 2, 46, 6);
#endif
if (center_line == CENTER_LINE_NONE)
@@ -2185,7 +2261,6 @@ void UI_DisplayMain(void)
else
#endif
#ifdef ENABLE_RSSI_BAR
if (rx) {
center_line = CENTER_LINE_RSSI;
@@ -2271,13 +2346,18 @@ void UI_DisplayMain(void)
if (isMainOnly() && !gDTMF_InputMode)
{
sprintf(String, "VFO %s", activeTxVFO ? "B" : "A");
GUI_DisplaySmallest(String, 107, 50, false, true);
#ifdef ENABLE_FEAT_F4HWN
GUI_DisplaySmallestInverse(String, 107, 6, false, true, 127);
#else
GUI_DisplaySmallest(String, 107, 49, false, true);
gFrameBuffer[6][105] ^= 0x7C;
for (uint8_t x = 106; x < 127; x++) {
gFrameBuffer[6][x] ^= 0xFE;
}
gFrameBuffer[6][127] ^= 0x7C;
#endif
/*
UI_PrintStringSmallBold(String, 92, 0, 6);
+6 -5
View File
@@ -421,7 +421,9 @@ const char* const gSubMenu_SCRAMBLER[] =
const char* const gSubMenu_SET_LCK[] =
{
"KEYS",
"KEYS+PTT"
"KEYS\nACTIONS",
"KEYS\nPTT",
"KEYS\nACTIONS\nPTT"
};
const char* const gSubMenu_SET_MET[] =
@@ -496,10 +498,6 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] =
#endif
#ifdef ENABLE_TX1750
{"1750Hz", ACTION_OPT_1750},
#endif
#ifdef ENABLE_REGA
{"REGA\nALARM", ACTION_OPT_REGA_ALARM},
{"REGA\nTEST", ACTION_OPT_REGA_TEST},
#endif
{"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK},
{"VFO A\nVFO B", ACTION_OPT_A_B},
@@ -524,6 +522,9 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] =
#ifdef ENABLE_FEAT_F4HWN_BEAM
{"BEAM", ACTION_OPT_BEAM},
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
{"RF LOG", ACTION_OPT_RXTX_LOG},
#endif
#endif
};
+1 -1
View File
@@ -195,7 +195,7 @@ extern const char* const gSubMenu_D_RSP[4];
extern const char* const gSubMenu_SET_PWR[7];
extern const char* const gSubMenu_SET_PTT[2];
extern const char* const gSubMenu_SET_TOT[4];
extern const char* const gSubMenu_SET_LCK[2];
extern const char* const gSubMenu_SET_LCK[];
extern const char* const gSubMenu_SET_MET[2];
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
extern const char* const gSubMenu_SET_SCN[2];
+122 -81
View File
@@ -18,6 +18,9 @@
#include "app/app.h"
#include "app/chFrScanner.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
@@ -44,8 +47,6 @@ static void convertTime(uint8_t *line, uint8_t type)
uint8_t m = t / 60;
uint8_t s = t - (m * 60); // Replace modulo with subtraction for efficiency
gStatusLine[0] = gStatusLine[7] = gStatusLine[14] = 0x00; // Quick fix on display (on scanning I, II, etc.)
char str[6];
sprintf(str, "%02u:%02u", m, s);
UI_PrintStringSmallBufferNormal(str, line);
@@ -64,28 +65,58 @@ void UI_DisplayStatus()
uint8_t *line = gStatusLine;
unsigned int x = 0;
unsigned int x1 = 0;
char str[8] = "";
#ifdef ENABLE_NOAA
// NOAA indicator
if (!(gScanStateDir != SCAN_OFF || SCANNER_IsScanning()) && gIsNoaaMode) { // NOASS SCAN indicator
memcpy(line + x, BITMAP_NOAA, sizeof(BITMAP_NOAA));
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
// The filter label reuses the leftmost slot (pixels 2..16) normally
// reserved by the power-save and scan indicators; those two are skipped
// on the log screen so the rest of the bar keeps its usual layout.
const bool isRxTxLogScreen = gScreenToDisplay == DISPLAY_RXTX_LOG;
if (isRxTxLogScreen) {
const char *filter = RXTX_LOG_GetFilterName();
const uint8_t end = (filter[2] == 0) ? 10 : 14;
GUI_DisplaySmallestInverse(filter, 2, 0, true, true, end);
}
// Power Save indicator
else if (gCurrentFunction == FUNCTION_POWER_SAVE) {
memcpy(line + x, gFontPowerSave, sizeof(gFontPowerSave));
}
x += 8;
#else
// Power Save indicator
if (gCurrentFunction == FUNCTION_POWER_SAVE) {
memcpy(line + x, gFontPowerSave, sizeof(gFontPowerSave));
}
x += 8;
#endif
unsigned int x1 = x;
#if defined(ENABLE_FEAT_F4HWN_RX_TX_TIMER) && !defined(ENABLE_FEAT_F4HWN_DEBUG)
bool isTransmit = gCurrentFunction == FUNCTION_TRANSMIT;
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
if (!isRxTxLogScreen && gSetting_set_tmr && (isTransmit || FUNCTION_IsRx())) {
#else
if (gSetting_set_tmr && (isTransmit || FUNCTION_IsRx())) {
#endif
convertTime(line, !isTransmit);
x += 39;
} else {
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
if (!isRxTxLogScreen)
#endif
{
#ifdef ENABLE_NOAA
// NOAA indicator
if (!(gScanStateDir != SCAN_OFF || SCANNER_IsScanning()) && gIsNoaaMode) { // NOASS SCAN indicator
memcpy(line + x, BITMAP_NOAA, sizeof(BITMAP_NOAA));
}
// Power Save indicator
else if (gCurrentFunction == FUNCTION_POWER_SAVE) {
memcpy(line + x, gFontPowerSave, sizeof(gFontPowerSave));
}
#else
// Power Save indicator
if (gCurrentFunction == FUNCTION_POWER_SAVE) {
memcpy(line + x, gFontPowerSave, sizeof(gFontPowerSave));
}
#endif
}
x += 8;
x1 = x;
#ifdef ENABLE_DTMF_CALLING
if (gSetting_KILLED) {
@@ -95,9 +126,12 @@ void UI_DisplayStatus()
else
#endif
{ // SCAN indicator
if (gScanStateDir != SCAN_OFF || SCANNER_IsScanning()) {
if ((gScanStateDir != SCAN_OFF || SCANNER_IsScanning())
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
&& !isRxTxLogScreen
#endif
) {
if (IS_MR_CHANNEL(gNextMrChannel) && !SCANNER_IsScanning()) { // channel mode
uint8_t end = 0;
if(gEeprom.SCAN_LIST_DEFAULT == MR_CHANNELS_LIST + 1)
@@ -124,6 +158,9 @@ void UI_DisplayStatus()
end += 4;
}
#ifdef ENABLE_FEAT_F4HWN
GUI_DisplaySmallestInverse(str, 2, 0, true, true, end);
#else
GUI_DisplaySmallest(str, 2, 1, true, true);
gStatusLine[0] ^= 0x3E;
@@ -132,6 +169,7 @@ void UI_DisplayStatus()
gStatusLine[x] ^= 0x7F;
}
gStatusLine[end] ^= 0x3E;
#endif
}
else { // frequency mode
memcpy(line + x + 1, gFontS, sizeof(gFontS));
@@ -161,54 +199,49 @@ void UI_DisplayStatus()
#endif
if(!SCANNER_IsScanning()) {
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
bool isTransmit = gCurrentFunction == FUNCTION_TRANSMIT;
if (gSetting_set_tmr && (isTransmit || FUNCTION_IsRx())) {
convertTime(line, !isTransmit);
}
else
#endif
{
if(!gAirCopyBootMode) {
const void *src = NULL; // Pointer to the font/bitmap to copy
size_t sSize = 0; // Size of the font/bitmap
uint8_t sOff = 2; // Offset relative to the reference position
if(!gAirCopyBootMode) {
const void *src = NULL; // Pointer to the font/bitmap to copy
size_t sSize = 0; // Size of the font/bitmap
uint8_t sOff = 2; // Offset relative to the reference position
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (gEeprom.MENU_LOCK) {
src = gFontRO;
sSize = sizeof(gFontRO);
} else
#endif
{
uint8_t xb = (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF);
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (gEeprom.MENU_LOCK) {
src = gFontRO;
sSize = sizeof(gFontRO);
} else
#endif
{
uint8_t xb = (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF);
if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) {
if (gDualWatchActive) { // DWR - dual watch + respond
src = gFontDWR;
sOff = xb ? 2 : 0;
sSize = sizeof(gFontDWR) - (xb ? 5 : 0);
} else {
src = gFontHold;
sOff = 3;
sSize = sizeof(gFontHold);
}
if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) {
if (gDualWatchActive) { // DWR - dual watch + respond
src = gFontDWR;
sOff = xb ? 2 : 0;
sSize = sizeof(gFontDWR) - (xb ? 5 : 0);
} else {
src = xb ? gFontXB : gFontMO; // XB - crossband
sSize = xb ? sizeof(gFontXB) : sizeof(gFontMO); // MO - main only
src = gFontHold;
sOff = 3;
sSize = sizeof(gFontHold);
}
} else {
src = xb ? gFontXB : gFontMO; // XB - crossband
sSize = xb ? sizeof(gFontXB) : sizeof(gFontMO); // MO - main only
}
}
// Perform the memcpy if a source was selected
if (src) {
memcpy(line + x + sOff, src, sSize);
}
// Perform the memcpy if a source was selected
if (src) {
memcpy(line + x + sOff, src, sSize);
}
}
}
x += sizeof(gFontDWR) + 3;
#endif
#if defined(ENABLE_FEAT_F4HWN_RX_TX_TIMER) && !defined(ENABLE_FEAT_F4HWN_DEBUG)
}
#endif
#ifdef ENABLE_VOX
// VOX indicator
if (gEeprom.VOX_SWITCH) {
@@ -255,8 +288,19 @@ void UI_DisplayStatus()
}
#endif
else if (gBackLight) {
src = gFontLight;
size = sizeof(gFontLight);
// Distinguish the manual backlight sub-state: BACKLIGHT_TIME == 0
// means the light is currently off (hollow bulb), otherwise it is
// forced on (bulb with filament).
if (gEeprom.BACKLIGHT_TIME == 0) {
src = gFontLightOff;
size = sizeof(gFontLightOff);
}
else
{
src = gFontLight;
size = sizeof(gFontLight);
}
}
#ifdef ENABLE_FEAT_F4HWN_CHARGING_C
else if (gChargingWithTypeC) {
@@ -275,38 +319,35 @@ void UI_DisplayStatus()
UI_DrawBattery(line + x2, gBatteryDisplayLevel, gLowBatteryBlink);
bool BatTxt = true;
switch (gSetting_battery_text) {
default:
case 0:
BatTxt = false;
break;
case 1: // voltage
const uint16_t voltage = (gBatteryVoltageAverage <= 999) ? gBatteryVoltageAverage : 999; // limit to 9.99V
sprintf(str, "%u.%02u", voltage / 100, voltage % 100);
break;
case 1: // voltage
case 2: // percentage
//gBatteryVoltageAverage = 999;
sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage));
if (gSetting_battery_text == 1) {
const uint16_t voltage = MIN(gBatteryVoltageAverage, 999); // limit to 9.99V
sprintf(str, "%u.%02u", voltage / 100, voltage % 100);
} else {
//gBatteryVoltageAverage = 999;
sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage));
}
x2 -= (7 * strlen(str));
UI_PrintStringSmallBufferNormal(str, line + x2);
/*
uint8_t shift = (strlen(str) < 5) ? 92 : 88;
GUI_DisplaySmallest(str, shift, 1, true, true);
for (uint8_t i = shift - 2; i < 110; i++) {
gStatusLine[i] ^= 0x7F; // invert
}
*/
break;
}
if (BatTxt) {
x2 -= (7 * strlen(str));
UI_PrintStringSmallBufferNormal(str, line + x2);
/*
uint8_t shift = (strlen(str) < 5) ? 92 : 88;
GUI_DisplaySmallest(str, shift, 1, true, true);
for (uint8_t i = shift - 2; i < 110; i++) {
gStatusLine[i] ^= 0x7F; // invert
}
*/
}
// **************
ST7565_BlitStatusLine();
+4 -4
View File
@@ -30,8 +30,8 @@
#ifdef ENABLE_FMRADIO
#include "ui/fmradio.h"
#endif
#ifdef ENABLE_REGA
#include "app/rega.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#include "ui/inputbox.h"
#include "ui/main.h"
@@ -61,8 +61,8 @@ void (*UI_DisplayFunctions[])(void) = {
[DISPLAY_AIRCOPY] = &UI_DisplayAircopy,
#endif
#ifdef ENABLE_REGA
[DISPLAY_REGA] = &UI_DisplayREGA,
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
[DISPLAY_RXTX_LOG] = &UI_DisplayRxTxLog,
#endif
};
+2 -2
View File
@@ -34,8 +34,8 @@ enum GUI_DisplayType_t
DISPLAY_AIRCOPY,
#endif
#ifdef ENABLE_REGA
DISPLAY_REGA,
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
DISPLAY_RXTX_LOG,
#endif
DISPLAY_N_ELEM,
+4 -4
View File
@@ -29,8 +29,8 @@
#include "version.h"
#include "bitmaps.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_LOGO
@@ -355,7 +355,7 @@ void UI_DisplayWelcome(void)
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(true);
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(true);
#endif
}
+18 -9
View File
@@ -44,11 +44,10 @@
"ENABLE_BYP_RAW_DEMODULATORS": false,
"ENABLE_BLMIN_TMP_OFF": false,
"ENABLE_SCAN_RANGES": true,
"ENABLE_REGA": false,
"ENABLE_EXTRA_UART_CMD": false,
"ENABLE_FEAT_F4HWN": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_SCREENSHOT": false,
"ENABLE_FEAT_F4HWN_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_SPECTRUM": true,
"ENABLE_FEAT_F4HWN_RX_TX_TIMER": true,
"ENABLE_FEAT_F4HWN_CHARGING_C": false,
@@ -70,14 +69,19 @@
"ENABLE_FEAT_F4HWN_MEM": false,
"ENABLE_FEAT_F4HWN_SCAN_PROGRESS": false,
"ENABLE_FEAT_F4HWN_SCAN_FASTER": false,
"ENABLE_FEAT_F4HWN_SCAN_RSSI": false,
"ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false,
"ENABLE_FEAT_F4HWN_BEAM": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_LOGO": false,
"ENABLE_FEAT_F4HWN_LOGO_SAV": false,
"ENABLE_AGC_SHOW_DATA": false,
"ENABLE_UART_RW_BK_REGS": false,
"ENABLE_SWD": false,
"VERSION_STRING_1": "v0.22",
"VERSION_STRING_2": "v5.5.0"
"VERSION_STRING_2": "v5.7.0"
}
},
{
@@ -96,7 +100,7 @@
"ENABLE_FMRADIO": false,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_SCREENSHOT": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
@@ -113,7 +117,7 @@
"ENABLE_FMRADIO": true,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_SCREENSHOT": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
@@ -130,7 +134,7 @@
"ENABLE_FMRADIO": true,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": false,
"ENABLE_FEAT_F4HWN_SCREENSHOT": false,
"ENABLE_FEAT_F4HWN_K5VIEWER": false,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_SPECTRUM": false,
"ENABLE_FEAT_F4HWN_PMR": true,
@@ -154,7 +158,7 @@
"ENABLE_FMRADIO": false,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_SCREENSHOT": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": false,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
@@ -172,7 +176,7 @@
"ENABLE_FMRADIO": true,
"ENABLE_VOX": false,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_SCREENSHOT": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": true,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
@@ -189,7 +193,7 @@
"ENABLE_FMRADIO": true,
"ENABLE_VOX": true,
"ENABLE_AIRCOPY": true,
"ENABLE_FEAT_F4HWN_SCREENSHOT": true,
"ENABLE_FEAT_F4HWN_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_GAME": true,
"ENABLE_FEAT_F4HWN_PMR": true,
"ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true,
@@ -199,7 +203,12 @@
"ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true,
"ENABLE_FEAT_F4HWN_SCAN_PROGRESS": true,
"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_RXTX_LOG": true,
"ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false,
"ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true,
"ENABLE_FEAT_F4HWN_QRCODE": true,
"ENABLE_FEAT_F4HWN_MEM": true,
"ENABLE_FEAT_F4HWN_LOGO": true,
+77
View File
@@ -0,0 +1,77 @@
F4HWN Edition Fusion firmware for Quansheng UV-K1 and UV-K5 V3
================================================================
This product includes software originally developed by Dual Tachyon
for the Quansheng UV-K5 open-source firmware project:
https://github.com/DualTachyon/uv-k5-firmware
This product includes work derived from Egzumer's UV-K5 custom
firmware:
https://github.com/egzumer/uv-k5-firmware-custom
This product includes work developed by Armel / F4HWN and contributors
for the F4HWN firmware projects, including the F4HWN Edition Fusion
port for UV-K1 and UV-K5 V3 radios:
https://github.com/armel/uv-k5-firmware-custom
https://github.com/armel/uv-k1-k5v3-firmware-custom
The UV-K1 and UV-K5 V3 port is the result of joint work by muzkr and
Armel / F4HWN.
Additional attribution notices
------------------------------
This product includes contributions and/or derived work from, among
others:
Dual Tachyon
OneOfEleven
fagci
Egzumer
muzkr
Armel / F4HWN
Project-specific derived components
-----------------------------------
The spectrum analyzer implementation includes work originally developed
by fagci:
https://github.com/fagci
Third-party components
----------------------
This product includes CherryUSB by sakumisu, licensed under the
Apache License, Version 2.0. Its license is included at:
Middlewares/CherryUSB/LICENSE
This product includes CMSIS Version 5 by Arm, licensed under the
Apache License, Version 2.0. Its license is included at:
App/external/CMSIS_5/LICENSE.txt
This product includes the embedded printf implementation by Marco
Paland, licensed under the MIT License. Its license is included at:
App/external/printf/LICENSE
License and endorsement notice
------------------------------
This NOTICE file is provided for attribution purposes only and does not
modify the terms of any applicable license.
The main firmware is licensed under the Apache License, Version 2.0.
Redistributions of this product or derivative works, in source or object
form, must comply with the Apache License, Version 2.0, including its
conditions on license copies, modified-file notices, retained copyright
and attribution notices, and NOTICE files.
Attribution in this file does not imply endorsement by Dual Tachyon,
OneOfEleven, fagci, Egzumer, muzkr, Armel / F4HWN or any other
contributor.
+1 -1
View File
@@ -42,7 +42,7 @@ Anyway, have fun.
# Donations
Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN and Marcin Kusaj for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻
Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C / F4WFS (3 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN, Marcin Kusaj, Flavio Cottarelli, Bob N1MLZ and Carlos EA1IJ for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻
## Table of Contents
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
-88
View File
@@ -1,88 +0,0 @@
# Quansheng K5Viewer
K5Viewer is a lightweight viewer that displays the live screen output of the Quansheng K5 transceiver (running F4HWN firmware), transmitted via UART to your computer through a Baofeng/Kenwood-style USB-to-Serial cable.
https://github.com/user-attachments/assets/578c8605-9820-4a2c-90e4-5a93b2be7d2b
> [!WARNING]
> K5Viewer IS NOT A REMOTE CONTROL TOOL.
It simply displays the live screen of the Quansheng K5 on your computer and allows you to take screenshots.
It can be useful for illustrating documentation, creating tutorials, or demonstrating how K5Viewer works — whether by screen sharing or using a video projector.
It's also a handy tool for development and debugging, especially for display-related features.
## 🚀 Features
- Realtime display of 128×64 monochrome screen via serial connection (UART)
- Delta frame updates to minimize bandwidth usage
- Capture screen snapshots in PNG format
- Switch background color (gray, blue, or orange)
- Toggle inverted video mode
- Toggle LCD pixel rendering mode
- Resize the window (zoom in/out)
- Display current FPS in the window title
## 🛠️ Requirements
- Python **3.6+**
- pip (Python package installer)
### 📦 Install dependencies:
If necessary, use this command to install dependencies:
```bash
pip install pyserial pygame
```
## ▶️ How to Run
1. Connect your **Quansheng K5** (running **F4HWN firmware** v4.2) to your computer using a Baofeng/Kenwood-style USB-to-Serial cable.
2. Run the `k5viewer` with the appropriate serial port:
```bash
./k5viewer.py -port /dev/ttyUSB0 # Linux
./k5viewer.py -port /dev/cu.usbserial-xxxx # macOS
./k5viewer.py -port COM3 # Windows
```
> [!NOTE]
> If no -port is provided, the script defaults to /dev/ttyUSB0.
Alternatively, you can manually edit the script and change the `DEFAULT_PORT` variable near the top of the file:
```python
DEFAULT_PORT = '/dev/ttyUSB0' # Linux
# or
DEFAULT_PORT = '/dev/cu.usbserial-xxxx' # macOS
# or
DEFAULT_PORT = 'COM3' # Windows
```
You can also list available serial ports to help you choose:
```bash
./k5viewer.py --list-ports
```
## 🎮 Controls
| Key | Action |
|-----------|---------------------------------|
| `Q` | Quit the viewer |
| `G` | Set background to **gray** |
| `B` | Set background to **blue** |
| `O` | Set background to **orange** |
| `W` | Set background to **white** |
| `I` | Toggle **video inversion** |
| `P` | Toggle **LCD pixel mode** |
| `SPACE` | Save a screenshot as PNG |
| `UP` | Increase window size |
| `DOWN` | Decrease window size |
Screenshots are saved as `screenshot_YYYYMMDD_HHMMSS.png` in the same directory.
## 📬 Contact
If you encounter issues or have suggestions, feel free to open an issue or submit a pull request. Enjoy building with your Quansheng K5! 📡
-228
View File
@@ -1,228 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import time
import datetime
import argparse
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
import pygame
import serial
from serial.tools import list_ports
# Version
VERSION = '1.0'
# Serial configuration
DEFAULT_PORT = '/dev/ttyUSB0' # Change if needed (/dev/cu.usbserial-11130)
BAUDRATE = 38400
TIMEOUT = 0.5
# Screen configuration
WIDTH, HEIGHT = 128, 64
FRAME_SIZE = 1024
# Protocol
HEADER = b'\xAA\x55'
TYPE_SCREENSHOT = b'\x01'
TYPE_DIFF = b'\x02'
# Framebuffer
framebuffer = bytearray([0] * FRAME_SIZE)
COLOR_SETS = { # {key: (name, foreground, background)}
"g": ("Grey", pygame.Color(0, 0, 0), pygame.Color(202, 202, 202)),
"o": ("Orange", pygame.Color(0, 0, 0), pygame.Color(255, 193, 37)),
"b": ("Blue", pygame.Color(0, 0, 0), pygame.Color(28, 134, 228)),
"w": ("White", pygame.Color(0, 0, 0), pygame.Color(255, 255, 255)),
}
DEFAULT_COLOR = "g" # Must be a key of "COLOR_SETS"
def send_keepalive(ser: serial.Serial):
# Send keepalive frame
try:
ser.write(b'\x55\xAA\x00\x00') # Keepalive frame
except serial.SerialException:
pass
def read_frame(ser: serial.Serial) -> bytearray:
global framebuffer
while True:
try:
b = ser.read(1)
except serial.SerialException as e:
#print(f"[ERROR] Serial read failed: {e}")
print("[!] Your USB serial cable is probably being used by another application such as Chirp or Chrome.")
sys.exit(1)
if not b:
return None
if b == HEADER[0:1]:
b2 = ser.read(1)
if b2 == HEADER[1:2]:
t = ser.read(1)
size_bytes = ser.read(2)
size = int.from_bytes(size_bytes, 'big')
if t == TYPE_SCREENSHOT and size == FRAME_SIZE:
payload = ser.read(FRAME_SIZE)
framebuffer = bytearray(payload)
return framebuffer
elif t == TYPE_DIFF and size % 9 == 0:
payload = ser.read(size)
framebuffer = apply_diff(framebuffer, payload)
return framebuffer
def apply_diff(framebuffer: bytearray, diff_payload: bytes) -> bytearray:
i = 0
while i + 9 <= len(diff_payload):
block_index = diff_payload[i]
i += 1
if block_index >= 128:
break
framebuffer[block_index * 8 : block_index * 8 + 8] = diff_payload[i : i + 8]
i += 8
return framebuffer
def draw_frame(screen: pygame.Surface, framebuffer: bytearray, bg_color: pygame.Color, fg_color: pygame.Color, pixel_size: int = 4, pixel_lcd: int = 0) -> pygame.Surface:
def get_bit(bit_idx):
byte_idx = bit_idx // 8
bit_pos = bit_idx % 8
if byte_idx < len(framebuffer):
return (framebuffer[byte_idx] >> bit_pos) & 0x01
return 0
screen.fill(bg_color)
bit_index = 0
for y in range(64):
for x in range(128):
if get_bit(bit_index):
px = x * (pixel_size - 1)
py = y * pixel_size
pygame.draw.rect(screen, fg_color, (px, py, pixel_size - 1 - pixel_lcd, pixel_size - pixel_lcd))
bit_index += 1
pygame.display.flip()
return pygame.display.get_surface().copy()
def run_viewer(args: argparse.Namespace, ser: serial.Serial):
pixel_size = 5
pixel_lcd = 0
pygame.init()
screen = pygame.display.set_mode((WIDTH * (pixel_size - 1), HEIGHT * pixel_size))
base_title = f"Quansheng K5Viewer v{VERSION} by F4HWN"
pygame.display.set_caption(f"{base_title} – No data")
fg_color, bg_color = COLOR_SETS[DEFAULT_COLOR][1:]
last_surface = None
frame_count = 0
frame_lost = 0
last_time = time.monotonic()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
raise KeyboardInterrupt
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
raise KeyboardInterrupt
if event.key == pygame.K_SPACE and last_surface:
filename = datetime.datetime.now().strftime("screenshot_%Y%m%d_%H%M%S.png")
pygame.image.save(last_surface, filename)
print(f"[✔] Screenshot saved: {filename}")
elif event.key == pygame.K_p:
pixel_lcd = 1 - pixel_lcd
draw_frame(screen, framebuffer, bg_color, fg_color, pixel_size, pixel_lcd)
elif event.key == pygame.K_i:
if bg_color == pygame.Color(0, 0, 0):
bg_color, fg_color = fg_color, pygame.Color(0, 0, 0)
else:
bg_color, fg_color = pygame.Color(0, 0, 0), bg_color
draw_frame(screen, framebuffer, bg_color, fg_color, pixel_size, pixel_lcd)
elif event.key == pygame.K_UP:
if pixel_size < 12:
pixel_size += 1
#print(f"[✔] Resize: {pixel_size, (WIDTH * (pixel_size - 1)), HEIGHT * pixel_size}")
screen = pygame.display.set_mode((WIDTH * (pixel_size - 1), HEIGHT * pixel_size))
draw_frame(screen, framebuffer, bg_color, fg_color, pixel_size, pixel_lcd)
elif event.key == pygame.K_DOWN:
if pixel_size > 3:
pixel_size -= 1
#print(f"[✔] Resize: {pixel_size, (WIDTH * (pixel_size - 1)), HEIGHT * pixel_size}")
screen = pygame.display.set_mode((WIDTH * (pixel_size - 1), HEIGHT * pixel_size))
draw_frame(screen, framebuffer, bg_color, fg_color, pixel_size, pixel_lcd)
pressed_key = event.unicode
if pressed_key in COLOR_SETS.keys():
fg_color, bg_color = COLOR_SETS[pressed_key][1:]
frame = read_frame(ser)
if frame:
last_surface = draw_frame(screen, framebuffer, bg_color, fg_color, pixel_size, pixel_lcd)
frame_count += 1
now = time.monotonic()
if now - last_time >= 1.0:
fps = frame_count / (now - last_time)
pygame.display.set_caption(f"{base_title} – FPS: {fps:>04.1f}")
frame_count = 0
last_time = now
frame_lost = 0
else:
frame_lost = min(frame_lost + 1, 5)
if frame_lost == 5:
pygame.display.set_caption(f"{base_title} – No data")
send_keepalive(ser)
def cmd_list_ports(args: argparse.Namespace):
ports = list_ports.comports()
print("Available ports:")
for port in ports:
if port.vid is None: # Skipping virtual or non-USB ports
continue
description = " - ".join(filter(None, (port.product, port.manufacturer)))
if description:
print(f"- {description} : {port.device}")
else:
print(f"- {port.device}")
def main():
parser = argparse.ArgumentParser(
prog="K5Viewer",
description="A live viewer for UV-K5 radios with F4HWN firmware",
epilog="F4HWN repo: https://github.com/armel/uv-k5-firmware-custom"
)
parser.add_argument("--list-ports", action="store_true", help="list available ports and exit")
parser.add_argument("--port", type=str, help="serial port to use (in place of 'DEFAULT_PORT')")
parser.add_argument("--version", action="version", version=f"%(prog)s {VERSION}", help="show program's version number and exit")
args = parser.parse_args()
if args.list_ports:
cmd_list_ports(args)
exit(0)
# Running viewer
if not args.port and not DEFAULT_PORT:
print("Please specify the serial port to use or set 'DEFAULT_PORT', do 'k5viewer.py --help' for help")
exit(1)
serial_port = args.port or DEFAULT_PORT
try:
ser = serial.Serial(serial_port, BAUDRATE, timeout=TIMEOUT)
except serial.SerialException as e:
print(f"[!] Serial error: {e}")
sys.exit(1)
try:
run_viewer(args, ser)
except KeyboardInterrupt:
print("[✔] Exiting")
ser.close()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()