#!/usr/bin/env python3 # -*- coding: utf-8 -*- ################################################################################################################################################ # # GD-77 Firmware uploader. By Roger VK3KYY # # # This script has only been tested on Windows and Linux, it may or may not work on OSX # # On Windows,.. # the driver the system installs for the GD-77, which is the HID driver, needs to be replaced by the LibUSB-win32 using Zadig # for USB device with idVendor=0x15a2, idProduct=0x0073 # Once this driver is installed the CPS and official firmware loader will no longer work as they can't find the device # To use the CPS etc again, use the DeviceManager to uninstall the driver associated with idVendor=0x15a2, idProduct=0x0073 (this will appear as a libusb-win32 device) # Then unplug the GD-77 and reconnect, and the HID driver will be re-installed # # # On Linux, depending of you distro, you need to install a special udev rule to automatically unbind the USB HID device to usbhid driver. # # # You also need python3-usb, enum34 and urllib3 # ################################################################################################################################################ ######################### Error codes ######################### # # -1: Missing firmware file # -2: Wrong SGL file format # -3: Unencrypted firmware # -4: Firmware file is too large # -5: Unknown HT model type # -6: Command line parsing error # -7: Online download firmware location error # -8: Online download firmware binary error # -9: Online firmware download failure # -10: Firmare/HT mismatch # -99: Unsupported GD-77S (will be removed in the futur) # ############################################################### import usb import getopt, sys import ntpath import os.path from array import array import enum import urllib3 import re import tempfile class SGLFormatOutput(enum.Enum): GD_77 = 0 GD_77S = 1 DM_1801 = 2 RD_5R = 3 UNKNOWN = 4 def __int__(self): return self.value # Globals responseOK = [0x41] outputModes = ["GD-77", "GD-77S", "DM-1801", "RD-5R", "Unknown"] outputFormat = SGLFormatOutput.GD_77 downloadedFW = "" ######################################################################## # Utilities to dump hex for testing ######################################################################## def hexdump(buf): cbuf = "" for b in buf: cbuf = cbuf + "0x%0.2X " % ord(b) return cbuf def hexdumpArray(buf): cbuf = "" for b in buf: cbuf = cbuf + "0x%0.2X " % b return cbuf def hexdumpArray2(buf): cbuf = "" for b in buf: cbuf = cbuf + "%0.2X-" % b return cbuf[:-1] def strdumpArray(buf): cbuf = "" for b in buf: cbuf = cbuf + chr(b) return cbuf def downloadFirmware(downloadStable): url = "https://github.com/rogerclarkmelbourne/OpenGD77/releases" urlBase = "http://github.com" httpPool = urllib3.PoolManager() pattern = "" fwVersion = "UNKNOWN" fwVersionPatternFormat = r'/{}([0-9\.]+)/' urlFW = "" webContent = "" print(" - " + "Try to download the firmware for your {} from the project page".format(outputModes[int(outputFormat)])) print(" - " + "Retrieve firmware location"); try: response = httpPool.request('GET', url) except urllib3.URLError as e: print("".format(e.reason)) sys.exit(-7) webContent = str(response.data) if (outputFormat == SGLFormatOutput.GD_77): patternFormat = r'/rogerclarkmelbourne/OpenGD77/releases/download/{}([0-9\.]+)/OpenGD77\.sgl' elif (outputFormat == SGLFormatOutput.GD_77S): patternFormat = r'/rogerclarkmelbourne/OpenGD77/releases/download/{}([0-9\.]+)/OpenGD77S\.sgl' elif (outputFormat == SGLFormatOutput.DM_1801): patternFormat = r'/rogerclarkmelbourne/OpenGD77/releases/download/{}([0-9\.]+)/OpenDM1801\.sgl' elif (outputFormat == SGLFormatOutput.RD_5R): patternFormat = r'/rogerclarkmelbourne/OpenGD77/releases/download/{}([0-9\.]+)/OpenDM5R\.sgl' pattern = patternFormat.format("R" if downloadStable == True else "D") fwVersionPattern = fwVersionPatternFormat.format("R" if downloadStable == True else "D") contentArray = webContent.split("\n") for l in contentArray: m = re.search(pattern, l) if (m != None): urlFW = urlBase + m.group(0) m = re.search(fwVersionPattern, urlFW) if (m != None): fwVersion = m.group(0).strip('/') break if (len(urlFW)): global downloadedFW downloadedFW = os.path.join(tempfile.gettempdir(), next(tempfile._get_candidate_names()) + '.sgl') print(" - " + "Downloading the firmware version {}, please wait".format(fwVersion)); try: response = httpPool.request('GET', urlFW, preload_content=False) except urllib3.URLError as e: print("".format(e.reason)) sys.exit(-8) length = response.getheader('content-length') if (length != None): length = int(length) blocksize = max(4096, (length//100)) else: blocksize = 4096 # Download data with open(downloadedFW, "w+b") as f: while True: data = response.read(blocksize) if not data: break f.write(data) f.close() return True return False ######################################################################## # Send the data packet to the GD-77 and return response ######################################################################## def sendAndGetResponse(dev, cmd): USB_WRITE_ENDPOINT = 0x02 USB_READ_ENDPOINT = 0x81 TRANSFER_LENGTH = 38 headerData = [0x0] * 4 headerData[0] = 1 headerData[1] = 0 headerData[2] = ((len(cmd) >> 0) & 0xff) headerData[3] = ((len(cmd) >> 8) & 0xff) cmd = headerData + cmd #print("TX: " + hexdumpArray2(cmd)) #print("TX: '{}'".format(strdumpArray(cmd[4:]))) ret = dev.write(USB_WRITE_ENDPOINT, cmd) ret = dev.read(USB_READ_ENDPOINT, TRANSFER_LENGTH + 4, 5000) #print("RX: " + hexdumpArray2(ret[4:])) #print("RX: '{}'".format(strdumpArray(ret[4:]))) return ret[4:] ######################################################################## # Send the data packet to the GD-77 and confirm the response is correct ######################################################################## def sendAndCheckResponse(dev, cmd, resp): USB_WRITE_ENDPOINT = 0x02 USB_READ_ENDPOINT = 0x81 TRANSFER_LENGTH = 38 zeroPad = [0x0] * TRANSFER_LENGTH headerData = [0x0] * 4 headerData[0] = 1 headerData[1] = 0 headerData[2] = ((len(cmd) >> 0) & 0xff) headerData[3] = ((len(cmd) >> 8) & 0xff) if (len(resp) < TRANSFER_LENGTH): resp = resp + zeroPad[0:TRANSFER_LENGTH - len(resp)] cmd = headerData + cmd #print("TX: " + hexdumpArray2(cmd)) #print("TX: '{}'".format(strdumpArray(cmd[4:]))) ret = dev.write(USB_WRITE_ENDPOINT, cmd) ret = dev.read(USB_READ_ENDPOINT, TRANSFER_LENGTH + 4, 5000) expected = array("B", resp) #print("RX: " + hexdumpArray2(ret[4:])) #print("RX: '{}'".format(strdumpArray(ret[4:]))) if (expected == ret[4:]): return True else: print("Error. Read returned: " + str(ret)) return False ############################## # Create checksum data packet ############################## def createChecksumData(buf, startAddress, endAddress): #checksum data starts with a small header, followed by the 32 bit checksum value, least significant byte first checkSumData = [ 0x45, 0x4e, 0x44, 0xff, 0xDE, 0xAD, 0xBE, 0xEF ] cs = 0 for i in range(startAddress, endAddress): cs = cs + buf[i] checkSumData[4] = (cs % 256) & 0xff checkSumData[5] = ((cs >> 8) % 256) & 0xff checkSumData[6] = ((cs >> 16) % 256) & 0xff checkSumData[7] = ((cs >> 24) % 256) & 0xff return checkSumData def updateBlockAddressAndLength(buf, address, length): buf[5] = ((length) % 256) & 0xff buf[4] = ((length >> 8) % 256) & 0xff buf[3] = ((address) % 256) & 0xff buf[2] = ((address >> 8) % 256) & 0xff buf[1] = ((address >> 16) % 256) & 0xff buf[0] = ((address >> 24) % 256) & 0xff return buf ##################################################### # Open firmware file on disk and sent it to the GD-77 ###########################################b########## def sendFileData(fileBuf, dev): dataHeader = [0x00] * (0x20 + 0x06) BLOCK_LENGTH = 1024 #1k DATA_TRANSFER_SIZE = 0x20 checksumStartAddress = 0 address = 0 fileLength = len(fileBuf) totalBlocks = (fileLength // BLOCK_LENGTH) + 1 while address < fileLength: if ((address % BLOCK_LENGTH) == 0): checksumStartAddress = address dataHeader = updateBlockAddressAndLength(dataHeader, address, DATA_TRANSFER_SIZE) if ((address + DATA_TRANSFER_SIZE) < fileLength): for i in range(DATA_TRANSFER_SIZE): dataHeader[6 + i] = fileBuf[address + i] if (sendAndCheckResponse(dev, dataHeader, responseOK) == False): print("Error sending data") return False break address = address + DATA_TRANSFER_SIZE if ((address % 0x400) == 0): print("\r - Sent block " + str(address // BLOCK_LENGTH) + " of "+ str(totalBlocks), end='') sys.stdout.flush() if (sendAndCheckResponse(dev, createChecksumData(fileBuf, checksumStartAddress, address), responseOK) == False): print("Error sending checksum") return False break else: print("\r - Sending last block ", end='') sys.stdout.flush() DATA_TRANSFER_SIZE = fileLength - address dataHeader = updateBlockAddressAndLength(dataHeader, address, DATA_TRANSFER_SIZE) for i in range(DATA_TRANSFER_SIZE): dataHeader[6 + i] = fileBuf[address + i] if (sendAndCheckResponse(dev, dataHeader, responseOK) == False): print("Error sending data") return False break address = address + DATA_TRANSFER_SIZE if (sendAndCheckResponse(dev, createChecksumData(fileBuf, checksumStartAddress, address), responseOK) == False): print("Error sending checksum") return False break print("") return True ##################################################### # Probe connected model ###########################################b########## def probeModel(dev): commandLetterA = [ 0x41 ] # 'A' command0 = [[ 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44 ], [ 0x23, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x3f ]] # 'DOWNLOAD' command1 = [ commandLetterA, responseOK ] commandDummy = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ] #commandMOD = [ 0x46, 0x2D, 0x4D, 0x4F, 0x44, 0xff, 0xff, 0xff ] # F-MOD... #commandEND = [ 0x45, 0x4E, 0x44, 0xFF ] # END. commandID = [ command0, command1 ] models = [[ 'DV01', SGLFormatOutput.GD_77 ], [ 'DV02', SGLFormatOutput.GD_77S ], [ 'DV03', SGLFormatOutput.DM_1801 ]] # RD-5R also have "DV02" id commandNumber = 0 while commandNumber < len(commandID): if sendAndCheckResponse(dev, commandID[commandNumber][0], commandID[commandNumber][1]) == False: return SGLFormatOutput.UNKNOWN commandNumber = commandNumber + 1 resp = sendAndGetResponse(dev, commandDummy) ##dummy = sendAndGetResponse(dev, command0[0]) for x in models: if (x[0] == str(resp[:4].tobytes().decode("ascii"))): return x[1] return SGLFormatOutput.UNKNOWN ########################################################################################################################################### # Send commands to the GD-77 to verify we are the updater, prepare to program including erasing the internal program flash memory ########################################################################################################################################### def sendInitialCommands(dev, encodeKey): commandLetterA =[ 0x41] #A command0 =[[0x44,0x4f,0x57,0x4e,0x4c,0x4f,0x41,0x44],[0x23,0x55,0x50,0x44,0x41,0x54,0x45,0x3f]] # DOWNLOAD command1 =[commandLetterA,responseOK] command3 =[[0x46, 0x2d, 0x50, 0x52, 0x4f, 0x47, 0xff, 0xff],responseOK] #... F-PROG.. if (outputFormat == SGLFormatOutput.GD_77): command2 =[[0x44, 0x56, 0x30, 0x31, (0x61 + 0x00), (0x61 + 0x0C), (0x61 + 0x0D), (0x61 + 0x01)],[0x44, 0x56, 0x30, 0x31]] #.... DV01enhi (DV01enhi comes from deobfuscated sgl file) command4 =[[0x53, 0x47, 0x2d, 0x4d, 0x44, 0x2d, 0x37, 0x36, 0x30, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],responseOK] #SG-MD-760 command5 =[[0x4d, 0x44, 0x2d, 0x37, 0x36, 0x30, 0xff, 0xff],responseOK] #MD-760.. elif (outputFormat == SGLFormatOutput.GD_77S): command2 =[[0x44, 0x56, 0x30, 0x32, 0x6D, 0x40, 0x7D, 0x63],[0x44, 0x56, 0x30, 0x32]] #.... DV02Gpmj (thanks Wireshark) command4 =[[0x53, 0x47, 0x2d, 0x4d, 0x44, 0x2d, 0x37, 0x33, 0x30, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],responseOK] #SG-MD-730 command5 =[[0x4d, 0x44, 0x2d, 0x37, 0x33, 0x30, 0xff, 0xff],responseOK] #MD-730.. elif (outputFormat == SGLFormatOutput.DM_1801): command2 =[[0x44, 0x56, 0x30, 0x33, 0x74, 0x21, 0x44, 0x39],[0x44, 0x56, 0x30, 0x33]] #.... last 4 bytes of the command are the offset encoded as letters a - p (hard coded fr command4 =[[0x42, 0x46, 0x2d, 0x44, 0x4d, 0x52, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],responseOK] #BF-DMR command5 =[[0x31, 0x38, 0x30, 0x31, 0xff, 0xff, 0xff, 0xff],responseOK] # MD-1801 elif (outputFormat == SGLFormatOutput.RD_5R): command2 =[[0x44, 0x56, 0x30, 0x32, 0x53, 0x36, 0x37, 0x62],[0x44, 0x56, 0x30, 0x32]] #.... last 4 bytes of the command are the offset encoded as letters a - p (hard coded fr command4 =[[0x42, 0x46, 0x2D, 0x35, 0x52, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],responseOK] #BF-5R command5 =[[0x42, 0x46, 0x2D, 0x35, 0x52, 0xff, 0xff, 0xff],responseOK] # BF-5R command6 =[[0x56, 0x31, 0x2e, 0x30, 0x30, 0x2e, 0x30, 0x31],responseOK] #V1.00.01 commandErase =[[0x46, 0x2d, 0x45, 0x52, 0x41, 0x53, 0x45, 0xff],responseOK] #F-ERASE commandPostErase =[commandLetterA,responseOK] commandProgram =[[0x50, 0x52, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0xf],responseOK] #PROGRAM commands =[command0,command1,command2,command3,command4,command5,command6,commandErase,commandPostErase,commandProgram] commandNames =["Sending Download command", "Sending ACK", "Sending encryption key", "Sending F-PROG command", "Sending radio modem number", "Sending radio modem number 2", "Sending version", "Sending erase command", "Send post erase command", "Sending Program command"] commandNumber = 0 # Buffer.BlockCopy(encodeKey, 0, command2[0], 4, 4); command2[0] = command2[0][0:4] + encodeKey # Send the commands which the GD-77 expects before the start of the data while commandNumber < len(commands): print(" - " + commandNames[commandNumber]) if sendAndCheckResponse(dev, commands[commandNumber][0], commands[commandNumber][1]) == False: print("Error sending command") return False break commandNumber = commandNumber + 1 return True ########################################################################################################################################### # ########################################################################################################################################### def checkForSGLAndReturnEncryptedData(fileBuf): header_tag = list("SGL!") headerModel = [] buf_in_4 = list("".join(map(chr, fileBuf[0:4]))) headerModel.append(fileBuf[11]) if buf_in_4 == header_tag: # read and decode offset and xor tag buf_in_4 = list(fileBuf[0x000C : 0x000C + 4]) for i in range(0, 4): buf_in_4[i] = buf_in_4[i] ^ ord(header_tag[i]) offset = buf_in_4[0] + 256 * buf_in_4[1] xor_data = [ buf_in_4[2], buf_in_4[3] ] # read and decode part of the header buf_in_512 = list(fileBuf[offset + 0x0006 : offset + 0x0006 + 512]) xor_idx = 0; for i in range(0, 512): buf_in_512[i] = buf_in_512[i] ^ xor_data[xor_idx] xor_idx += 1 if xor_idx == 2: xor_idx = 0 # encodeKey = buf_in_512[0x005D : 0x005D + 4] # extract length length1 = buf_in_512[0x0000] length2 = buf_in_512[0x0001] length3 = buf_in_512[0x0002] length4 = buf_in_512[0x0003] length = (length4 << 24) + (length3 << 16) + (length2 << 8) + length1 # extract encoded raw firmware retBuf = [0x00] * length; retBuf = fileBuf[len(fileBuf) - length : len(fileBuf) - length + len(retBuf) ] return retBuf, encodeKey, headerModel print("ERROR: SGL! header is missing.") return None, None, None ########################################################################################################################################### # ########################################################################################################################################### def usage(): print("Usage:") print(" " + ntpath.basename(sys.argv[0]) + " [OPTION]") print("") print(" -h, --help : Display this help text") print(" -f, --firmware= : Flash instead of default file \"firmware.sgl\"") print(" -m, --model= : Select transceiver model. Models are: {}".format(", ".join(str(x) for x in outputModes[:-1])) + ".") print(" -d, --download : Download firmware from the project website") print(" -S, --stable : Select the stable version while downloading from the project page") print(" -U, --unstable : Select the development version while downloading from the project page") print("") ##################################################### # Main function. ##################################################### def main(): global outputFormat sglFile = "firmware.sgl" downloadStable = True doDownload = False doForce = False # Command line argument parsing try: opts, args = getopt.getopt(sys.argv[1:], "hf:m:dSUF", ["help", "firmware=", "model=", "download", "stable", "unstable", "force"]) except getopt.GetoptError as err: print(str(err)) usage() sys.exit(-6) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit(0) elif opt in ("-f", "--firmware"): sglFile = arg elif opt in ("-m", "--model"): try: index = outputModes.index(arg) except ValueError as e: print("Model \"{}\" is unknown".format(arg)) sys.exit(-5) outputFormat = SGLFormatOutput(index) if (outputFormat == SGLFormatOutput.UNKNOWN): print("Unsupported model") sys.exit(-5) elif opt in ["-d", "--download"]: doDownload = True elif opt in ["-S", "--stable"]: downloadStable = True elif opt in ["-U", "--unstable"]: downloadStable = False elif opt in ["-F", "--force"]: doForce = True else: assert False, "Unhandled option" # Try to connect USB device dev = usb.core.find(idVendor=0x15a2, idProduct=0x0073) if (dev): # Needed on Linux try: if dev.is_kernel_driver_active(0): dev.detach_kernel_driver(0) except NotImplementedError as e: pass #seems to be needed for the usb to work ! dev.set_configuration() if (outputFormat == SGLFormatOutput.UNKNOWN): outputFormat = probeModel(dev) if (outputFormat == SGLFormatOutput.UNKNOWN): print("Error. Failed to detect you transceiver model.") sys.exit(-5) print(" - Detected model: {}".format(outputModes[int(outputFormat)])) # Try to download the firmware if (doDownload == True): if (downloadFirmware(downloadStable) == True): sglFile = downloadedFW else: print("Firmware download failed") sys.exit(-9) if (os.path.isfile(sglFile) == False): print("Firmware file \"" + sglFile + "\" is missing.") sys.exit(-1) print(" - " + "Now flashing your {} with \"{}\"".format(outputModes[int(outputFormat)], sglFile)) with open(sglFile, 'rb') as f: fileBuf = f.read() # Check firmware filename, file_extension = os.path.splitext(sglFile) # Define encodeKey according to HT model if (outputFormat == SGLFormatOutput.GD_77): encodeKey = [ (0x61 + 0x00), (0x61 + 0x0C), (0x61 + 0x0D), (0x61 + 0x01) ] elif (outputFormat == SGLFormatOutput.GD_77S): encodeKey = [ (0x6D), (0x40), (0x7D), (0x63) ] ## Original header (smaller filelength): was (0x47), (0x70), (0x6d), (0x4a) elif (outputFormat == SGLFormatOutput.DM_1801): encodeKey = [ (0x74), (0x21), (0x44), (0x39) ] elif (outputFormat == SGLFormatOutput.RD_5R): encodeKey = [ (0x53), (0x36), (0x37), (0x62) ] if (file_extension == ".sgl"): firmwareModelTag = { SGLFormatOutput.GD_77: 0x1B , SGLFormatOutput.GD_77S: 0x70, SGLFormatOutput.DM_1801: 0x4F, SGLFormatOutput.RD_5R: 0x5C} ## Could be a SGL file ! fileBuf, encodeKey, headerModel = checkForSGLAndReturnEncryptedData(fileBuf) if (fileBuf == None): print("Error. Missing SGL in .sgl file header") sys.exit(-2) print(" - " + "Firmware file confirmed as SGL") if (doForce == False): # Check if the firmware matches the transceiver model if (headerModel[0] != firmwareModelTag[outputFormat]): print("Error. The firmware doesn't match the transceiver model.") sys.exit(-10) else: print("Firmware file is an unencrypted binary. Exiting") sys.exit(-3) if len(fileBuf) > 0x7b000: print("Error. Firmware file too large.") sys.exit(-4) if (sendInitialCommands(dev, encodeKey) == True): if (sendFileData(fileBuf, dev) == True): print("Firmware update complete. Please reboot the {}.".format(outputModes[int(outputFormat)])) else: print("Error while sending data") else: print("Error while sending initial commands") usb.util.dispose_resources(dev) #free up the USB device else: print("Error. Can't find your transceiver.") # Remove downloaded firmware, if any if (len(downloadedFW)): if (os.path.isfile(downloadedFW)): os.remove(downloadedFW) ## Run the program main() sys.exit(0)