Merge pull request #457 from armel/feature_update_v5

Feature update v5
This commit is contained in:
Armel FAUVEAU authored and GitHub committed 2026-07-19 01:41:58 +02:00
commit 3bd3ebba2c
46 files changed
+2091 -950

No files matched your search

+2 -1
View File
@@ -8,6 +8,7 @@ firmware
compile_commands.json
.vscode
.claude
.agents
/docs
k5_eeprom.raw
@@ -15,4 +16,4 @@ k5_eeprom.raw
__pycache__/
.DS_Store
AGENTS.md
AGENTS.md
+11 -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)
@@ -211,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;
}
+37 -18
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)
@@ -228,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
}
@@ -772,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
@@ -1115,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
@@ -1576,6 +1582,10 @@ void APP_TimeSlice10ms(void)
SETTINGS_SaveVfoIndicesFlush();
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
RXTX_LOG_Task10ms();
#endif
BACKLIGHT_Update();
gFlashLightBlinkCounter++;
@@ -1656,15 +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);
} else if (SCREENSHOT_HasPendingStateChange()) {
SCREENSHOT_Update(false);
K5VIEWER_Update(false);
} else if (K5VIEWER_HasPendingStateChange()) {
K5VIEWER_Update(false);
}
#endif
@@ -1806,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)
@@ -2294,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)
@@ -2311,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;
@@ -2325,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
}
+1 -1
View File
@@ -29,7 +29,7 @@ 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
+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
+7 -7
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
@@ -2461,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)
@@ -2532,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
}
+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
+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;
}
+5 -5
View File
@@ -14,11 +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);
bool SCREENSHOT_HasPendingStateChange(void);
void K5VIEWER_Update(bool force);
void K5VIEWER_ParseInput(void);
bool K5VIEWER_HasPendingStateChange(void);
#endif
+7
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;
+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
+7
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
@@ -1272,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)
-226
View File
@@ -1,226 +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"
#include "driver/bk4819.h"
// 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;
// FNV-1a over one 8-byte chunk, folded to 16 bits
static uint16_t SCREENSHOT_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 SCREENSHOT_ParseInput(void)
{
if (SCREENSHOT_IsLocked())
return;
if (UART_IsCableConnected()) {
keepAlive = 15;
hasConnectionPing = true;
gUSB_ScreenshotEnabled = false;
}
else if (VCP_ScreenshotPing()) {
keepAlive = 15;
hasConnectionPing = true;
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);
}
}
enum {
SCREENSHOT_CHUNK_SIZE = 8,
SCREENSHOT_CHUNKS_PER_LINE = 16,
SCREENSHOT_HALF_LINE_COLUMNS = LCD_WIDTH / 2,
SCREENSHOT_MARKER_BASE = 0xF0,
SCREENSHOT_FLAG_DEEP_SLEEP = 1 << 0,
SCREENSHOT_FLAG_LED_RED = 1 << 1,
SCREENSHOT_FLAG_LED_GREEN = 1 << 2,
};
static uint8_t SCREENSHOT_StateFlags(void)
{
uint8_t flags = 0;
#ifdef ENABLE_FEAT_F4HWN_SLEEP
if (gWakeUp)
flags |= SCREENSHOT_FLAG_DEEP_SLEEP;
#endif
if (BK4819_IsGpioOutSet(BK4819_GPIO5_PIN1_RED))
flags |= SCREENSHOT_FLAG_LED_RED;
if (BK4819_IsGpioOutSet(BK4819_GPIO6_PIN2_GREEN))
flags |= SCREENSHOT_FLAG_LED_GREEN;
return flags;
}
bool SCREENSHOT_HasPendingStateChange(void)
{
if (gUART_LockScreenshot > 0 || keepAlive == 0 || !hasConnectionPing)
return false;
return !wasConnected || SCREENSHOT_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 SCREENSHOT_Chunk(uint8_t chunkIdx, uint8_t *dest)
{
const uint8_t chunkInLine = chunkIdx % SCREENSHOT_CHUNKS_PER_LINE;
const uint8_t line = chunkIdx / SCREENSHOT_CHUNKS_PER_LINE;
const uint8_t bit = chunkInLine / 2;
const uint8_t columnBase = (chunkInLine % 2) * SCREENSHOT_HALF_LINE_COLUMNS;
const uint8_t *src = (line == 0 ? gStatusLine : gFrameBuffer[line - 1])
+ columnBase;
for (uint8_t j = 0; j < SCREENSHOT_CHUNK_SIZE; j++) {
uint8_t acc = 0;
for (uint8_t k = 0; k < SCREENSHOT_CHUNK_SIZE; k++) {
if (src[j * SCREENSHOT_CHUNK_SIZE + k] & (1 << bit)) acc |= (1 << k);
}
dest[j] = gSetting_set_inv ? ~acc : acc;
}
}
void SCREENSHOT_Update(bool force)
{
if (SCREENSHOT_IsLocked())
return;
if (keepAlive > 0) {
if (--keepAlive == 0) {
// Connection just lost → reset state for next reconnection
wasConnected = false;
hasConnectionPing = false;
previousStateFlags = 0xFF;
return;
}
} else {
return;
}
// Connection is alive — detect reconnection and force full frame
if (!wasConnected) {
force = true;
}
const uint8_t stateFlags = SCREENSHOT_StateFlags();
const bool stateChanged = (stateFlags != previousStateFlags);
// ==== 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++) {
SCREENSHOT_Chunk(chunkIdx, &chunk[1]);
bool changed = SCREENSHOT_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)
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 and state flags ====
// 0xF0 keeps a resync-safe marker before the standard AA 55 header.
uint8_t versionMarker = SCREENSHOT_MARKER_BASE | stateFlags;
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 ====
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
if (!(changedBitmap[chunkIdx >> 3] & (1 << (chunkIdx & 7))))
continue;
chunk[0] = chunkIdx;
SCREENSHOT_Chunk(chunkIdx, &chunk[1]);
SCREENSHOT_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] = SCREENSHOT_Hash(&chunk[1]);
}
uint8_t end = 0x0A;
SCREENSHOT_Send(&end, 1);
previousStateFlags = stateFlags;
wasConnected = true;
}
+2 -8
View File
@@ -477,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;
@@ -492,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;
@@ -510,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
}
@@ -1119,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)
@@ -1134,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;
@@ -1151,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
+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];
+45 -17
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
@@ -66,31 +69,52 @@ void UI_DisplayStatus()
char str[8] = "";
#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);
}
#endif
#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_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));
}
x += 8;
#else
// Power Save indicator
if (gCurrentFunction == FUNCTION_POWER_SAVE) {
memcpy(line + x, gFontPowerSave, sizeof(gFontPowerSave));
}
x += 8;
#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;
@@ -102,7 +126,11 @@ 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;
+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
}
+14 -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,
@@ -73,13 +72,16 @@
"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.6.1"
"VERSION_STRING_2": "v5.7.0"
}
},
{
@@ -98,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,
@@ -115,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,
@@ -132,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,
@@ -156,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,
@@ -174,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,
@@ -191,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,
@@ -204,6 +206,9 @@
"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,
+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 / 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 and Bob N1MLZ 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.
-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()