Merge pull request #446 from armel/feature_update_v4

🇺🇸 EN
- Optimization of the build script
- Code refactoring and memory usage improvements (thanks Reppad)
- Integration of the k5viewer feature in Bandscope, Broadcast, and RescueOps editions
- Development of the k5viewer Python script
- Development of the k5viewer Web application
- Improvement of the k5viewer Python script (thanks Michael)
- Add Narrower feature in the Basic edition

🇫🇷 FR
- Optimisation du script de compilation
- Refactorisation du code et amélioration de la gestion mémoire (merci Reppad)
- Intégration de la fonctionnalité k5viewer dans les éditions Bandscope, Broadcast et RescueOps
- Développement du script Python k5viewer
- Développement de l’application Web k5viewer
- Amélioration du script Python k5viewer (merci Michael)
- Ajout de la fonctionnalité Narrower dans l’édition Basic

Bandscope edition, 391 bytes left.
Broadcast edition, 1 311 bytes left.
RescueOps edition, 3 559 bytes left.
Basic edition, 51 bytes left.
Game edition, 599 bytes left.
This commit is contained in:
Armel FAUVEAU authored and GitHub committed 2025-08-07 04:05:48 +02:00
commit 5efea6c8b9
29 files changed
+614 -111

No files matched your search

+63 -48
View File
@@ -4,46 +4,56 @@
# 1 = enable
# ---- STOCK QUANSHENG FEATURES ----
ENABLE_FMRADIO ?= 0
ENABLE_UART ?= 1
ENABLE_AIRCOPY ?= 0
ENABLE_NOAA ?= 0
ENABLE_VOICE ?= 0
ENABLE_VOX ?= 1
ENABLE_ALARM ?= 0
ENABLE_TX1750 ?= 1
ENABLE_PWRON_PASSWORD ?= 0
ENABLE_DTMF_CALLING ?= 0
ENABLE_FLASHLIGHT ?= 1
ENABLE_FMRADIO ?= 0
ENABLE_UART ?= 1
ENABLE_AIRCOPY ?= 0
ENABLE_NOAA ?= 0
ENABLE_VOICE ?= 0
ENABLE_VOX ?= 1
ENABLE_ALARM ?= 0
ENABLE_TX1750 ?= 1
ENABLE_PWRON_PASSWORD ?= 0
ENABLE_DTMF_CALLING ?= 0
ENABLE_FLASHLIGHT ?= 1
# ---- CUSTOM MODS ----
ENABLE_SPECTRUM ?= 0
ENABLE_BIG_FREQ ?= 1
ENABLE_SMALL_BOLD ?= 1
ENABLE_CUSTOM_MENU_LAYOUT ?= 1
ENABLE_KEEP_MEM_NAME ?= 1
ENABLE_WIDE_RX ?= 1
ENABLE_TX_WHEN_AM ?= 0
ENABLE_F_CAL_MENU ?= 0
ENABLE_CTCSS_TAIL_PHASE_SHIFT ?= 0
ENABLE_BOOT_BEEPS ?= 0
ENABLE_SHOW_CHARGE_LEVEL ?= 0
ENABLE_REVERSE_BAT_SYMBOL ?= 0
ENABLE_NO_CODE_SCAN_TIMEOUT ?= 1
ENABLE_AM_FIX ?= 1
ENABLE_SQUELCH_MORE_SENSITIVE ?= 1
ENABLE_FASTER_CHANNEL_SCAN ?= 1
ENABLE_RSSI_BAR ?= 1
ENABLE_AUDIO_BAR ?= 1
ENABLE_COPY_CHAN_TO_VFO ?= 1
ENABLE_REDUCE_LOW_MID_TX_POWER ?= 0
ENABLE_BYP_RAW_DEMODULATORS ?= 0
ENABLE_BLMIN_TMP_OFF ?= 0
ENABLE_SCAN_RANGES ?= 1
ENABLE_FEAT_F4HWN ?= 1
ENABLE_FEAT_F4HWN_GAME ?= 0
ENABLE_FEAT_F4HWN_SCREENSHOT ?= 0
ENABLE_FEAT_F4HWN_SPECTRUM ?= 1
ENABLE_SPECTRUM ?= 0
ENABLE_BIG_FREQ ?= 1
ENABLE_SMALL_BOLD ?= 1
ENABLE_CUSTOM_MENU_LAYOUT ?= 1
ENABLE_KEEP_MEM_NAME ?= 1
ENABLE_WIDE_RX ?= 1
ENABLE_TX_WHEN_AM ?= 0
ENABLE_F_CAL_MENU ?= 0
ENABLE_CTCSS_TAIL_PHASE_SHIFT ?= 0
ENABLE_BOOT_BEEPS ?= 0
ENABLE_SHOW_CHARGE_LEVEL ?= 0
ENABLE_REVERSE_BAT_SYMBOL ?= 0
ENABLE_NO_CODE_SCAN_TIMEOUT ?= 1
ENABLE_AM_FIX ?= 1
ENABLE_SQUELCH_MORE_SENSITIVE ?= 1
ENABLE_FASTER_CHANNEL_SCAN ?= 1
ENABLE_RSSI_BAR ?= 1
ENABLE_AUDIO_BAR ?= 1
ENABLE_COPY_CHAN_TO_VFO ?= 1
ENABLE_REDUCE_LOW_MID_TX_POWER ?= 0
ENABLE_BYP_RAW_DEMODULATORS ?= 0
ENABLE_BLMIN_TMP_OFF ?= 0
ENABLE_SCAN_RANGES ?= 1
# ---- CONTRIB MODS ----
# Thank you @markusb
ENABLE_REGA ?= 0
# Thank you @reppad
ENABLE_EXTRA_UART_CMD ?= 0
# ---- F4HWN MODS ----
ENABLE_FEAT_F4HWN ?= 1
ENABLE_FEAT_F4HWN_GAME ?= 0
ENABLE_FEAT_F4HWN_SCREENSHOT ?= 0
ENABLE_FEAT_F4HWN_SPECTRUM ?= 1
ENABLE_FEAT_F4HWN_RX_TX_TIMER ?= 1
ENABLE_FEAT_F4HWN_CHARGING_C ?= 0
ENABLE_FEAT_F4HWN_SLEEP ?= 1
@@ -56,20 +66,19 @@ ENABLE_FEAT_F4HWN_VOL ?= 0
ENABLE_FEAT_F4HWN_RESET_CHANNEL ?= 0
ENABLE_FEAT_F4HWN_PMR ?= 0
ENABLE_FEAT_F4HWN_GMRS_FRS_MURS ?= 0
ENABLE_FEAT_F4HWN_CA ?= 1
ENABLE_FEAT_F4HWN_CA ?= 1
ENABLE_FEAT_F4HWN_DEBUG ?= 0
ENABLE_REGA ?= 0
# ---- DEBUGGING ----
ENABLE_AM_FIX_SHOW_DATA ?= 0
ENABLE_AGC_SHOW_DATA ?= 0
ENABLE_UART_RW_BK_REGS ?= 0
ENABLE_AM_FIX_SHOW_DATA ?= 0
ENABLE_AGC_SHOW_DATA ?= 0
ENABLE_UART_RW_BK_REGS ?= 0
# ---- COMPILER/LINKER OPTIONS ----
ENABLE_CLANG ?= 0
ENABLE_SWD ?= 0
ENABLE_OVERLAY ?= 0
ENABLE_LTO ?= 1
ENABLE_CLANG ?= 0
ENABLE_SWD ?= 0
ENABLE_OVERLAY ?= 0
ENABLE_LTO ?= 1
#############################################################
@@ -154,6 +163,9 @@ OBJS += app/menu.o
ifeq ($(ENABLE_SPECTRUM), 1)
OBJS += app/spectrum.o
endif
ifeq ($(ENABLE_FEAT_F4HWN_SCREENSHOT), 1)
OBJS += screenshot.o
endif
ifeq ($(ENABLE_FEAT_F4HWN_GAME), 1)
OBJS += app/breakout.o
endif
@@ -234,7 +246,7 @@ ifeq ($(ENABLE_FEAT_F4HWN),1)
VERSION_STRING_1 ?= v0.22
AUTHOR_STRING_2 ?= F4HWN
VERSION_STRING_2 ?= v4.1
VERSION_STRING_2 ?= v4.2
EDITION_STRING ?= Custom
@@ -485,6 +497,9 @@ endif
ifeq ($(ENABLE_FEAT_F4HWN_DEBUG),1)
CFLAGS += -DENABLE_FEAT_F4HWN_DEBUG
endif
ifeq ($(ENABLE_EXTRA_UART_CMD),1)
CFLAGS += -DENABLE_EXTRA_UART_CMD
endif
LDFLAGS =
LDFLAGS += -z noexecstack -mcpu=cortex-m0 -nostartfiles -Wl,-T,firmware.ld -Wl,--gc-sections
+2 -1
View File
@@ -119,7 +119,7 @@ Special thanks to Jean-Cyrille F6IWW (2 times), Fabrice 14RC123, David F4BPP, Ol
* scan list 2,
* scan list 3,
* scan lists [1, 2, 3],
* scan all (all channels with ou without list),
* scan all (all channels with or without list),
* add scan list shortcuts,
* add resume mode on startup (scan, bandscope and broadcast FM),
* new actions:
@@ -153,6 +153,7 @@ Special thanks to Jean-Cyrille F6IWW (2 times), Fabrice 14RC123, David F4BPP, Ol
* remove 200Tx, 350Tx and 500Tx,
* unlock TX on all bands needs only to be repeat 3 times,
* code refactoring and many memory optimization,
* displays the live screen of the Quansheng K5 on your computer via a USB-to-Serial cable,
* and more...
## Main features from Egzumer:
-9
View File
@@ -46,10 +46,6 @@
#include "app/rega.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#endif
#if defined(ENABLE_FMRADIO)
static void ACTION_Scan_FM(bool bRestart);
#endif
@@ -349,11 +345,6 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
funcShort = funcLong;
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot();
#endif
if (!bKeyPressed) //ignore release if held
return;
}
+5 -2
View File
@@ -51,6 +51,9 @@ static void AIRCOPY_clear()
{
crc[i] = 0;
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot(true);
#endif
}
bool AIRCOPY_SendMessage(void)
@@ -78,7 +81,7 @@ bool AIRCOPY_SendMessage(void)
if (++gAirCopyBlockNumber >= 0x78) {
gAircopyState = AIRCOPY_COMPLETE;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot();
getScreenShot(false);
#endif
//NVIC_SystemReset();
}
@@ -139,7 +142,7 @@ void AIRCOPY_StorePacket(void)
if (Offset == 0x1E00) {
gAircopyState = AIRCOPY_COMPLETE;
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot();
getScreenShot(false);
#endif
}
+16 -2
View File
@@ -75,6 +75,10 @@
#include "ui/status.h"
#include "ui/ui.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#endif
static bool flagSaveVfo;
static bool flagSaveSettings;
static bool flagSaveChannel;
@@ -1381,13 +1385,23 @@ void APP_TimeSlice10ms(void)
#endif
}
if (gUpdateDisplay) {
bool gUpdateDisplayCurrent = gUpdateDisplay;
bool gUpdateStatusCurrent = gUpdateStatus;
if (gUpdateDisplayCurrent) {
gUpdateDisplay = false;
GUI_DisplayScreen();
}
if (gUpdateStatus)
if (gUpdateStatusCurrent) {
UI_DisplayStatus();
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
if (gUpdateDisplayCurrent || gUpdateStatusCurrent) {
getScreenShot(false);
}
#endif
// Skipping authentic device checks
+9
View File
@@ -16,6 +16,10 @@
#include "app/breakout.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#endif
static uint32_t randSeed = 1;
static uint8_t blockAnim = 0;
@@ -478,6 +482,11 @@ void APP_RunBreakout(void) {
if(swap == 0)
{
blockAnim = (blockAnim + 1) % 4;
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot(false);
#endif
}
swap = (swap + 1) % 4;
+4 -4
View File
@@ -728,10 +728,6 @@ static void ToggleBacklight()
{
BACKLIGHT_TurnOff();
}
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot();
#endif
}
static void ToggleStepsCount()
@@ -1645,6 +1641,10 @@ static void Tick()
if (redrawScreen)
{
Render();
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot(false);
#endif
redrawScreen = false;
}
}
+13 -1
View File
@@ -106,6 +106,7 @@ typedef struct {
} Data;
} REPLY_051D_t;
#ifdef ENABLE_EXTRA_UART_CMD
typedef struct {
Header_t Header;
struct {
@@ -127,6 +128,7 @@ typedef struct {
Header_t Header;
uint32_t Response[4];
} CMD_052D_t;
#endif
typedef struct {
Header_t Header;
@@ -136,10 +138,13 @@ typedef struct {
} Data;
} REPLY_052D_t;
#ifdef ENABLE_EXTRA_UART_CMD
typedef struct {
Header_t Header;
uint32_t Timestamp;
} CMD_052F_t;
#endif
static const uint8_t Obfuscation[16] =
{
@@ -329,6 +334,7 @@ static void CMD_051D(const uint8_t *pBuffer)
SendReply(&Reply, sizeof(Reply));
}
#ifdef ENABLE_EXTRA_UART_CMD
// read RSSI
static void CMD_0527(void)
{
@@ -438,6 +444,7 @@ static void CMD_052F(const uint8_t *pBuffer)
SendVersion();
}
#endif
#ifdef ENABLE_UART_RW_BK_REGS
static void CMD_0601_ReadBK4819Reg(const uint8_t *pBuffer)
@@ -591,7 +598,8 @@ void UART_HandleCommand(void)
case 0x0521: // Not implementing non-authentic command
break;
#ifdef ENABLE_EXTRA_UART_CMD
case 0x0527:
CMD_0527();
break;
@@ -609,6 +617,7 @@ void UART_HandleCommand(void)
case 0x052F:
CMD_052F(UART_Command.Buffer);
break;
#endif
case 0x05DD: // reset
#if defined(ENABLE_OVERLAY)
@@ -628,4 +637,7 @@ void UART_HandleCommand(void)
break;
#endif
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
gUART_LockScreenshot = 20; // lock screenshot
#endif
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26 -9
View File
@@ -9,15 +9,28 @@ mkdir -p "$FIRMWARE_DIR"
# Clean previously compiled firmware files
rm -f "$FIRMWARE_DIR"/*
# Build image only if it doesn't already exist
if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Docker image '$IMAGE_NAME' not found, building..."
if ! docker build --build-arg BUILDPLATFORM=linux/amd64 -t "$IMAGE_NAME" .; then
echo "❌ Failed to build docker image"
exit 1
fi
# Clean up old Docker artifacts
echo "🧽 Cleaning up old Docker artifacts..."
docker system prune -f --volumes >/dev/null 2>&1 || true
# Always rebuild the Docker image to ensure latest code changes
echo "⚙️ Rebuilding Docker image '$IMAGE_NAME'..."
docker rmi "$IMAGE_NAME" 2>/dev/null || true
if ! docker build --build-arg BUILDPLATFORM=linux/amd64 -t "$IMAGE_NAME" .; then
echo "❌ Failed to build docker image"
exit 1
fi
# -------------------- CLEAN ALL ---------------------
clean() {
echo "🧽 Cleaning all"
docker rmi uvk5
docker buildx prune -f
docker buildx history ls | awk 'NR>1 {print $1}' | xargs docker buildx history rm
make clean
}
# ------------------ BUILD VARIANTS ------------------
custom() {
@@ -50,6 +63,7 @@ bandscope() {
ENABLE_FMRADIO=0 \
ENABLE_VOX=0 \
ENABLE_AIRCOPY=1 \
ENABLE_FEAT_F4HWN_SCREENSHOT=1 \
ENABLE_FEAT_F4HWN_GAME=0 \
ENABLE_FEAT_F4HWN_PMR=1 \
ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \
@@ -68,6 +82,7 @@ broadcast() {
ENABLE_FMRADIO=1 \
ENABLE_VOX=1 \
ENABLE_AIRCOPY=1 \
ENABLE_FEAT_F4HWN_SCREENSHOT=1 \
ENABLE_FEAT_F4HWN_GAME=0 \
ENABLE_FEAT_F4HWN_PMR=1 \
ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \
@@ -96,7 +111,7 @@ basic() {
ENABLE_FEAT_F4HWN_CHARGING_C=0 \
ENABLE_FEAT_F4HWN_INV=1 \
ENABLE_FEAT_F4HWN_CTR=0 \
ENABLE_FEAT_F4HWN_NARROWER=0 \
ENABLE_FEAT_F4HWN_NARROWER=1 \
ENABLE_FEAT_F4HWN_RESCUE_OPS=0 \
EDITION_STRING=Basic \
TARGET=f4hwn.basic \
@@ -111,6 +126,7 @@ rescueops() {
ENABLE_FMRADIO=0 \
ENABLE_VOX=1 \
ENABLE_AIRCOPY=1 \
ENABLE_FEAT_F4HWN_SCREENSHOT=1 \
ENABLE_FEAT_F4HWN_GAME=0 \
ENABLE_FEAT_F4HWN_PMR=1 \
ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \
@@ -142,6 +158,7 @@ game() {
# ------------------ MENU ------------------
case "$1" in
clean) clean ;;
custom) custom ;;
standard) standard ;;
bandscope) bandscope ;;
@@ -157,7 +174,7 @@ case "$1" in
game
;;
*)
echo "Usage: $0 {custom|standard|bandscope|broadcast|basic|rescueops|game|all} [--rebuild]"
echo "Usage: $0 {clean|custom|standard|bandscope|broadcast|basic|rescueops|game|all}"
exit 1
;;
esac
+13
View File
@@ -15,6 +15,7 @@
*/
#include <stdbool.h>
#include <string.h>
#include "bsp/dp32g030/dma.h"
#include "bsp/dp32g030/syscon.h"
#include "bsp/dp32g030/uart.h"
@@ -102,3 +103,15 @@ void UART_LogSend(const void *pBuffer, uint32_t Size)
UART_Send(pBuffer, Size);
}
}
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
bool UART_IsCableConnected(void) {
for (size_t i = 0; i < sizeof(UART_DMA_Buffer); i++) {
if (UART_DMA_Buffer[i] == 0x55) {
UART_DMA_Buffer[i] = 0x00; // Clear only the matched byte
return true;
}
}
return false;
}
#endif
+4
View File
@@ -26,5 +26,9 @@ 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
bool UART_IsCableConnected(void);
#endif
#endif
+88
View File
@@ -0,0 +1,88 @@
# 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
@@ -0,0 +1,228 @@
#!/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()
+3
View File
@@ -190,6 +190,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
#endif
#endif
volatile uint16_t gTailNoteEliminationCountdown_10ms;
+3
View File
@@ -256,6 +256,9 @@ 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
#endif
#endif
extern volatile uint16_t gTailNoteEliminationCountdown_10ms;
+123
View File
@@ -0,0 +1,123 @@
/* 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"
void getScreenShot(bool force)
{
static uint8_t previousFrame[1024] = {0}; // Last transmitted frame
static uint8_t forcedBlock = 0; // Block forced for refresh on each frame
static uint8_t keepAlive = 10; // Keepalive counter
// Use a single buffer to reduce stack usage
static uint8_t currentFrame[1024]; // Current frame
static uint8_t deltaFrame[128 * 9]; // Delta frame buffer
uint16_t index = 0;
uint8_t acc = 0;
uint8_t bitCount = 0;
if (gUART_LockScreenshot > 0) // Lock screenshot if Chirp is in used
{
gUART_LockScreenshot--;
return;
}
if (UART_IsCableConnected()) {
keepAlive = 10;
}
if (keepAlive > 0) {
if (--keepAlive == 0) return;
}
else
{
return;
}
// Build current frame from status line (first 8 lines)
for (uint8_t b = 0; b < 8; b++) {
for (uint8_t i = 0; i < 128; i++) {
uint8_t bit = (gStatusLine[i] >> b) & 0x01;
acc |= (bit << bitCount++);
if (bitCount == 8) {
currentFrame[index++] = acc;
acc = 0;
bitCount = 0;
}
}
}
// Build remaining part of the frame (7 * 8 lines)
for (uint8_t l = 0; l < 7; l++) {
for (uint8_t b = 0; b < 8; b++) {
for (uint8_t i = 0; i < 128; i++) {
uint8_t bit = (gFrameBuffer[l][i] >> b) & 0x01;
acc |= (bit << bitCount++);
if (bitCount == 8) {
currentFrame[index++] = acc;
acc = 0;
bitCount = 0;
}
}
}
}
if (bitCount > 0)
currentFrame[index++] = acc;
if (index != 1024)
return; // Frame size mismatch, abort
// Generate delta frame
uint16_t deltaLen = 0;
for (uint8_t block = 0; block < 128; block++) {
uint8_t *cur = &currentFrame[block * 8];
uint8_t *prev = &previousFrame[block * 8];
bool changed = memcmp(cur, prev, 8) != 0;
bool isForced = (block == forcedBlock);
bool fullUpdate = force;
if (changed || isForced || fullUpdate) {
deltaFrame[deltaLen++] = block;
memcpy(&deltaFrame[deltaLen], cur, 8);
deltaLen += 8;
memcpy(prev, cur, 8); // Update stored frame
}
}
forcedBlock = (forcedBlock + 1) % 128;
if (deltaLen == 0)
return; // No update needed
// Send the delta frame over UART
uint8_t header[5] = {
0xAA, 0x55, 0x02,
(uint8_t)(deltaLen >> 8),
(uint8_t)(deltaLen & 0xFF)
};
UART_Send(header, 5);
UART_Send(deltaFrame, deltaLen);
uint8_t end = 0x0A;
UART_Send(&end, 1);
}
+4 -33
View File
@@ -14,38 +14,9 @@
* limitations under the License.
*/
#include "debugging.h"
#include "driver/st7565.h"
#ifndef SCREENSHOT_H
#define SCREENSHOT_H
static inline void getScreenShot(void)
{
char str[2] = "";
void getScreenShot(bool force);
LogUart("P1\n");
LogUart("128 64\n");
for(uint8_t b = 0; b < 8; b++)
{
for(uint8_t i = 0; i < 128; i++)
{
sprintf(str, "%d ", ((gStatusLine[i] >> b) & 0x01));
LogUart(str);
}
LogUart("\n");
}
for(uint8_t l = 0; l < 7; l++)
{
for(uint8_t b = 0; b < 8; b++)
{
for(uint8_t i = 0; i < 128; i++)
{
sprintf(str, "%d ", ((gFrameBuffer[l][i] >> b) & 0x01));
LogUart(str);
}
}
LogUart("\n");
}
LogUart("\n----------------\n");
}
#endif
+2 -2
View File
@@ -217,10 +217,10 @@ typedef struct {
bool SCAN_LIST_ENABLED[3];
uint8_t SCANLIST_PRIORITY_CH1[3];
uint8_t SCANLIST_PRIORITY_CH2[3];
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
//#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE // Fix me !!! What the hell is this?
uint8_t CURRENT_STATE;
uint8_t CURRENT_LIST;
#endif
//#endif // Fix me !!! What the hell is this?
uint8_t field29_0x26;
uint8_t field30_0x27;
+8
View File
@@ -28,6 +28,10 @@
#include "version.h"
#include "bitmaps.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#endif
void UI_DisplayReleaseKeys(void)
{
memset(gStatusLine, 0, sizeof(gStatusLine));
@@ -198,5 +202,9 @@ void UI_DisplayWelcome(void)
//ST7565_BlitStatusLine(); // blank status line : I think it's useless
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
getScreenShot(true);
#endif
}
}