# -*- coding: utf-8 -*-
# (c) Copyright 2019 Sensirion AG, Switzerland
from __future__ import absolute_import, division, print_function
from .errors import ShdlcFirmwareImageSignatureError
from .types import FirmwareVersion
from struct import unpack
import logging
log = logging.getLogger(__name__)
[docs]class ShdlcFirmwareImage(object):
"""
This class represents a firmware image for an SHDLC device. It is used to
load and verify Intel-Hex files for performing firmware updates over SHDLC.
Since the different SHDLC devices use different memory layouts, this class
needs to know the bootloader base address and application base address (see
constructor parameters). Drivers for specific SHDLC devices should create a
subclass to provide a new type which already contains the correct
addresses, so users don't have to care about these details.
.. note:: This class is intended only for devices which contain the SHDLC
bootloader. Devices which support firmware updates with another
system aren't supported by this class.
.. note:: The package ``intelhex`` must be installed to use this class. See
:ref:`firmware-updater-dependencies` for details.
"""
_PRODUCT_TYPE_SIZE = 4 # Product type size
[docs] def __init__(self, hexfile, bl_start_addr, app_start_addr,
signature=b'\x4A\x47\x4F\x4B', bl_version_offset=0x1004):
"""
Constructor which loads and parses the firmware from a hex file.
:param str/file hexfile: The filename or file-like object containing
the firmware in Intel-Hex format (\\*.hex).
:param int bl_start_addr: The base address of the bootloader inside
the firmware image.
:param int app_start_addr: The base address of the application
inside the firmware image.
:param bytes signature: Signature bytes used for the application.
:param int bl_version_offset: Bootloader version address offset.
:raise ~sensirion_shdlc_driver.errors.ShdlcFirmwareImageSignatureError:
If the signature of the image is invalid.
"""
# Import intelhex here to allow importing the firmware_image module
# without having the intelhex package installed (it's an optional
# dependency, so it might be missing).
from intelhex import IntelHex
self._bl_start_addr = int(bl_start_addr)
self._app_start_addr = int(app_start_addr)
self._signature = bytes(bytearray(signature))
self._bl_version_offset = int(bl_version_offset)
self._app_data_index = 0
self._data = IntelHex(hexfile)
self._data.padding = 0xFF # is returned when reading undefined regions
log.debug("Loaded hex file: {} [minaddr=0x{:08X}, maxaddr=0x{:08X}]"
.format(hexfile, self._data.minaddr(), self._data.maxaddr()))
self._check_signature()
log.debug("Signature: OK")
self._product_type = self._read_product_type()
log.debug("Product type: 0x{:08X}".format(self._product_type))
self._bootloader_version = self._read_bootloader_version()
log.debug("Bootloader version: {}".format(self._bootloader_version))
self._application_version = self._read_application_version()
log.debug("Application version: {}".format(self._application_version))
self._app_data = self._read_application_data()
log.debug("Application size: {:.2f} kB".format(self.size / 1024))
self._checksum = self._calc_application_checksum()
log.debug("Application checksum: 0x{:02X}".format(self._checksum))
@property
def product_type(self):
"""
Get the product type for which the loaded firmware is made.
:return: Product type as an integer.
:rtype: int
"""
return self._product_type
@property
def bootloader_version(self):
"""
Get the bootloader version which is contained in the loaded image.
:return: Bootloader version (note: debug flag is not supported, it's
always False).
:rtype: ~sensirion_shdlc_driver.types.FirmwareVersion
"""
return self._bootloader_version
@property
def application_version(self):
"""
Get the application firmware version which is contained in the loaded
image.
:return: Application firmware version (note: debug flag is not
supported, it's always False).
:rtype: ~sensirion_shdlc_driver.types.FirmwareVersion
"""
return self._application_version
@property
def checksum(self):
"""
Get the checksum over the application firmware part of the loaded
image. This is the checksum which needs to be sent to the product
bootloader.
:return: Checksum as a byte.
:rtype: byte
"""
return self._checksum
@property
def size(self):
"""
Get the size of the application firmware.
:return: Size in bytes.
:rtype: int
"""
return len(self._app_data)
@property
def available_bytes(self):
"""
Get the count of available bytes left.
:return: Count of available bytes.
:rtype: int
"""
return len(self._app_data) - self._app_data_index
[docs] def read(self, size=-1):
"""
Read the next bytes of the application firmware.
:param int size: Maximum count of bytes to read (-1 reads all
available)
:return: Firmware data block.
:rtype: bytes
"""
if size < 0:
size = self.available_bytes
else:
size = min(size, self.available_bytes)
data = self._app_data[self._app_data_index:self._app_data_index+size]
self._app_data_index += len(data)
return bytes(data) # immutable type to avoid modifying image data
def _check_signature(self):
"""
Check the signature of the loaded image and throw an exception if it's
invalid.
"""
signature = self._read_bytes(
self._app_start_addr, len(self._signature))
if signature != self._signature:
raise ShdlcFirmwareImageSignatureError(signature)
def _read_product_type(self):
"""
Read the product type from the loaded image.
:return: The read product type.
:rtype: int
"""
address = self._app_start_addr + len(self._signature)
return self._read_uint32(address)
def _read_bootloader_version(self):
"""
Read the bootloader version from the loaded image.
:return: The read bootloader version.
:rtype: ~sensirion_shdlc_driver.types.FirmwareVersion
"""
addr_major = self._bl_start_addr + self._bl_version_offset + 1
addr_minor = self._bl_start_addr + self._bl_version_offset
return FirmwareVersion(major=self._data[addr_major],
minor=self._data[addr_minor],
debug=False)
def _read_application_version(self):
"""
Read the application version from the loaded image.
:return: The read application version.
:rtype: ~sensirion_shdlc_driver.types.FirmwareVersion
"""
addr_major = self._app_start_addr + len(self._signature) + \
self._PRODUCT_TYPE_SIZE + 1
addr_minor = self._app_start_addr + len(self._signature) + \
self._PRODUCT_TYPE_SIZE
return FirmwareVersion(major=self._data[addr_major],
minor=self._data[addr_minor],
debug=False)
def _read_application_data(self):
"""
Read the application data block from the loaded image.
:return: The read application data block.
:rtype: bytearray
"""
# Skip the signature because it must not be sent to the bootloader!
start_addr = self._app_start_addr + len(self._signature)
if self._bl_start_addr > self._app_start_addr:
end_addr = self._bl_start_addr - 1 # Don't include bootloader
else:
end_addr = self._data.maxaddr()
return bytearray(self._data.tobinarray(start=start_addr, end=end_addr))
def _read_uint32(self, address):
"""
Read an uint32 at a specific image address.
:param int address: The address to read from.
:return: The integer at the specified address.
:rtype: int
"""
return unpack("<I", self._data.tobinarray(start=address, size=4))[0]
def _read_bytes(self, address, number_of_bytes):
"""
Read at a specific image address.
:param int address: The address to read from.
:param int number_of_bytes: Number of bytes to read
:return: The bytes from the specified address.
:rtype: bytes
"""
return self._data.tobinstr(start=address, size=number_of_bytes)
def _calc_application_checksum(self):
"""
Calculate the checksum over the application data, as needed for the
firmware download command.
:return: Checksum of application data
:rtype: byte
"""
return (sum(self._app_data) % 256) ^ 0xFF