383 lines
10 KiB
Python
383 lines
10 KiB
Python
|
#!/usr/bin/python3
|
||
|
#
|
||
|
#
|
||
|
# This Source Code Form is subject to the terms of the MIT License.
|
||
|
# If a copy of the MIT License was not distributed with this file,
|
||
|
# you can obtain one at https://opensource.org/licenses/MIT
|
||
|
#
|
||
|
|
||
|
import argparse
|
||
|
import os.path
|
||
|
from datetime import datetime
|
||
|
import subprocess
|
||
|
import signal
|
||
|
import time
|
||
|
|
||
|
from prompt_toolkit import prompt
|
||
|
from prompt_toolkit.contrib.completers import WordCompleter
|
||
|
from prompt_toolkit.history import InMemoryHistory
|
||
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||
|
|
||
|
|
||
|
def auto_int(x):
|
||
|
return int(x, 0)
|
||
|
|
||
|
|
||
|
def parse_args():
|
||
|
parser = argparse.ArgumentParser(
|
||
|
description='Firmware Extractor User Interface',
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'SerialDeviceFILE',
|
||
|
help='Device File to read from (e.g., /dev/ttyUSB0)'
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'-i',
|
||
|
'--interactive',
|
||
|
action='store_true',
|
||
|
help='Use interactive mode'
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'-s',
|
||
|
'--start',
|
||
|
type=auto_int,
|
||
|
default=0x00,
|
||
|
help='Set start address',
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'-l',
|
||
|
'--length',
|
||
|
type=auto_int,
|
||
|
default=0x10000,
|
||
|
help='Number of bytes to extract',
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'-e',
|
||
|
'--endianess',
|
||
|
default='little',
|
||
|
choices=['little', 'big'],
|
||
|
help='Set endianess',
|
||
|
)
|
||
|
parser.add_argument(
|
||
|
'-o',
|
||
|
'--outfile',
|
||
|
default='data-{}.bin'.format(datetime.now().strftime('%Y%m%d_%H%M')),
|
||
|
help='Set output file path',
|
||
|
)
|
||
|
|
||
|
return parser.parse_args()
|
||
|
|
||
|
|
||
|
finish = False
|
||
|
|
||
|
def sigalarm_handler(signo, frame):
|
||
|
# I want to print the statistics in the end of the program.
|
||
|
# We can create an infinite loop with this sigalarm_handler,
|
||
|
# as multiline mode of send_cmd() uses SIGALARM as well.
|
||
|
# The global variable finish handles this case. And helps
|
||
|
# us terminate the program when the timeout is fired a second
|
||
|
# time.
|
||
|
global finish
|
||
|
if finish:
|
||
|
print()
|
||
|
print('Programm finished.')
|
||
|
exit()
|
||
|
|
||
|
print('End of data.')
|
||
|
print()
|
||
|
finish = True
|
||
|
UART('/dev/ttyUSB0').send_cmd('P', True)
|
||
|
|
||
|
|
||
|
def decode_ascii(s, outfile):
|
||
|
out = ''
|
||
|
for i in range(0, len(s), 2):
|
||
|
char = int(s[i:i+2], 16)
|
||
|
outfile.write(char.to_bytes(1, 'little'))
|
||
|
if char > 31 and char < 127:
|
||
|
out += chr(char)
|
||
|
else:
|
||
|
out += '.'
|
||
|
return out
|
||
|
|
||
|
|
||
|
def print_error(errcode):
|
||
|
print()
|
||
|
print('StatusCode: {:02X}'.format(errcode))
|
||
|
|
||
|
if errcode == 0x20:
|
||
|
print('Status OK')
|
||
|
elif errcode == 0x40:
|
||
|
print('Wait/Retry requested (bus access was not granted in time)')
|
||
|
elif errcode == 0x06:
|
||
|
print('Wait requested + additional OK (previous command OK, but no bus access)')
|
||
|
elif errcode == 0x80:
|
||
|
print('Fault during command execution (command error (access denied etc.))')
|
||
|
elif errcode == 0xA0:
|
||
|
print('Fault after successful command execution (reading invalid memory address?).')
|
||
|
elif errcode == 0xE0:
|
||
|
print('Failure during communication (check connection, no valid status reply received)')
|
||
|
else:
|
||
|
print('Unknown status code')
|
||
|
|
||
|
|
||
|
def _read_enddata(fd):
|
||
|
buf = ''
|
||
|
nchar = 0
|
||
|
state = 'stat'
|
||
|
print()
|
||
|
# Print remaining data from uart until timeout is reached.
|
||
|
while True:
|
||
|
signal.alarm(1)
|
||
|
char = fd.read(1)
|
||
|
signal.alarm(0)
|
||
|
|
||
|
print(char, end='')
|
||
|
buf += char
|
||
|
|
||
|
if char == '!':
|
||
|
state = 'err'
|
||
|
|
||
|
if state == 'err':
|
||
|
if nchar == 25:
|
||
|
print_error(int(buf[-8:], 16))
|
||
|
nchar += 1
|
||
|
|
||
|
|
||
|
def read_ascii(fd, outfile):
|
||
|
l = 0
|
||
|
c = 0
|
||
|
line = ''
|
||
|
lineraw = ''
|
||
|
outfile = open(outfile, 'ab', 16)
|
||
|
|
||
|
while True:
|
||
|
line += '0x{:08X}: '.format(16*l)
|
||
|
decoded_line = ''
|
||
|
|
||
|
while c < 32:
|
||
|
char = fd.read(1)
|
||
|
|
||
|
if char == ' ':
|
||
|
continue
|
||
|
|
||
|
lineraw += char
|
||
|
|
||
|
if c % 2 == 0 and c != 0:
|
||
|
line += ' '
|
||
|
if c % 16 == 0 and c != 0:
|
||
|
line += ' '
|
||
|
|
||
|
# Reached end of data.
|
||
|
# Flush all buffers and read special stuff at the end.
|
||
|
if char == '\r' or char == '\n':
|
||
|
try:
|
||
|
line += ' |' + decode_ascii(lineraw, outfile) + '|'
|
||
|
except ValueError:
|
||
|
pass
|
||
|
print(line)
|
||
|
outfile.flush()
|
||
|
outfile.close()
|
||
|
_read_enddata(fd)
|
||
|
c += 1
|
||
|
line += char
|
||
|
|
||
|
line += ' |' + decode_ascii(lineraw, outfile) + '|'
|
||
|
|
||
|
print(line)
|
||
|
line = ''
|
||
|
lineraw = ''
|
||
|
l += 1
|
||
|
c = 0
|
||
|
|
||
|
|
||
|
class UART:
|
||
|
|
||
|
def __init__(self, devnode):
|
||
|
self.devnode = devnode
|
||
|
subprocess.call( [
|
||
|
'stty',
|
||
|
'--file={}'.format(self.devnode),
|
||
|
'115200',
|
||
|
'raw',
|
||
|
'-echok',
|
||
|
'-echo',
|
||
|
])
|
||
|
|
||
|
def send_cmd(self, code, multiline=False):
|
||
|
readfd = open(self.devnode, 'r')
|
||
|
writefd = open(self.devnode, 'w')
|
||
|
time.sleep(0.1)
|
||
|
writefd.write(code + '\n')
|
||
|
|
||
|
while True:
|
||
|
try:
|
||
|
if multiline:
|
||
|
signal.alarm(1)
|
||
|
char = readfd.read(1)
|
||
|
signal.alarm(0)
|
||
|
if not multiline:
|
||
|
if char == '\r' or char == '\n':
|
||
|
print()
|
||
|
break
|
||
|
print(char, end='')
|
||
|
except UnicodeDecodeError:
|
||
|
print('Received garbage from target.')
|
||
|
print('Is it already running?')
|
||
|
print('Shutting down...')
|
||
|
exit(1)
|
||
|
|
||
|
writefd.close()
|
||
|
readfd.close()
|
||
|
|
||
|
def open(self, mode):
|
||
|
return open(self.devnode, mode)
|
||
|
|
||
|
|
||
|
class REPL:
|
||
|
|
||
|
def __init__(self, start=0x00, length=0x10000, mode='bin',
|
||
|
byteorder='little', devnode='/dev/ttyUSB0'):
|
||
|
self.history = InMemoryHistory()
|
||
|
self.promt = '> '
|
||
|
self.config = {
|
||
|
'start': start,
|
||
|
'length': length,
|
||
|
'byteorder': byteorder,
|
||
|
'outfile': 'data-{}.bin'.format(datetime.now().strftime('%Y%m%d_%H%M')),
|
||
|
}
|
||
|
self.uart = UART(devnode)
|
||
|
|
||
|
def handle_cmd(self, cmd, *args):
|
||
|
if cmd == 'set':
|
||
|
self.set_config(args[0], args[1])
|
||
|
elif cmd == 'run':
|
||
|
self.apply_config()
|
||
|
self.uart.send_cmd('S')
|
||
|
fd = self.uart.open('r')
|
||
|
print()
|
||
|
read_ascii(fd, self.config['outfile'])
|
||
|
fd.close()
|
||
|
elif cmd == 'cmd':
|
||
|
self.uart.send_cmd(args[0], True)
|
||
|
elif cmd == 'help':
|
||
|
self.show_help()
|
||
|
elif cmd == 'exit':
|
||
|
print('Exit command received. Leaving...')
|
||
|
exit(0)
|
||
|
else:
|
||
|
print('Error: Unknown command.')
|
||
|
print("Try 'help'")
|
||
|
|
||
|
def show_config(self):
|
||
|
print('# Current Configuration')
|
||
|
lines = []
|
||
|
if self.config:
|
||
|
tpl = ' {:<12}: '
|
||
|
for key, val in self.config.items():
|
||
|
if isinstance(val, int):
|
||
|
fstr = tpl + '0x{:X}'
|
||
|
else:
|
||
|
fstr = tpl + '{}'
|
||
|
lines.append(fstr.format(key, val))
|
||
|
print('\n'.join(sorted(lines)))
|
||
|
|
||
|
def show_help(self):
|
||
|
print('# Supported commands')
|
||
|
print(' set KEY VAL : Set configuration value KEY to VAL')
|
||
|
print(' cmd CODE : Send command code to UART')
|
||
|
print(' run : Start reading out code')
|
||
|
print(' help : Show this help page')
|
||
|
print(' exit : Terminate programm')
|
||
|
|
||
|
def set_config(self, key, val):
|
||
|
if key == 'byteorder':
|
||
|
if val not in ('little', 'big'):
|
||
|
print('Error: Wrong byteorder. Choose "little" or "big".')
|
||
|
return
|
||
|
elif key == 'start':
|
||
|
val = int(val, 0)
|
||
|
if val > 0x10000:
|
||
|
print('Error: Start address is too large.')
|
||
|
return
|
||
|
elif key == 'length':
|
||
|
val = int(val, 0)
|
||
|
elif key == 'outfile':
|
||
|
self.config[key] = str(val)
|
||
|
else:
|
||
|
print('Error: Config key does not exist.')
|
||
|
return
|
||
|
|
||
|
self.config[key] = val
|
||
|
self.show_config()
|
||
|
|
||
|
def apply_config(self):
|
||
|
self.uart.send_cmd('A{:X}'.format(self.config['start']))
|
||
|
self.uart.send_cmd('L{:X}'.format(self.config['length']))
|
||
|
# Hardcode HEX mode
|
||
|
self.uart.send_cmd('H')
|
||
|
cmd = 'e' if self.config['byteorder'] == 'little' else 'E'
|
||
|
self.uart.send_cmd(cmd)
|
||
|
|
||
|
def run_loop(self):
|
||
|
self.show_help()
|
||
|
print()
|
||
|
self.show_config()
|
||
|
|
||
|
try:
|
||
|
while True:
|
||
|
cmd = prompt(self.promt,
|
||
|
history=self.history,
|
||
|
auto_suggest=AutoSuggestFromHistory(),
|
||
|
)
|
||
|
|
||
|
cmd = cmd.strip().split()
|
||
|
cmd = cmd if cmd else ''
|
||
|
|
||
|
if cmd != '':
|
||
|
self.handle_cmd(*cmd)
|
||
|
except KeyboardInterrupt:
|
||
|
print('KeyboardInterrupt received. Shutting down...')
|
||
|
exit(1)
|
||
|
except EOFError:
|
||
|
print('Reached end of line. Leaving...')
|
||
|
exit(0)
|
||
|
|
||
|
|
||
|
def main():
|
||
|
args = parse_args()
|
||
|
signal.signal(signal.SIGALRM, sigalarm_handler)
|
||
|
|
||
|
if not os.path.exists(args.SerialDeviceFILE):
|
||
|
print('Error: No such file.')
|
||
|
exit(1)
|
||
|
|
||
|
if args.interactive:
|
||
|
REPL(devnode=args.SerialDeviceFILE).run_loop()
|
||
|
exit(0)
|
||
|
|
||
|
# If the script is not in interactive mode, issue this stuff
|
||
|
# manually.
|
||
|
uart = UART(args.SerialDeviceFILE)
|
||
|
uart.send_cmd('A{:X}'.format(args.start))
|
||
|
uart.send_cmd('L{:X}'.format(args.length))
|
||
|
uart.send_cmd('H') # Hardcode HEX mode
|
||
|
if args.endianess == 'big':
|
||
|
uart.send_cmd('E')
|
||
|
else:
|
||
|
uart.send_cmd('e')
|
||
|
uart.send_cmd('S')
|
||
|
fd = uart.open('r')
|
||
|
print()
|
||
|
|
||
|
try:
|
||
|
read_ascii(fd, args.outfile)
|
||
|
except KeyboardInterrupt:
|
||
|
print('Leaving...')
|
||
|
finally:
|
||
|
fd.close()
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|