Files

300 lines
8.1 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2025 muzkr
#
# https://github.com/muzkr
#
# Licensed under the MIT License (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at the root of this repository.
#
# 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.
#
import argparse
import signal
from time import sleep
import os
import _fwcodec as fc
def load_image(file: str) -> bytes:
a = bytearray()
with open(file, "rb") as fd:
buf = bytearray(512)
while True:
len1 = fd.readinto(buf)
if len1 > 0:
a.extend(memoryview(buf)[:len1])
if len1 < len(buf):
break
return a
def main_dump(args, ser):
import _dump as dd
dump_file: str = args.file
print("Dump file: {}".format(dump_file))
if os.path.exists(dump_file):
print("Dump file exists. Will be overwritten")
if args.config:
dump_what = dd.DUMP_CONFIG
print("Dump configuration..")
elif args.calib:
dump_what = dd.DUMP_CALIB
print("Dump calibration data..")
else:
dump_what = dd.DUMP_ALL
print("Dump all..")
quit_flag = False
def quit_handler(sig, frame):
nonlocal quit_flag
quit_flag = True
signal.signal(signal.SIGINT, quit_handler)
dump = dd.EepromDump(ser, dump_what, dump_file)
while (not quit_flag) and dump.loop():
sleep(0)
def main_restore(args, ser):
import _dump as dd
import _restore as rr
dump_file: str = args.file
print("Dump file: {}".format(dump_file))
if not os.path.exists(dump_file):
print("Dump file not exist")
return
if args.config:
dump_what = dd.DUMP_CONFIG
print("Restore configuration..")
elif args.calib:
dump_what = dd.DUMP_CALIB
print("Restore calibration data..")
else:
dump_what = dd.DUMP_ALL
print("Restore all..")
quit_flag = False
def quit_handler(sig, frame):
nonlocal quit_flag
quit_flag = True
signal.signal(signal.SIGINT, quit_handler)
dump = rr.EepromDump(ser, dump_what, dump_file)
while (not quit_flag) and dump.loop():
sleep(0)
def main_flash(args, ser):
import _prog as pp
bl_ver: str = args.bl_ver
fw_file: str = args.file
try:
fw_image = load_image(fw_file)
if 0 == len(fw_image):
print("Invalid firmware image: {}: empty file".format(fw_file))
return
except Exception as e:
print("Cannot load firmware image '{}': {}".format(fw_file, e))
return
if len(bl_ver) > 4:
print("Invalid bootloader version '{}': more than 4 characters".format(bl_ver))
return
print("Firmware image loaded: {}, size = {}".format(fw_file, len(fw_image)))
quit_flag = False
def quit_handler(sig, frame):
nonlocal quit_flag
quit_flag = True
signal.signal(signal.SIGINT, quit_handler)
prog = pp.Programmer(ser, fw_image, bl_ver)
while (not quit_flag) and prog.loop():
sleep(0)
def get_raw_output_path(file: str) -> str:
root, ext = os.path.splitext(file)
if ext:
file_name = "{}.raw{}".format(os.path.basename(root), ext)
candidate = "{}.raw{}".format(root, ext)
else:
file_name = os.path.basename(file) + ".raw.bin"
candidate = file + ".raw.bin"
out_dir = os.path.dirname(candidate) or "."
if os.access(out_dir, os.W_OK):
return candidate
return os.path.join(os.getcwd(), file_name)
def main_decode(args):
fw_file: str = args.file
out_file: str = args.output or get_raw_output_path(fw_file)
try:
fw_image = load_image(fw_file)
if 0 == len(fw_image):
print("Invalid firmware image: {}: empty file".format(fw_file))
return
except Exception as e:
print("Cannot load firmware image '{}': {}".format(fw_file, e))
return
if fc.is_raw_image(fw_image):
print("Firmware image already looks raw")
raw_image = fw_image
version = None
else:
try:
raw_image, version = fc.decode_packed_image(fw_image)
except Exception as e:
print("Cannot decode firmware image '{}': {}".format(fw_file, e))
return
with open(out_file, "wb") as fd:
fd.write(raw_image)
print("Raw firmware image written: {}, size = {}".format(out_file, len(raw_image)))
if version:
print("Packed version field: {}".format(version))
def main():
# Usage:
# serialtool.py --port <port> subcmd ..
# serialtool.py .. flash [--bl-ver <ver>] <file>
# serialtool.py .. dump {--config | --calib [| --all]} file
# serialtool.py .. restore {--config | --calib [| --all]} file
# serialtool.py decode <packed.bin> [raw.bin]
ap = argparse.ArgumentParser(description="UV-K5 V2 serial tool")
# TODO: have to add option to each of subcommands ??
# ap.add_argument(
# "--port", "-p", help="serial port, eg., '/dev/ttyUSB0'", required=False
# )
sp = ap.add_subparsers(required=True, dest="subcommand")
ap_flash = sp.add_parser("flash", help="flash firmware")
ap_flash.add_argument(
"--port", "-p", help="serial port, eg., '/dev/ttyUSB0'", required=True
)
ap_flash.add_argument(
"--bl-ver",
help="bootloader version, eg. '1.01'. Max 4 characters. Default '?'",
required=False,
default="?",
)
ap_flash.add_argument("file", help="firmware image file")
ap_dump = sp.add_parser("dump", help="dump configuration or calibration data")
ap_dump.add_argument(
"--port", "-p", help="serial port, eg., '/dev/ttyUSB0'", required=True
)
ag = ap_dump.add_mutually_exclusive_group()
ag.add_argument("--config", action="store_true", help="dump configuration")
ag.add_argument("--calib", action="store_true", help="dump calibration data")
ag.add_argument(
"--all",
"-a",
action="store_true",
help="dump both configuration and calibration data. This is default",
)
ap_dump.add_argument("file", help="output dump file")
ap_restore = sp.add_parser(
"restore", help="restore configuration or calibration data from previous dump"
)
ap_restore.add_argument(
"--port", "-p", help="serial port, eg., '/dev/ttyUSB0'", required=True
)
ag = ap_restore.add_mutually_exclusive_group()
ag.add_argument("--config", action="store_true", help="restore configuration")
ag.add_argument("--calib", action="store_true", help="restore calibration data")
ag.add_argument(
"--all",
"-a",
action="store_true",
help="restore both configuration and calibration data. This is default",
)
ap_restore.add_argument("file", help="input dump file")
ap_decode = sp.add_parser(
"decode", help="decode a packed Quansheng stock firmware into a raw image"
)
ap_decode.add_argument("file", help="input firmware image file")
ap_decode.add_argument(
"output",
nargs="?",
help="output raw firmware image file (default: <input>.raw.bin)",
)
args = ap.parse_args()
sub_name: str = args.subcommand
print(ap.description)
# print("Press Ctrl-C to quit")
if "decode" == sub_name:
main_decode(args)
return
port: str = args.port
try:
import serial
ser = serial.Serial(port, baudrate=38400, timeout=0.0001, write_timeout=None)
except Exception as e:
print("Cannot open port '{}': {}".format(port, e))
return
match sub_name:
case "flash":
main_flash(args, ser)
case "dump":
main_dump(args, ser)
case "restore":
main_restore(args, ser)
ser.close()
print("Quit")
if __name__ == "__main__":
main()