kopia lustrzana https://github.com/OpenRTX/OpenRTX
Add loading scripts, update README.md, requirements.txt and .gitignore
rodzic
f709cb0388
commit
ab306eab66
|
@ -30,3 +30,16 @@
|
|||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Binaries
|
||||
*.bin
|
||||
*.elf
|
||||
*.hex
|
||||
*.map
|
||||
|
||||
# Python byte-compiled
|
||||
*/__pycache__
|
||||
|
||||
# Kdevelop files
|
||||
*.kdev4
|
||||
.kdev4/
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
pyusb
|
|
@ -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"
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
Ładowanie…
Reference in New Issue