diff --git a/Makefile b/Makefile index 66380bbc..d15b494c 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 2f712cc9..087c0890 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/app/action.c b/app/action.c index ae908d9c..68b3bbaa 100644 --- a/app/action.c +++ b/app/action.c @@ -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; } diff --git a/app/aircopy.c b/app/aircopy.c index bc844fe1..99ca635a 100644 --- a/app/aircopy.c +++ b/app/aircopy.c @@ -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 } diff --git a/app/app.c b/app/app.c index 3fa9e1f9..1e6791f9 100644 --- a/app/app.c +++ b/app/app.c @@ -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 diff --git a/app/breakout.c b/app/breakout.c index ddeabd49..c482ec8e 100644 --- a/app/breakout.c +++ b/app/breakout.c @@ -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; diff --git a/app/spectrum.c b/app/spectrum.c index 891e8387..b01a2bba 100644 --- a/app/spectrum.c +++ b/app/spectrum.c @@ -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; } } diff --git a/app/uart.c b/app/uart.c index 87aba889..bf15ce29 100644 --- a/app/uart.c +++ b/app/uart.c @@ -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 } diff --git a/archive/f4hwn.bandscope.packed.v4.2.bin b/archive/f4hwn.bandscope.packed.v4.2.bin new file mode 100644 index 00000000..b87773a5 Binary files /dev/null and b/archive/f4hwn.bandscope.packed.v4.2.bin differ diff --git a/archive/f4hwn.bandscope.packed.v4.2.sa818.bin b/archive/f4hwn.bandscope.packed.v4.2.sa818.bin new file mode 100644 index 00000000..3b5fdd27 Binary files /dev/null and b/archive/f4hwn.bandscope.packed.v4.2.sa818.bin differ diff --git a/archive/f4hwn.basic.packed.v4.2.bin b/archive/f4hwn.basic.packed.v4.2.bin new file mode 100644 index 00000000..1fa1cfe3 Binary files /dev/null and b/archive/f4hwn.basic.packed.v4.2.bin differ diff --git a/archive/f4hwn.basic.packed.v4.2.sa818.bin b/archive/f4hwn.basic.packed.v4.2.sa818.bin new file mode 100644 index 00000000..5fdb43a9 Binary files /dev/null and b/archive/f4hwn.basic.packed.v4.2.sa818.bin differ diff --git a/archive/f4hwn.broadcast.packed.v4.2.bin b/archive/f4hwn.broadcast.packed.v4.2.bin new file mode 100644 index 00000000..118c6a27 Binary files /dev/null and b/archive/f4hwn.broadcast.packed.v4.2.bin differ diff --git a/archive/f4hwn.broadcast.packed.v4.2.sa818.bin b/archive/f4hwn.broadcast.packed.v4.2.sa818.bin new file mode 100644 index 00000000..030151f9 Binary files /dev/null and b/archive/f4hwn.broadcast.packed.v4.2.sa818.bin differ diff --git a/archive/f4hwn.game.packed.v4.2.bin b/archive/f4hwn.game.packed.v4.2.bin new file mode 100644 index 00000000..c428f857 Binary files /dev/null and b/archive/f4hwn.game.packed.v4.2.bin differ diff --git a/archive/f4hwn.game.packed.v4.2.sa818.bin b/archive/f4hwn.game.packed.v4.2.sa818.bin new file mode 100644 index 00000000..95f7c732 Binary files /dev/null and b/archive/f4hwn.game.packed.v4.2.sa818.bin differ diff --git a/archive/f4hwn.rescueops.packed.v4.2.bin b/archive/f4hwn.rescueops.packed.v4.2.bin new file mode 100644 index 00000000..1dbcd247 Binary files /dev/null and b/archive/f4hwn.rescueops.packed.v4.2.bin differ diff --git a/archive/f4hwn.rescueops.packed.v4.2.sa818.bin b/archive/f4hwn.rescueops.packed.v4.2.sa818.bin new file mode 100644 index 00000000..0dcb27d6 Binary files /dev/null and b/archive/f4hwn.rescueops.packed.v4.2.sa818.bin differ diff --git a/compile-with-docker.sh b/compile-with-docker.sh index 9135af11..8a52a821 100755 --- a/compile-with-docker.sh +++ b/compile-with-docker.sh @@ -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 diff --git a/driver/uart.c b/driver/uart.c index 5ea744c4..b3392928 100644 --- a/driver/uart.c +++ b/driver/uart.c @@ -15,6 +15,7 @@ */ #include +#include #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 diff --git a/driver/uart.h b/driver/uart.h index 256e0947..f25a5523 100644 --- a/driver/uart.h +++ b/driver/uart.h @@ -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 diff --git a/k5viewer/README.md b/k5viewer/README.md new file mode 100644 index 00000000..ec64ed96 --- /dev/null +++ b/k5viewer/README.md @@ -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! 📡 diff --git a/k5viewer/k5viewer.py b/k5viewer/k5viewer.py new file mode 100755 index 00000000..0d36d2ab --- /dev/null +++ b/k5viewer/k5viewer.py @@ -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() \ No newline at end of file diff --git a/misc.c b/misc.c index 82243f50..49759b92 100644 --- a/misc.c +++ b/misc.c @@ -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; diff --git a/misc.h b/misc.h index 6b65c0db..dfb53629 100644 --- a/misc.h +++ b/misc.h @@ -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; diff --git a/screenshot.c b/screenshot.c new file mode 100644 index 00000000..da147b2a --- /dev/null +++ b/screenshot.c @@ -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 = ¤tFrame[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); +} \ No newline at end of file diff --git a/screenshot.h b/screenshot.h index 64d31ed3..d2811879 100644 --- a/screenshot.h +++ b/screenshot.h @@ -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"); -} \ No newline at end of file +#endif \ No newline at end of file diff --git a/settings.h b/settings.h index c42be020..35639b58 100644 --- a/settings.h +++ b/settings.h @@ -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; diff --git a/ui/welcome.c b/ui/welcome.c index 023cee67..d85f339b 100644 --- a/ui/welcome.c +++ b/ui/welcome.c @@ -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 } }