mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Update README and more
This commit is contained in:
1 parent
6e4a634b93
commit
668c553b4f
2 files changed
-311
No files matched your search
@@ -1,83 +0,0 @@
|
||||
# Quansheng K5 Viewer
|
||||
|
||||
This Python script 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
|
||||
|
||||
|
||||
## 🚀 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