Add loading scripts, update README.md, requirements.txt and .gitignore

replace/89c7af81023d3166efe6e62625f6440ab997d909
Silvano Seva 2020-03-30 16:58:03 +02:00 zatwierdzone przez Niccolò Izzo
rodzic f709cb0388
commit ab306eab66
7 zmienionych plików z 1142 dodań i 0 usunięć

13
.gitignore vendored
Wyświetl plik

@ -30,3 +30,16 @@
*.exe
*.out
*.app
# Binaries
*.bin
*.elf
*.hex
*.map
# Python byte-compiled
*/__pycache__
# Kdevelop files
*.kdev4
.kdev4/

50
README.md 100644
Wyświetl plik

@ -0,0 +1,50 @@
# OpenDMR
## Open source firmware for the TYT MD380
This firmware is *highly experimental* and is not in a usable state right now,
however contributions and testing are welcome and accepted.
## Installation
To build and install the firmware, first clone this repository:
```
git clone https://github.com/n1zzo/OpenDMR
```
To build the firmware you need to have a toolchain for the ARM ISA installed
on you system, you can install one using your package manager.
You can then proceed in building the firmware:
```
cd OpenDMR
make
```
If everithing compiled without errors you can connect your radio via USB,
put it in recovery mode (by powering it on with the PTT and the button
above it pressed), and flash the firmware:
```
make flash
```
Now you can power cycle your radio and enjoy the new breath of freedom!
## License
This software is released under the GNU GPL v3, the modified wrapping scripts
from Travis Goodspeed are licensed in exchange of two liters of India Pale Ale,
we still owe you the two liters, Travis!
## Credits
OpenDMR was created by:
- Niccolò Izzo IU2KIN <n@izzo.sh>
- Silvano Seva IU2KWO <silseva@fastwebnet.it>
All this was made possible by the huge reverse engineering effort of
Travis Goodspeed and all the contributors of [md380tools](https://github.com/travisgoodspeed/md380tools).
A huge thank goes to Roger Clark, and his [OpenGD77](https://github.com/rogerclarkmelbourne/OpenGD77) which inspired this project,
and which we aspire becoming a part of.

1
requirements.txt 100644
Wyświetl plik

@ -0,0 +1 @@
pyusb

293
scripts/DFU.py 100644
Wyświetl plik

@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
import struct
import sys
import time
class Enumeration(object):
def __init__(self, id, name):
self._id = id
self._name = name
setattr(self.__class__, name, self)
self.map[id] = self
def __int__(self):
return self.id
def __repr__(self):
return self.name
@property
def id(self):
return self._id
@property
def name(self):
return self._name
@classmethod
def create_from_map(cls):
for id, name in list(cls.map.items()):
cls(id, name)
class Request(Enumeration):
map = {
0: 'DETACH',
1: 'DNLOAD',
2: 'UPLOAD',
3: 'GETSTATUS',
4: 'CLRSTATUS',
5: 'GETSTATE',
6: 'ABORT',
}
Request.create_from_map()
class State(Enumeration):
map = {
0: 'appIDLE',
1: 'appDETACH',
2: 'dfuIDLE',
3: 'dfuDNLOAD_SYNC',
4: 'dfuDNBUSY',
5: 'dfuDNLOAD_IDLE',
6: 'dfuMANIFEST_SYNC',
7: 'dfuMANIFEST',
8: 'dfuMANIFEST_WAIT_RESET',
9: 'dfuUPLOAD_IDLE',
10: 'dfuERROR',
}
State.create_from_map()
class Status(Enumeration):
map = {
0x00: 'OK',
0x01: 'errTARGET',
0x02: 'errFILE',
0x03: 'errWRITE',
0x04: 'errERASE',
0x05: 'errCHECK_ERASED',
0x06: 'errPROG',
0x07: 'errVERIFY',
0x08: 'errADDRESS',
0x09: 'errNOTDONE',
0x0A: 'errFIRMWARE',
0x0B: 'errVENDOR',
0x0C: 'errUSBR',
0x0D: 'errPOR',
0x0E: 'errUNKNOWN',
0x0F: 'errSTALLEDPKT',
}
Status.create_from_map()
class DFU(object):
verbose = False
def __init__(self, device, alt):
device.set_interface_altsetting(interface=0, alternate_setting=alt)
self._device = device
def detach(self):
"""Detaches from the DFU target."""
self._device.ctrl_transfer(0x21, Request.DETACH, 0, 0, None)
def get_string(self, i=0):
"""Gets a USB descriptor string, to distinguish firmware types."""
import usb
# Linux and Mac have different calling conventions for usb.util.get_string(),
# so we'll try each of them and hope for the best.
try:
# Mac calling convention.
return usb.util.get_string(self._device, 255, i, None)
except:
# Linux calling convention.
return usb.util.get_string(self._device, i, None)
def bcd(self, b):
"""Converts a byte from BCD to integer."""
return int("%02x" % b)
def get_time(self):
"""Returns a datetime object for the radio's current time."""
self.md380_custom(0x91, 0x01) # Programming Mode
self.md380_custom(0xA2, 0x08) # Access the clock memory.
time = self.upload(0, 7) # Read the time bytes as BCD.
# hexdump("Time is: "+time);
from datetime import datetime
dt = datetime(self.bcd(time[0]) * 100 + self.bcd(time[1]),
self.bcd(time[2]),
self.bcd(time[3]),
self.bcd(time[4]),
self.bcd(time[5]),
self.bcd(time[6]))
return dt
def set_time(self):
from datetime import datetime
if len(sys.argv) == 3:
try:
time_to_set = datetime.strptime(sys.argv[2], '%m/%d/%Y %H:%M:%S')
except ValueError:
print("Usage: md380-dfu settime \"mm/dd/yyyy HH:MM:SS\" (with quotes)")
exit()
else:
time_to_set = datetime.now()
dt = datetime.strftime(time_to_set, '%Y%m%d%H%M%S').decode("hex")
self.md380_custom(0x91, 0x02)
self.download(0, b"\xb5" + dt)
self.wait_till_ready()
self.md380_reboot()
def download(self, block_number, data):
self._device.ctrl_transfer(0x21, Request.DNLOAD, block_number, 0, data)
# time.sleep(0.1);
def set_address(self, address):
a = address & 0xFF
b = (address >> 8) & 0xFF
c = (address >> 16) & 0xFF
d = (address >> 24) & 0xFF
self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [0x21, a, b, c, d])
self.get_status() # this changes state
status = self.get_status() # this gets the status
if status[2] == State.dfuDNLOAD_IDLE:
if self.verbose:
print("Set pointer to 0x%08x." % address)
self.enter_dfu_mode()
else:
if self.verbose:
print("Failed to set pointer.")
return False
return True
def erase_block(self, address):
a = address & 0xFF
b = (address >> 8) & 0xFF
c = (address >> 16) & 0xFF
d = (address >> 24) & 0xFF
self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [0x41, a, b, c, d])
# time.sleep(0.5);
self.get_status() # this changes state
status = self.get_status() # this gets the status
if status[2] == State.dfuDNLOAD_IDLE:
if self.verbose:
print("Erased 0x%08x." % address)
self.enter_dfu_mode()
else:
if self.verbose:
print("Failed to erase block.")
return False
return True
def md380_custom(self, a, b):
"""Sends a secret MD380 command."""
a &= 0xFF
b &= 0xFF
self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [a, b])
self.get_status() # this changes state
time.sleep(0.1)
status = self.get_status() # this gets the status
if status[2] == State.dfuDNLOAD_IDLE:
if self.verbose:
print("Sent custom %02x %02x." % (a, b))
self.enter_dfu_mode()
else:
print("Failed to send custom %02x %02x." % (a, b))
return False
return True
def md380_reboot(self):
"""Sends the MD380's secret reboot command."""
a = 0x91
b = 0x05
self._device.ctrl_transfer(0x21, Request.DNLOAD, 0, 0, [a, b])
try:
self.get_status() # this changes state
except:
pass
return True
def upload(self, block_number, length, index=0):
if self.verbose:
print("Fetching block 0x%x." % block_number)
data = self._device.ctrl_transfer(0xA1, # request type
Request.UPLOAD, # request
block_number, # wValue
index, # index
length) # length
return data
def get_command(self):
data = self._device.ctrl_transfer(0xA1, # request type
Request.UPLOAD, # request
0, # wValue
0, # index
32) # length
self.get_status()
return data
def get_status(self):
status_packed = self._device.ctrl_transfer(0xA1, Request.GETSTATUS, 0, 0, 6)
status = struct.unpack('<BBBBBB', status_packed)
return (Status.map[status[0]], (((status[1] << 8) | status[2]) << 8) | status[3],
State.map[status[4]], status[5])
def clear_status(self):
self._device.ctrl_transfer(0x21, Request.CLRSTATUS, 0, 0, None)
def get_state(self):
state_packed = self._device.ctrl_transfer(0xA1, Request.GETSTATE, 0, 0, 1)
return State.map[struct.unpack('<B', state_packed)[0]]
def abort(self):
self._device.ctrl_transfer(0x21, Request.ABORT, 0, 0, None)
def wait_till_ready(self, desired_state=State.dfuIDLE):
state = 11
status, timeout, state, discarded = self.get_status()
while state != State.dfuIDLE:
self.clear_status()
status, timeout, state, discarded = self.get_status()
return
def enter_dfu_mode(self):
action_map = {
State.dfuDNLOAD_SYNC: self.abort,
State.dfuDNLOAD_IDLE: self.abort,
State.dfuMANIFEST_SYNC: self.abort,
State.dfuUPLOAD_IDLE: self.abort,
State.dfuERROR: self.clear_status,
State.appIDLE: self.detach,
State.appDETACH: self._wait,
State.dfuDNBUSY: self._wait,
State.dfuMANIFEST: self.abort,
State.dfuMANIFEST_WAIT_RESET: self._wait,
State.dfuIDLE: self._wait
}
while True:
state = self.get_state()
if state == State.dfuIDLE:
break
action = action_map[state]
action()
def _wait(self):
time.sleep(0.1)
def widestr(self, str):
tr = ""
for c in str:
tr = tr + c + "\0"
return tr + "\0\0"

Wyświetl plik

@ -0,0 +1,88 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
# Copyright 2011 Dominic Spill
# Copyright 2010 TheSeven
#
# This file was forked form Project Ubertooth.
#
from struct import pack, unpack
crc_table = []
# From freemyipod.org
for i in range(256):
t = i
for j in range(8):
if t & 1:
t = (t >> 1) ^ 0xedb88320
else:
t >>= 1
crc_table.append(t)
def crc32(data):
crc = 0xffffffff
for byte in data:
crc = (crc >> 8) ^ crc_table[(crc ^ ord(byte)) & 0xff]
return crc
def check_suffix(firmware):
"""Check the dfu suffix"""
print('Checking firmware signature')
data = firmware[:-4]
length = ord(firmware[-5])
suffix = firmware[-length:]
# Will always have these fields
dwCRC = unpack('<L', suffix[12:])[0]
bLength = unpack('<B', suffix[11])[0]
ucDfuSig = unpack('<3s', suffix[8:11])[0]
bcdDFU = unpack('<H', suffix[6:8])[0]
# bcdDFU, ucDfuSig, bLength, dwCRC = unpack('<H3sBL', suffix[6:])
if bLength != 16:
raise Exception("Unknown DFU suffix length: %s" % type(bLength))
# We only know about dfu version 1.0/1.1
# This needs to be smarter to support other versions if/when they exist
if bcdDFU != 0x0100:
raise Exception("Unknown DFU version: %d" % bcdDFU)
# Suffix bytes are reversed
if ucDfuSig != 'UFD':
raise Exception("DFU Signature mismatch: not a DFU file")
crc = crc32(data)
if crc != dwCRC:
raise Exception("CRC mismatch: calculated: 0x%x, found: 0x%x" % (crc, dwCRC))
# Extract additional fields now that we know the suffix contains them
idVendor = unpack('<H', suffix[4:6])[0]
idProduct = unpack('<H', suffix[2:4])[0]
# Version information that we can't verify
bcdDevice = unpack('<H', suffix[0:2])[0]
return length, idVendor, idProduct
def add_suffix(firmware, vendor, product):
bcdDevice = 0
idProduct = product
idVendor = vendor
bcdDFU = 0x0100
ucDfuSig = 'UFD'
bLength = 16
suffix = pack('<4H3sB', bcdDevice, idProduct, idVendor, bcdDFU, ucDfuSig, bLength)
firmware += suffix
crc = crc32(firmware)
firmware += pack('<I', crc)
return firmware

Wyświetl plik

@ -0,0 +1,493 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2010, 2011 Michael Ossmann
# Copyright 2015 Travis Goodspeed
# Copyright 2020 Niccolò Izzo, Silvano Seva
#
# This file was forked from Project Ubertooth as a DFU client for the
# TYT MD380, an amateur radio for the DMR protocol on the UHF bands.
# This script implements a lot of poorly understood extensions unique
# to the MD380.
import sys
import time
import usb.core
import dfu_suffix
from DFU import DFU, State, Status
# The tricky thing is that *THREE* different applications all show up
# as this same VID/PID pair.
#
# 1. The Tytera application image.
# 2. The Tytera bootloader at 0x08000000
# 3. The mask-rom bootloader from the STM32F405.
md380_vendor = 0x0483
md380_product = 0xdf11
# application_offset = 0x08000000
# ram_offset = 0x20000000
# application_size = 0x00040000
def download(dfu, data, flash_address):
block_size = 1 << 8
sector_size = 1 << 12
if flash_address & (sector_size - 1) != 0:
raise Exception('Download must start at flash sector boundary')
block_number = flash_address / block_size
assert block_number * block_size == flash_address
try:
while len(data) > 0:
packet, data = data[:block_size], data[block_size:]
if len(packet) < block_size:
packet += b'\xFF' * (block_size - len(packet))
dfu.download(block_number, packet)
status, timeout, state, discarded = dfu.get_status()
sys.stdout.write('.')
sys.stdout.flush()
block_number += 1
finally:
print()
def download_codeplug(dfu, data):
"""Downloads a codeplug to the MD380."""
block_size = 1024
dfu.md380_custom(0x91, 0x01) # Programming Mode
dfu.md380_custom(0x91, 0x01) # Programming Mode
# dfu.md380_custom(0xa2,0x01); #Returns "DR780...", seems to crash client.
# hexdump(dfu.get_command()); #Gets a string.
dfu.md380_custom(0xa2, 0x02)
hexdump(dfu.get_command()) # Gets a string.
time.sleep(2)
dfu.md380_custom(0xa2, 0x02)
dfu.md380_custom(0xa2, 0x03)
dfu.md380_custom(0xa2, 0x04)
dfu.md380_custom(0xa2, 0x07)
dfu.erase_block(0x00000000)
dfu.erase_block(0x00010000)
dfu.erase_block(0x00020000)
dfu.erase_block(0x00030000)
dfu.set_address(0x00000000) # Zero address, used by configuration tool.
# sys.exit();
status, timeout, state, discarded = dfu.get_status()
# print(status, timeout, state, discarded)
block_number = 2
try:
while len(data) > 0:
packet, data = data[:block_size], data[block_size:]
if len(packet) < block_size:
packet += b'\xFF' * (block_size - len(packet))
dfu.download(block_number, packet)
state = 11
while state != State.dfuDNLOAD_IDLE:
status, timeout, state, discarded = dfu.get_status()
# print(status, timeout, state, discarded)
sys.stdout.write('.')
sys.stdout.flush()
block_number += 1
finally:
print()
def hexdump(string):
"""God awful hex dump function for testing."""
buf = ""
i = 0
for c in string:
buf += "%02x" % c
i += 1
if i & 3 == 0:
buf += " "
if i & 0xf == 0:
buf += " "
if i & 0x1f == 0:
buf += "\n"
print(buf)
def upload_bootloader(dfu, filename):
"""Dumps the bootloader, but only on Mac."""
# dfu.set_address(0x00000000); # Address is ignored, so it doesn't really matter.
# Bootloader stretches from 0x08000000 to 0x0800C000, but our
# address and block number are ignored, so we set the block size
# ot 0xC000 to yank the entire thing in one go. The application
# comes later, I think.
block_size = 0xC000 # 0xC000;
f = None
if filename is not None:
f = open(filename, 'wb')
print("Dumping bootloader. This only works in radio mode, not programming mode.")
try:
data = dfu.upload(2, block_size)
status, timeout, state, discarded = dfu.get_status()
if len(data) == block_size:
print("Got it all!")
else:
print("Only got %i bytes. Older versions would give it all." % len(data))
# raise Exception('Upload failed to read full block. Got %i bytes.' % len(data))
if f is not None:
f.write(data)
else:
hexdump(data)
finally:
print("Done.")
def upload_codeplug(dfu, filename):
"""Uploads a codeplug from the radio to the host."""
dfu.md380_custom(0x91, 0x01) # Programming Mode
# dfu.md380_custom(0xa2,0x01); #Returns "DR780...", seems to crash client.
# hexdump(dfu.get_command()); #Gets a string.
dfu.md380_custom(0xa2, 0x02)
dfu.md380_custom(0xa2, 0x02)
dfu.md380_custom(0xa2, 0x03)
dfu.md380_custom(0xa2, 0x04)
dfu.md380_custom(0xa2, 0x07)
dfu.set_address(0x00000000) # Zero address, used by configuration tool.
f = open(filename, 'wb')
block_size = 1024
try:
# Codeplug region is 0 to 3ffffff, but only the first 256k are used.
for block_number in range(2, 0x102):
data = dfu.upload(block_number, block_size)
status, timeout, state, discarded = dfu.get_status()
# print("Status is: %x %x %x %x" % (status, timeout, state, discarded))
sys.stdout.write('.')
sys.stdout.flush()
if len(data) == block_size:
f.write(data)
# hexdump(data);
else:
raise Exception('Upload failed to read full block. Got %i bytes.' % len(data))
# dfu.md380_reboot()
finally:
print("Done.")
def download_firmware(dfu, data):
""" Download new firmware binary to the radio. """
addresses = [
0x0800c000,
0x08010000,
0x08020000,
0x08040000,
0x08060000,
0x08080000,
0x080a0000,
0x080c0000,
0x080e0000]
sizes = [0x4000, # 0c
0x10000, # 1
0x20000, # 2
0x20000, # 4
0x20000, # 6
0x20000, # 8
0x20000, # a
0x20000, # c
0x20000] # e
block_ends = [0x11, 0x41, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81]
try:
# Are we in the right mode?
mfg = dfu.get_string(1)
if mfg != u'AnyRoad Technology':
print("""ERROR: You forgot to enter the bootloader.
Please hold PTT and the button above it while rebooting. You
should see the LED blinking green and red, and then your
radio will be radio to accept this firmware update.""")
sys.exit(1)
print("Beginning firmware upgrade.")
sys.stdout.flush() # let text appear immediately (for mingw)
status, timeout, state, discarded = dfu.get_status()
assert state == State.dfuIDLE
dfu.md380_custom(0x91, 0x01)
dfu.md380_custom(0x91, 0x31)
for address in addresses:
if dfu.verbose:
print("Erasing address@ 0x%x" % address)
sys.stdout.flush()
dfu.erase_block(address)
block_size = 1024
block_start = 2
address_idx = 0
if data[0:14] == b"OutSecurityBin": # skip header if present
if dfu.verbose:
print("Skipping 0x100 byte header in data file")
header, data = data[:0x100], data[0x100:]
print("Writing firmware:")
assert len(addresses) == len(sizes)
numaddresses = len(addresses)
while address_idx < numaddresses: # for each section
print("%0d%% complete" % (address_idx * 100 / numaddresses))
sys.stdout.flush() # let text appear immediately (for mingw)
address = addresses[address_idx]
size = sizes[address_idx]
dfu.set_address(address)
if address_idx != len(addresses) - 1:
assert address + size == addresses[address_idx + 1]
datawritten = 0
block_number = block_start
while len(data) > 0 and size > datawritten: # for each block
assert block_number <= block_ends[address_idx]
packet, data = data[:block_size], data[block_size:]
if len(packet) < block_size:
packet += b'\xFF' * (block_size - len(packet))
dfu.download(block_number, packet)
dfu.wait_till_ready()
datawritten += len(packet)
block_number += 1
# if dfu.verbose: sys.stdout.write('.'); sys.stdout.flush()
# if dfu.verbose: sys.stdout.write('_\n'); sys.stdout.flush()
address_idx += 1
print("100% complete, now safe to disconnect and/or reboot radio")
return True
except Exception as e:
print(e)
return False
def upload(dfu, flash_address, length, path):
# block_size = 1 << 8
block_size = 1 << 14
print("Address: 0x%08x" % flash_address)
print("Block Size: 0x%04x" % block_size)
if flash_address & (block_size - 1) != 0:
raise Exception('Upload must start at block boundary')
block_number = flash_address / block_size
assert block_number * block_size == flash_address
# block_number=0x8000;
print("Block Number: 0x%04x" % block_number)
cmds = dfu.get_command()
print("%i supported commands." % len(cmds))
for cmd in cmds:
print("Command %02x is supported by UPLOAD." % cmd)
dfu.set_address(0x08001000) # RAM
block_number = 2
f = open(path, 'wb')
try:
while length > 0:
data = dfu.upload(block_number, block_size)
status, timeout, state, discarded = dfu.get_status()
print("Status is: %x %x %x %x" % (status, timeout, state, discarded))
sys.stdout.write('.')
sys.stdout.flush()
if len(data) == block_size:
f.write(data)
else:
raise Exception('Upload failed to read full block. Got %i bytes.' % len(data))
block_number += 1
length -= len(data)
finally:
f.close()
print()
def detach(dfu):
if dfu.get_state() == State.dfuIDLE:
dfu.detach()
print('Detached')
else:
print('In unexpected state: %s' % dfu.get_state())
def init_dfu(alt=0):
dev = usb.core.find(idVendor=md380_vendor,
idProduct=md380_product)
if dev is None:
raise RuntimeError('Device not found')
dfu = DFU(dev, alt)
dev.default_timeout = 6000
try:
dfu.enter_dfu_mode()
except usb.core.USBError as e:
if len(e.args) > 0 and e.args[0] == 'Pipe error':
raise RuntimeError('Failed to enter DFU mode. Is bootloader running?')
else:
raise e
return dfu
def usage():
print("""
Usage: md380-dfu <command> <arguments>
Write a codeplug to the radio. Supported file types: RDT (from official Tytera editor), DFU (with suffix) and raw binaries
md380-dfu write <codeplug.rdt>
md380-dfu write <codeplug.dfu>
md380-dfu write <codeplug.bin>
Write firmware to the radio.
md380-dfu upgrade <firmware.bin>
Read a codeplug and write it to a file.
md380-dfu read <codeplug.bin>
Dump the bootloader from Flash memory.
md380-dfu readboot <filename.bin>
Print the time from the MD380.
md380-dfu time
Set time and date on MD380 to system time or specified time.
md380-dfu settime
md380-dfu settime "mm/dd/yyyy HH:MM:SS" (with quotes)
Detach the bootloader and execute the application firmware:
md380-dfu detach
Close the bootloader session.
md380-dfu reboot
Upgrade to new firmware:
md380-dfu upgrade foo.bin
""")
def main():
try:
if len(sys.argv) == 3:
if sys.argv[1] == 'read':
dfu = init_dfu()
upload_codeplug(dfu, sys.argv[2])
print('Read complete')
elif sys.argv[1] == 'readboot':
print("This only works from OS X. Use the one in md380-tool with patched firmware for other bootloaders.")
dfu = init_dfu()
upload_bootloader(dfu, sys.argv[2])
elif sys.argv[1] == "upgrade":
dfu = init_dfu()
with open(sys.argv[2], 'rb') as f:
data = f.read()
result = download_firmware(dfu, data)
elif sys.argv[1] == 'write':
f = open(sys.argv[2], 'rb')
data = f.read()
f.close()
firmware = None
if sys.argv[2][-4:] == '.dfu':
suf_len, vendor, product = dfu_suffix.check_suffix(data)
dfu = init_dfu()
firmware = data[:-suf_len]
elif sys.argv[2][-4:] == '.rdt':
if len(data) == 262709 and data[0:5] == 'DfuSe':
dfu = init_dfu()
firmware = data[549:len(data) - 16]
else:
print('%s not a valid codeplug (wrong size, or wrong magic).' % sys.argv[2])
else:
dfu = init_dfu()
firmware = data
if firmware is not None:
download_codeplug(dfu, firmware)
print('Write complete')
elif sys.argv[1] == 'sign':
filename = sys.argv[2]
f = open(filename, 'rb')
firmware = f.read()
f.close()
data = dfu_suffix.add_suffix(firmware, md380_vendor, md380_product)
dfu_file = filename[:-4] + '.dfu'
f = open(dfu_file, 'wb')
f.write(data)
f.close()
print("Signed file written: %s" % dfu_file)
elif sys.argv[1] == 'settime':
dfu = init_dfu()
dfu.set_time()
else:
usage()
elif len(sys.argv) == 2:
if sys.argv[1] == 'detach':
dfu = init_dfu()
dfu.set_address(0x08000000) # Radio Application
detach(dfu)
elif sys.argv[1] == 'time':
dfu = init_dfu()
print(dfu.get_time())
elif sys.argv[1] == 'settime':
dfu = init_dfu()
dfu.set_time()
elif sys.argv[1] == 'reboot':
dfu = init_dfu()
dfu.md380_custom(0x91, 0x01) # Programming Mode
dfu.md380_custom(0x91, 0x01) # Programming Mode
# dfu.md380_custom(0x91,0x01); #Programming Mode
# dfu.drawtext("Rebooting",160,50);
dfu.md380_reboot()
elif sys.argv[1] == 'abort':
dfu = init_dfu()
dfu.abort()
else:
usage()
else:
usage()
except RuntimeError as e:
print(e.args[0])
exit(1)
except Exception as e:
print(e)
# print(dfu.get_status())
exit(1)
if __name__ == '__main__':
main()

204
scripts/md380_fw.py 100755
Wyświetl plik

@ -0,0 +1,204 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This script was originally part of md380tools codebase by Travis Goodspeed
import argparse
import binascii
import struct
import sys
class TYTFW(object):
def pad(self, align=512, byte=b'\xff'):
pad_length = (align - len(self.app) % align) % align
self.app += byte * pad_length
def unwrap(self, img):
header = struct.Struct(self.header_fmt)
header = header.unpack(img[:256])
self.start = header[6]
app_len = header[7]
self.app = self.crypt(img[256:256 + app_len])
assert header[0].startswith(self.magic)
assert header[1].startswith(self.jst)
assert header[3].startswith(self.foo)
assert header[4] == self.bar
assert 0x8000000 <= header[6] < 0x8200000
assert header[7] == len(img) - 512
def crypt(self, data):
return self.xor(data, self.key)
@staticmethod
def xor(a, b):
# FIXME: optimized version
out = bytearray()
l = max(len(a), len(b))
for i in range(l):
out += bytes([a[i % len(a)] ^ b[i % len(b)]])
return out
class MD380FW(TYTFW):
# The stream cipher of MD-380 OEM firmware updates boils down
# to a cyclic, static XOR key block, and here it is:
key = (b'\x2e\xdf\x40\xb5\xbd\xda\x91\x35\x21\x42\xe3\xe2\x6d\xa9\x0b\x90'
b'\x31\x30\x3a\xfa\x4f\x05\x74\x64\x0a\x29\x44\x7e\x60\x77\xad\x8c'
b'\x9a\xe2\x63\xc4\x21\xfe\x3c\xf7\x93\xc2\xe1\x74\x16\x8c\xc9\x2a'
b'\xed\x65\x68\x0c\x49\x86\xa3\xba\x61\x1c\x88\x5d\xc4\x49\x3c\xd2'
b'\xee\x6b\x34\x0c\x1a\xa0\xa8\xb3\x58\x8a\x45\x11\xdf\x4f\x23\x2f'
b'\xa4\xe4\xf6\x3b\x2c\x8c\x88\x2d\x9e\x9b\x67\xab\x1c\x80\xda\x29'
b'\x53\x02\x1a\x54\x51\xca\xbf\xb1\x97\x22\x79\x81\x70\xfc\x00\xe9'
b'\x81\x36\x4e\x4f\xa0\x1c\x0b\x07\xea\x2f\x49\x2f\x0f\x25\x71\xd7'
b'\xf1\x30\x7d\x66\x6e\x83\x68\x38\x79\x13\xe3\x8c\x70\x9a\x4a\x9e'
b'\xa9\xe2\xd6\x10\x4f\x40\x14\x8e\x6c\x5e\x96\xb2\x46\x3e\xe8\x25'
b'\xef\x7c\xc5\x08\x18\xd4\x8b\x92\x26\xe3\xed\xfa\x88\x32\xe8\x97'
b'\x47\x70\xf8\x46\xde\xff\x8b\x0c\x4d\xb3\xb6\xfc\x69\xd6\x27\x5b'
b'\x76\x6f\x5b\x03\xf7\xc3\x11\x05\xc5\x1d\xfe\x92\x5f\xcb\xc2\x1c'
b'\x81\x69\x1b\xb8\xf8\x62\x58\xc7\xb4\xb3\x11\xd5\x1f\xf2\x16\xc1'
b'\xad\x8f\xa5\x1e\xb4\x5b\xe0\xda\x7f\x46\x7d\x1d\x9e\x6d\xc0\x74'
b'\x7f\x54\xa6\x2f\x43\x6f\x64\x08\xca\xe8\x0f\x05\x10\x9c\x9d\x9f'
b'\xbd\x67\x0c\x23\xf7\xa1\xe1\x59\x7b\xe8\xd4\x64\xec\x20\xca\xe9'
b'\x6a\xb9\x03\x73\x67\x30\x95\x16\xb6\xd9\x19\x53\xe5\xdb\xa4\x3c'
b'\xcd\x7c\xf9\xd8\x67\x9f\xfc\xc9\xe2\x8a\x6a\x2c\xf2\xed\xc8\xc1'
b'\x6a\x20\x99\x4c\x0d\xad\xd4\x3b\xa1\x0e\x95\x88\x46\xb8\x13\xe1'
b'\x06\x58\xd2\x07\xad\x5c\x1a\x74\xdb\xb5\xa7\x40\x57\xdb\xa2\x45'
b'\xa6\x12\xd0\x82\xdd\xed\x0a\xbd\xb3\x10\xed\x6c\xda\x39\xd2\xd6'
b'\x90\x82\x00\x76\x71\xe0\x21\xa0\x8f\xf0\xf3\x67\xc4\xf3\x40\xbd'
b'\x47\x16\x10\xdc\x7e\xf8\x1d\xe5\x13\x66\x87\xc7\x4a\x69\xc9\x63'
b'\x92\x82\xec\xee\x5a\x34\xfb\x96\x25\xc3\xb6\x68\xe1\x3c\x8a\x71'
b'\x74\xb5\xc1\x23\x99\xd6\xf7\xfb\xea\x98\xcd\x61\x3d\x4d\xe1\xd0'
b'\x34\xe1\xfd\x36\x10\x5f\x8e\x9e\xc6\xb6\x58\x0c\x55\xbe\x69\xa8'
b'\x56\x76\x4b\x1f\xd5\x90\x7e\x47\x5f\x2f\x25\x02\x5c\xef\x00\x64'
b'\xa0\x26\x9a\x18\x3c\x69\xc4\xff\x9a\x52\x41\x1b\xc9\x81\xc3\xac'
b'\x15\xe1\x17\x98\xdb\x2c\x9c\x10\x9b\xb2\xf9\x71\x4f\x56\x0f\x68'
b'\xfb\xd9\x2d\x5a\x86\x5b\x83\x03\xc8\x1e\xda\x5d\xe4\x8e\x82\xc3'
b'\xd8\x7e\x8b\x56\x52\xb5\x38\xa0\xc6\xa9\xb0\x77\xbd\x8a\xf7\x24'
b'\x70\x82\x1d\xc5\x95\x3c\xb5\xf0\x79\xa3\x89\x99\x4f\xec\x8c\x36'
b'\xc7\xd6\x10\x20\xe3\x30\x39\x3d\x07\x9c\xb2\xdc\x4f\x94\x9e\xe0'
b'\x24\xaa\xd2\x21\x12\x14\x41\x0f\xd4\x67\xb7\x99\xb1\xa3\xcb\x4d'
b'\x0c\x70\x0f\xc0\x36\xa7\x89\x30\x86\x14\x67\x68\xac\x7b\xee\xe4'
b'\x42\xd8\xb4\x36\xa4\xeb\x0f\xa8\x02\xf4\xcd\x23\xb3\xbc\x25\x4f'
b'\xcc\xd4\xee\xfc\xf2\x21\x0f\xc1\x6c\x99\x37\xe2\x7c\x47\xce\x77'
b'\xf0\x95\x2b\xcb\xf4\xca\x07\x03\x2a\xd2\x31\x00\xfd\x3e\x84\x86'
b'\x32\x8b\x17\x9d\xbf\xa7\xb3\x37\xe1\xb1\x8a\x14\x69\x00\x25\xe3'
b'\x56\x68\x9f\xaa\xa9\xb8\x11\x67\x75\x87\x4d\xf8\x36\x31\xcf\x38'
b'\x63\x1c\xf0\x6b\x47\x40\x5d\xdc\x0c\xe6\xc8\xc4\x19\xaf\xdd\x6e'
b'\x9e\xd9\x78\x99\x6c\xbe\x15\x1e\x0b\x9d\x88\xd2\x06\x9d\xee\xae'
b'\x8a\x0f\xe3\x2d\x2f\xf4\xf5\xf6\x16\xbf\x59\xbb\x34\x5c\xdd\x61'
b'\xed\x70\x1e\x61\xe5\xe3\xfb\x6e\x13\x9c\x49\x58\x17\x8b\xc8\x30'
b'\xcd\xed\x56\xad\x22\xcb\x63\xce\x26\xc4\xa5\xc1\x63\x0d\x0d\x04'
b'\x6e\xb6\xf9\xca\xbb\x2f\xab\xa0\xb5\x0a\xfa\x50\x0e\x02\x47\x05'
b'\x54\x3d\xb3\xb1\xc6\xce\x8f\xac\x65\x7e\x15\x9e\x4e\xcc\x55\x9e'
b'\x46\x32\x71\x9b\x97\xaa\x0d\xfb\x1b\x71\x02\x83\x96\x0b\x52\x77'
b'\x48\x87\x61\x02\xc3\x04\x62\xd7\xfb\x74\x0f\x19\x9c\xa0\x9d\x79'
b'\xa0\x6d\xef\x9e\x20\x5d\x0a\xc9\x6a\x58\xc9\xb9\x55\xad\xd1\xcc'
b'\xd1\x54\xc8\x68\xc2\x76\xc2\x99\x0f\x2e\xfc\xfb\xf5\x92\xcd\xdb'
b'\xa2\xed\xd9\x99\xff\x4f\x88\x50\xcd\x48\xb7\xb9\xf3\xf0\xad\x4d'
b'\x16\x2a\x50\xaa\x6b\x2a\x98\x38\xc9\x35\x45\x0c\x03\xa8\xcd\x0d'
b'\x74\x3c\x99\x55\xdb\x88\x70\xda\x6a\xc8\x34\x4d\x19\xdc\xcc\x42'
b'\x40\x94\x61\x92\x65\x2a\xcd\xfd\x52\x10\x50\x14\x6b\xec\x85\x57'
b'\x3f\xe2\x95\x9a\x5d\x11\xab\xad\x69\x60\xa8\x3b\x6f\x7a\x17\xf3'
b'\x76\x17\x63\xe6\x59\x7e\x47\x30\xd2\x47\x87\xdb\xd8\x66\xde\x00'
b'\x2b\x65\x37\x2f\x2d\xf1\x20\x11\xf3\x98\x7b\x4c\x9c\xd1\x76\xa7'
b'\xe1\x3d\xbe\x6f\xee\x2c\xf0\x19\x70\x63\x51\x28\xf0\x1d\xbe\x52'
b'\x5f\x4f\xe6\xde\xf2\x30\xb6\x50\x30\xf9\x15\x48\x49\xe9\xd2\xa8'
b'\xa9\x8d\xda\xf5\xcd\x3e\xaf\x00\x55\xeb\x15\xc5\x5b\x19\x0f\x93'
b'\x04\x27\x09\x6d\x54\xd7\x57\xb1\x47\x0a\xde\xf7\x1d\xcb\x11\x3c'
b'\xf5\x8f\x20\x40\x9d\xbb\x6b\x2c\xa9\x67\x3d\x78\xc2\x62\xb7\x0c')
def __init__(self, base_address=0x800c000):
self.magic = b'OutSecurityBin'
self.jst = b'JST51'
self.foo = b'\x30\x02\x00\x30\x00\x40\x00\x47'
self.bar = (b'\x01\x0d\x02\x03\x04\x05\x06\x07'
b'\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
b'\x10\x11\x12\x13\x14\x15\x16\x17'
b'\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f'
b'\x20')
self.start = base_address
self.app = None
self.footer = b'OutputBinDataEnd'
self.header_fmt = '<16s7s9s16s33s47sLL120s'
self.footer_fmt = '<240s16s'
def wrap(self):
bin = b''
header = struct.Struct(self.header_fmt)
footer = struct.Struct(self.footer_fmt)
self.pad()
app = self.crypt(self.app)
bin += header.pack(
self.magic, self.jst, b'\xff' * 9, self.foo,
self.bar, b'\xff' * 47, self.start, len(app),
b'\xff' * 120)
bin += self.crypt(self.app)
bin += footer.pack(b'\xff' * 240, self.footer)
return bin
def main():
def hex_int(x):
return int(x, 0)
parser = argparse.ArgumentParser(description='Wrap and unwrap MD-380 firmware')
parser.add_argument('--wrap', '-w', dest='wrap', action='store_true',
default=False,
help='wrap app into firmware image')
parser.add_argument('--unwrap', '-u', dest='unwrap', action='store_true',
default=False,
help='unwrap app from firmware image')
parser.add_argument('--addr', '-a', dest='addr', type=hex_int,
default=0x800c000,
help='base address in flash')
parser.add_argument('--offset', '-o', dest='offset', type=hex_int,
default=0,
help='offset to skip in app binary')
parser.add_argument('input', nargs=1, help='input file')
parser.add_argument('output', nargs=1, help='output file')
args = parser.parse_args()
if not (args.wrap ^ args.unwrap):
sys.stderr.write('ERROR: --wrap or --unwrap?')
sys.exit(5)
print('DEBUG: reading "%s"' % args.input[0])
with open(args.input[0], 'rb') as f:
input = f.read()
if args.wrap:
if args.offset > 0:
print('INFO: skipping 0x%x bytes in input file' % args.offset)
md = MD380FW(args.addr)
md.app = input[args.offset:]
if len(md.app) == 0:
sys.stderr.write('ERROR: seeking beyond end of input file\n')
sys.exit(5)
output = md.wrap()
print('INFO: base address 0x{0:x}'.format(md.start))
print('INFO: length 0x{0:x}'.format(len(md.app)))
elif args.unwrap:
md = MD380FW(args.addr)
try:
md.unwrap(input)
except AssertionError:
sys.stderr.write('WARNING: Funky header:\n')
for i in range(0, 256, 16):
hl = binascii.hexlify(input[i:i + 16])
hl = ' '.join(hl[i:i + 2] for i in range(0, 32, 2))
sys.stderr.write(hl + '\n')
sys.stderr.write('Trying anyway.\n')
output = md.app
#print('INFO: base address 0x{0:x}'.format(md.start))
print('INFO: length 0x{0:x}'.format(len(md.app)))
print('DEBUG: writing "%s"' % args.output[0])
with open(args.output[0], 'wb') as f:
f.write(output)
if __name__ == "__main__":
main()