2016-08-17 15:08:22 +00:00
#!/usr/bin/env python
#
# ESP32 partition table generation tool
#
# Converts partition tables to/from CSV and binary formats.
#
# See the sdkng README.md file for details about how to use this tool.
import struct
import argparse
import sys
2016-11-07 04:32:21 +00:00
MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature
2016-08-17 15:08:22 +00:00
__version__ = ' 1.0 '
quiet = False
def status ( msg ) :
""" Print status message to stderr """
if not quiet :
critical ( msg )
def critical ( msg ) :
""" Print critical message to stderr """
if not quiet :
sys . stderr . write ( msg )
sys . stderr . write ( ' \n ' )
class PartitionTable ( list ) :
def __init__ ( self ) :
super ( PartitionTable , self ) . __init__ ( self )
@classmethod
def from_csv ( cls , csv_contents ) :
res = PartitionTable ( )
lines = csv_contents . split ( " \n " )
for line_no in range ( len ( lines ) ) :
line = lines [ line_no ] . strip ( )
if line . startswith ( " # " ) or len ( line ) == 0 :
continue
try :
res . append ( PartitionDefinition . from_csv ( line ) )
except InputError as e :
raise InputError ( " Error at line %d : %s " % ( line_no + 1 , e ) )
except Exception :
critical ( " Unexpected error parsing line %d : %s " % ( line_no + 1 , line ) )
raise
# fix up missing offsets & negative sizes
last_end = 0x5000 # first offset after partition table
for e in res :
if e . offset is None :
pad_to = 0x10000 if e . type == PartitionDefinition . APP_TYPE else 4
if last_end % pad_to != 0 :
last_end + = pad_to - ( last_end % pad_to )
e . offset = last_end
if e . size < 0 :
e . size = - e . size - e . offset
last_end = e . offset + e . size
return res
def __getitem__ ( self , item ) :
""" Allow partition table access via name as well as by
numeric index . """
if isinstance ( item , str ) :
for x in self :
if x . name == item :
return x
raise ValueError ( " No partition entry named ' %s ' " % item )
else :
return super ( PartitionTable , self ) . __getitem__ ( item )
def verify ( self ) :
# verify each partition individually
for p in self :
p . verify ( )
# check for overlaps
last = None
for p in sorted ( self ) :
if p . offset < 0x5000 :
raise InputError ( " Partition offset 0x %x is below 0x5000 " % p . offset )
if last is not None and p . offset < last . offset + last . size :
raise InputError ( " Partition at 0x %x overlaps 0x %x -0x %x " % ( p . offset , last . offset , last . offset + last . size - 1 ) )
last = p
@classmethod
def from_binary ( cls , b ) :
result = cls ( )
for o in range ( 0 , len ( b ) , 32 ) :
2016-11-07 04:45:57 +00:00
data = b [ o : o + 32 ]
if len ( data ) != 32 :
raise InputError ( " Ran out of partition table data before reaching end marker " )
if data == ' \xFF ' * 32 :
break # end of partition table
result . append ( PartitionDefinition . from_binary ( data ) )
2016-08-17 15:08:22 +00:00
return result
def to_binary ( self ) :
2016-11-07 04:32:21 +00:00
result = " " . join ( e . to_binary ( ) for e in self )
if len ( result ) > = MAX_PARTITION_LENGTH :
raise InputError ( " Binary partition table length ( %d ) longer than max " % len ( result ) )
result + = " \xFF " * ( MAX_PARTITION_LENGTH - len ( result ) ) # pad the sector, for signing
return result
2016-08-17 15:08:22 +00:00
def to_csv ( self , simple_formatting = False ) :
rows = [ " # Espressif ESP32 Partition Table " ,
" # Name, Type, SubType, Offset, Size " ]
rows + = [ x . to_csv ( simple_formatting ) for x in self ]
return " \n " . join ( rows ) + " \n "
class PartitionDefinition ( object ) :
APP_TYPE = 0x00
DATA_TYPE = 0x01
TYPES = {
" app " : APP_TYPE ,
" data " : DATA_TYPE ,
}
2016-11-15 10:35:10 +00:00
# Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
2016-08-17 15:08:22 +00:00
SUBTYPES = {
APP_TYPE : {
" factory " : 0x00 ,
" test " : 0x20 ,
} ,
DATA_TYPE : {
" ota " : 0x00 ,
2016-11-15 10:35:10 +00:00
" phy " : 0x01 ,
" nvs " : 0x02 ,
" esphttpd " : 0x80 ,
" fat " : 0x81 ,
" spiffs " : 0x82 ,
2016-08-17 15:08:22 +00:00
} ,
}
MAGIC_BYTES = " \xAA \x50 "
ALIGNMENT = {
APP_TYPE : 0x1000 ,
DATA_TYPE : 0x04 ,
}
# add subtypes for the 16 OTA slot values ("ota_XXX, etc.")
for ota_slot in range ( 16 ) :
SUBTYPES [ TYPES [ " app " ] ] [ " ota_ %d " % ota_slot ] = 0x10 + ota_slot
def __init__ ( self ) :
self . name = " "
self . type = None
self . subtype = None
self . offset = None
self . size = None
@classmethod
def from_csv ( cls , line ) :
""" Parse a line from the CSV """
line_w_defaults = line + " ,,, " # lazy way to support default fields
fields = [ f . strip ( ) for f in line_w_defaults . split ( " , " ) ]
res = PartitionDefinition ( )
res . name = fields [ 0 ]
res . type = res . parse_type ( fields [ 1 ] )
res . subtype = res . parse_subtype ( fields [ 2 ] )
res . offset = res . parse_address ( fields [ 3 ] )
res . size = res . parse_address ( fields [ 4 ] )
if res . size is None :
raise InputError ( " Size field can ' t be empty " )
return res
def __eq__ ( self , other ) :
return self . name == other . name and self . type == other . type \
and self . subtype == other . subtype and self . offset == other . offset \
and self . size == other . size
def __repr__ ( self ) :
def maybe_hex ( x ) :
return " 0x %x " % x if x is not None else " None "
return " PartitionDefinition( ' %s ' , 0x %x , 0x %x , %s , %s ) " % ( self . name , self . type , self . subtype or 0 ,
maybe_hex ( self . offset ) , maybe_hex ( self . size ) )
def __str__ ( self ) :
return " Part ' %s ' %d / %d @ 0x %x size 0x %x " % ( self . name , self . type , self . subtype , self . offset or - 1 , self . size or - 1 )
def __cmp__ ( self , other ) :
return self . offset - other . offset
def parse_type ( self , strval ) :
if strval == " " :
raise InputError ( " Field ' type ' can ' t be left empty. " )
return parse_int ( strval , self . TYPES )
def parse_subtype ( self , strval ) :
if strval == " " :
return 0 # default
return parse_int ( strval , self . SUBTYPES . get ( self . type , { } ) )
def parse_address ( self , strval ) :
if strval == " " :
return None # PartitionTable will fill in default
return parse_int ( strval )
def verify ( self ) :
if self . type is None :
raise ValidationError ( " Type field is not set " )
if self . subtype is None :
raise ValidationError ( " Subtype field is not set " )
if self . offset is None :
raise ValidationError ( " Offset field is not set " )
align = self . ALIGNMENT . get ( self . type , 4 )
if self . offset % align :
raise ValidationError ( " %s offset 0x %x is not aligned to 0x %x " % ( self . name , self . offset , align ) )
if self . size is None :
raise ValidationError ( " Size field is not set " )
STRUCT_FORMAT = " <2sBBLL16sL "
@classmethod
def from_binary ( cls , b ) :
if len ( b ) != 32 :
raise InputError ( " Partition definition length must be exactly 32 bytes. Got %d bytes. " % len ( b ) )
res = cls ( )
( magic , res . type , res . subtype , res . offset ,
res . size , res . name , reserved ) = struct . unpack ( cls . STRUCT_FORMAT , b )
if " \x00 " in res . name : # strip null byte padding from name string
res . name = res . name [ : res . name . index ( " \x00 " ) ]
if magic != cls . MAGIC_BYTES :
raise InputError ( " Invalid magic bytes ( %r ) for partition definition " % magic )
if reserved != 0 :
critical ( " WARNING: Partition definition had unexpected reserved value 0x %08x . Newer binary format? " % reserved )
return res
def to_binary ( self ) :
return struct . pack ( self . STRUCT_FORMAT ,
self . MAGIC_BYTES ,
self . type , self . subtype ,
self . offset , self . size ,
self . name ,
0 ) # reserved
def to_csv ( self , simple_formatting = False ) :
def addr_format ( a , include_sizes ) :
if not simple_formatting and include_sizes :
for ( val , suffix ) in [ ( 0x100000 , " M " ) , ( 0x400 , " K " ) ] :
if a % val == 0 :
return " %d %s " % ( a / val , suffix )
return " 0x %x " % a
def lookup_keyword ( t , keywords ) :
for k , v in keywords . items ( ) :
if simple_formatting == False and t == v :
return k
return " %d " % t
return " , " . join ( [ self . name ,
lookup_keyword ( self . type , self . TYPES ) ,
lookup_keyword ( self . subtype , self . SUBTYPES . get ( self . type , { } ) ) ,
addr_format ( self . offset , False ) ,
addr_format ( self . size , True ) ] )
class InputError ( RuntimeError ) :
def __init__ ( self , e ) :
super ( InputError , self ) . __init__ ( e )
def parse_int ( v , keywords = { } ) :
""" Generic parser for integer fields - int(x,0) with provision for
k / m / K / M suffixes and ' keyword ' value lookup .
"""
try :
for letter , multiplier in [ ( " k " , 1024 ) , ( " m " , 1024 * 1024 ) ] :
if v . lower ( ) . endswith ( letter ) :
return parse_int ( v [ : - 1 ] , keywords ) * multiplier
return int ( v , 0 )
except ValueError :
if len ( keywords ) == 0 :
raise InputError ( " Invalid field value %s " % v )
try :
return keywords [ v . lower ( ) ]
except KeyError :
raise InputError ( " Value ' %s ' is not valid. Known keywords: %s " % ( v , " , " . join ( keywords ) ) )
def main ( ) :
global quiet
parser = argparse . ArgumentParser ( description = ' ESP32 partition table utility ' )
parser . add_argument ( ' --verify ' , ' -v ' , help = ' Verify partition table fields ' , default = True , action = ' store_false ' )
parser . add_argument ( ' --quiet ' , ' -q ' , help = " Don ' t print status messages to stderr " , action = ' store_true ' )
parser . add_argument ( ' input ' , help = ' Path to CSV or binary file to parse. Will use stdin if omitted. ' , type = argparse . FileType ( ' r ' ) , default = sys . stdin )
parser . add_argument ( ' output ' , help = ' Path to output converted binary or CSV file. Will use stdout if omitted, unless the --display argument is also passed (in which case only the summary is printed.) ' ,
nargs = ' ? ' ,
default = ' - ' )
args = parser . parse_args ( )
quiet = args . quiet
input = args . input . read ( )
input_is_binary = input [ 0 : 2 ] == PartitionDefinition . MAGIC_BYTES
if input_is_binary :
status ( " Parsing binary partition input... " )
table = PartitionTable . from_binary ( input )
else :
status ( " Parsing CSV input... " )
table = PartitionTable . from_csv ( input )
if args . verify :
status ( " Verifying table... " )
table . verify ( )
if input_is_binary :
output = table . to_csv ( )
else :
output = table . to_binary ( )
with sys . stdout if args . output == ' - ' else open ( args . output , ' w ' ) as f :
f . write ( output )
if __name__ == ' __main__ ' :
try :
main ( )
except InputError as e :
print ( e )
sys . exit ( 2 )