mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Remove legacy Python k5viewer tool
This commit is contained in:
1 parent
46034ec39f
commit
676f26be34
2 files changed
-316
No files matched your search
@@ -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! 📡
|
|
||||||
@@ -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()
|
|
||||||
Reference in new issue
Block a user