Motorola S-record - old Python code enhanced #2
https://techcoderadio.blogspot.com/2025/04/motorola-s-record-old-python-code.html
https://github.com/gabtremblay/pysrec
Code optimized
New option -a which stores data section as ASCII
Tested with Python 3.14.4
#!/usr/bin/python
# srecparser.py
#
# Copyright (C) 2011 Gabriel Tremblay - initnull hat gmail.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
Motorola S-Record parser
- Kudos to Montreal CISSP Groupies
"""
import sys
import srecutils
from optparse import OptionParser
def __generate_option_parser():
parser = OptionParser(usage="usage: %prog [options] filename")
parser.add_option("-r", action="store_true", dest="readable",
help="Human-readable output [default: %default]", default=False)
parser.add_option("-w", action="store_true", dest="wraparound",
help="Wrap around characters [default: %default]", default=True)
parser.add_option("-c", action="store_true", dest="validate_checksum",
help="Disable checksum validation [default: %default]", default=False)
parser.add_option("-l", action="store_true", dest="print_lines_number",
help="Print line number [default: %default]", default=False)
parser.add_option("-d", action="store_true", dest="data_only",
help="Data only [default: %default]", default=False)
parser.add_option("-o", metavar="OFFSET", dest="offset",
help="Add OFFSET to bytes [default: %default]", default=0)
parser.add_option("-a", action="store_true", dest="ascii",
help="Ascii [default: %default]", default=False)
return parser
if __name__ == "__main__":
parser = __generate_option_parser()
(options, args) = parser.parse_args(sys.argv)
if len(args) <= 1 or len(args) > 2:
parser.print_help()
sys.exit()
offset_value = int(options.offset)
# Use context managers for file handling
with open(args[1]) as scn_file, open("srec-output.txt", "w", encoding="utf-8") as output_file:
for linecount, srec in enumerate(scn_file):
# Strip line endings
srec = srec.strip()
# Validate checksum and parse record
if options.validate_checksum and not srecutils.validate_srec_checksum(srec):
print("Invalid checksum found!")
continue
# Extract data from the srec
record_type, data_len, addr, data, checksum = srecutils.parse_srec(srec)
if record_type in ('S1', 'S2', 'S3'):
# Apply offset (default is 0)
data = srecutils.offset_data(data, offset_value, options.readable, options.wraparound)
# Get checksum of the new offset srec
int_checksum = srecutils.compute_srec_checksum(record_type + data_len + addr + data)
checksum = srecutils.int_to_padded_hex_byte(int_checksum)
if options.ascii:
if data.find("S") == -1:
data = bytes.fromhex(data).decode("ISO-8859-1")
if not options.data_only:
data = record_type + data_len + addr + data + checksum
if options.print_lines_number:
data = f"{linecount}: {data}"
output_file.write(f"{data}\n")
else:
# All the other record types
output_str = srec
if options.print_lines_number:
output_str = f"{linecount}: {output_str}"
output_file.write(f"{output_str}\n")
# srecutils.py
#
# Copyright (C) 2011 Gabriel Tremblay - initnull hat gmail.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
Motorola S-Record utis
- Kudos to Montreal CISSP Groupies
"""
# Address len in bytes for S* types
# http://www.amelek.gda.pl/avr/uisp/srecord.htm
__ADDR_LEN = {'S0' : 2,
'S1' : 2,
'S2' : 3,
'S3' : 4,
'S5' : 2,
'S7' : 4,
'S8' : 3,
'S9' : 2}
def int_to_padded_hex_byte(integer):
"""
Convert an int to a 0-padded hex byte string
example: 65 == 41, 10 == 0A
Returns: The hex byte as string (ex: "0C")
"""
return format(integer, '02X')
def compute_srec_checksum(srec):
"""
Compute the checksum byte of a given S-Record
Returns: The checksum as a string hex byte (ex: "0C")
"""
# Get the summable data from srec (skip the 'S*' record type)
data = srec[2:]
# Sum all bytes (step every 2 characters to form a byte)
total = sum(int(data[pos:pos+2], 16) for pos in range(0, len(data), 2))
# Extract least significant byte and compute 8-bit one's complement
least_significant_byte = total & 0xFF
computed_checksum = (~least_significant_byte) & 0xFF
return computed_checksum
def validate_srec_checksum(srec):
"""
Validate if the checksum of the supplied s-record is valid
Returns: True if valid, False if not
"""
# Strip the original checksum and compare with the computed one
return compute_srec_checksum(srec[:-2]) == int(srec[-2:], 16)
def get_readable_string(integer):
r"""
Convert an integer to a readable 2-character representation. This is useful for reversing
examples: 41 == ".A", 13 == "\n", 20 (space) == "__"
Returns a readable 2-char representation of an int.
"""
special_chars = {9: "\\t", 10: "\\r", 13: "\\n", 32: '__'}
if integer in special_chars:
return special_chars[integer]
elif 33 <= integer <= 126: # Readable ascii
return chr(integer) + '.'
else:
return int_to_padded_hex_byte(integer)
def offset_byte_in_data(target_data, offset, target_byte_pos, readable=False, wraparound=False):
"""
Offset a given byte in the provided data payload (kind of rot(x))
readable will return a human-readable representation of the byte+offset
wraparound will wrap around 255 to 0 (ex: 257 = 2)
Returns: the offseted byte
"""
byte_pos = target_byte_pos * 2
prefix = target_data[:byte_pos]
suffix = target_data[byte_pos+2:]
int_value = int(target_data[byte_pos:byte_pos+2], 16) + offset
# Wraparound
if wraparound and int_value > 255:
int_value -= 256
# Get representation (readable or hex)
if readable and 0 < int_value < 256:
offset_byte = get_readable_string(int_value)
else:
offset_byte = int_to_padded_hex_byte(int_value)
return prefix + offset_byte + suffix
# offset can be from -255 to 255
def offset_data(data_section, offset, readable=False, wraparound=False):
"""
Offset the whole data section.
see offset_byte_in_data for more information
Returns: the entire data section + offset on each byte
"""
for pos in range(len(data_section) // 2):
data_section = offset_byte_in_data(data_section, offset, pos, readable, wraparound)
return data_section
def parse_srec(srec):
"""
Extract the data portion of a given S-Record (without checksum)
Returns: the record type, the lenght of the data section, the write address, the data itself and the checksum
"""
record_type = srec[:2]
data_len = srec[2:4]
addr_len = __ADDR_LEN.get(record_type) * 2
addr_end = 4 + addr_len
return record_type, data_len, srec[4:addr_end], srec[addr_end:-2], srec[-2:]
Comments
Post a Comment