#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# This python script was built hacking 'pydfu.py', available at https://github.com/dhylands/stm32-test:
#
#   This file is part of the OpenMV project.
#   Copyright (c) 2013/2014 Ibrahim Abdelkader <i.abdalkader@gmail.com>
#   This work is licensed under the MIT license, see the file LICENSE for
#   details.
#
#
# And python code available on the Travis Goodspeed's md380tools, available at https://github.com/travisgoodspeed/md380tools.git
#
#   Copyright 2010, 2011 Michael Ossmann
#   Copyright 2015 Travis Goodspeed
#
#
# Adapted & hacked for the OpenGD77 (for STM32) project by:
#
#   Copyright (C) 2023-2025 Daniel Caujolle-Bert, F1RMB
#                           Roger Clark, VK3KYY / G4KYF
#
#################################################################################################################################
#
######################### Error codes #########################
#  0:  No error
# -1:  Missing firmware file
# -2:  Wrong SGL file format
# -3:  Unsupported Firmware file format
# -4:  Firmware file is too large
# -5:  Unknown device model type
# -6:  Unable to connect the device
# -7:  Command line parsing error
# -8:  Unable to patch the firmware
###############################################################


from __future__ import print_function

import argparse
import collections
import inspect
import re
import struct
import sys
import usb.core
import usb.util
import zlib
import os.path
from pathlib import Path
import configparser
import hashlib
from typing import Final
import enum
import time


# USB request __TIMEOUT
__TIMEOUT = 4000

# DFU commands
__DFU_DETACH = 0
__DFU_DNLOAD = 1
__DFU_UPLOAD = 2
__DFU_GETSTATUS = 3
__DFU_CLRSTATUS = 4
__DFU_GETSTATE = 5
__DFU_ABORT = 6

# DFU status
__DFU_STATE_APP_IDLE = 0x00
__DFU_STATE_APP_DETACH = 0x01
__DFU_STATE_DFU_IDLE = 0x02
__DFU_STATE_DFU_DOWNLOAD_SYNC = 0x03
__DFU_STATE_DFU_DOWNLOAD_BUSY = 0x04
__DFU_STATE_DFU_DOWNLOAD_IDLE = 0x05
__DFU_STATE_DFU_MANIFEST_SYNC = 0x06
__DFU_STATE_DFU_MANIFEST = 0x07
__DFU_STATE_DFU_MANIFEST_WAIT_RESET = 0x08
__DFU_STATE_DFU_UPLOAD_IDLE = 0x09
__DFU_STATE_DFU_ERROR = 0x0A

_DFU_DESCRIPTOR_TYPE = 0x21

__DFU_STATUS_STR = {
    __DFU_STATE_APP_IDLE: "STATE_APP_IDLE",
    __DFU_STATE_APP_DETACH: "STATE_APP_DETACH",
    __DFU_STATE_DFU_IDLE: "STATE_DFU_IDLE",
    __DFU_STATE_DFU_DOWNLOAD_SYNC: "STATE_DFU_DOWNLOAD_SYNC",
    __DFU_STATE_DFU_DOWNLOAD_BUSY: "STATE_DFU_DOWNLOAD_BUSY",
    __DFU_STATE_DFU_DOWNLOAD_IDLE: "STATE_DFU_DOWNLOAD_IDLE",
    __DFU_STATE_DFU_MANIFEST_SYNC: "STATE_DFU_MANIFEST_SYNC",
    __DFU_STATE_DFU_MANIFEST: "STATE_DFU_MANIFEST",
    __DFU_STATE_DFU_MANIFEST_WAIT_RESET: "STATE_DFU_MANIFEST_WAIT_RESET",
    __DFU_STATE_DFU_UPLOAD_IDLE: "STATE_DFU_UPLOAD_IDLE",
    __DFU_STATE_DFU_ERROR: "STATE_DFU_ERROR",
}

# USB device handle
__dev = None

# Configuration descriptor of the device
__cfg_descr = None

__verbose = None

# USB DFU interface
__DFU_INTERFACE = 0

defaultVID = 0x0483
defaultPID = 0xDF11
version:Final = "1.1.1"

class FWPlatformOutput(enum.Enum):
    MD_9600 = 0
    MD_UV380 = 1
    DM_1701 = 2
    MD_2017 = 3
    MD_380 = 4
    UNKNOWN = 5

    def __int__(self):
        return self.value


config = []
FWOutputPlatforms = ["MD-9600", "MD-UV380", "DM-1701", "MD-2017", "MD-380", "Unknown"]
FWPlatformFormat = FWPlatformOutput.UNKNOWN
FW2645_SHA256_Checksum: Final = "d8a653307222e576ee416ab6d4704d14758fa71c1b0e826ffd840f26266cf11f"
BLOCK_WRITE_SIZE:Final = 1024

MD9600_ENCODE_CIPHER:Final = [
    0xA2, 0xFA, 0xBB, 0x4B, 0x90, 0x8F, 0x17, 0x20, 0x96, 0x36, 0x43, 0x84, 0xF7, 0xAC, 0x4E, 0x55,
    0xEA, 0xE5, 0xB4, 0x36, 0x55, 0xB9, 0x39, 0xE2, 0xD8, 0xDA, 0x18, 0xC0, 0x0D, 0x09, 0x5D, 0xB8,
    0x0E, 0x89, 0x90, 0x46, 0x38, 0xD4, 0x93, 0xCC, 0x2F, 0x8E, 0xCD, 0x2D, 0x22, 0xB7, 0x89, 0x97,
    0x51, 0x24, 0x98, 0xA0, 0xCC, 0x30, 0x3E, 0x95, 0x7D, 0xAF, 0x4C, 0x0E, 0x68, 0x23, 0x89, 0xC6,
    0x32, 0x33, 0x56, 0xAA, 0xE0, 0x58, 0x92, 0x30, 0xE2, 0xDA, 0xBC, 0xEA, 0x50, 0xFB, 0x57, 0x5B,
    0x73, 0x71, 0x93, 0x09, 0x87, 0x1A, 0x29, 0xD3, 0xBF, 0xEC, 0x87, 0x85, 0x8A, 0x2B, 0x2D, 0xAA,
    0x15, 0xDE, 0x57, 0xA2, 0x11, 0x83, 0xDC, 0xF4, 0xB6, 0x02, 0x56, 0xE5, 0x08, 0xE0, 0x83, 0x49,
    0x59, 0xB5, 0xEB, 0x99, 0x0F, 0xE0, 0xC3, 0x46, 0xA7, 0x79, 0x12, 0x4D, 0xFA, 0x87, 0x12, 0x0C,
    0xBF, 0x73, 0xD9, 0x53, 0x52, 0xBD, 0x38, 0xBF, 0xB4, 0xEE, 0xE4, 0x43, 0xD2, 0xCE, 0xD3, 0x08,
    0x0A, 0xD6, 0xE9, 0x77, 0xEB, 0xE8, 0xD4, 0x94, 0x3C, 0x3E, 0x35, 0x8D, 0x40, 0xA1, 0x00, 0x92,
    0x39, 0xDB, 0x25, 0xE8, 0x2B, 0x6E, 0x70, 0x39, 0xE2, 0x86, 0xAD, 0x2F, 0x36, 0x2D, 0x11, 0x41,
    0x8E, 0xBE, 0xD5, 0xCC, 0xA3, 0x9C, 0x24, 0x65, 0x87, 0x23, 0x37, 0x6E, 0xE5, 0xDF, 0xBF, 0xE7,
    0x8A, 0xFC, 0x83, 0x87, 0x24, 0xFE, 0x4A, 0x0B, 0x4A, 0xB3, 0xFB, 0xCF, 0xBD, 0x65, 0x03, 0x9B,
    0xEE, 0x53, 0xF7, 0xBF, 0xC0, 0x63, 0x7A, 0x62, 0x8E, 0x11, 0x62, 0x17, 0x70, 0xAB, 0x16, 0xB1,
    0xBA, 0xC0, 0x3A, 0x59, 0xC6, 0xD6, 0x8F, 0xDD, 0xF4, 0x5B, 0x14, 0x4B, 0xEE, 0xDE, 0x72, 0xBF,
    0x31, 0x7F, 0x96, 0x79, 0xC9, 0xA4, 0xA0, 0x32, 0x5B, 0xEE, 0xFC, 0xB0, 0x69, 0x6C, 0xCE, 0x99,
    0xD2, 0x0E, 0x94, 0x85, 0x98, 0x5C, 0x07, 0x56, 0xE6, 0x67, 0x41, 0xCC, 0x52, 0x00, 0x25, 0x54,
    0x5F, 0x29, 0xFC, 0x21, 0x46, 0xC9, 0x5C, 0x7E, 0xF6, 0xA4, 0x4E, 0x63, 0x59, 0x89, 0xAF, 0x46,
    0xD9, 0xCD, 0xD7, 0x33, 0x23, 0xF9, 0x79, 0x1F, 0x2A, 0xC0, 0xCA, 0x7A, 0x6F, 0x34, 0xE6, 0x03,
    0x81, 0x39, 0x6F, 0xE0, 0xBF, 0x39, 0x77, 0xEE, 0x65, 0x19, 0xA0, 0x56, 0xC7, 0x6C, 0x81, 0x61,
    0xD7, 0xE7, 0x4C, 0x8D, 0xED, 0x15, 0xAE, 0xE0, 0xC8, 0x4C, 0xF7, 0x7C, 0xD0, 0xE0, 0x7B, 0x74,
    0x9D, 0x96, 0x38, 0xDE, 0xBD, 0x5C, 0xB9, 0x29, 0xB2, 0x37, 0x3A, 0xB1, 0x3B, 0x7C, 0x0C, 0x91,
    0xD5, 0x43, 0x3B, 0xB8, 0x80, 0x19, 0x6F, 0x40, 0xC6, 0xF5, 0x10, 0xFB, 0xFA, 0x6E, 0xAD, 0x4E,
    0xBE, 0x2A, 0x9F, 0x42, 0xC7, 0x9A, 0xE9, 0xD8, 0xE5, 0xE4, 0x63, 0x9D, 0x3D, 0x21, 0x18, 0x7F,
    0xD9, 0xC9, 0xEC, 0xDF, 0x64, 0x6B, 0x82, 0xE7, 0x2E, 0xA2, 0x5C, 0x1E, 0x77, 0x44, 0x44, 0x39,
    0xE9, 0xDC, 0xEB, 0x35, 0x66, 0x5B, 0xD1, 0xA2, 0x04, 0x0A, 0x64, 0x42, 0x56, 0xC3, 0x6C, 0xD2,
    0xEE, 0x61, 0xA6, 0x28, 0x1F, 0x75, 0xAF, 0x7E, 0x08, 0x3B, 0x24, 0x0E, 0xCD, 0xCC, 0x08, 0xDF,
    0x28, 0x94, 0x66, 0xDE, 0x21, 0x07, 0x37, 0x30, 0x19, 0x90, 0x85, 0xC7, 0x0D, 0xCA, 0xD1, 0x33,
    0x19, 0xF3, 0xB3, 0xBB, 0x3B, 0x9E, 0xC0, 0xAD, 0x5A, 0xA7, 0xB0, 0xF2, 0x87, 0x6C, 0xC1, 0xE5,
    0x82, 0x3A, 0x56, 0x66, 0x80, 0x06, 0xE4, 0x29, 0x2B, 0x5E, 0x0E, 0x54, 0xEB, 0x9F, 0x0F, 0x4A,
    0x64, 0x67, 0x59, 0xC1, 0x40, 0x4D, 0x7B, 0x1B, 0x2E, 0xD0, 0x48, 0xF3, 0x2A, 0x8E, 0x36, 0xF6,
    0x00, 0xB7, 0x04, 0xF4, 0x0B, 0xC0, 0xA0, 0x36, 0x43, 0x5C, 0x47, 0x13, 0x77, 0xA8, 0xEE, 0xBE,
    0xD6, 0xA5, 0xE1, 0x62, 0xB4, 0xEC, 0xAA, 0x71, 0x8B, 0x9D, 0x34, 0x39, 0x40, 0x99, 0x30, 0xB8,
    0xA8, 0xF1, 0xB8, 0xB1, 0x4B, 0x9E, 0x32, 0xFF, 0x68, 0x72, 0x78, 0x2A, 0x39, 0x4E, 0x36, 0x38,
    0x77, 0x96, 0x93, 0xC5, 0x21, 0xE2, 0x13, 0x56, 0x7A, 0xF6, 0xBB, 0xEB, 0x51, 0xF5, 0x77, 0xD3,
    0x84, 0xD1, 0xBA, 0xC4, 0xC7, 0x06, 0x64, 0x2B, 0xA2, 0x88, 0xE8, 0xC1, 0xB9, 0xF9, 0xAE, 0x5F,
    0x50, 0x20, 0xB6, 0x13, 0x0E, 0x97, 0x7F, 0x73, 0x01, 0xC3, 0x27, 0x31, 0xE3, 0x09, 0xD3, 0xF0,
    0x9C, 0x3F, 0x51, 0x56, 0x07, 0x61, 0xFC, 0x63, 0xF9, 0x86, 0xE0, 0x01, 0x80, 0x12, 0x1F, 0xDC,
    0x68, 0x2C, 0x94, 0x73, 0x04, 0x73, 0xB5, 0x70, 0x2B, 0xEC, 0xBE, 0x34, 0x80, 0x3F, 0x0C, 0xB7,
    0xF6, 0x24, 0xC6, 0x8F, 0x94, 0x18, 0xC3, 0x4E, 0x76, 0x54, 0xA8, 0x11, 0x15, 0xFF, 0x51, 0x56,
    0xC8, 0xA3, 0x73, 0x0E, 0x8A, 0xDE, 0x7F, 0xF4, 0xFD, 0x5A, 0xC9, 0x1C, 0xAF, 0xFE, 0xE9, 0xCF,
    0x9C, 0x66, 0x61, 0x96, 0xF5, 0x91, 0x81, 0x95, 0x20, 0xDA, 0x88, 0x1A, 0x00, 0x2A, 0x0C, 0x76,
    0x76, 0x6B, 0x9C, 0x0C, 0x28, 0x40, 0xA3, 0xA7, 0x81, 0xF3, 0x8F, 0x11, 0xF9, 0xAF, 0x33, 0xE1,
    0x96, 0xEF, 0x6A, 0x94, 0xB2, 0x36, 0xFE, 0xDF, 0x00, 0x01, 0xC8, 0x44, 0xCA, 0xF9, 0x18, 0xE4,
    0x7C, 0x6E, 0x57, 0x94, 0x66, 0x01, 0xEA, 0x32, 0xBE, 0xA0, 0x5A, 0x3A, 0xE4, 0xB8, 0xB2, 0x94,
    0xEA, 0xA5, 0x29, 0xB0, 0x54, 0x6E, 0x01, 0xD5, 0x1C, 0xAF, 0xAF, 0xB6, 0xFA, 0xD6, 0x3C, 0x47,
    0xE2, 0x92, 0xEB, 0xCE, 0xCD, 0x89, 0x1C, 0x3D, 0xBC, 0x4A, 0x70, 0xBF, 0xFA, 0x82, 0x2E, 0x91,
    0xA2, 0x72, 0xE6, 0x13, 0x62, 0xA0, 0x54, 0x1F, 0x7E, 0xCD, 0x86, 0x99, 0x18, 0x28, 0x41, 0x47,
    0xAE, 0xC1, 0xA2, 0xE3, 0xE4, 0x40, 0x01, 0x6F, 0x84, 0xD7, 0x1A, 0xC9, 0xC3, 0x75, 0x6F, 0x7F,
    0xC6, 0x3D, 0xE8, 0xE4, 0x64, 0x36, 0xBD, 0x64, 0x2E, 0x44, 0x95, 0x14, 0xAC, 0x57, 0xF0, 0x8D,
    0xEA, 0xE2, 0xC2, 0xFB, 0x33, 0x8F, 0x60, 0x71, 0x1D, 0x31, 0xA0, 0x80, 0xC6, 0xF9, 0x3C, 0x07,
    0x5C, 0xEE, 0x78, 0x4C, 0xE3, 0x97, 0x05, 0x4C, 0x32, 0xFA, 0x24, 0x50, 0x3F, 0xCB, 0x0F, 0xC1,
    0x9D, 0xDD, 0x94, 0x3D, 0x43, 0xDC, 0x03, 0xEA, 0x8F, 0x3E, 0x4A, 0x0B, 0x8B, 0x77, 0x5F, 0xD1,
    0x6E, 0x6C, 0xDE, 0x73, 0x66, 0x2B, 0xF4, 0x81, 0x94, 0xD9, 0x7B, 0x75, 0x58, 0xEB, 0x66, 0x8B,
    0xD0, 0x9A, 0x60, 0xD2, 0x9B, 0x90, 0xB0, 0x83, 0xE3, 0xE8, 0x60, 0x92, 0x9A, 0x55, 0x9E, 0x84,
    0x03, 0xA1, 0x62, 0x80, 0x75, 0x5A, 0x51, 0xA8, 0x5C, 0xC8, 0xE2, 0xAA, 0x80, 0x21, 0xBF, 0x91,
    0x8A, 0x00, 0x6E, 0xE2, 0xC4, 0x14, 0x30, 0xE4, 0x20, 0x15, 0x29, 0x3F, 0x7C, 0xFD, 0xC2, 0xC8,
    0x24, 0x74, 0x4C, 0x9C, 0x98, 0x8C, 0xE6, 0x6C, 0x90, 0xAE, 0xA0, 0x17, 0x3E, 0xD5, 0xE0, 0x7E,
    0xD3, 0xF9, 0x05, 0x94, 0x44, 0xCF, 0x4B, 0xB4, 0x4E, 0xAF, 0xEE, 0x38, 0xB8, 0xD5, 0x93, 0x47,
    0xD8, 0xCD, 0xE3, 0xEE, 0x58, 0x29, 0x79, 0x72, 0x3A, 0x75, 0xFE, 0xE5, 0x1A, 0x6D, 0x92, 0xF8,
    0xB3, 0x6D, 0x6E, 0x10, 0xA5, 0x28, 0xC8, 0x9C, 0x76, 0x9D, 0xF7, 0xA5, 0xD6, 0x47, 0xD8, 0xA6,
    0x27, 0x94, 0x70, 0x9F, 0x3C, 0x99, 0xD3, 0x65, 0x61, 0x04, 0x44, 0x3C, 0x9C, 0x52, 0x9D, 0xA7,
    0x33, 0x42, 0xF2, 0x7F, 0x6E, 0x89, 0x71, 0x43, 0x9E, 0xC7, 0x8C, 0xAF, 0x5E, 0xBA, 0x5B, 0x90,
    0x19, 0xB1, 0x3B, 0xD6, 0xCD, 0x44, 0xBC, 0xEB, 0x0E, 0x43, 0xBA, 0x43, 0x4D, 0xEC, 0xC9, 0x35
]

MDUV380_ENCODE_CIPHER:Final = [
    0x00, 0xaa, 0x89, 0x89, 0x1f, 0x4b, 0xec, 0xcf, 0x42, 0x45, 0x14, 0x54, 0x00, 0x65, 0xeb, 0x66,
    0x41, 0x7d, 0x4c, 0x88, 0x49, 0x5a, 0x21, 0x0d, 0xf2, 0xf5, 0xc8, 0xe6, 0x38, 0xed, 0xbc, 0xb9,
    0xfb, 0x35, 0x71, 0x33, 0x01, 0x0a, 0x7f, 0x9e, 0x3b, 0x29, 0x03, 0xb6, 0x49, 0x3e, 0x42, 0xb8,
    0x3f, 0x9f, 0x90, 0xbd, 0xaa, 0x3a, 0x71, 0x46, 0xce, 0xcd, 0xfd, 0x18, 0x32, 0x55, 0x89, 0x4a,
    0x5f, 0xc8, 0x83, 0x9c, 0xe4, 0x06, 0x9e, 0x0a, 0x9d, 0x0d, 0x2f, 0xa1, 0x35, 0x6d, 0xd7, 0x92,
    0xea, 0xfd, 0x63, 0x85, 0x90, 0xcb, 0xf0, 0x2f, 0xd9, 0x59, 0x53, 0x27, 0xd3, 0x06, 0xb8, 0xf5,
    0xb2, 0xca, 0x88, 0x6c, 0xd0, 0x26, 0x91, 0x3b, 0xf2, 0x5b, 0x61, 0xbe, 0xcd, 0xdb, 0xf2, 0x1a,
    0xc9, 0xfd, 0x8d, 0x88, 0x04, 0xf4, 0xe8, 0xf1, 0x9a, 0x02, 0x92, 0xbc, 0x24, 0xe9, 0x90, 0xe4,
    0x7e, 0xa3, 0x49, 0x4d, 0xce, 0x52, 0x9f, 0x58, 0xc1, 0x7a, 0x5f, 0xb5, 0x18, 0x6e, 0xdb, 0x78,
    0x64, 0x08, 0xd5, 0x6f, 0x0d, 0x9d, 0x9f, 0xb3, 0x99, 0x30, 0x81, 0x7e, 0x2b, 0xe6, 0x5b, 0x3c,
    0x4a, 0xba, 0x8b, 0xe5, 0xe4, 0x72, 0x11, 0x89, 0x93, 0xd2, 0xf1, 0x2d, 0x1e, 0x0f, 0xd9, 0xd5,
    0x43, 0x87, 0x04, 0xe2, 0xb4, 0xae, 0x5e, 0x9e, 0x5f, 0x4c, 0xe9, 0x16, 0xf2, 0xe6, 0x5f, 0x28,
    0x9f, 0x79, 0x19, 0xdc, 0x1d, 0x6f, 0x2f, 0xf8, 0xef, 0xcb, 0xe1, 0xce, 0xe8, 0xa7, 0x36, 0x59,
    0xef, 0xe0, 0xe2, 0x88, 0x00, 0x10, 0x6d, 0xda, 0x73, 0xbc, 0x92, 0x2b, 0x81, 0xcf, 0xe6, 0xce,
    0x04, 0x47, 0xba, 0xdb, 0x7e, 0x2f, 0x41, 0xca, 0x5d, 0xcd, 0xf6, 0x41, 0x7d, 0x1c, 0x38, 0x2b,
    0xef, 0x7d, 0x37, 0x0a, 0xf9, 0xa9, 0x14, 0x8e, 0x5d, 0xea, 0x44, 0x66, 0xde, 0x8b, 0x36, 0x56,
    0x01, 0x8c, 0x35, 0x8a, 0x11, 0x9b, 0x8f, 0x2a, 0x65, 0x40, 0xf7, 0x2e, 0xe6, 0x58, 0x28, 0x74,
    0xcb, 0xc4, 0xcb, 0x0f, 0xa7, 0x62, 0x9b, 0xe3, 0xa6, 0x3c, 0xc7, 0x6f, 0x13, 0x00, 0x97, 0xe9,
    0x1e, 0xb1, 0x53, 0x90, 0xdd, 0x9b, 0x61, 0x3e, 0x90, 0x8c, 0xad, 0x3c, 0x29, 0x41, 0x4e, 0x5b,
    0x0c, 0x1f, 0x66, 0x40, 0x13, 0x24, 0x49, 0x00, 0xd5, 0x1c, 0xe2, 0xed, 0x28, 0x18, 0x53, 0xaf,
    0xe4, 0x1c, 0xdc, 0x96, 0xea, 0x18, 0xfe, 0x2e, 0x65, 0x19, 0xe0, 0x14, 0x50, 0xc1, 0xf1, 0x09,
    0x39, 0xf5, 0xcf, 0x45, 0x44, 0xd5, 0x68, 0x0d, 0x72, 0xf1, 0x5f, 0x88, 0x23, 0xb9, 0xb1, 0xcf,
    0xda, 0x36, 0x98, 0x43, 0x41, 0xf8, 0xaf, 0x23, 0x6d, 0x50, 0x57, 0x5e, 0x62, 0xbf, 0x5a, 0xa5,
    0xda, 0xad, 0xcf, 0xc5, 0x42, 0x5e, 0x3e, 0x34, 0x06, 0x23, 0x04, 0xe9, 0x0e, 0xcd, 0xf8, 0x71,
    0x89, 0x67, 0x4e, 0x40, 0xe9, 0x25, 0xbc, 0x45, 0x2e, 0x97, 0xdc, 0xc1, 0x68, 0x22, 0xd2, 0x58,
    0x77, 0xb1, 0x2e, 0x69, 0x16, 0xa8, 0x14, 0x9b, 0x18, 0x1a, 0x9a, 0xb8, 0xf0, 0x3b, 0x71, 0xbf,
    0x77, 0x18, 0xc8, 0x34, 0xea, 0x85, 0x6d, 0xbb, 0x32, 0x57, 0x35, 0xe5, 0x69, 0xd4, 0x9f, 0x4a,
    0x99, 0x68, 0xb4, 0xd8, 0xc7, 0x9a, 0x31, 0x6a, 0x30, 0x3d, 0xe8, 0x9c, 0xd2, 0xeb, 0x64, 0xde,
    0x2e, 0xaf, 0xcc, 0xc8, 0x4d, 0x02, 0x09, 0xae, 0x01, 0xf9, 0x2b, 0x73, 0x6d, 0xbc, 0x09, 0xa2,
    0xc7, 0x3a, 0x28, 0xba, 0x5d, 0x1b, 0xdf, 0xca, 0xd6, 0xf6, 0xb8, 0x3e, 0xbb, 0xc5, 0x18, 0xf9,
    0x36, 0x96, 0x23, 0xa4, 0x19, 0x83, 0xda, 0x45, 0x21, 0xe3, 0x86, 0x13, 0x7d, 0xc2, 0x5a, 0x89,
    0x8a, 0x8f, 0x54, 0xb9, 0xe1, 0x15, 0x64, 0xe3, 0x93, 0xad, 0xd0, 0x46, 0xb3, 0xb1, 0xd7, 0x36,
    0x15, 0x33, 0x95, 0x6f, 0x56, 0xef, 0x26, 0xa9, 0x1c, 0x7f, 0x0e, 0x6c, 0x9f, 0xce, 0xd8, 0x26,
    0x69, 0xcf, 0xfe, 0x7b, 0x5a, 0x6f, 0x09, 0xdc, 0xee, 0xc8, 0xf9, 0x5b, 0xc3, 0x97, 0xe7, 0xbd,
    0x55, 0xf0, 0xe9, 0xd1, 0x0c, 0x30, 0x36, 0x01, 0x7a, 0x34, 0x8b, 0x27, 0xdd, 0xc8, 0xcd, 0xa2,
    0xec, 0x62, 0xef, 0xa8, 0xd0, 0x11, 0x16, 0xdd, 0x70, 0xb0, 0xfb, 0x25, 0xf1, 0x5f, 0x91, 0xb7,
    0x7d, 0x34, 0xe9, 0x74, 0x44, 0x2d, 0x52, 0x76, 0xc1, 0x69, 0xc4, 0xeb, 0x3f, 0x98, 0x7f, 0x24,
    0x9b, 0xb1, 0xef, 0xe9, 0x4b, 0xe3, 0xd3, 0x10, 0x9f, 0xcd, 0x9e, 0x4e, 0x47, 0xf1, 0x1d, 0x4c,
    0x16, 0x66, 0x5b, 0xfd, 0x06, 0xce, 0xc2, 0x30, 0x7b, 0x88, 0x82, 0x61, 0xcc, 0x27, 0x37, 0xd5,
    0xff, 0x22, 0xc6, 0xe6, 0xd4, 0xcc, 0x87, 0x9b, 0x06, 0x87, 0xaa, 0x7b, 0xcd, 0x35, 0xd3, 0xa3,
    0xa7, 0xf0, 0x08, 0x17, 0x58, 0xfb, 0xcd, 0x56, 0x2f, 0xf8, 0x8d, 0x31, 0x8c, 0x5b, 0x3c, 0xdc,
    0x9f, 0x1e, 0x3b, 0x46, 0x72, 0xb7, 0x7c, 0xa6, 0x2a, 0x47, 0xe6, 0x56, 0x8a, 0x14, 0xfb, 0xe5,
    0xb8, 0x39, 0xb8, 0x68, 0x44, 0x9c, 0xbc, 0x10, 0x66, 0x21, 0xad, 0x02, 0x87, 0x1d, 0xd8, 0x62,
    0x03, 0x0e, 0x17, 0xb1, 0x2e, 0x89, 0xf8, 0x5a, 0x95, 0x73, 0x1b, 0x87, 0x86, 0x74, 0xdc, 0x39,
    0xd2, 0xa9, 0x32, 0x98, 0xd1, 0x99, 0xd7, 0x88, 0xa7, 0x6b, 0xaa, 0x7c, 0xc6, 0x56, 0x51, 0x8f,
    0xb4, 0x58, 0x22, 0xd1, 0x0f, 0x2b, 0x44, 0xde, 0xce, 0x75, 0x11, 0xb6, 0xc9, 0x3f, 0xbf, 0xc8,
    0x7c, 0xa8, 0x40, 0x50, 0x07, 0xda, 0x66, 0xe3, 0x7a, 0x3e, 0x4b, 0x48, 0x50, 0xec, 0xf0, 0x8a,
    0x39, 0x66, 0x24, 0x4b, 0x1d, 0x85, 0xa8, 0x5b, 0x5d, 0xb3, 0x90, 0x8a, 0x5c, 0x5b, 0xec, 0xba,
    0x3e, 0x9e, 0xa8, 0x38, 0xef, 0x48, 0xb1, 0x4c, 0x67, 0x02, 0x59, 0x0e, 0x2d, 0xc9, 0xfd, 0x7c,
    0x1a, 0x9e, 0xe5, 0xca, 0x60, 0x7f, 0x6b, 0xf9, 0xcb, 0x97, 0x60, 0xab, 0x46, 0xb2, 0xab, 0x36,
    0xa0, 0xf3, 0x33, 0xf7, 0x90, 0xc9, 0x00, 0xe9, 0xf7, 0x1f, 0x9d, 0x75, 0x66, 0xd3, 0xc0, 0x8c,
    0xe0, 0x6a, 0x2c, 0xf4, 0xe1, 0x02, 0xd7, 0xdf, 0x9e, 0x87, 0x48, 0xc2, 0x8f, 0x2a, 0x44, 0x64,
    0x2b, 0x0f, 0xa9, 0x36, 0xf3, 0x46, 0x9a, 0xe2, 0xb1, 0xfd, 0xdc, 0x26, 0x02, 0xf4, 0x80, 0xe3,
    0x12, 0x31, 0xc3, 0x71, 0xa7, 0xf4, 0x32, 0x36, 0x61, 0xed, 0x12, 0x77, 0x40, 0xad, 0xfe, 0x6d,
    0x66, 0x5b, 0xd2, 0x9c, 0x1e, 0xa8, 0xc8, 0x60, 0x1e, 0x04, 0xe1, 0xc9, 0x09, 0x13, 0x87, 0xa8,
    0x38, 0x5a, 0x70, 0xea, 0xba, 0x3f, 0xc5, 0x25, 0x99, 0x30, 0x84, 0x71, 0x5f, 0x22, 0x23, 0x79,
    0xd9, 0x3d, 0x76, 0xd2, 0x1b, 0xd5, 0xd2, 0x8b, 0xc4, 0x9d, 0x73, 0x05, 0x84, 0x17, 0x1b, 0x04,
    0xdb, 0x4f, 0xfc, 0x07, 0x23, 0xc9, 0xd8, 0xd5, 0xd0, 0xb8, 0x67, 0x59, 0xf7, 0x70, 0xf9, 0xaf,
    0x0d, 0x1e, 0x5c, 0x7f, 0xf2, 0xb7, 0x00, 0x8a, 0x2d, 0x2e, 0x59, 0x82, 0x7a, 0xea, 0x85, 0x1f,
    0x82, 0x77, 0x2f, 0x6f, 0xe9, 0x7c, 0xb3, 0x6e, 0x8d, 0xed, 0x82, 0xd6, 0x0d, 0x81, 0xc9, 0x38,
    0x89, 0x67, 0x4d, 0x4c, 0xa9, 0x35, 0x99, 0x86, 0xe1, 0x21, 0x5c, 0xe9, 0xf3, 0x73, 0x0d, 0x20,
    0xb5, 0x3a, 0xd0, 0xcb, 0x14, 0x3e, 0x9d, 0x17, 0x59, 0x37, 0x9f, 0x91, 0xab, 0x3c, 0xda, 0x3c,
    0xd5, 0x7e, 0x11, 0xe0, 0x4a, 0x36, 0xe7, 0xa6, 0x66, 0xdc, 0x44, 0xe2, 0xf7, 0x9a, 0xfa, 0x30,
    0xfc, 0x00, 0xa9, 0xc2, 0xad, 0xf9, 0xe0, 0xf8, 0xbb, 0xfe, 0x84, 0x31, 0xd8, 0x89, 0x76, 0xe2
]

MD380_ENCODE_CIPHER:Final = [
    0x2e, 0xdf, 0x40, 0xb5, 0xbd, 0xda, 0x91, 0x35, 0x21, 0x42, 0xe3, 0xe2, 0x6d, 0xa9, 0x0b, 0x90,
    0x31, 0x30, 0x3a, 0xfa, 0x4f, 0x05, 0x74, 0x64, 0x0a, 0x29, 0x44, 0x7e, 0x60, 0x77, 0xad, 0x8c,
    0x9a, 0xe2, 0x63, 0xc4, 0x21, 0xfe, 0x3c, 0xf7, 0x93, 0xc2, 0xe1, 0x74, 0x16, 0x8c, 0xc9, 0x2a,
    0xed, 0x65, 0x68, 0x0c, 0x49, 0x86, 0xa3, 0xba, 0x61, 0x1c, 0x88, 0x5d, 0xc4, 0x49, 0x3c, 0xd2,
    0xee, 0x6b, 0x34, 0x0c, 0x1a, 0xa0, 0xa8, 0xb3, 0x58, 0x8a, 0x45, 0x11, 0xdf, 0x4f, 0x23, 0x2f,
    0xa4, 0xe4, 0xf6, 0x3b, 0x2c, 0x8c, 0x88, 0x2d, 0x9e, 0x9b, 0x67, 0xab, 0x1c, 0x80, 0xda, 0x29,
    0x53, 0x02, 0x1a, 0x54, 0x51, 0xca, 0xbf, 0xb1, 0x97, 0x22, 0x79, 0x81, 0x70, 0xfc, 0x00, 0xe9,
    0x81, 0x36, 0x4e, 0x4f, 0xa0, 0x1c, 0x0b, 0x07, 0xea, 0x2f, 0x49, 0x2f, 0x0f, 0x25, 0x71, 0xd7,
    0xf1, 0x30, 0x7d, 0x66, 0x6e, 0x83, 0x68, 0x38, 0x79, 0x13, 0xe3, 0x8c, 0x70, 0x9a, 0x4a, 0x9e,
    0xa9, 0xe2, 0xd6, 0x10, 0x4f, 0x40, 0x14, 0x8e, 0x6c, 0x5e, 0x96, 0xb2, 0x46, 0x3e, 0xe8, 0x25,
    0xef, 0x7c, 0xc5, 0x08, 0x18, 0xd4, 0x8b, 0x92, 0x26, 0xe3, 0xed, 0xfa, 0x88, 0x32, 0xe8, 0x97,
    0x47, 0x70, 0xf8, 0x46, 0xde, 0xff, 0x8b, 0x0c, 0x4d, 0xb3, 0xb6, 0xfc, 0x69, 0xd6, 0x27, 0x5b,
    0x76, 0x6f, 0x5b, 0x03, 0xf7, 0xc3, 0x11, 0x05, 0xc5, 0x1d, 0xfe, 0x92, 0x5f, 0xcb, 0xc2, 0x1c,
    0x81, 0x69, 0x1b, 0xb8, 0xf8, 0x62, 0x58, 0xc7, 0xb4, 0xb3, 0x11, 0xd5, 0x1f, 0xf2, 0x16, 0xc1,
    0xad, 0x8f, 0xa5, 0x1e, 0xb4, 0x5b, 0xe0, 0xda, 0x7f, 0x46, 0x7d, 0x1d, 0x9e, 0x6d, 0xc0, 0x74,
    0x7f, 0x54, 0xa6, 0x2f, 0x43, 0x6f, 0x64, 0x08, 0xca, 0xe8, 0x0f, 0x05, 0x10, 0x9c, 0x9d, 0x9f,
    0xbd, 0x67, 0x0c, 0x23, 0xf7, 0xa1, 0xe1, 0x59, 0x7b, 0xe8, 0xd4, 0x64, 0xec, 0x20, 0xca, 0xe9,
    0x6a, 0xb9, 0x03, 0x73, 0x67, 0x30, 0x95, 0x16, 0xb6, 0xd9, 0x19, 0x53, 0xe5, 0xdb, 0xa4, 0x3c,
    0xcd, 0x7c, 0xf9, 0xd8, 0x67, 0x9f, 0xfc, 0xc9, 0xe2, 0x8a, 0x6a, 0x2c, 0xf2, 0xed, 0xc8, 0xc1,
    0x6a, 0x20, 0x99, 0x4c, 0x0d, 0xad, 0xd4, 0x3b, 0xa1, 0x0e, 0x95, 0x88, 0x46, 0xb8, 0x13, 0xe1,
    0x06, 0x58, 0xd2, 0x07, 0xad, 0x5c, 0x1a, 0x74, 0xdb, 0xb5, 0xa7, 0x40, 0x57, 0xdb, 0xa2, 0x45,
    0xa6, 0x12, 0xd0, 0x82, 0xdd, 0xed, 0x0a, 0xbd, 0xb3, 0x10, 0xed, 0x6c, 0xda, 0x39, 0xd2, 0xd6,
    0x90, 0x82, 0x00, 0x76, 0x71, 0xe0, 0x21, 0xa0, 0x8f, 0xf0, 0xf3, 0x67, 0xc4, 0xf3, 0x40, 0xbd,
    0x47, 0x16, 0x10, 0xdc, 0x7e, 0xf8, 0x1d, 0xe5, 0x13, 0x66, 0x87, 0xc7, 0x4a, 0x69, 0xc9, 0x63,
    0x92, 0x82, 0xec, 0xee, 0x5a, 0x34, 0xfb, 0x96, 0x25, 0xc3, 0xb6, 0x68, 0xe1, 0x3c, 0x8a, 0x71,
    0x74, 0xb5, 0xc1, 0x23, 0x99, 0xd6, 0xf7, 0xfb, 0xea, 0x98, 0xcd, 0x61, 0x3d, 0x4d, 0xe1, 0xd0,
    0x34, 0xe1, 0xfd, 0x36, 0x10, 0x5f, 0x8e, 0x9e, 0xc6, 0xb6, 0x58, 0x0c, 0x55, 0xbe, 0x69, 0xa8,
    0x56, 0x76, 0x4b, 0x1f, 0xd5, 0x90, 0x7e, 0x47, 0x5f, 0x2f, 0x25, 0x02, 0x5c, 0xef, 0x00, 0x64,
    0xa0, 0x26, 0x9a, 0x18, 0x3c, 0x69, 0xc4, 0xff, 0x9a, 0x52, 0x41, 0x1b, 0xc9, 0x81, 0xc3, 0xac,
    0x15, 0xe1, 0x17, 0x98, 0xdb, 0x2c, 0x9c, 0x10, 0x9b, 0xb2, 0xf9, 0x71, 0x4f, 0x56, 0x0f, 0x68,
    0xfb, 0xd9, 0x2d, 0x5a, 0x86, 0x5b, 0x83, 0x03, 0xc8, 0x1e, 0xda, 0x5d, 0xe4, 0x8e, 0x82, 0xc3,
    0xd8, 0x7e, 0x8b, 0x56, 0x52, 0xb5, 0x38, 0xa0, 0xc6, 0xa9, 0xb0, 0x77, 0xbd, 0x8a, 0xf7, 0x24,
    0x70, 0x82, 0x1d, 0xc5, 0x95, 0x3c, 0xb5, 0xf0, 0x79, 0xa3, 0x89, 0x99, 0x4f, 0xec, 0x8c, 0x36,
    0xc7, 0xd6, 0x10, 0x20, 0xe3, 0x30, 0x39, 0x3d, 0x07, 0x9c, 0xb2, 0xdc, 0x4f, 0x94, 0x9e, 0xe0,
    0x24, 0xaa, 0xd2, 0x21, 0x12, 0x14, 0x41, 0x0f, 0xd4, 0x67, 0xb7, 0x99, 0xb1, 0xa3, 0xcb, 0x4d,
    0x0c, 0x70, 0x0f, 0xc0, 0x36, 0xa7, 0x89, 0x30, 0x86, 0x14, 0x67, 0x68, 0xac, 0x7b, 0xee, 0xe4,
    0x42, 0xd8, 0xb4, 0x36, 0xa4, 0xeb, 0x0f, 0xa8, 0x02, 0xf4, 0xcd, 0x23, 0xb3, 0xbc, 0x25, 0x4f,
    0xcc, 0xd4, 0xee, 0xfc, 0xf2, 0x21, 0x0f, 0xc1, 0x6c, 0x99, 0x37, 0xe2, 0x7c, 0x47, 0xce, 0x77,
    0xf0, 0x95, 0x2b, 0xcb, 0xf4, 0xca, 0x07, 0x03, 0x2a, 0xd2, 0x31, 0x00, 0xfd, 0x3e, 0x84, 0x86,
    0x32, 0x8b, 0x17, 0x9d, 0xbf, 0xa7, 0xb3, 0x37, 0xe1, 0xb1, 0x8a, 0x14, 0x69, 0x00, 0x25, 0xe3,
    0x56, 0x68, 0x9f, 0xaa, 0xa9, 0xb8, 0x11, 0x67, 0x75, 0x87, 0x4d, 0xf8, 0x36, 0x31, 0xcf, 0x38,
    0x63, 0x1c, 0xf0, 0x6b, 0x47, 0x40, 0x5d, 0xdc, 0x0c, 0xe6, 0xc8, 0xc4, 0x19, 0xaf, 0xdd, 0x6e,
    0x9e, 0xd9, 0x78, 0x99, 0x6c, 0xbe, 0x15, 0x1e, 0x0b, 0x9d, 0x88, 0xd2, 0x06, 0x9d, 0xee, 0xae,
    0x8a, 0x0f, 0xe3, 0x2d, 0x2f, 0xf4, 0xf5, 0xf6, 0x16, 0xbf, 0x59, 0xbb, 0x34, 0x5c, 0xdd, 0x61,
    0xed, 0x70, 0x1e, 0x61, 0xe5, 0xe3, 0xfb, 0x6e, 0x13, 0x9c, 0x49, 0x58, 0x17, 0x8b, 0xc8, 0x30,
    0xcd, 0xed, 0x56, 0xad, 0x22, 0xcb, 0x63, 0xce, 0x26, 0xc4, 0xa5, 0xc1, 0x63, 0x0d, 0x0d, 0x04,
    0x6e, 0xb6, 0xf9, 0xca, 0xbb, 0x2f, 0xab, 0xa0, 0xb5, 0x0a, 0xfa, 0x50, 0x0e, 0x02, 0x47, 0x05,
    0x54, 0x3d, 0xb3, 0xb1, 0xc6, 0xce, 0x8f, 0xac, 0x65, 0x7e, 0x15, 0x9e, 0x4e, 0xcc, 0x55, 0x9e,
    0x46, 0x32, 0x71, 0x9b, 0x97, 0xaa, 0x0d, 0xfb, 0x1b, 0x71, 0x02, 0x83, 0x96, 0x0b, 0x52, 0x77,
    0x48, 0x87, 0x61, 0x02, 0xc3, 0x04, 0x62, 0xd7, 0xfb, 0x74, 0x0f, 0x19, 0x9c, 0xa0, 0x9d, 0x79,
    0xa0, 0x6d, 0xef, 0x9e, 0x20, 0x5d, 0x0a, 0xc9, 0x6a, 0x58, 0xc9, 0xb9, 0x55, 0xad, 0xd1, 0xcc,
    0xd1, 0x54, 0xc8, 0x68, 0xc2, 0x76, 0xc2, 0x99, 0x0f, 0x2e, 0xfc, 0xfb, 0xf5, 0x92, 0xcd, 0xdb,
    0xa2, 0xed, 0xd9, 0x99, 0xff, 0x4f, 0x88, 0x50, 0xcd, 0x48, 0xb7, 0xb9, 0xf3, 0xf0, 0xad, 0x4d,
    0x16, 0x2a, 0x50, 0xaa, 0x6b, 0x2a, 0x98, 0x38, 0xc9, 0x35, 0x45, 0x0c, 0x03, 0xa8, 0xcd, 0x0d,
    0x74, 0x3c, 0x99, 0x55, 0xdb, 0x88, 0x70, 0xda, 0x6a, 0xc8, 0x34, 0x4d, 0x19, 0xdc, 0xcc, 0x42,
    0x40, 0x94, 0x61, 0x92, 0x65, 0x2a, 0xcd, 0xfd, 0x52, 0x10, 0x50, 0x14, 0x6b, 0xec, 0x85, 0x57,
    0x3f, 0xe2, 0x95, 0x9a, 0x5d, 0x11, 0xab, 0xad, 0x69, 0x60, 0xa8, 0x3b, 0x6f, 0x7a, 0x17, 0xf3,
    0x76, 0x17, 0x63, 0xe6, 0x59, 0x7e, 0x47, 0x30, 0xd2, 0x47, 0x87, 0xdb, 0xd8, 0x66, 0xde, 0x00,
    0x2b, 0x65, 0x37, 0x2f, 0x2d, 0xf1, 0x20, 0x11, 0xf3, 0x98, 0x7b, 0x4c, 0x9c, 0xd1, 0x76, 0xa7,
    0xe1, 0x3d, 0xbe, 0x6f, 0xee, 0x2c, 0xf0, 0x19, 0x70, 0x63, 0x51, 0x28, 0xf0, 0x1d, 0xbe, 0x52,
    0x5f, 0x4f, 0xe6, 0xde, 0xf2, 0x30, 0xb6, 0x50, 0x30, 0xf9, 0x15, 0x48, 0x49, 0xe9, 0xd2, 0xa8,
    0xa9, 0x8d, 0xda, 0xf5, 0xcd, 0x3e, 0xaf, 0x00, 0x55, 0xeb, 0x15, 0xc5, 0x5b, 0x19, 0x0f, 0x93,
    0x04, 0x27, 0x09, 0x6d, 0x54, 0xd7, 0x57, 0xb1, 0x47, 0x0a, 0xde, 0xf7, 0x1d, 0xcb, 0x11, 0x3c,
    0xf5, 0x8f, 0x20, 0x40, 0x9d, 0xbb, 0x6b, 0x2c, 0xa9, 0x67, 0x3d, 0x78, 0xc2, 0x62, 0xb7, 0x0c
]

DM1701_ENCODE_CIPHER:Final = [
    0x3a, 0x04, 0x74, 0xad, 0x90, 0xae, 0xb3, 0x78, 0xde, 0xfa, 0x73, 0x8d, 0xdc, 0x8c, 0x53, 0x67,
    0x27, 0x61, 0xf3, 0x6f, 0x95, 0xc8, 0xf1, 0x1f, 0xc5, 0xad, 0x17, 0x68, 0xfb, 0x1d, 0xd2, 0x51,
    0x2b, 0x07, 0xe6, 0xe2, 0x54, 0x40, 0x61, 0x40, 0x10, 0xca, 0xcd, 0x19, 0x68, 0x56, 0xaa, 0x42,
    0xec, 0x07, 0x1c, 0x9f, 0x04, 0x79, 0xe0, 0x44, 0x81, 0x02, 0x84, 0xd9, 0x77, 0x38, 0xd8, 0x43,
    0x4f, 0xb3, 0xa0, 0x81, 0x18, 0x14, 0x8b, 0xd3, 0x1e, 0x45, 0x69, 0x22, 0xbe, 0x03, 0x98, 0x9c,
    0x78, 0x9a, 0xbf, 0x9f, 0x46, 0xf0, 0xbf, 0xd8, 0x2c, 0xc5, 0xe7, 0xac, 0x11, 0x39, 0x68, 0xd6,
    0xcd, 0x8f, 0x08, 0x52, 0x83, 0x30, 0x1a, 0x7a, 0x31, 0xf3, 0xad, 0x70, 0x85, 0x9b, 0x04, 0xbb,
    0xf3, 0xa2, 0x46, 0x35, 0x04, 0x35, 0x77, 0x24, 0xf1, 0x7f, 0xa7, 0xa7, 0x70, 0x2a, 0x69, 0x54,
    0xce, 0x23, 0x87, 0x1f, 0x3e, 0x9e, 0xf4, 0x7d, 0x71, 0x5b, 0x02, 0xca, 0x66, 0x27, 0xd5, 0xe8,
    0x84, 0xa5, 0x18, 0x2a, 0xe6, 0x4e, 0xee, 0x70, 0xf6, 0xb7, 0x2c, 0x93, 0x3d, 0x12, 0xc5, 0x03,
    0x79, 0xf8, 0x86, 0xae, 0xf0, 0x66, 0x03, 0x24, 0x05, 0x05, 0xd1, 0xf9, 0x09, 0xae, 0xf5, 0x6b,
    0x53, 0x2d, 0x9d, 0x45, 0x93, 0x45, 0x0e, 0x04, 0x63, 0xf5, 0xde, 0x37, 0x1f, 0xfa, 0x63, 0x2c,
    0xf7, 0x95, 0x6b, 0xc8, 0x42, 0x8e, 0x2d, 0xb7, 0x15, 0x79, 0x80, 0xc6, 0x15, 0x38, 0x4b, 0x8c,
    0x89, 0xc1, 0x3d, 0x50, 0xb4, 0x22, 0xbe, 0x27, 0x61, 0xc2, 0x25, 0x5d, 0xbf, 0xe9, 0x2b, 0x16,
    0x6f, 0x82, 0x9f, 0x35, 0xdc, 0x20, 0x5c, 0x7d, 0xcb, 0x40, 0x79, 0xf6, 0x33, 0xce, 0xbf, 0x93,
    0x4e, 0xea, 0x60, 0x11, 0xf0, 0xeb, 0xe5, 0x22, 0x18, 0xa4, 0x69, 0xcb, 0xc4, 0xe7, 0x05, 0x0b,
    0x0a, 0x48, 0x8b, 0xbd, 0x65, 0x23, 0x77, 0xbf, 0x4d, 0xe1, 0x22, 0x54, 0x0a, 0x76, 0x39, 0xc7,
    0xc9, 0x2e, 0x6e, 0x52, 0xf0, 0xaa, 0x6d, 0x3d, 0xaf, 0x25, 0x12, 0x4a, 0xd7, 0xfd, 0xd9, 0x51,
    0xf0, 0x6e, 0x96, 0x28, 0x86, 0xa0, 0x66, 0xc5, 0xc3, 0xe4, 0xe5, 0xa7, 0x43, 0x3a, 0xa1, 0x72,
    0x23, 0x18, 0xcf, 0xd9, 0x5b, 0x66, 0x3d, 0xc0, 0x4e, 0xcd, 0x88, 0xa2, 0xa0, 0x31, 0x8f, 0x31,
    0x48, 0x7c, 0x27, 0x3d, 0xe6, 0x9e, 0x11, 0xd7, 0x56, 0xd2, 0x29, 0xb6, 0x85, 0x22, 0xdf, 0xda,
    0x83, 0x2d, 0xeb, 0x6e, 0xda, 0x28, 0x3d, 0xf3, 0x1f, 0x23, 0x33, 0x9b, 0xc6, 0x8d, 0x0f, 0xf3,
    0x3a, 0xfb, 0xa8, 0xc5, 0x2e, 0x25, 0x60, 0x3d, 0x2d, 0x31, 0x55, 0x4a, 0x79, 0x35, 0xdc, 0x47,
    0x12, 0xf7, 0x2b, 0xdb, 0x15, 0xf7, 0x55, 0x1e, 0x47, 0xaf, 0x7c, 0xfd, 0xf2, 0x19, 0x41, 0xdf,
    0xf0, 0x72, 0x80, 0x89, 0x05, 0x3e, 0x3b, 0x3e, 0x72, 0x8c, 0xd3, 0x2b, 0xc6, 0x7b, 0x7e, 0x03,
    0xf8, 0xfd, 0xf5, 0xe7, 0xb3, 0xdb, 0x6d, 0x88, 0xf1, 0xf9, 0xc9, 0x8f, 0xcb, 0xdc, 0x0e, 0x3d,
    0x8f, 0x69, 0x17, 0x4e, 0x14, 0xef, 0x8a, 0x24, 0x4a, 0x68, 0x0a, 0x21, 0x15, 0xfc, 0xae, 0x55,
    0x5c, 0xc7, 0xb2, 0x59, 0x5d, 0xdc, 0x6d, 0x7a, 0x43, 0x8a, 0x83, 0x1a, 0xfa, 0xde, 0x5c, 0x54,
    0x42, 0x68, 0xd5, 0xdf, 0x02, 0x42, 0x35, 0x35, 0xdf, 0x4f, 0x62, 0xf4, 0x0e, 0xc1, 0x55, 0x84,
    0x66, 0xde, 0xcb, 0xfa, 0xba, 0x03, 0x3d, 0x3c, 0x65, 0xe9, 0x13, 0x66, 0x26, 0x27, 0x15, 0x6d,
    0x2f, 0xf8, 0x22, 0x03, 0x78, 0x3f, 0x24, 0xba, 0x59, 0xc8, 0x43, 0x6b, 0x58, 0xd1, 0x59, 0xd9,
    0x40, 0xc9, 0xa6, 0x92, 0x73, 0x57, 0xc6, 0x16, 0x80, 0x9e, 0xdf, 0x3b, 0xf8, 0xc0, 0x1f, 0xd0,
    0x7e, 0xa0, 0x66, 0x81, 0x1e, 0xed, 0x3f, 0xfa, 0xe0, 0x5c, 0x15, 0x50, 0x9c, 0x34, 0xa4, 0x9c,
    0x0f, 0x10, 0xad, 0xe9, 0x2f, 0xe0, 0xee, 0x50, 0xbc, 0x32, 0x51, 0x61, 0x18, 0xb0, 0x64, 0xc5,
    0x58, 0xe9, 0x09, 0x22, 0x9b, 0x54, 0x6f, 0x3f, 0x9a, 0x91, 0x40, 0x69, 0x81, 0xf3, 0x1c, 0x15,
    0xfe, 0x3c, 0x47, 0xc6, 0x97, 0xa7, 0x9e, 0x31, 0x40, 0x2c, 0xd0, 0x9f, 0x2d, 0xff, 0xca, 0x94,
    0xe5, 0x5a, 0x73, 0xae, 0x98, 0x7c, 0x9a, 0xcf, 0xb2, 0xf2, 0x2c, 0x7f, 0xb0, 0x15, 0xab, 0x8b,
    0x32, 0xd4, 0xdb, 0xf2, 0x52, 0xb3, 0xbf, 0x02, 0x35, 0x14, 0xc3, 0xbf, 0xdf, 0xb5, 0x3b, 0x84,
    0x4c, 0x7b, 0x0c, 0xed, 0xbc, 0x6e, 0xa9, 0xf3, 0x4e, 0x04, 0x42, 0x59, 0xd0, 0xa2, 0x38, 0x48,
    0xd6, 0x60, 0xd3, 0x36, 0x09, 0x0d, 0x37, 0x0c, 0xc2, 0x73, 0x94, 0x87, 0xd8, 0xdb, 0x9e, 0xde,
    0xb6, 0xd4, 0x3d, 0xa6, 0xb0, 0x31, 0x85, 0xf3, 0x97, 0x51, 0xe8, 0xc1, 0x8a, 0xa3, 0xaa, 0x92,
    0x10, 0x68, 0x96, 0x58, 0x64, 0xbb, 0xf0, 0x94, 0x10, 0xcf, 0xaa, 0xc0, 0xbd, 0x79, 0xda, 0xeb,
    0x4a, 0xee, 0x6c, 0xa3, 0x1b, 0xcd, 0x14, 0x17, 0xb4, 0x60, 0x87, 0x7d, 0x86, 0x1f, 0xeb, 0xb2,
    0x08, 0x75, 0x8c, 0x20, 0x0a, 0xc7, 0xd0, 0xe5, 0x47, 0xb3, 0x6d, 0x32, 0x39, 0x95, 0xd9, 0xf0,
    0x31, 0x50, 0x03, 0xaa, 0xa6, 0x4b, 0x40, 0xa7, 0xcd, 0xb9, 0x88, 0x56, 0x6b, 0x1e, 0xe2, 0xf0,
    0xe8, 0x0e, 0x1d, 0x58, 0xa4, 0x38, 0xc1, 0x46, 0x8e, 0xa4, 0x45, 0xa4, 0xf2, 0x39, 0x82, 0x38,
    0x92, 0x82, 0x68, 0x84, 0xf9, 0xb2, 0xf0, 0xea, 0x0c, 0xe5, 0x51, 0x14, 0xe2, 0xa8, 0x77, 0x93,
    0xd5, 0xbc, 0xb1, 0xc6, 0xd9, 0x17, 0xaa, 0xfe, 0x0d, 0x2c, 0x9a, 0xdf, 0x90, 0x6c, 0xbd, 0x0a,
    0x96, 0x0d, 0x05, 0xf9, 0xbb, 0x0a, 0x0c, 0x29, 0x97, 0x6b, 0x4c, 0x7f, 0x92, 0xc6, 0x92, 0xe5,
    0xf9, 0x06, 0xb0, 0x34, 0x52, 0x6b, 0x72, 0x56, 0xed, 0xd2, 0xd4, 0xac, 0xbc, 0x37, 0x72, 0xad,
    0x64, 0x78, 0x40, 0xd0, 0x94, 0x5b, 0x7b, 0xac, 0x96, 0xd3, 0xe0, 0x5e, 0x24, 0x7f, 0x1b, 0x2c,
    0x7c, 0x74, 0x82, 0x68, 0xb7, 0x3c, 0x03, 0x96, 0x56, 0x1e, 0x5b, 0xd1, 0x1e, 0xa0, 0x89, 0x6a,
    0x26, 0x4b, 0x83, 0xd3, 0x2e, 0xae, 0x27, 0xbb, 0x32, 0xa6, 0x74, 0x7b, 0x3f, 0xdc, 0xfa, 0xb1,
    0x86, 0x8e, 0x8f, 0x2a, 0xaf, 0x93, 0x44, 0x06, 0x6f, 0x99, 0x98, 0x16, 0x5d, 0xb2, 0xeb, 0x89,
    0x01, 0x0f, 0x35, 0xc8, 0x2e, 0x0a, 0xf7, 0x9e, 0x92, 0x6a, 0x72, 0x9c, 0x8c, 0xe3, 0x18, 0xbc,
    0x3d, 0xdd, 0x40, 0x44, 0xe2, 0x77, 0x1d, 0xed, 0x61, 0xca, 0xf1, 0x45, 0x21, 0x72, 0x7e, 0x52,
    0x1f, 0x4b, 0xbd, 0x78, 0x3f, 0x78, 0xd3, 0x9c, 0xe0, 0xaa, 0x41, 0x8a, 0xb2, 0x9e, 0x5b, 0x94,
    0xcc, 0xe8, 0xfb, 0x7c, 0xf9, 0xf0, 0x76, 0x95, 0x53, 0x3a, 0xcf, 0x24, 0x14, 0xea, 0x2b, 0x0c,
    0xa8, 0x87, 0x85, 0xab, 0x07, 0xff, 0xa3, 0xff, 0x41, 0xeb, 0x49, 0x0d, 0x5a, 0x15, 0xac, 0x83,
    0x59, 0x37, 0x29, 0x9c, 0x9c, 0x06, 0x37, 0x44, 0x6e, 0x6f, 0x9b, 0x7d, 0xdc, 0x21, 0xdb, 0x01,
    0xc4, 0x4b, 0xf4, 0x28, 0x2e, 0xa7, 0x4f, 0x0d, 0xdf, 0xb7, 0xf2, 0xec, 0x2c, 0x4f, 0xf4, 0xcf,
    0x0d, 0x53, 0x33, 0x6a, 0x72, 0xc2, 0x49, 0x43, 0xda, 0xf3, 0xbb, 0x16, 0x21, 0x1f, 0x74, 0x77,
    0x99, 0x20, 0x72, 0xb9, 0x5d, 0x78, 0xc0, 0x0f, 0xe2, 0x95, 0xa4, 0xf1, 0xcf, 0x54, 0x19, 0xc1,
    0x0f, 0xc3, 0x7f, 0xaf, 0x24, 0x2b, 0x92, 0xda, 0xbe, 0x4e, 0x99, 0xb7, 0x8c, 0xed, 0xdf, 0xb7
]


# Python 3 deprecated getargspec in favour of getfullargspec, but
# Python 2 doesn't have the latter, so detect which one to use
# F1RMB: make it work with Python >= 3.11
try:
    getargspec = getattr(inspect, "getfullargspec", inspect.getargspec)
except AttributeError:
    getargspec = getattr(inspect, "getfullargspec", inspect.getfullargspec)

if "length" in getargspec(usb.util.get_string).args:
    # PyUSB 1.0.0.b1 has the length argument
    def get_string(dev, index):
        return usb.util.get_string(dev, 255, index)
else:
    # PyUSB 1.0.0.b2 dropped the length argument
    def get_string(dev, index):
        return usb.util.get_string(dev, index)


def GetSHA256Checksum(filename):
    fHash = ''

    try:
        with open(filename,"rb") as f:
            bytes = f.read()

        fHash = hashlib.sha256(bytes).hexdigest()
    except:
        return ''

    return fHash


def dfu_get_string(i=0):
    try:
        # Mac calling convention.
        return usb.util.get_string(__dev, 255, i, None)
    except:
        # Linux calling convention.
        return usb.util.get_string(__dev, i, None)


def check_if_in_bootloader():
    bootl = dfu_get_string(1)
    return (bootl == u'AnyRoad Technology')


def find_dfu_cfg_descr(descr):
    if len(descr) == 9 and descr[0] == 9 and descr[1] == _DFU_DESCRIPTOR_TYPE:
        nt = collections.namedtuple(
            "CfgDescr",
            [
                "bLength",
                "bDescriptorType",
                "bmAttributes",
                "wDetachTimeOut",
                "wTransferSize",
                "bcdDFUVersion",
            ],
        )
        return nt(*struct.unpack("<BBBHHH", bytearray(descr)))
    return None


def init(**kwargs):
    """Initializes the found DFU device so that we can program it."""
    global __dev, __cfg_descr
    devices = get_dfu_devices(**kwargs)
    if not devices:
        raise ValueError(" !!! " + "No DFU device found")
    if len(devices) > 1:
        raise ValueError(" !!! " + "Multiple DFU devices found")
    __dev = devices[0]
    __dev.set_configuration()

    # Claim DFU interface
    usb.util.claim_interface(__dev, __DFU_INTERFACE)

    # Find the DFU configuration descriptor, either in the device or interfaces
    __cfg_descr = None
    for cfg in __dev.configurations():
        __cfg_descr = find_dfu_cfg_descr(cfg.extra_descriptors)
        if __cfg_descr:
            break
        for itf in cfg.interfaces():
            __cfg_descr = find_dfu_cfg_descr(itf.extra_descriptors)
            if __cfg_descr:
                break

    # Get device into idle state
    for attempt in range(4):
        status = get_status()
        if status == __DFU_STATE_DFU_IDLE:
            break
        elif status == __DFU_STATE_DFU_DOWNLOAD_IDLE or status == __DFU_STATE_DFU_UPLOAD_IDLE:
            abort_request()
        else:
            clr_status()


def abort_request():
    """Sends an abort request."""
    __dev.ctrl_transfer(0x21, __DFU_ABORT, 0, __DFU_INTERFACE, None, __TIMEOUT)


def clr_status():
    """Clears any error status (perhaps left over from a previous session)."""
    __dev.ctrl_transfer(0x21, __DFU_CLRSTATUS, 0, __DFU_INTERFACE, None, __TIMEOUT)


def get_status():
    """Get the status of the last operation."""
    try:
        stat = __dev.ctrl_transfer(0xA1, __DFU_GETSTATUS, 0, __DFU_INTERFACE, 6, 20000)
    except:
        return
    # firmware can provide an optional string for any error
    if stat[5]:
        message = get_string(__dev, stat[5])
        if message:
            print(message)

    return stat[4]


def check_status(stage, expected):
    status = get_status()
    if status != expected:
        raise SystemExit(" !!! DFU: %s failed (%s)" % (stage, __DFU_STATUS_STR.get(status, status)))

def wait_for_idle():
    # Seems to be something strange with the GetStatus command that effects the internal logic state of the bootloader
    # These commands can't be optimised to not pre-read the GetStatus, possibly if the status is Idle it should not be cleared.
    ##STDFU_GetStatus(ref hDevice, ref status);
    status = get_status()
    while (status != __DFU_STATE_DFU_IDLE):
        clr_status()
        status = get_status()


def page_erase(addr):
    """Erases a single page."""
    if __verbose:
        print("Erasing page: 0x%x..." % (addr))

    # Send DNLOAD with first byte=0x41 and page address
    buf = struct.pack("<BI", 0x41, addr)
    __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)

    # Execute last command
    check_status("erase", __DFU_STATE_DFU_DOWNLOAD_BUSY)

    # Check command state
    check_status("erase", __DFU_STATE_DFU_DOWNLOAD_IDLE)


def set_address(addr):
    """Sets the address for the next operation."""
    # Send DNLOAD with first byte=0x21 and page address
    buf = struct.pack("<BI", 0x21, addr)
    __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, buf, __TIMEOUT)

    # Execute last command
    check_status("set address", __DFU_STATE_DFU_DOWNLOAD_BUSY)

    # Check command state
    check_status("set address", __DFU_STATE_DFU_DOWNLOAD_IDLE)


def send_custom_command(a, b):
    a &= 0xFF
    b &= 0xFF
    __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, 0, [a, b])
    status = get_status()  # this changes state
    time.sleep(0.1)
    status = get_status()  # this gets the status
    wait_for_idle()


def write_firmware_data(blockNumber, chunk):
    """Download a chunk of bytes, for block number. This routine assumes that memory has
    already been erased.
    """
    __dev.ctrl_transfer(0x21, __DFU_DNLOAD, blockNumber, __DFU_INTERFACE, chunk, __TIMEOUT)

    # Execute last command
    check_status("write memory", __DFU_STATE_DFU_DOWNLOAD_BUSY)

    # Check command state
    check_status("write memory", __DFU_STATE_DFU_DOWNLOAD_IDLE)


def exit_dfu(bootloaderHT=False):
    """Exit DFU mode, and start running the program."""
    # Set jump address
    set_address(0x08000000)

    # Send DNLOAD with 0 length to exit DFU
    __dev.ctrl_transfer(0x21, __DFU_DNLOAD, 0, __DFU_INTERFACE, None, __TIMEOUT)

    try:
        # Execute last command
        status = get_status()
        if (((bootloaderHT == False) and (status != __DFU_STATE_DFU_MANIFEST)) or (status != None)):
            print("Failed to reset device {}".format(status))

        # Release device
        usb.util.dispose_resources(__dev)
    except:
        pass


class FilterDFU(object):
    """Class for filtering USB devices to identify devices which are in DFU
    mode.
    """

    def __call__(self, device):
        for cfg in device:
            for intf in cfg:
                return intf.bInterfaceClass == 0xFE and intf.bInterfaceSubClass == 1


def get_dfu_devices(*args, **kwargs):
    """Returns a list of USB devices which are currently in DFU mode.
    Additional filters (like idProduct and idVendor) can be passed in
    to refine the search.
    """

    # Convert to list for compatibility with newer PyUSB
    return list(usb.core.find(*args, find_all=True, custom_match=FilterDFU(), **kwargs))


def named(values, names):
    """Creates a dict with `names` as fields, and `values` as values."""
    return dict(zip(names.split(), values))


def get_memory_layout(device):
    """Returns an array which identifies the memory layout. Each entry
    of the array will contain a dictionary with the following keys:
        addr        - Address of this memory segment.
        last_addr   - Last address contained within the memory segment.
        size        - Size of the segment, in bytes.
        num_pages   - Number of pages in the segment.
        page_size   - Size of each page, in bytes.
    """

    cfg = device[0]
    intf = cfg[(0, 0)]
    mem_layout_str = get_string(device, intf.iInterface)
    mem_layout = mem_layout_str.split("/")
    result = []
    for mem_layout_index in range(1, len(mem_layout), 2):
        addr = int(mem_layout[mem_layout_index], 0)
        segments = mem_layout[mem_layout_index + 1].split(",")
        seg_re = re.compile(r"(\d+)\*(\d+)(.)(.)")
        for segment in segments:
            seg_match = seg_re.match(segment)
            num_pages = int(seg_match.groups()[0], 10)
            page_size = int(seg_match.groups()[1], 10)
            multiplier = seg_match.groups()[2]
            if multiplier == "K":
                page_size *= 1024
            if multiplier == "M":
                page_size *= 1024 * 1024
            size = num_pages * page_size
            last_addr = addr + size - 1
            result.append(
                named(
                    (addr, last_addr, size, num_pages, page_size),
                    "addr last_addr size num_pages page_size",
                )
            )
            addr += size
    return result


def list_dfu_devices(*args, **kwargs):
    """Prints a lits of devices detected in DFU mode."""
    devices = get_dfu_devices(*args, **kwargs)
    if not devices:
        raise SystemExit(" !!! No DFU capable devices found")
    for device in devices:
        print(
            "Bus {} Device {:03d}: ID {:04x}:{:04x}".format(
                device.bus, device.address, device.idVendor, device.idProduct
            )
        )
        layout = get_memory_layout(device)
        print("Memory Layout")
        for entry in layout:
            print(
                "    0x{:x} {:2d} pages of {:3d}K bytes".format(
                    entry["addr"], entry["num_pages"], entry["page_size"] // 1024
                )
            )


def cli_progress(addr, offset, size, strPrefix=None):
    """Prints a progress report suitable for use on the command line."""
    width = 25
    done = offset * width // size
    print(
        "\r{}0x{:08x} {:7d} [{}{}] {:3d}% ".format((strPrefix if strPrefix != None else ""),
            addr, size, "=" * done, " " * (width - done), offset * 100 // size
        ),
        end="",
    )
    try:
        sys.stdout.flush()
    except OSError:
        pass  # Ignore Windows CLI "WinError 87" on Python 3.6
    if offset == size:
        print("")


def patch_and_download_firmware(firmwareFile, langFile, platform, progress=None):
    """Patch the open firmware using the Official one as codec source, then download to the device."""
    encBuf = []
    officialFirmware = ""
    firmwareSize = 0
    foundOfficialFirmware = True
    OFFICIAL_FIRMWARE_HEADER = [ 0x4F, 0x75, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x42, 0x69, 0x6E, 0x00, 0x00 ]
    addresses = [
        0x0800C000,
        0x08010000,
        0x08020000,
        0x08040000,
        0x08060000,
        0x08080000,
        0x080A0000,
        0x080C0000,
        0x080E0000,
        0x08100000
    ]
    sizes = [
        0x4000,   # 0c
        0x10000,  # 1
        0x20000,  # 2
        0x20000,  # 4
        0x20000,  # 6
        0x20000,  # 8
        0x20000,  # a
        0x20000,  # c
        0x20000,  # e
        0x20000   # 10
    ]
    blockEnds = [0x11, 0x41, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81]
    firmwareStartAddress = addresses[0]

    try:
        with open(firmwareFile, 'rb') as f:
            openFirmwareData = bytearray(f.read())
    except:
        print(" !!! " + "Unable to read {}. Exiting...".format(firmwareFile))
        sys.exit(-1)

    firmwareSize = len(openFirmwareData)

    # Check firmware
    filename, fileExtension = os.path.splitext(firmwareFile)
    if (fileExtension != '.bin'):
        print(" !!! " + "Wrong file extension: {}".format(fileExtension))
        sys.exit(-3)

    # Patch the firmware with the specified User language
    if (langFile != None):
        langMergedSuccess = False

        # Ignore languages (e.g JA builds)
        if (openFirmwareData.find(b'IGNRLANG') >= 0):
            print(" III " + "Ignore language rule found")
            langMergedSuccess = True
        else:
            try:
                with open(langFile, 'rb') as lf:
                    langFileData = bytearray(lf.read())
            except:
                print(" !!! " + "Unable to read language file '{}'. Exiting...".format(langFile))
                sys.exit(-1)

            # Check if user language is valid (per header)
            if (langFileData.find(b'GD77LANG', 0, 8) == 0):
                langVersion = langFileData[8] << 24 | langFileData[9] << 16 | langFileData[10] << 8 | langFileData[11]
                found = openFirmwareData.find(b'GD77LANG')

                while (found > 0):
                    # Search for the first language (English)
                    if ((found > 0) and (openFirmwareData.find(b'English', (found + 12), ((found + 12) + 7)) == (found + 12))):
                        firmwareLangVersion = openFirmwareData[found + 8] << 24 | openFirmwareData[found + 9] << 16 | openFirmwareData[found + 10] << 8 | openFirmwareData[found + 11]

                        # The specified language file matches the firmware language version: good
                        if (firmwareLangVersion == langVersion):
                            # Looking for the language #2 tag
                            found = openFirmwareData.find(b'GD77LANG', found + 12)
                            if (found > 0):
                                # Copy the language data into the firmware
                                for i in range(0, len(langFileData)):
                                    openFirmwareData[found + i] = langFileData[i]

                                print(" *** " + "{} merged".format(os.path.splitext(os.path.basename(langFile))[0]))
                                langMergedSuccess = True
                        else:
                            print(" !!! " + "Language version missmatch: FW:{}, File:{}".format(hex(firmwareLangVersion), hex(langVersion)))


                        break;

                    found = openFirmwareData.find(b'GD77LANG', found + 8)
            else:
                print(" !!! " + "Wrong Language file header: {} {} {} {} {} {} {} {}".format(hex(langFileData[0]), hex(langFileData[1]), hex(langFileData[2]), hex(langFileData[3]), hex(langFileData[4]), hex(langFileData[5]), hex(langFileData[6]), hex(langFileData[7])))

        if (langMergedSuccess == False):
            print(" !!! " + "Failed to merge language file {}".format(langFile))

    # Flash the firmware now
    print(" *** " + "Flashing Firmware: {}".format(firmwareFile))

    # Check if specified FW is not the official one
    for i in range(0, len(OFFICIAL_FIRMWARE_HEADER)):
        if (openFirmwareData[i] != OFFICIAL_FIRMWARE_HEADER[i]):
            foundOfficialFirmware = False;
            break

    if (foundOfficialFirmware == True):
        print(" !!! " + "You can't flash the official donor firmware file. Exiting...")
        sys.exit(-2)

    ## Get the codec source firmware
    try:
        officialFirmware = config['GLOBAL']['SourceSTM32Firmware']
    except KeyError:
        officialFirmware = ""

    if ((officialFirmware != '') and (os.path.isfile(officialFirmware) == True)):
        print(" *** " + "Patching for DMR" + " (using " + officialFirmware + ")")
        try:
            with open(officialFirmware, "rb") as f:
                f.seek(0xC2C7C)
                encBuf = bytearray(f.read(0x48BB0))
        except:
            print(" !!! " + "Patching failed. Exiting...")
            sys.exit(-8)

	## Decrypt the official MD9600 firmware
        AMBE_SECTION_CIPHER_OFSET = (0x6937c % 1024)
        for j in range(0, len(encBuf)):
            encBuf[j] ^= MD9600_ENCODE_CIPHER[(j + AMBE_SECTION_CIPHER_OFSET) % 1024]

        ## Merge the section of the official fw into the open firmware.
        if (len(openFirmwareData) >= len(encBuf) + 0x6937c):
            for j in range(0, len(encBuf)):
                openFirmwareData[0x6937c + j] = encBuf[j];

    else:
        print(" *** " + "Flashing FM Only firmware")

    ## Encode the open firmware
    print(" *** " + "Flashing a ", end="")
    if (platform == FWPlatformOutput.MD_9600):
        print("MD-9600", end="")
        for j in range(0, len(openFirmwareData)):
            openFirmwareData[j] ^= MD9600_ENCODE_CIPHER[j % 1024]
    elif (platform == FWPlatformOutput.MD_UV380) or (platform == FWPlatformOutput.MD_2017):
        print(("MD-UV380" if platform == FWPlatformOutput.MD_UV380 else "MD-2017"), end="")
        for j in range(0, len(openFirmwareData)):
            openFirmwareData[j] ^= MDUV380_ENCODE_CIPHER[j % 1024]
    elif (platform == FWPlatformOutput.MD_380):
        print("MD-380", end="")
        for j in range(0, len(openFirmwareData)):
            openFirmwareData[j] ^= MD380_ENCODE_CIPHER[j % 1024]
    elif (platform == FWPlatformOutput.DM_1701):
        print("DM-1701", end="")
        for j in range(0, len(openFirmwareData)):
            openFirmwareData[j] ^= DM1701_ENCODE_CIPHER[j % 1024];
    print(":")

    wait_for_idle()

    send_custom_command(0x91, 0x01)
    send_custom_command(0x91, 0x31)

    for address in addresses:
        print("\r ***   > Erasing address@ 0x%x" % address,end="")
        sys.stdout.flush()
        page_erase(address)

    bytesWrote = 0
    totalBlocks = len(openFirmwareData) / BLOCK_WRITE_SIZE
    totalBytes = totalBlocks * BLOCK_WRITE_SIZE
    blockStart = 2
    addressIndex = 0
    numAddresses = len(addresses)

    while (addressIndex < numAddresses):  # for each section
        address = addresses[addressIndex]
        size = sizes[addressIndex]

        if progress:
            progress(address, 0, size, " ***   > ")

        set_address(address)

        if addressIndex != len(addresses) - 1:
            assert address + size == addresses[addressIndex + 1]

        dataWritten = 0
        blockNumber = blockStart

        while len(openFirmwareData) > 0 and size > dataWritten:  # for each block
            assert blockNumber <= blockEnds[addressIndex]

            chunk, openFirmwareData = openFirmwareData[:BLOCK_WRITE_SIZE], openFirmwareData[BLOCK_WRITE_SIZE:]

            ## Filling data chunk with 0xFF to reach BLOCK_WRITE_SIZE length
            if len(chunk) < BLOCK_WRITE_SIZE:
                chunk += b'\xFF' * (BLOCK_WRITE_SIZE - len(chunk))

            write_firmware_data(blockNumber, chunk)

            if progress and blockNumber % 2 == 0:
                progress(address, dataWritten, size, " ***   > ")

            chunkLen = len(chunk)
            dataWritten += chunkLen
            bytesWrote += chunkLen
            blockNumber += 1

        addressIndex += 1

    print("\r *** 100% complete, now safe to disconnect and/or reboot the radio.")
    exit_dfu(True)


def main():
    """Test program for verifying this files functionality."""
    global __verbose
    global config
    home = str(Path.home())
    configFilename = home + "/.gd77firmwareloader.ini"
    config = configparser.ConfigParser()

    config.read(configFilename)

    try:
        officialFirmware = config['GLOBAL']['SourceSTM32Firmware']
    except KeyError:
        officialFirmware = ""

    # Parse CMD args
    parser = argparse.ArgumentParser(
        prog="OpenGD77 firmware loader for STM32 transceivers based",
        description="OpenGD77 STM32 FW Loader v{}".format(version),
        epilog=" -- DMR Enabled -- " if (officialFirmware != '' and os.path.isfile(officialFirmware) == True) else " -- FM Only -- "
    )

    parser.add_argument(
        "-l", "--list", help="list available DFU devices.", action="store_true", default=False
    )
    parser.add_argument("--vid", help="USB Vendor ID (default: 0x{:X}).".format(defaultVID), type=lambda x: int(x, 0), default=defaultVID)
    parser.add_argument("--pid", help="USB Product ID (default: 0x{:X}).".format(defaultPID), type=lambda x: int(x, 0), default=defaultPID)
    parser.add_argument(
        "-s", "--source", help="official firmware file used as codec source (**Note**: you have to select a source firmware at least once, the file location is stored and used by default).", dest="source", default=False
    )
    parser.add_argument(
        "-f", "--firmware", help="flash the specified firmware.", dest="firmware", default=False
    )
    models="Select transceiver model. Models are: {}".format(", ".join(str(x) for x in FWOutputPlatforms[:-1])) + "."
    parser.add_argument(
        "-m", "--model", help=models, dest="model", default=False
    )
    parser.add_argument(
        "-L", "--lang", help="use <LANG>.gla file as secondary language.", dest="lang", default=None
    )
    #parser.add_argument(
    #    "-v", "--verbose", help="increase output verbosity", action="store_true", default=False
    #)
    args = parser.parse_args()

    #__verbose = args.verbose


    print("OpenGD77 STM32 FW Loader v{}\n".format(version))

    kwargs = {}
    if args.vid:
        kwargs["idVendor"] = args.vid


    if args.pid:
        kwargs["idProduct"] = args.pid


    if args.list:
        list_dfu_devices(**kwargs)
        return


    if args.source:
        fwFile = args.source
        hash256 = GetSHA256Checksum(fwFile)

        # The checksum doesn't match, ignoring this SGL file
        if (hash256 != FW2645_SHA256_Checksum):
            print(" !!! " + "The specified codec source firmware isn't valid...")
            fwFile = ''

        try:
            config.set('GLOBAL', 'SourceSTM32Firmware', fwFile)
        except:
            config['GLOBAL'] = { 'SourceSTM32Firmware': fwFile }

        # Save configuration file
        if (fwFile != ''):
            print(" *** " + "Use {} as codec source.".format(fwFile))
        else:
            print(" *** " + "Unset codec source firmware file.")

        with open(configFilename, 'w') as cfgFile:
            config.write(cfgFile)

        # Re-read the configuration file
        config.read(configFilename)
        officialFirmware = fwFile


    if args.model:
        try:
            index = FWOutputPlatforms.index(args.model)
        except ValueError as e:
            print(" !!! " + "Model \"{}\" is unknown. Available models are: {}. Exiting...".format(args.model, ", ".join(str(x) for x in FWOutputPlatforms[:-1])))
            sys.exit(-5)

        global FWPlatformFormat
        FWPlatformFormat = FWPlatformOutput(index)

        if (FWPlatformFormat == FWPlatformOutput.UNKNOWN):
            print(" !!! " + "Unsupported model. Exiting...")
            sys.exit(-5)

    ## Try to connect the device
    deviceConnected = True
    try:
        init(**kwargs)
    except:
        deviceConnected = False

    command_run = False


    ## Upload the firmware (optionnaly, patch it first)
    if args.firmware and deviceConnected == True:
        ## A radio model HAS to be specified, there is no default here.
        if (FWPlatformFormat == FWPlatformOutput.UNKNOWN):
            print(" !!! " + "You should specify a radio model (available models are: {}). Exiting...".format(", ".join(str(x) for x in FWOutputPlatforms[:-1])))
            sys.exit(-5)

        ## Download the firmware (and optionnaly patch it for the codec) to the device.
        patch_and_download_firmware(args.firmware, args.lang, FWPlatformFormat, cli_progress)
        command_run = True


    if command_run:
        print(" *** " + "Finished")
    else:
        if deviceConnected == False:
            print(" !!! " + "Failed to connect to the device.")
            sys.exit(-6)
        else:
            print(" !!! " + "No command specified")
            sys.exit(-7)

    sys.exit(0)


if __name__ == "__main__":
    main()
