diff --git a/tools/decode-config.py b/tools/decode-config.py index dc51e0f42..284904fb9 100644 --- a/tools/decode-config.py +++ b/tools/decode-config.py @@ -19,23 +19,25 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . + Requirements: - Python - pip install json pycurl urllib2 configargparse + Instructions: - Execute command with option -d to retrieve config data from device or - use -f to read out a previously saved configuration file. + Execute command with option -d to retrieve config data from a host + or use -f to read out a configuration file saved using Tasmota Web-UI For help execute command with argument -h Usage: decode-config.py [-h] [-f ] [-d ] [-u ] - [-p ] [--format ] - [--json-indent ] [--json-compact] - [--sort ] [--raw] [--unhide-pw] [-o ] - [-c ] [-V] + [-p ] [--json-indent ] + [--json-compact] [--unsort] [--raw] [--unhide-pw] + [-o ] [--output-file-format ] + [-c ] [--exit-on-error-only] [-V] Decode configuration of Sonoff-Tasmota device. Args that start with '--' (eg. -f) can also be set in a config file (specified via -c). Config file syntax @@ -48,89 +50,84 @@ Usage: -c , --config Config file, can be used instead of command parameter (default: None) + --exit-on-error-only exit on error only (default: exit on ERROR and + WARNING). Not recommended, used by your own + responsibility! source: -f , --file file to retrieve Tasmota configuration from (default: - None) + None)' -d , --device hostname or IP address to retrieve Tasmota configuration from (default: None) -u , --username - host http access username (default: admin) + host HTTP access username (default: admin) -p , --password - host http access password (default: None) + host HTTP access password (default: None) output: - --format output format ("json" or "text", default: "json") --json-indent pretty-printed JSON output using indent level - (default: "None") + (default: 'None') --json-compact compact JSON output by eliminate whitespace (default: - "not compact") - --sort sort result - can be "none" or "name" (default: - "name") - --raw output raw values (default: processed) + normal) + --unsort do not sort results (default: sort) + --raw output raw values (default: process) --unhide-pw unhide passwords (default: hide) -o , --output-file - file to store decrypted raw binary configuration to - (default: None) + file to store configuration to (default: None) Macros: + @v=Tasmota version, @f=friendly name + --output-file-format + output format ('json' or 'binary', default: 'json') info: -V, --version show program's version number and exit Either argument -d or -f must be given. - -Examples: - Read configuration from hostname 'sonoff1' and output default json config - ./decode-config.py -d sonoff1 - - Read configuration from file 'Config__6.2.1.dmp' and output default json config - ./decode-config.py -f Config__6.2.1.dmp - - Read configuration from hostname 'sonoff1' using web login data - ./decode-config.py -d sonoff1 -u admin -p xxxx - - Read configuration from hostname 'sonoff1' using web login data and unhide passwords - ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw - - Read configuration from hostname 'sonoff1' using web login data, unhide passwords - and sort key names - ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw --sort name """ import os.path import io import sys -import configargparse -import collections import struct import re -import json +import math +from datetime import datetime +try: + import json +except ImportError: + print("module not found. Try 'pip install json' to install it") + sys.exit(9) +try: + import configargparse +except ImportError: + print("module not found. Try 'pip install configargparse' to install it") + sys.exit(9) try: import pycurl except ImportError: - print("module not found. Try 'pip pycurl' to install it") + print("module not found. Try 'pip install pycurl' to install it") sys.exit(9) try: import urllib2 except ImportError: - print("module not found. Try 'pip urllib2' to install it") + print("module not found. Try 'pip install urllib2' to install it") sys.exit(9) -VER = '1.5.0009' +VER = '1.5.0011' PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER) CONFIG_FILE_XOR = 0x5A - args = {} DEFAULTS = { 'DEFAULT': { 'configfile': None, + 'exitonwarning':True, }, 'source': { @@ -141,15 +138,16 @@ DEFAULTS = { }, 'output': { - 'format': 'json', 'jsonindent': None, 'jsoncompact': False, - 'sort': 'name', + 'unsort': False, 'raw': False, 'unhide-pw': False, 'outputfile': None, + 'outputfileformat': 'json', }, } +exitcode = 0 """ @@ -163,11 +161,35 @@ Settings dictionary describes the config file fields definition: format Define the data interpretation. - For details see struct module format string - https://docs.python.org/2.7/library/struct.html#format-strings + It is either a string or a tuple containing a string and a + sub-Settings dictionary. + 'xxx': + A string is used to interpret the data at + The string defines the format interpretion as described + in 'struct module format string', see + https://docs.python.org/2.7/library/struct.html#format-strings + In addition to this format string there is as special + meaning of a dot '.' - this means a bit with an optional + prefix length. If no prefix is given, 1 is assumed. + {}: + A dictionary describes itself a 'Settings' dictonary (recursive) baseaddr - The address (starting from 0) within config data + The address (starting from 0) within config data. + For bit fields must be a tuple. + n: + Defines a simple address within config data. + must be a positive integer. + (n, b, s): + A tuple defines a bit field: + + is the address within config data (integer) + + how many bits are used (positive integer) + + bit shift (integer) + positive shift the result right bits + negative shift the result left bits datadef Define the field interpretation different from simple @@ -182,91 +204,74 @@ Settings dictionary describes the config file fields definition: Defines a one-dimensional array of size [n, n <,n...>] Defines a multi-dimensional array - [{} <,{}...] - Defines a bit struct. The items are simply dict - {'bitname', bitlen}, the dict order is important. convert (optional) Define an output/conversion methode, can be a simple string or a previously defined function name. - 'xxx': - a string defines a format specification of the string - formatter, see - https://docs.python.org/2.7/library/string.html#format-string-syntax + 'xxx?': + a string will be evaluate as is replacing all '?' chars + with the current value. This can also be contain pyhton + code. func: a function defines the name of a formating function """ # config data conversion function and helper -def baudrate(value): - return value * 1200 - def int2ip(value): return '{:d}.{:d}.{:d}.{:d}'.format(value & 0xff, value>>8 & 0xff, value>>16 & 0xff, value>>24 & 0xff) -def int2geo(value): - return float(value) / 1000000 - def password(value): if args.unhidepw: return value return '********' -def fingerprintstr(value): - s = list(value) - result = '' - for c in s: - if c in '0123456789abcdefABCDEF': - result += c - return result - - Setting_6_2_1 = { 'cfg_holder': (' from fielddef[0] """ + return fielddef[0] - length=0 - if fielddef[2] is not None: - # fielddef[2] contains a array or int - # calc size recursive by sum of all elements +def GetFieldBaseAddr(fielddef): + """ + Return the format item of field definition - # tuple 2 contains a list with integer or an integer value - if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int): - for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ): - # multidimensional array - if isinstance(fielddef[2], list) and len(fielddef[2])>1: - length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) ) - else: - length += GetFieldLength( (fielddef[0], fielddef[1], None) ) - else: - if fielddef[0][-1:].lower() in ['b','c','?']: - length=1 - elif fielddef[0][-1:].lower() in ['h']: - length=2 - elif fielddef[0][-1:].lower() in ['i','l','f']: - length=4 - elif fielddef[0][-1:].lower() in ['q','d']: - length=8 - elif fielddef[0][-1:].lower() in ['s','p']: - # s and p needs prefix as length - match = re.search("\s*(\d+)", fielddef[0]) - if match: - length=int(match.group(0)) + @param fielddef: + field format - see "Settings dictionary" above + + @return: ,, from fielddef[1] + + """ + baseaddr = fielddef[1] + if isinstance(baseaddr, tuple): + return baseaddr[0], baseaddr[1], baseaddr[2] + + return baseaddr, 0, 0 + + +def MakeFieldBaseAddr(baseaddr, bitlen, bitshift): + """ + Return a based on given arguments + + @param baseaddr: + baseaddr from Settings definition + @param bitlen: + 0 or bitlen + @param bitshift: + 0 or bitshift + + @return: (,,) if bitlen != 0 + baseaddr if bitlen == 0 + + """ + if bitlen!=0: + return (baseaddr, bitlen, bitshift) + return baseaddr - # it's a single value - return length def ConvertFieldValue(value, fielddef, raw=False): """ @@ -1682,21 +1850,94 @@ def ConvertFieldValue(value, fielddef, raw=False): @param value: original value read from binary data @param fielddef - field definition (contains possible conversion defiinition) + field definition - see "Settings dictionary" above @param raw return raw values (True) or converted values (False) @return: (un)converted value """ if not raw and len(fielddef)>3: - if isinstance(fielddef[3],str): # use a format string - return fielddef[3].format(value) - elif callable(fielddef[3]): # use a format function - return fielddef[3](value) + convert = fielddef[3] + if isinstance(convert,str): # evaluate strings + try: + return eval(convert.replace('?','value')) + except: + return value + elif callable(convert): # use as format function + return convert(value) return value -def GetField(dobj, fieldname, fielddef, raw=False): +def GetFieldLength(fielddef): + """ + Return length of a field in bytes based on field format definition + + @param fielddef: + field format - see "Settings dictionary" above + + @return: length of field in bytes + + """ + + length=0 + format_ = GetFieldFormat(fielddef) + + # get datadef from field definition + datadef = None + if len(fielddef)>2: + datadef = fielddef[2] + + if datadef is not None: + # fielddef[2] contains a array or int + # calc size recursive by sum of all elements + + # contains a integer list or an single integer value + if (isinstance(datadef, list) \ + and len(datadef)>0 \ + and isinstance(datadef[0], int)) \ + or isinstance(datadef, int): + + for i in range(0, datadef[0] if isinstance(datadef, list) else datadef ): + + # multidimensional array + if isinstance(datadef, list) and len(datadef)>1: + length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) ) + + # single array + else: + length += GetFieldLength( (fielddef[0], fielddef[1], None) ) + + else: + if isinstance(fielddef[0], dict): + # -> iterate through format_ + addr = -1 + setting = fielddef[0] + for name in setting: + baseaddr, bitlen, bitshift = GetFieldBaseAddr(setting[name]) + len_ = GetFieldLength(setting[name]) + if addr != baseaddr: + addr = baseaddr + length += len_ + + else: + if format_[-1:].lower() in ['b','c','?']: + length=1 + elif format_[-1:].lower() in ['h']: + length=2 + elif format_[-1:].lower() in ['i','l','f']: + length=4 + elif format_[-1:].lower() in ['q','d']: + length=8 + elif format_[-1:].lower() in ['s','p']: + # s and p may have a prefix as length + match = re.search("\s*(\d+)", format_) + if match: + length=int(match.group(0)) + + return length + + +def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): """ Get field value from definition @@ -1714,45 +1955,78 @@ def GetField(dobj, fieldname, fielddef, raw=False): result = None - if fielddef[2] is not None: + # get format from field definition + format_ = GetFieldFormat(fielddef) + + # get baseaddr from field definition + baseaddr, bitlen, bitshift = GetFieldBaseAddr(fielddef) + + # get datadef from field definition + datadef = None + if len(fielddef)>2: + datadef = fielddef[2] + + if datadef is not None: result = [] - # tuple 2 contains a list with integer or an integer value - if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int): - addr = fielddef[1] - for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ): + # contains a integer list or an single integer value + if (isinstance(datadef, list) \ + and len(datadef)>0 \ + and isinstance(datadef[0], int)) \ + or isinstance(datadef, int): + + offset = 0 + for i in range(0, datadef[0] if isinstance(datadef, list) else datadef): + # multidimensional array - if isinstance(fielddef[2], list) and len(fielddef[2])>1: - subfielddef = (fielddef[0], addr, fielddef[2][1:], None if len(fielddef)<4 else fielddef[3]) - else: # single array - subfielddef = (fielddef[0], addr, None, None if len(fielddef)<4 else fielddef[3]) + if isinstance(datadef, list) and len(datadef)>1: + if len(fielddef)<4: + subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:]) + else: + subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:], fielddef[3]) + + # single array + else: + if len(fielddef)<4: + subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None) + else: + subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None, fielddef[3]) + length = GetFieldLength(subfielddef) if length != 0: - result.append(GetField(dobj, fieldname, subfielddef, raw)) - addr += length - # tuple 2 contains a list with dict - elif isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], dict): - d = {} - value = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0] - d['base'] = ConvertFieldValue(value, fielddef, raw); - union = fielddef[2] - i = 0 - for l in union: - for name,bits in l.items(): - bitval = (value & ( ((1<> i - d[name] = bitval - i += bits - result = d + result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset)) + offset += length + else: - # it's a single value - if GetFieldLength(fielddef) != 0: - result = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0] - if fielddef[0][-1:].lower() in ['s','p']: - if ord(result[:1])==0x00 or ord(result[:1])==0xff: - result = '' - s = str(result).split('\0')[0] - result = unicode(s, errors='replace') - result = ConvertFieldValue(result, fielddef, raw) + # contains a dict + if isinstance(fielddef[0], dict): + # -> iterate through format_ + setting = fielddef[0] + config = {} + for name in setting: + config[name] = GetField(dobj, name, setting[name], raw=args.raw, addroffset=addroffset) + result = config + else: + # a simple value + if GetFieldLength(fielddef) != 0: + result = struct.unpack_from(format_, dobj, baseaddr+addroffset)[0] + + if not format_[-1:].lower() in ['s','p']: + if bitshift>=0: + result >>= bitshift + else: + result <<= abs(bitshift) + if bitlen>0: + result &= (1< 127 + result = unicode(s, errors='ignore') + + result = ConvertFieldValue(result, fielddef, raw) return result @@ -1779,6 +2053,8 @@ def Decode(obj): @param obj: binary config data (decrypted) + + @return: configuration dictionary """ # get header data version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True) @@ -1792,16 +2068,20 @@ def Decode(obj): # if we did not found a mathching setting if template is None: - exit(2, "Can't handle Tasmota configuration data for version 0x{:x}".format(version) ) - + exit(2, "Tasmota configuration version 0x{:x} not supported".format(version) ) + setting = template[2] # check size if exists if 'cfg_size' in setting: cfg_size = GetField(obj, 'cfg_size', setting['cfg_size'], raw=True) - # if we did not found a mathching setting - if cfg_size != template[1]: - exit(2, "Data size does not match. Expected {} bytes, read {} bytes.".format(template[1], cfg_size) ) + # read size should be same as definied in template + if cfg_size > template[1]: + # may be processed + exit(3, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, template[1]), typ='WARNING', doexit=args.exitonwarning) + elif cfg_size < template[1]: + # less number of bytes can not be processed + exit(3, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, template[1]), typ='ERROR') # check crc if exists if 'cfg_crc' in setting: @@ -1809,25 +2089,34 @@ def Decode(obj): else: cfg_crc = GetSettingsCrc(obj) if cfg_crc != GetSettingsCrc(obj): - exit(3, 'Data crc error' ) + exit(4, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(obj)), typ='WARNING', doexit=args.exitonwarning) config = {} - config['version_template'] = '0x{:x}'.format(template[0]) for name in setting: - config[name] = GetField(obj, name, setting[name], args.raw) + config[name] = GetField(obj, name, setting[name], raw=args.raw) - if args.sort == 'name': - config = collections.OrderedDict(sorted(config.items())) - - if args.format == 'json': - print json.dumps(config, sort_keys=args.sort=='name', indent=args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') ) - else: - for key,value in config.items(): - print '{} = {}'.format(key, repr(value)) + # add header info + timestamp = datetime.now() + config['header'] = { 'timestamp': timestamp.strftime("%Y-%m-%d %H:%M:%S"), + 'data': { + 'crc': hex(GetSettingsCrc(obj)), + 'size': len(obj), + 'template_version': hex(template[0]), + 'content': { + 'crc': hex(cfg_crc), + 'size': cfg_size, + 'version': hex(version), + }, + }, + 'scriptname': os.path.basename(__file__), + 'scriptversion': VER, + } + return config if __name__ == "__main__": + # program argument processing parser = configargparse.ArgumentParser(description='Decode configuration of Sonoff-Tasmota device.', epilog='Either argument -d or -f must be given.') @@ -1836,80 +2125,89 @@ if __name__ == "__main__": metavar='', dest='tasmotafile', default=DEFAULTS['source']['tasmotafile'], - help='file to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['tasmotafile'])) + help="file to retrieve Tasmota configuration from (default: {})'".format(DEFAULTS['source']['tasmotafile'])) source.add_argument('-d', '--device', metavar='', dest='device', default=DEFAULTS['source']['device'], - help='hostname or IP address to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['device']) ) + help="hostname or IP address to retrieve Tasmota configuration from (default: {})".format(DEFAULTS['source']['device']) ) source.add_argument('-u', '--username', metavar='', dest='username', default=DEFAULTS['source']['username'], - help='host http access username (default: {})'.format(DEFAULTS['source']['username'])) + help="host HTTP access username (default: {})".format(DEFAULTS['source']['username'])) source.add_argument('-p', '--password', metavar='', dest='password', default=DEFAULTS['source']['password'], - help='host http access password (default: {})'.format(DEFAULTS['source']['password'])) + help="host HTTP access password (default: {})".format(DEFAULTS['source']['password'])) output = parser.add_argument_group('output') - output.add_argument('--format', - metavar='', - dest='format', - choices=['json', 'text'], - default=DEFAULTS['output']['format'], - help='output format ("json" or "text", default: "{}")'.format(DEFAULTS['output']['format']) ) output.add_argument('--json-indent', metavar='', dest='jsonindent', type=int, default=DEFAULTS['output']['jsonindent'], - help='pretty-printed JSON output using indent level (default: "{}")'.format(DEFAULTS['output']['jsonindent']) ) + help="pretty-printed JSON output using indent level (default: '{}')".format(DEFAULTS['output']['jsonindent']) ) output.add_argument('--json-compact', dest='jsoncompact', action='store_true', default=DEFAULTS['output']['jsoncompact'], - help='compact JSON output by eliminate whitespace (default: "{}")'.format('compact' if DEFAULTS['output']['jsoncompact'] else 'not compact') ) - output.add_argument('--sort', - metavar='', - dest='sort', - choices=['none', 'name'], - default=DEFAULTS['output']['sort'], - help='sort result - can be "none" or "name" (default: "{}")'.format(DEFAULTS['output']['sort']) ) + help="compact JSON output by eliminate whitespace (default: {})".format('normal' if not DEFAULTS['output']['jsoncompact'] else 'compact') ) + output.add_argument('--unsort', + dest='unsort', + action='store_true', + default=DEFAULTS['output']['unsort'], + help="do not sort results (default: {})".format('sort' if not DEFAULTS['output']['unsort'] else 'unsort') ) output.add_argument('--raw', dest='raw', action='store_true', default=DEFAULTS['output']['raw'], - help='output raw values (default: {})'.format('raw' if DEFAULTS['output']['raw'] else 'processed') ) + help="output raw values (default: {})".format('raw' if DEFAULTS['output']['raw'] else 'process') ) output.add_argument('--unhide-pw', dest='unhidepw', action='store_true', default=DEFAULTS['output']['unhide-pw'], - help='unhide passwords (default: {})'.format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') ) + help="unhide passwords (default: {})".format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') ) output.add_argument('-o', '--output-file', metavar='', dest='outputfile', default=DEFAULTS['output']['outputfile'], - help='file to store decrypted raw binary configuration to (default: {})'.format(DEFAULTS['output']['outputfile'])) + help="file to store configuration to (default: {}) Macros: @v=Tasmota version, @f=friendly name".format(DEFAULTS['output']['outputfile'])) + output.add_argument('--output-file-format', + metavar='', + dest='outputfileformat', + choices=['json', 'binary'], + default=DEFAULTS['output']['outputfileformat'], + help="output format ('json' or 'binary', default: '{}')".format(DEFAULTS['output']['outputfileformat']) ) parser.add_argument('-c', '--config', metavar='', dest='configfile', default=DEFAULTS['DEFAULT']['configfile'], is_config_file=True, - help='Config file, can be used instead of command parameter (default: {})'.format(DEFAULTS['DEFAULT']['configfile']) ) + help="Config file, can be used instead of command parameter (default: {})".format(DEFAULTS['DEFAULT']['configfile']) ) + parser.add_argument('--exit-on-error-only', + dest='exitonwarning', + action='store_false', + default=DEFAULTS['DEFAULT']['exitonwarning'], + help="exit on error only (default: {}). Not recommended, used by your own responsibility!".format('exit on ERROR and WARNING' if DEFAULTS['DEFAULT']['exitonwarning'] else 'exit on ERROR') ) info = parser.add_argument_group('info') info.add_argument('-V', '--version', action='version', version=PROG) args = parser.parse_args() - + + # default no configuration available configobj = None - + + # check source args + if args.device is not None and args.tasmotafile is not None: + exit(6, "Only one source allowed. Do not use -d and -f together") + + # read config direct from device via http if args.device is not None: - # read config direct from device via http buffer = io.BytesIO() url = str("http://{}/dl".format(args.device)) c = pycurl.Curl() @@ -1930,11 +2228,11 @@ if __name__ == "__main__": configobj = buffer.getvalue() + # read config from a file elif args.tasmotafile is not None: - # read config from a file if not os.path.isfile(args.tasmotafile): # check file exists - exit(1, "file '{}' not found".format(args.tasmotafile)) + exit(1, "File '{}' not found".format(args.tasmotafile)) try: tasmotafile = open(args.tasmotafile, "rb") configobj = tasmotafile.read() @@ -1942,22 +2240,59 @@ if __name__ == "__main__": except Exception, e: exit(e[0], e[1]) + # no config source given else: parser.print_help() sys.exit(0) + if configobj is not None and len(configobj)>0: cfg = DeEncrypt(configobj) - if args.outputfile is not None: - outputfile = open(args.outputfile, "wb") - outputfile.write(cfg) - outputfile.close() + config = Decode(cfg) - Decode(cfg) + # output to file + if args.outputfile is not None: + outputfilename = args.outputfile + v = f1 = f2 = f3 = f4 = '' + if 'version' in config: + ver = int(str(config['version']), 0) + major = ((ver>>24) & 0xff) + minor = ((ver>>16) & 0xff) + release = ((ver>> 8) & 0xff) + subrelease = (ver & 0xff) + if major>=6: + if subrelease>0: + subreleasestr = str(subrelease) + else: + subreleasestr = '' + else: + if subrelease>0: + subreleasestr = str(chr(subrelease+ord('a')-1)) + else: + subreleasestr = '' + v = "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if major>=6 else '', subreleasestr) + outputfilename = outputfilename.replace('@v', v) + if 'friendlyname' in config: + outputfilename = outputfilename.replace('@f', config['friendlyname'][0] ) + + if args.outputfileformat == 'binary': + outputfile = open(outputfilename, "wb") + outputfile.write(struct.pack('