Permalink
Cannot retrieve contributors at this time
executable file
899 lines (788 sloc)
31.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python2 | |
| # -*- coding: utf-8 -*- | |
| # md380-tool by KK4VCZ and Friends | |
| # This is the client for the patched MD380 firmware. It does all | |
| # sorts of clever things that the official clients can't, but it | |
| # probably has bugs and will do all sorts of unsavory things. Do not | |
| # expose it to light, do not feed it after midnight, and *NEVER* give | |
| # it water. | |
| from __future__ import print_function | |
| import json | |
| import struct | |
| import sys | |
| import time | |
| import usb.core | |
| from collections import namedtuple | |
| from DFU import DFU, Request, State | |
| # 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 | |
| class UsersDB(object): | |
| """List of registered DMR-MARC users.""" | |
| users = {} | |
| def __init__(self, filename=None): | |
| """Loads the database.""" | |
| import csv | |
| try: | |
| if filename is None: | |
| filename = sys.path[0] + '/user.bin' | |
| with open(filename, 'rb') as csvfile: | |
| reader = csv.reader(csvfile) | |
| for row in reader: | |
| if len(row) > 0: | |
| self.users[int(row[0])] = row | |
| except: | |
| # print("WARNING: Unable to load user.bin.") | |
| pass | |
| def getuser(self, id): | |
| """Returns a user from the ID.""" | |
| try: | |
| return self.users[id] | |
| except: | |
| call = "" | |
| name = "" | |
| nickname = "" | |
| city = "" | |
| state = "" | |
| country = "" | |
| comment = "" | |
| return ["%i" % id, call, name, nickname, city, state, country, comment] | |
| def getusername(self, id): | |
| """Returns a formatted username from the ID.""" | |
| user = self.getuser(id) | |
| return ("%s %s (%s)" % ( | |
| user[1], | |
| user[2], | |
| user[0])) | |
| # Quick to load, so might as well do it early. | |
| users = UsersDB() | |
| class Tool(DFU): | |
| """Client class for extra features patched into the MD380's firmware. | |
| None of this will work with the official firmware, of course.""" | |
| def __init__(self, device, alt): | |
| super(Tool, self).__init__(device, alt) | |
| # We need to read the manufacturer string to hook the added USB functions | |
| # Some systems (Raspian Jessie) don't have this property | |
| getattr(device, "manufacturer") | |
| def drawtext(self, str, a, b): | |
| """Sends a new MD380 command to draw text on the screen..""" | |
| cmd = 0x80 # Drawtext | |
| a = a & 0xFF | |
| b = b & 0xFF | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, chr(cmd) + chr(a) + chr(b) + self.widestr(str)) | |
| 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 read_framebuf_line(self, y): | |
| """Reads a single line of pixels from the framebuffer.""" | |
| # simple test - the firmware can also send larger tiles. | |
| nloops = 0 | |
| while nloops<3: | |
| cmdstr = (chr(0x84) + # TDFU_READ_FRAMEBUFFER | |
| chr(0) + # x1 (x1,y1) = tile's upper left corner | |
| chr(y) + # y1 | |
| chr(159) + # x2 (x2,y2) = tile's lower right corner | |
| chr(y) # y2 | |
| ) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| # read 5-byte header (echo of cmdstr) followed by | |
| # 160 pixels per line * 3 bytes per pixel (BLUE,GREEN,RED): | |
| rd_result = self.upload(1, 160*3+5, 0); | |
| if rd_result[4] == y: # y2 ok -> got the expected response | |
| return rd_result[5:] # strip the header, return pixel data only | |
| nloops = nloops+1 # try again, up to 3 times... | |
| time.sleep(0.01) # about 10 ms later | |
| return "" # error -> empty result | |
| def send_keyboard_event(self, key_ascii, pressed_or_released ): | |
| """Sends a keyboard event to remotely control an MD380.""" | |
| cmdstr = ( chr(0x85) + # TDFU_REMOTE_KEY_EVENT | |
| key_ascii + # 2nd arg: single char 'M'(enu), 'U'(p), 'D'(own), 'B'(ack), etc | |
| chr(pressed_or_released) ) # 2nd arg: pressed (1) or released (0) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, cmdstr ) | |
| self.get_status() # this changes state | |
| time.sleep(0.05) | |
| status = self.get_status() # this gets the status | |
| def peek(self, adr, size): | |
| """Returns so many bytes from an address.""" | |
| self.set_address(adr) | |
| return self.upload(1, size, 0) | |
| def spiflashgetid(self): | |
| size = 4 | |
| """Returns SPI Flash ID.""" | |
| cmd = 0x05 # SPIFLASHGETID | |
| cmdstr = (chr(cmd)) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| return self.upload(1, size, 0) | |
| def spiflashpeek(self, adr, size=1024): | |
| """Returns so many bytes from SPI Flash.""" | |
| cmd = 0x01 # SPIFLASHREAD | |
| cmdstr = (chr(cmd) + | |
| chr(adr & 0xFF) + | |
| chr((adr >> 8) & 0xFF) + | |
| chr((adr >> 16) & 0xFF) + | |
| chr((adr >> 24) & 0xFF) | |
| ) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| return self.upload(1, size, 0) | |
| def spiflash_erase64kblock(self, adr, size=1024): | |
| """Clear 64kb block on spi flash.""" | |
| cmd = 0x03 # SPIFLASHWRITE | |
| cmdstr = (chr(cmd) + | |
| chr(adr & 0xFF) + | |
| chr((adr >> 8) & 0xFF) + | |
| chr((adr >> 16) & 0xFF) + | |
| chr((adr >> 24) & 0xFF) | |
| ) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| time.sleep(0.1) | |
| status = self.get_status() # this gets the status | |
| return self.upload(1, size, 0) | |
| def spiflashpoke(self, adr, size, data): | |
| """Returns so many bytes from SPI Flash.""" | |
| cmd = 0x04 # SPIFLASHWRITE_NEW | |
| # print(size) | |
| cmdstr = (chr(cmd) + | |
| chr(adr & 0xFF) + | |
| chr((adr >> 8) & 0xFF) + | |
| chr((adr >> 16) & 0xFF) + | |
| chr((adr >> 24) & 0xFF) + | |
| chr(size & 0xFF) + | |
| chr((size >> 8) & 0xFF) + | |
| chr((size >> 16) & 0xFF) + | |
| chr((size >> 24) & 0xFF) | |
| ) | |
| for i in range(0, size, 1): | |
| cmdstr = cmdstr + data[i] | |
| # print(len(cmdstr)) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| # print(status) | |
| return self.upload(1, size, 0) | |
| def getinbox(self, address): | |
| """return non-deleted messages from inbox""" | |
| buf = self.spiflashpeek(address, 50 * 4) | |
| messages = [] | |
| for i in range(0, 50 * 4, 4): | |
| message = {} | |
| header = buf[i:i + 4] | |
| message["deleted"] = (header[0]) != 0x01 | |
| if header[1] == 0x1: | |
| message["read"] = True | |
| elif header[1] == 0x2: | |
| message["read"] = False | |
| else: | |
| message["read"] = "N/A" | |
| message["order"] = (header[2]) | |
| message["index"] = (header[3]) | |
| if not message["deleted"]: | |
| messages.append(message) | |
| for message in messages: | |
| message_bytes = self.spiflashpeek(address + 50 * 4 + message["index"] * 0x124, 50 * 4 + message["index"] * 0x124 + 0x124) | |
| message["srcaddr"] = message_bytes[0] + (message_bytes[1] << 8) + (message_bytes[2] << 16) | |
| message["flags"] = message_bytes[4] | |
| message_text = "" | |
| for i in range(4, 0x124, 2): | |
| c = chr(message_bytes[i]) | |
| if c != '\0': | |
| message_text = message_text + c | |
| else: | |
| break | |
| message["text"] = message_text | |
| return messages | |
| def getkey(self, index): | |
| """Returns an Enhanced Privacy key from SPI Flash. 1-indexed""" | |
| buf = self.spiflashpeek(0x59c0 + 16 * index - 16, | |
| 16) | |
| return buf | |
| def c5000peek(self, reg): | |
| """Returns one byte from a C5000 register.""" | |
| cmd = 0x11 # C5000 Read Reg | |
| cmdstr = (chr(cmd) + | |
| chr(reg & 0xFF) | |
| ) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| buf = self.upload(1, 1024, 0) # Peek the 1024 byte dmesg buffer. | |
| return buf[0] | |
| def c5000poke(self, reg, val): | |
| """Writes a byte into a C5000 register.""" | |
| cmd = 0x10 # C5000 Write Reg | |
| cmdstr = (chr(cmd) + | |
| chr(reg & 0xFF) + | |
| chr(val & 0xFF) | |
| ) | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, | |
| cmdstr) | |
| self.get_status() # this changes state | |
| status = self.get_status() # this gets the status | |
| def custom(self, cmd): | |
| """Returns the 1024 byte DMESG buffer.""" | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, chr(cmd)) | |
| self.get_status() # this changes state | |
| # time.sleep(0.1); | |
| status = self.get_status() # this gets the status | |
| def reboot_to_bootloader(self): | |
| """Reboot into the bootloader with a DFU command. | |
| This will erase (part of) the firmware from flash, | |
| so you must reprogram the firmware afterwards if you'd like a working radio. | |
| """ | |
| cmd = 0x86 # reboot_to_bootloader | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, chr(cmd)) | |
| self.get_status() # this changes state | |
| def getdmesg(self): | |
| """Returns the 1024 byte DMESG buffer.""" | |
| cmd = 0x00 # DMESG | |
| self._device.ctrl_transfer(0x21, Request.DNLOAD, 1, 0, chr(cmd)) | |
| self.get_status() # this changes state | |
| # time.sleep(0.1); | |
| status = self.get_status() # this gets the status | |
| buf = self.upload(1, 1024, 0) # Peek the 1024 byte dmesg buffer. | |
| # Okay, so at this point we have the buffer, but it's a ring | |
| # buffer that might have already looped, so we need to reorder | |
| # if that is the case or crop it if it isn't. | |
| tail = "" | |
| head = None | |
| for b in buf: | |
| if head is None: | |
| if b > 0: | |
| tail += chr(b) | |
| else: | |
| head = "" | |
| else: | |
| if b > 0: | |
| head += chr(b) | |
| else: | |
| break | |
| if head is None: | |
| return tail | |
| return head + tail | |
| def parse_calibration_data(self, data): | |
| freqs_bcd = data[432:] # last 80 bytes represent 9*2 BCD frequencies,for example 00 35 10 40 == 401.03500 | |
| freqs = [] # parse into a list of frequency settings | |
| def bcd2freq(bcd): | |
| freq_whole = "%02x" % ord(bcd[3]) + ("%02x" % ord(bcd[2]))[0] | |
| freq_decimal = ("%02x" % ord(bcd[2]))[1] + "%02x" % ord(bcd[1]) + "%02x" % ord(bcd[0]) | |
| return "%s.%s" % (freq_whole, freq_decimal) | |
| Frequency = namedtuple("Frequency", "rx_freq tx_freq vox1 vox10 rx_low_voltage rx_full_voltage RSSI1 \ | |
| RSSI4 analog_mic digital_mic freq_adjust_high freq_adjust_mid freq_adjust_low1 \ | |
| tx_high_power tx_low_power rx_sensitivity open_sql_9 close_sql_9 open_sql_1 \ | |
| close_sql_1 max_volume ctcss_67hz ctcss_151_4hz ctcss_254_1hz dcs_mod2 dcs_mod1\ | |
| mod1_partial analog_voice_adjust lock_voltage_partial send_i_partial \ | |
| send_q_partial send_i_range send_q_range rx_i_partial rx_q_partial \ | |
| analog_send_i_range analog_send_q_range") | |
| rest = data[16:432] # first 16 bytes are settings which seem to be equal for all frequencies | |
| rest_i = "" | |
| for k in range(0, 9): # invert the 2d array so it's easier to map | |
| for j in range(0, 24): | |
| rest_i += rest[j * 16 + k] | |
| codes_per_freq = [] | |
| for i in range(0, 9): # 9 frequencies | |
| rx_freq = bcd2freq(freqs_bcd[i * 8:i * 8 + 4]) | |
| tx_freq = bcd2freq(freqs_bcd[i * 8 + 4:i * 8 + 8]) | |
| codes = data[:11] # from vox1 till freq_adjust_low , the mutual ones | |
| codes += rest_i[i * 24:i * 24 + 24] # the rest of info , each is a single 8 bit integer | |
| freqs.append(Frequency._asdict(Frequency._make((rx_freq, tx_freq) + struct.unpack("B" * 35, codes)))) | |
| return freqs | |
| # def calllog(dfu): | |
| # """Prints a call log to stdout, fetched from the MD380's memory.""" | |
| # dfu.drawtext("Hooking calls!",160,50); | |
| # | |
| # #Set the target address to the list of DMR addresses. | |
| # dfu.set_address(0x2001d098); | |
| # old1=0; | |
| # old2=0; | |
| # while 1: | |
| # data=dfu.upload(1,16,0);#Peek sixteen bytes. | |
| # llid0=(data[0]+ | |
| # (data[1]<<8)+ | |
| # (data[2]<<16)+ | |
| # (data[3]<<24)); | |
| # llid1=(data[4]+ | |
| # (data[5]<<8)+ | |
| # (data[6]<<16)+ | |
| # (data[7]<<24)); | |
| # llid2=(data[8]+ | |
| # (data[9]<<8)+ | |
| # (data[10]<<16)+ | |
| # (data[11]<<24)); | |
| # if old1!=llid1 or old2!=llid2: | |
| # old1=llid1; | |
| # old2=llid2; | |
| # print("DMR call from %s to %s." % ( | |
| # users.getusername(llid1), users.getusername(llid2))) | |
| # #get actual canel name | |
| # dfu.set_address(0x2001c9d4); | |
| # data=dfu.upload(1,32,0); | |
| # message="" | |
| # for i in range(0,32,2): | |
| # c=chr(data[i]&0x7F); | |
| # if c!='\0': | |
| # message=message+c; | |
| # print(message) | |
| # #get actual zone name | |
| # dfu.set_address(0x2001b958); | |
| # data=dfu.upload(1,32,0); | |
| # message="" | |
| # for i in range(0,32,2): | |
| # c=chr(data[i]&0x7F); | |
| # if c!='\0': | |
| # message=message+c; | |
| # print(message) | |
| # sys.stdout.flush(); | |
| def dmesg(dfu): | |
| """Prints the dmesg log from main memory.""" | |
| # dfu.drawtext("Dumping dmesg",160,50); | |
| print(dfu.getdmesg()) | |
| def parse_calibration(dfu): | |
| dfu.md380_custom(0xA2, 0x05) | |
| data = str(bytearray(dfu.upload(0, 512))) | |
| freqs = dfu.parse_calibration_data(data) | |
| print(json.dumps(freqs, indent=4)) | |
| def coredump(dfu, filename): | |
| """Dumps a corefile of RAM.""" | |
| with open(filename, 'wb') as f: | |
| for adr in range(0x20000000, | |
| 0x20000000 + (128 * 1024), | |
| 1024): | |
| # print("Fetching %08x" % adr) | |
| buf = dfu.peek(adr, 1024) | |
| f.write(buf) | |
| f.close() | |
| def screenshot(dfu, filename="screenshot.bmp"): | |
| """Reads the LCD framebuffer""" | |
| with open(filename, 'wb') as f: | |
| # Write a simple, hard-coded bitmap file header | |
| # (14 byte "file header" + 40 byte "info block" + 3*160*128 byte "data", | |
| # total size = 0x0000F036 bytes. Here written in little endian format: | |
| f.write( "BM" + chr(0x36)+chr(0xF0)+chr(0x00)+chr(0x00) | |
| + chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) | |
| + chr( 54 )+chr(0x00)+chr(0x00)+chr(0x00) ) | |
| # Next: Write the "bitmap info header". Keep it simple, use 40 bytes. | |
| f.write( chr( 40 )+chr(0x00)+chr(0x00)+chr(0x00) ) # sizeof(BITMAPINFOHEADER) | |
| f.write( chr( 160)+chr(0x00)+chr(0x00)+chr(0x00) ) # width (pixel) | |
| f.write( chr( 128)+chr(0x00)+chr(0x00)+chr(0x00) ) # height (pixel) | |
| f.write( chr( 1 )+chr(0x00) ) # number of bitplanes (anachronism) | |
| f.write( chr( 24 )+chr(0x00) ) # number of bits per pixel | |
| f.write( chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) ) # no compression | |
| f.write( chr(0x00)+chr(0xA0)+chr(0x00)+chr(0x00) ) # sizeof(pixel data) | |
| f.write( chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) ) # X pixel per meter | |
| f.write( chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) ) # P pixel per meter | |
| f.write( chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) ) # no colour palette | |
| f.write( chr(0x00)+chr(0x00)+chr(0x00)+chr(0x00) ) # number of colours "used" : 0 = all | |
| # Write image data with 160 pixels per line, 3 bytes per pixel. | |
| # For a start, just dump the pixels to the file unmodified. | |
| y = 127; # bmp files begin with the 'bottom line' (y=127) | |
| while y>=0: | |
| buf = dfu.read_framebuf_line(y) | |
| f.write(buf) | |
| y = y-1; | |
| f.close() | |
| def hexdump(dfu, address, length=512): | |
| """Dumps from memory to the screen""" | |
| adr = int(address, 16) | |
| buf = dfu.peek(adr, length) | |
| i = 0 | |
| cbuf = "" | |
| for b in buf: | |
| sys.stdout.write("%02x " % b) | |
| i = i + 1 | |
| if 32 < b < 127: | |
| cbuf += chr(b) | |
| else: | |
| cbuf += "." | |
| if i % 16 == 0: | |
| sys.stdout.write(" " + cbuf) | |
| sys.stdout.write("\n") | |
| cbuf = "" | |
| elif i % 8 == 0: | |
| sys.stdout.write(" ") | |
| def hexwatch(dfu, address): | |
| """Dumps from memory to the screen""" | |
| while True: | |
| hexdump(dfu, address, 16) | |
| time.sleep(0.05) | |
| def dump(dfu, filename, address): | |
| """1k Binary dumps""" | |
| adr = int(address, 16) | |
| with open(filename, 'wb') as f: | |
| buf = dfu.peek(adr, 1024) | |
| f.write(buf) | |
| f.close() | |
| def flashgetid(dfu): | |
| size = 0 | |
| buf = dfu.spiflashgetid() | |
| print("SPI Flash ID: %02x %02x %02x" % (buf[0], buf[1], buf[2])) | |
| if buf[0] == 0xef and buf[1] == 0x40: | |
| if buf[2] == 0x18: | |
| sys.stdout.write("W25Q128FV 16MByte\n") | |
| size = 16 * 1024 * 1024 | |
| elif buf[2] == 0x14: | |
| sys.stdout.write("W25Q80BL 1MByte\n") | |
| size = 1 * 1024 * 1024 | |
| elif buf[0] == 0x10 and buf[1] == 0xdc: | |
| if buf[2] == 0x01: | |
| sys.stdout.write("W25Q128FV 16MByte maybe\n") | |
| size = 16 * 1024 * 1024 | |
| elif buf[0] == 0x70 and buf[1] == 0xf1 and buf[2] == 0x01: | |
| sys.stdout.write("Bad LibUSB connection. Please see the advice from N6YN at https://github.com/travisgoodspeed/md380tools/issues/186\n") | |
| else: | |
| sys.stdout.write("Unknown SPI Flash - please report\n") | |
| return size | |
| def flashdump(dfu, filename): | |
| """Dumps flash.""" | |
| with open(filename, 'wb') as f: | |
| for adr in range(0x08000000, | |
| 0x08000000 + (1024 * 1024), | |
| 1024): | |
| # print("Fetching %08x" % adr) | |
| buf = dfu.peek(adr, 1024) | |
| f.write(buf) | |
| f.close() | |
| def spiflashdump(dfu, filename): | |
| """Dumps SPI Flash.""" | |
| with open(filename, 'wb') as f: | |
| for adr in range(0x00000000, | |
| 0x00000000 + (16 * 1024 * 1024), | |
| 1024): | |
| # print("Fetching %08x" % adr) | |
| buf = dfu.spiflashpeek(adr, 1024) | |
| f.write(buf) | |
| f.close() | |
| def spiflashwrite(dfu, filename, adr): | |
| """Programm SPI Flash.""" | |
| if flashgetid(dfu) == 16 * 1024 * 1024: | |
| with open(filename, 'rb') as f: | |
| data = f.read() | |
| size = len(data) | |
| dfu.md380_custom(0x91, 0x01) # disable any radio and UI events | |
| # while on spi flash | |
| print("erase %d bytes @ 0x%x" % (size, adr)) | |
| for n in range(adr, adr + size + 1, 0x1000): | |
| # print("erase %x " % n) | |
| dfu.spiflash_erase64kblock(n) | |
| fullparts = int(size / 1024) | |
| print("flashing %d bytes @ 0x%x" % (size, adr)) | |
| if fullparts > 0: | |
| for n in range(0, fullparts, 1): | |
| # print("%d %d %x %d " % (fullparts, n, adr + n * 1024, 1024)) | |
| dfu.spiflashpoke(adr + n * 1024, 1024, data[n * 1024:(n + 1) * 1024]) | |
| lastpartsize = size - fullparts * 1024 | |
| if lastpartsize > 0: | |
| # print("%d %x %d " % (fullparts, adr + fullparts * 1024, lastpartsize)) | |
| dfu.spiflashpoke(adr + fullparts * 1024, lastpartsize, data[fullparts * 1024:fullparts * 1024 + lastpartsize]) | |
| sys.stdout.write("reboot radio now\n") | |
| dfu.md380_reboot() | |
| f.close() | |
| else: | |
| sys.stdout.write("can't programm spi flash wrong flash type\n") | |
| def dmesgfasttail(dfu): | |
| """Keeps printing the dmesg buffer.""" | |
| while True: | |
| sys.stdout.write(dfu.getdmesg()) | |
| # time.sleep(0.1); | |
| sys.stdout.flush() | |
| def dmesgtail(dfu): | |
| """Keeps printing the dmesg buffer.""" | |
| while True: | |
| sys.stdout.write(dfu.getdmesg()) | |
| time.sleep(0.1) | |
| sys.stdout.flush() | |
| def c5000(dfu): | |
| """Prints some DMR registers.""" | |
| for r in range(0, 0x87): | |
| sys.stdout.write("[0x%02x]=0x%02x\t" % (r, dfu.c5000peek(r))) | |
| if r % 4 == 3: | |
| sys.stdout.write("\n") | |
| sys.stdout.flush() | |
| sys.stdout.write("\n") | |
| def rssi(dfu): | |
| """Graphs the RSSI value. Kinda useless.""" | |
| while True: | |
| rssih = dfu.c5000peek(0x43) | |
| rssil = dfu.c5000peek(0x44) | |
| rssi = (rssih << 8) | rssil | |
| print("%04x" % rssi) | |
| time.sleep(0.25) | |
| def messages(dfu): | |
| """Prints all the SMS messages.""" | |
| print("Inbox:") | |
| messages = dfu.getinbox(0x416d0)[::-1] | |
| for msg in messages: | |
| print("From: %s Text: %s" % (msg["srcaddr"], msg["text"])) | |
| print("Sent:") | |
| messages = dfu.getinbox(0x45100)[::-1] | |
| for msg in messages: | |
| print("To : %s Text: %s" % (msg["srcaddr"], msg["text"])) | |
| def keys(dfu): | |
| """Prints all the Enhanced Privacy keys.""" | |
| for i in range(1, 9): | |
| buf = dfu.getkey(i) | |
| keystr = "" # Keys are displayed as big-endian, stored as little. | |
| for b in buf: | |
| keystr = "%02x %s" % (b, keystr) | |
| print("%02i: %s" % (i, keystr)) | |
| def findcc(dfu): | |
| """Hunts for the color code of a transmitter.""" | |
| cc = 0 # Guess at the color code. | |
| while True: | |
| # Try a new color. | |
| cc = cc + 1 | |
| print("Trying color %d." % (cc & 0xf)) | |
| sys.stdout.flush() | |
| dfu.c5000poke(0x1f, (cc & 0xF) << 4) | |
| time.sleep(1.0) | |
| if dfu.c5000peek(0x0d) & 0x10: | |
| print("Got a match on color %i" % (cc & 0xf)) | |
| while dfu.c5000peek(0x0d) & 0x10: | |
| time.sleep(0.5) | |
| sys.stdout.write(".") | |
| sys.stdout.flush() | |
| sys.stdout.write("\n") | |
| def bcd(b): | |
| return int("%02x" % b) | |
| def calldate(dfu): | |
| """Print Time and Date to stdout, fetched from the MD380's RTC.""" | |
| dfu.set_address(0x40002800) # 2.032 | |
| data = dfu.upload(1, 8, 0) | |
| print("%02d.%02d.%02d %02d:%02d:%02d" % ( | |
| bcd(data[4] & (0x0f | 0x30)), | |
| bcd(data[5] & (0x0f)), | |
| bcd(data[6]), | |
| bcd(data[2]), | |
| bcd(data[1] & (0x0f | 0x70)), | |
| bcd(data[0] & (0x0f | 0x70)))) | |
| # def calladc1(dfu): | |
| # """Print ADC1 Voltage (Battery), fetched from the MD380's Memory (Update with DMA).""" | |
| # dfu.set_address(0x2001cfcc); # 2.032 | |
| # data=dfu.upload(1,4,0); | |
| # # 7.2V ~ 2.4V PA1 (BATT) ... 2715 ~ 6.5V ... 3.3V 12BIT | |
| # print("%f Volt" % (3.3 / 0xfff * ((data[3] << 8) + data[2]) * 3)) | |
| # | |
| # | |
| # def getchannel(dfu): | |
| # """Print actual Channel, fetched from the MD380's Memory.""" | |
| # dfu.set_address(0x2001d376); # 2.032 | |
| # data=dfu.upload(1,4,0); | |
| # print("%02d %02d %02d %02d" % (data[3], data[2], data[1], data[0])) | |
| def readword(dfu, address): | |
| print("%x" % (int(address, 0))) | |
| dfu.set_address(int(address, 0)) # 2.032 | |
| data = dfu.upload(1, 4 * 4, 0) | |
| print("%x %02x%02x%02x%02x" % (int(address, 0), data[3], data[2], data[1], data[0])) | |
| print("%x %02x %02x %02x %02x" % (int(address, 0), data[3], data[2], data[1], data[0])) | |
| print("%x %02x %02x %02x %02x" % (int(address, 0) + 4, data[7], data[6], data[5], data[4])) | |
| print("%x %02x %02x %02x %02x" % (int(address, 0) + 8, data[11], data[10], data[9], data[8])) | |
| print("%x %02x %02x %02x %02x" % (int(address, 0) + 12, data[15], data[14], data[13], data[12])) | |
| print("%d" % (data[3] << 24 | data[2] << 16 | data[1] << 8 | data[0])) | |
| 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 = Tool(dev, alt) | |
| dev.default_timeout = 3000 | |
| try: | |
| dfu.enter_dfu_mode() | |
| pass | |
| 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-tool <command> <arguments> | |
| Looks up the name by an ID number. | |
| md380-tool lookup 12345 | |
| Prints the dmesg buffer. | |
| md380-tool dmesg | |
| Follow the dmesg buffer. | |
| md380-tool dmesgtail | |
| Prints the C5000 baseband registers. | |
| md380-tool c5000 | |
| Scans for DMR traffic on all color codes. | |
| md380-tool findcc | |
| Dumps all the inbound and outbound text messages. | |
| md380-tool messages | |
| Dumps all the keys. | |
| md380-tool keys | |
| Dump a screenshot. | |
| md380-tool screenshot <filename.bmp> | |
| Prints the SPI Flash Type. | |
| md380-tool spiflashid | |
| Dump all of flash memory. | |
| md380-tool flashdump <filename.bin> | |
| Dump the complete SPI Flash image (16MByte). | |
| md380-tool spiflashdump <filename.bin> | |
| Dump a core file of RAM. | |
| md380-tool coredump <filename.bin> | |
| Dumps memory in hex. | |
| md380-tool hexdump <0xcafebabe> | |
| Watches a hex address. | |
| md380-tool hexwatch <0xcafebabe> | |
| Dump one word. | |
| md380-tool readword <0xcafebabe> | |
| Dump 1kB from arbitrary address | |
| md380-tool dump <filename.bin> <address> | |
| Dump calibration data | |
| md380-tool calibration | |
| Reboot into the bootloader (erases application, you _must_ reflash firmware afterwards): | |
| md380-tool reboot_to_bootloader | |
| Copy File to SPI flash. | |
| md380-tool spiflashwrite <filename> <address>" | |
| Copy users.csv to SPI flash: | |
| wc -c < db/users.csv > data ; cat db/users.csv >> data | |
| md380-tool spiflashwrite data 0x100000 | |
| """) | |
| def main(): | |
| try: | |
| if len(sys.argv) == 2: | |
| if sys.argv[1] == 'dmesg': | |
| dfu = init_dfu() | |
| dmesg(dfu) | |
| elif sys.argv[1] == 'dmesgtail': | |
| dfu = init_dfu() | |
| dmesgtail(dfu) | |
| # elif sys.argv[1] == 'calllog': | |
| # dfu=init_dfu(); | |
| # calllog(dfu); | |
| elif sys.argv[1] == 'date': | |
| dfu = init_dfu() | |
| calldate(dfu) | |
| # elif sys.argv[1] == 'adc1': | |
| # dfu=init_dfu(); | |
| # calladc1(dfu); | |
| # elif sys.argv[1] == 'channel': | |
| # dfu=init_dfu(); | |
| # getchannel(dfu); | |
| elif sys.argv[1] == "reboot_to_bootloader": | |
| dfu = init_dfu() | |
| print("It's okay to get a pipe error here, " | |
| "as long as it reboots into bootloader and " | |
| "has the red/green alternating LED.") | |
| dfu.reboot_to_bootloader() | |
| print("Now go reflash your firmware!") | |
| elif sys.argv[1] == 'c5000': | |
| dfu = init_dfu() | |
| c5000(dfu) | |
| elif sys.argv[1] == 'rssi': | |
| dfu = init_dfu() | |
| rssi(dfu) | |
| elif sys.argv[1] == 'findcc': | |
| dfu = init_dfu() | |
| findcc(dfu) | |
| elif sys.argv[1] == 'messages': | |
| dfu = init_dfu() | |
| messages(dfu) | |
| elif sys.argv[1] == 'keys': | |
| dfu = init_dfu() | |
| keys(dfu) | |
| elif sys.argv[1] == 'spiflashid': | |
| dfu = init_dfu() | |
| flashgetid(dfu) | |
| elif sys.argv[1] == "calibration": | |
| dfu = init_dfu() | |
| parse_calibration(dfu) | |
| elif sys.argv[1] == 'screenshot': | |
| dfu = init_dfu() | |
| screenshot(dfu) | |
| else: | |
| usage() | |
| elif len(sys.argv) == 3: | |
| if sys.argv[1] == 'flashdump': | |
| print("Dumping flash from 0x08000000 to '%s'." % sys.argv[2]) | |
| dfu = init_dfu() | |
| flashdump(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'spiflashdump': | |
| print("Dumping SPI Flash to '%s'." % sys.argv[2]) | |
| dfu = init_dfu() | |
| spiflashdump(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'coredump': | |
| print("Dumping ram from 0x20000000 to '%s'." % sys.argv[2]) | |
| dfu = init_dfu() | |
| coredump(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'hexdump': | |
| print("Dumping memory from %s." % sys.argv[2]) | |
| dfu = init_dfu() | |
| hexdump(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'ramdump': | |
| print("Dumping memory from %s." % sys.argv[3]) | |
| dfu = init_dfu() | |
| ramdump(dfu, sys.argv[2], sys.argv[3]) | |
| elif sys.argv[1] == 'hexwatch': | |
| print("Dumping memory from %s." % sys.argv[2]) | |
| dfu = init_dfu() | |
| hexwatch(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'lookup': | |
| print(users.getusername(int(sys.argv[2]))) | |
| elif sys.argv[1] == 'readword': | |
| dfu = init_dfu() | |
| readword(dfu, sys.argv[2]) | |
| elif sys.argv[1] == 'custom': | |
| dfu = init_dfu() | |
| dfu.custom(int(sys.argv[2], 16)) | |
| dmesg(dfu) | |
| elif sys.argv[1] == 'screenshot': | |
| dfu = init_dfu() | |
| screenshot(dfu, sys.argv[2]) | |
| else: | |
| usage() | |
| elif len(sys.argv) == 4: | |
| if sys.argv[1] == 'spiflashwrite': | |
| filename = sys.argv[2] | |
| adr = int(sys.argv[3], 16) | |
| if adr >= int("0x100000", 16): | |
| dfu = init_dfu() | |
| spiflashwrite(dfu, sys.argv[2], adr) | |
| else: | |
| print("address too low") | |
| elif sys.argv[1] == 'dump': | |
| print("Dumping memory from %s." % sys.argv[3]) | |
| dfu = init_dfu() | |
| dump(dfu, sys.argv[2], sys.argv[3]) | |
| else: | |
| usage() | |
| else: | |
| usage() | |
| except RuntimeError as e: | |
| print(e.args[0]) | |
| exit(1) | |
| except usb.core.USBError as ue: | |
| print(ue) | |
| if ue[0] == 32: | |
| print('Make sure the device is already flashed with custom firmware and NOT in DFU mode') | |
| exit(1) | |
| except Exception as e: | |
| print(e) | |
| # print(dfu.get_status()) | |
| exit(1) | |
| if __name__ == '__main__': | |
| main() |