Source code for alarmdecoder.util
"""
Provides utility classes for the `AlarmDecoder`_ (AD2) devices.
.. _AlarmDecoder: http://www.alarmdecoder.com
.. moduleauthor:: Scott Petersen <scott@nutech.com>
"""
import time
import threading
from io import open
[docs]class NoDeviceError(Exception):
    """
    No devices found.
    """
    pass
 
[docs]class CommError(Exception):
    """
    There was an error communicating with the device.
    """
    pass
 
[docs]class TimeoutError(Exception):
    """
    There was a timeout while trying to communicate with the device.
    """
    pass
 
[docs]class InvalidMessageError(Exception):
    """
    The format of the panel message was invalid.
    """
    pass
 
[docs]class UploadError(Exception):
    """
    Generic firmware upload error.
    """
    pass
 
[docs]class UploadChecksumError(UploadError):
    """
    The firmware upload failed due to a checksum error.
    """
    pass
 
[docs]class Firmware(object):
    """
    Represents firmware for the `AlarmDecoder`_ devices.
    """
    # Constants
    STAGE_START = 0
    STAGE_WAITING = 1
    STAGE_BOOT = 2
    STAGE_LOAD = 3
    STAGE_UPLOADING = 4
    STAGE_DONE = 5
    STAGE_ERROR = 98
    STAGE_DEBUG = 99
    # FIXME: Rewrite this monstrosity.
    @staticmethod
[docs]    def upload(dev, filename, progress_callback=None, debug=False):
        """
        Uploads firmware to an `AlarmDecoder`_ device.
        :param filename: firmware filename
        :type filename: string
        :param progress_callback: callback function used to report progress
        :type progress_callback: function
        :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError`
        """
        def do_upload():
            """
            Perform the actual firmware upload to the device.
            """
            with open(filename) as upload_file:
                line_cnt = 0
                for line in upload_file:
                    line_cnt += 1
                    line = line.rstrip()
                    if line[0] == ':':
                        dev.write(line + "\r")
                        response = dev.read_line(timeout=5.0, purge_buffer=True)
                        if debug:
                            stage_callback(Firmware.STAGE_DEBUG, data="line={0} - line={1} response={2}".format(line_cnt, line, response));
                        if '!ce' in response:
                            raise UploadChecksumError("Checksum error on line " + str(line_cnt) + " of " + filename);
                        elif '!no' in response:
                            raise UploadError("Incorrect data sent to bootloader.")
                        elif '!ok' in response:
                            break
                        else:
                            if progress_callback is not None:
                                progress_callback(Firmware.STAGE_UPLOADING)
                        time.sleep(0.0)
        def read_until(pattern, timeout=0.0):
            """
            Read characters until a specific pattern is found or the timeout is
            hit.
            """
            def timeout_event():
                """Handles the read timeout event."""
                timeout_event.reading = False
            timeout_event.reading = True
            timer = None
            if timeout > 0:
                timer = threading.Timer(timeout, timeout_event)
                timer.start()
            position = 0
            dev.purge()
            while timeout_event.reading:
                try:
                    char = dev.read()
                    if char is not None and char != '':
                        if char == pattern[position]:
                            position = position + 1
                            if position == len(pattern):
                                break
                        else:
                            position = 0
                except Exception as err:
                    pass
            if timer:
                if timer.is_alive():
                    timer.cancel()
                else:
                    raise TimeoutError('Timeout while waiting for line terminator.')
        def stage_callback(stage, **kwargs):
            """Callback to update progress for the specified stage."""
            if progress_callback is not None:
                progress_callback(stage, **kwargs)
        if dev is None:
            raise NoDeviceError('No device specified for firmware upload.')
        stage_callback(Firmware.STAGE_START)
        if dev.is_reader_alive():
            # Close the reader thread and wait for it to die, otherwise
            # it interferes with our reading.
            dev.stop_reader()
            while dev._read_thread.is_alive():
                stage_callback(Firmware.STAGE_WAITING)
                time.sleep(0.5)
        # Reboot the device and wait for the boot loader.
        retry = 3
        found_loader = False
        while retry > 0:
            try:
                stage_callback(Firmware.STAGE_BOOT)
                dev.write("=")
                read_until('!boot', timeout=15.0)
                # Get ourselves into the boot loader and wait for indication
                # that it's ready for the firmware upload.
                stage_callback(Firmware.STAGE_LOAD)
                dev.write("=")
                read_until('!load', timeout=15.0)
            except TimeoutError as err:
                retry -= 1
            else:
                retry = 0
                found_loader = True
        # And finally do the upload.
        if found_loader:
            try:
                do_upload()
            except UploadError as err:
                stage_callback(Firmware.STAGE_ERROR, error=str(err))
            else:
                stage_callback(Firmware.STAGE_DONE)
        else:
            stage_callback(Firmware.STAGE_ERROR, error="Error entering bootloader.")