2023-05-15 06:08:25 -03:00
#!/usr/bin/env python3
2017-11-09 02:46:30 -04:00
'''
tool to manipulate ArduPilot firmware files , changing default parameters
'''
import os , sys , struct , json , base64 , zlib , hashlib
import argparse
2019-08-22 05:20:48 -03:00
def to_ascii ( s ) :
''' get ascii string '''
if sys . version_info . major > = 3 :
return str ( s , ' ascii ' )
else :
return str ( s )
def to_bytes ( s ) :
''' get bytes string '''
if sys . version_info . major > = 3 :
if isinstance ( s , bytes ) :
return s
if isinstance ( s , int ) :
s = chr ( s )
return bytes ( s , ' ascii ' )
else :
return bytes ( s )
2017-11-09 02:46:30 -04:00
class embedded_defaults ( object ) :
''' class to manipulate embedded defaults in a firmware '''
def __init__ ( self , filename ) :
self . filename = filename
self . offset = 0
2018-03-24 02:11:50 -03:00
self . max_len = 0
2017-11-09 02:46:30 -04:00
self . extension = os . path . splitext ( filename ) [ 1 ]
if self . extension . lower ( ) in [ ' .apj ' , ' .px4 ' ] :
self . load_apj ( )
elif self . extension . lower ( ) in [ ' .abin ' ] :
self . load_abin ( )
else :
self . load_binary ( )
def load_binary ( self ) :
''' load firmware from binary file '''
2018-03-24 02:11:50 -03:00
f = open ( self . filename , ' rb ' )
2017-11-09 02:46:30 -04:00
self . firmware = f . read ( )
f . close ( )
print ( " Loaded binary file of length %u " % len ( self . firmware ) )
def load_abin ( self ) :
''' load firmware from abin file '''
f = open ( self . filename , ' r ' )
self . headers = [ ]
while True :
line = f . readline ( ) . rstrip ( )
if line == ' -- ' :
break
self . headers . append ( line )
if len ( self . headers ) > 50 :
print ( " Error: too many abin headers " )
sys . exit ( 1 )
self . firmware = f . read ( )
f . close ( )
print ( " Loaded abin file of length %u " % len ( self . firmware ) )
def load_apj ( self ) :
''' load firmware from a json apj or px4 file '''
f = open ( self . filename , ' r ' )
self . fw_json = json . load ( f )
f . close ( )
self . firmware = zlib . decompress ( base64 . b64decode ( self . fw_json [ ' image ' ] ) )
print ( " Loaded apj file of length %u " % len ( self . firmware ) )
def save_binary ( self ) :
''' save binary file '''
2018-12-04 18:37:35 -04:00
f = open ( self . filename , ' wb ' )
2017-11-09 02:46:30 -04:00
f . write ( self . firmware )
f . close ( )
print ( " Saved binary of length %u " % len ( self . firmware ) )
def save_apj ( self ) :
''' save apj file '''
2019-08-22 05:20:48 -03:00
self . fw_json [ ' image ' ] = to_ascii ( base64 . b64encode ( zlib . compress ( self . firmware , 9 ) ) )
2017-11-09 02:46:30 -04:00
f = open ( self . filename , ' w ' )
json . dump ( self . fw_json , f , indent = 4 )
f . truncate ( )
f . close ( )
print ( " Saved apj of length %u " % len ( self . firmware ) )
def save_abin ( self ) :
''' save abin file '''
f = open ( self . filename , ' w ' )
for i in range ( len ( self . headers ) ) :
line = self . headers [ i ]
if line . startswith ( ' MD5: ' ) :
h = hashlib . new ( ' md5 ' )
h . update ( self . firmware )
f . write ( ' MD5: %s \n ' % h . hexdigest ( ) )
else :
f . write ( line + ' \n ' )
f . write ( ' -- \n ' )
f . write ( self . firmware )
f . close ( )
print ( " Saved abin of length %u " % len ( self . firmware ) )
def find ( self ) :
''' find defaults in firmware '''
# these are the magic headers from AP_Param.cpp
2019-04-18 02:29:16 -03:00
magic_str = " PARMDEF " . encode ( ' ascii ' )
2017-11-09 02:46:30 -04:00
param_magic = [ 0x55 , 0x37 , 0xf4 , 0xa0 , 0x38 , 0x5d , 0x48 , 0x5b ]
2019-04-18 02:29:16 -03:00
def u_ord ( c ) :
2023-08-31 18:55:31 -03:00
return ord ( c ) if sys . version_info . major < 3 else c
2019-04-18 02:29:16 -03:00
2017-11-09 02:46:30 -04:00
while True :
i = self . firmware [ self . offset : ] . find ( magic_str )
if i == - 1 :
2018-03-24 02:11:50 -03:00
print ( " No param area found " )
2017-11-09 02:46:30 -04:00
return None
matched = True
for j in range ( len ( param_magic ) ) :
2019-04-18 02:29:16 -03:00
if u_ord ( self . firmware [ self . offset + i + j + 8 ] ) != param_magic [ j ] :
2017-11-09 02:46:30 -04:00
matched = False
break
if not matched :
self . offset + = i + 8
continue
self . offset + = i
self . max_len , self . length = struct . unpack ( " <HH " , self . firmware [ self . offset + 16 : self . offset + 20 ] )
return True
def contents ( self ) :
''' return current contents '''
contents = self . firmware [ self . offset + 20 : self . offset + 20 + self . length ]
# remove carriage returns
2019-08-22 05:20:48 -03:00
contents = contents . replace ( b ' \r ' , b ' ' )
2017-11-09 02:46:30 -04:00
return contents
def set_contents ( self , contents ) :
''' set new defaults as a string '''
length = len ( contents )
if length > self . max_len :
print ( " Error: Length %u larger than maximum %u " % ( length , self . max_len ) )
sys . exit ( 1 )
new_fw = self . firmware [ : self . offset + 18 ]
new_fw + = struct . pack ( " <H " , length )
2019-08-22 05:20:48 -03:00
new_fw + = to_bytes ( contents )
2017-11-09 02:46:30 -04:00
new_fw + = self . firmware [ self . offset + 20 + length : ]
self . firmware = new_fw
self . length = len ( contents )
def set_file ( self , filename ) :
''' set defaults to contents of a file '''
print ( " Setting defaults from %s " % filename )
f = open ( filename , ' r ' )
contents = f . read ( )
f . close ( )
# remove carriage returns from the file
contents = contents . replace ( ' \r ' , ' ' )
self . set_contents ( contents )
2019-08-22 05:20:48 -03:00
def split_multi ( self , s , separators ) :
2017-11-09 02:46:30 -04:00
''' split a string, handling multiple separators '''
for sep in separators :
2019-08-22 05:20:48 -03:00
s = s . replace ( to_bytes ( sep ) , b ' ' )
return s . split ( )
2017-11-09 02:46:30 -04:00
def set_one ( self , set ) :
''' set a single parameter '''
v = set . split ( ' = ' )
if len ( v ) != 2 :
print ( " Error: set takes form NAME=VALUE " )
sys . exit ( 1 )
2019-08-22 05:20:48 -03:00
param_name = to_bytes ( v [ 0 ] . upper ( ) )
param_value = to_bytes ( v [ 1 ] )
2017-11-09 02:46:30 -04:00
contents = self . contents ( )
2019-08-22 05:20:48 -03:00
lines = contents . strip ( ) . split ( b ' \n ' )
2017-11-09 02:46:30 -04:00
changed = False
for i in range ( len ( lines ) ) :
2019-08-22 05:20:48 -03:00
a = self . split_multi ( lines [ i ] , b " , = \t " )
2017-11-09 02:46:30 -04:00
if len ( a ) != 2 :
continue
if a [ 0 ] . upper ( ) == param_name :
2019-08-22 05:20:48 -03:00
separator = to_bytes ( lines [ i ] [ len ( param_name ) ] )
lines [ i ] = b ' %s %s %s ' % ( param_name , separator , param_value )
2017-11-09 02:46:30 -04:00
changed = True
if not changed :
2019-08-22 05:20:48 -03:00
lines . append ( to_bytes ( ' %s = %s ' % ( to_ascii ( param_name ) , to_ascii ( param_value ) ) ) )
contents = b ' \n ' . join ( lines )
contents = contents . lstrip ( ) + b ' \n '
2017-11-09 02:46:30 -04:00
self . set_contents ( contents )
def save ( self ) :
''' save new firmware '''
if self . extension . lower ( ) in [ ' .apj ' , ' .px4 ' ] :
self . save_apj ( )
elif self . extension . lower ( ) in [ ' .abin ' ] :
self . save_abin ( )
else :
self . save_binary ( )
def extract ( self ) :
''' extract firmware image to *.bin '''
a = os . path . splitext ( self . filename )
if len ( a ) == 1 :
a . append ( ' .bin ' )
else :
a = ( a [ 0 ] , ' .bin ' )
binfile = ' ' . join ( a )
print ( " Extracting firmware to %s " % binfile )
2018-12-04 18:37:35 -04:00
f = open ( binfile , ' wb ' )
2017-11-09 02:46:30 -04:00
f . write ( self . firmware )
f . close ( )
def defaults_contents ( firmware , ofs , length ) :
''' return current defaults contents '''
return firmware
2018-03-24 02:11:50 -03:00
if __name__ == ' __main__ ' :
parser = argparse . ArgumentParser ( description = ' manipulate parameter defaults in an ArduPilot firmware ' )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
parser . add_argument ( ' firmware_file ' )
parser . add_argument ( ' --set-file ' , type = str , default = None , help = ' replace parameter defaults from a file ' )
parser . add_argument ( ' --set ' , type = str , default = None , help = ' replace one parameter default, in form NAME=VALUE ' )
parser . add_argument ( ' --show ' , action = ' store_true ' , default = False , help = ' show current parameter defaults ' )
parser . add_argument ( ' --extract ' , action = ' store_true ' , default = False , help = ' extract firmware image to *.bin ' )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
args = parser . parse_args ( )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
defaults = embedded_defaults ( args . firmware_file )
2017-11-09 02:46:30 -04:00
2022-08-31 19:17:18 -03:00
have_defaults = defaults . find ( )
if not have_defaults and not args . extract :
2024-01-16 21:48:19 -04:00
print ( " Error: Param defaults support not found in firmware; see https://ardupilot.org/copter/docs/common-oem-customizations.html for embedding defaults.parm " )
2018-03-24 02:11:50 -03:00
sys . exit ( 1 )
2022-08-31 19:17:18 -03:00
if have_defaults :
print ( " Found param defaults max_length= %u length= %u " % ( defaults . max_len , defaults . length ) )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
if args . set_file :
# load new defaults from a file
defaults . set_file ( args . set_file )
defaults . save ( )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
if args . set :
# set a single parameter
defaults . set_one ( args . set )
defaults . save ( )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
if args . show :
# show all defaults
2019-08-22 05:20:48 -03:00
print ( to_ascii ( defaults . contents ( ) ) )
2017-11-09 02:46:30 -04:00
2018-03-24 02:11:50 -03:00
if args . extract :
defaults . extract ( )