From f87692eb8cff681b4c6c8cef13c21df24589d648 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Wed, 7 Oct 2015 15:07:49 -0700 Subject: [PATCH 1/6] Added retries for the reboot/bootloader entry. Attempting to fix timeout issues. --- alarmdecoder/util.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index 23abf4a..66b61e0 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -75,7 +75,7 @@ class Firmware(object): if line[0] == ':': dev.write(line + "\r") - dev.read_line(timeout=10.0) + dev.read_line(timeout=5.0, purge_buffer=True) if progress_callback is not None: progress_callback(Firmware.STAGE_UPLOADING) @@ -137,20 +137,25 @@ class Firmware(object): dev.stop_reader() while dev._read_thread.is_alive(): stage_callback(Firmware.STAGE_WAITING) - time.sleep(1) - - time.sleep(2) + time.sleep(0.5) # Reboot the device and wait for the boot loader. - stage_callback(Firmware.STAGE_BOOT) - dev.write("=") - read_until('......', 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) + retry = 3 + while retry > 0: + try: + stage_callback(Firmware.STAGE_BOOT) + dev.write("=") + read_until('......', 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, err: + retry -= 1 + else: + retry = 0 # And finally do the upload. do_upload() From 8b9a45d4eef4e441d3e73aa2997738e08eda5d6c Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Thu, 8 Oct 2015 14:54:24 -0700 Subject: [PATCH 2/6] More tweaks and error handling for the firmware upload. --- alarmdecoder/devices.py | 26 ++++++++++++++++++++ alarmdecoder/util.py | 54 ++++++++++++++++++++++++++++++++++------- bin/ad2-firmwareupload | 13 +++++++--- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/alarmdecoder/devices.py b/alarmdecoder/devices.py index e3acd8b..6b36b60 100644 --- a/alarmdecoder/devices.py +++ b/alarmdecoder/devices.py @@ -559,6 +559,12 @@ class USBDevice(Device): return ret + def purge(self): + """ + Purges read/write buffers. + """ + self._device.purge_buffers() + def _get_serial_number(self): """ Retrieves the FTDI device serial number. @@ -849,6 +855,13 @@ class SerialDevice(Device): return ret + def purge(self): + """ + Purges read/write buffers. + """ + self._device.flushInput() + self._device.flushOutput() + class SocketDevice(Device): """ @@ -1144,6 +1157,19 @@ class SocketDevice(Device): return ret + def purge(self): + """ + Purges read/write buffers. + """ + try: + self._device.setblocking(0) + while(self._device.recv(1)): + pass + except socket.error, err: + pass + finally: + self._device.setblocking(1) + def _init_ssl(self): """ Initializes our device as an SSL connection. diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index 66b61e0..27b90f2 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -38,6 +38,19 @@ class InvalidMessageError(Exception): pass +class UploadError(Exception): + """ + Generic firmware upload error. + """ + pass + +class UploadChecksumError(UploadError): + """ + The firmware upload failed due to a checksum error. + """ + pass + + class Firmware(object): """ Represents firmware for the `AlarmDecoder`_ devices. @@ -50,10 +63,12 @@ class Firmware(object): STAGE_LOAD = 3 STAGE_UPLOADING = 4 STAGE_DONE = 5 + STAGE_ERROR = 98 + STAGE_DEBUG = 99 # FIXME: Rewrite this monstrosity. @staticmethod - def upload(dev, filename, progress_callback=None): + def upload(dev, filename, progress_callback=None, debug=False): """ Uploads firmware to an `AlarmDecoder`_ device. @@ -70,15 +85,29 @@ class Firmware(object): 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") - dev.read_line(timeout=5.0, purge_buffer=True) + 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); - if progress_callback is not None: - progress_callback(Firmware.STAGE_UPLOADING) + 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) @@ -100,6 +129,8 @@ class Firmware(object): position = 0 + dev.purge() + while timeout_event.reading: try: char = dev.read() @@ -112,7 +143,7 @@ class Firmware(object): else: position = 0 - except Exception: + except Exception, err: pass if timer: @@ -121,10 +152,10 @@ class Firmware(object): else: raise TimeoutError('Timeout while waiting for line terminator.') - def stage_callback(stage): + def stage_callback(stage, **kwargs): """Callback to update progress for the specified stage.""" if progress_callback is not None: - progress_callback(stage) + progress_callback(stage, **kwargs) if dev is None: raise NoDeviceError('No device specified for firmware upload.') @@ -152,11 +183,16 @@ class Firmware(object): stage_callback(Firmware.STAGE_LOAD) dev.write("=") read_until('!load', timeout=15.0) + except TimeoutError, err: retry -= 1 else: retry = 0 # And finally do the upload. - do_upload() - stage_callback(Firmware.STAGE_DONE) + try: + do_upload() + except UploadError, err: + stage_callback(Firmware.STAGE_ERROR, error=err) + else: + stage_callback(Firmware.STAGE_DONE) diff --git a/bin/ad2-firmwareupload b/bin/ad2-firmwareupload index 8223f91..e68e44d 100755 --- a/bin/ad2-firmwareupload +++ b/bin/ad2-firmwareupload @@ -1,9 +1,10 @@ #!/usr/bin/env python +import os import sys, time import alarmdecoder -def handle_firmware(stage): +def handle_firmware(stage, **kwargs): if stage == alarmdecoder.util.Firmware.STAGE_START: handle_firmware.wait_tick = 0 handle_firmware.upload_tick = 0 @@ -30,6 +31,10 @@ def handle_firmware(stage): sys.stdout.flush() elif stage == alarmdecoder.util.Firmware.STAGE_DONE: print "\r\nDone!" + elif stage == alarmdecoder.util.Firmware.STAGE_ERROR: + print "\r\nError: {0}".format(kwargs.get("error", "")) + elif stage == alarmdecoder.util.Firmware.STAGE_DEBUG: + print "\r\nDBG: {0}".format(kwargs.get("data", "")) def main(): device = '/dev/ttyUSB0' @@ -47,18 +52,20 @@ def main(): if len(sys.argv) > 3: baudrate = sys.argv[3] + debug = os.environ.get("ALARMDECODER_DEBUG") is not None + print "Flashing device: {0} - {2} baud\r\nFirmware: {1}".format(device, firmware, baudrate) if ':' in device: hostname, port = device.split(':') dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port))) - dev.open() + dev.open(no_reader_thread=True) else: dev = alarmdecoder.devices.SerialDevice(interface=device) dev.open(baudrate=baudrate, no_reader_thread=True) time.sleep(3) - alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware) + alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) dev.close() From d75749b6127a9512a4b25046ce56f79f39f43601 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Fri, 23 Oct 2015 14:07:04 -0700 Subject: [PATCH 3/6] Better error handling in firmware upload. Made SocketDevice behave similarly to the others with regards to blocking. Added ability to retrieve current configuration string. --- alarmdecoder/decoder.py | 8 ++++---- alarmdecoder/devices.py | 14 ++++++++++++-- alarmdecoder/util.py | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index ac2198c..69de8e0 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -243,7 +243,9 @@ class AlarmDecoder(object): """ Sets configuration entries on the device. """ - config_string = '' + self.send("C{0}\r".format(self.get_config_string())) + + def get_config_string(self): config_entries = [] # HACK: This is ugly.. but I can't think of an elegant way of doing it. @@ -258,9 +260,7 @@ class AlarmDecoder(object): config_entries.append(('DEDUPLICATE', 'Y' if self.deduplicate else 'N')) config_entries.append(('MODE', PANEL_TYPES.keys()[PANEL_TYPES.values().index(self.mode)])) - config_string = '&'.join(['='.join(t) for t in config_entries]) - - self.send("C{0}\r".format(config_string)) + return '&'.join(['='.join(t) for t in config_entries]) def reboot(self): """ diff --git a/alarmdecoder/devices.py b/alarmdecoder/devices.py index 6b36b60..bddd107 100644 --- a/alarmdecoder/devices.py +++ b/alarmdecoder/devices.py @@ -20,6 +20,7 @@ import threading import serial import serial.tools.list_ports import socket +import select from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError from .event import event @@ -1002,7 +1003,6 @@ class SocketDevice(Device): self._init_ssl() self._device.connect((self._host, self._port)) - #self._device.setblocking(1) if self._use_ssl: while True: @@ -1082,7 +1082,10 @@ class SocketDevice(Device): data = None try: - data = self._device.recv(1) + read_ready, _, _ = select.select([self._device], [], [], 0) + + if (len(read_ready) != 0): + data = self._device.recv(1) except socket.error, err: raise CommError('Error while reading from device: {0}'.format(str(err)), err) @@ -1119,6 +1122,12 @@ class SocketDevice(Device): try: while timeout_event.reading: + read_ready, _, _ = select.select([self._device], [], [], 0) + + if (len(read_ready) == 0): + time.sleep(0.01) + continue + buf = self._device.recv(1) if buf != '': @@ -1130,6 +1139,7 @@ class SocketDevice(Device): if len(self._buffer) > 0: got_line = True break + else: time.sleep(0.01) diff --git a/alarmdecoder/util.py b/alarmdecoder/util.py index 27b90f2..afd7ebc 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -44,6 +44,7 @@ class UploadError(Exception): """ pass + class UploadChecksumError(UploadError): """ The firmware upload failed due to a checksum error. @@ -172,11 +173,12 @@ class Firmware(object): # 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('......', timeout=15.0) + read_until('!boot', timeout=15.0) # Get ourselves into the boot loader and wait for indication # that it's ready for the firmware upload. @@ -188,11 +190,15 @@ class Firmware(object): retry -= 1 else: retry = 0 + found_loader = True # And finally do the upload. - try: - do_upload() - except UploadError, err: - stage_callback(Firmware.STAGE_ERROR, error=err) + if found_loader: + try: + do_upload() + except UploadError, err: + stage_callback(Firmware.STAGE_ERROR, error=str(err)) + else: + stage_callback(Firmware.STAGE_DONE) else: - stage_callback(Firmware.STAGE_DONE) + stage_callback(Firmware.STAGE_ERROR, error="Error entering bootloader.") From ecd390487c292d3d4497872667c1f7c577700c7d Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Tue, 23 Feb 2016 11:23:17 -0800 Subject: [PATCH 4/6] Fixed incorrect defaults for panic status and address mask. --- alarmdecoder/decoder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index 69de8e0..3e219fe 100644 --- a/alarmdecoder/decoder.py +++ b/alarmdecoder/decoder.py @@ -104,13 +104,13 @@ class AlarmDecoder(object): self._armed_status = None self._fire_status = (False, 0) self._battery_status = (False, 0) - self._panic_status = None + self._panic_status = False self._relay_status = {} self._internal_address_mask = 0xFFFFFFFF self.address = 18 self.configbits = 0xFF00 - self.address_mask = 0x00000000 + self.address_mask = 0xFFFFFFF self.emulate_zone = [False for x in range(5)] self.emulate_relay = [False for x in range(4)] self.emulate_lrr = False From fcea42586613ce31aebe1a6764b91bef86bc6f66 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Tue, 23 Feb 2016 11:23:53 -0800 Subject: [PATCH 5/6] Updated tests for use of select.select and the changed ZoneTracking constructor. --- test/test_devices.py | 55 +++++++++++++++++++++++---------------- test/test_zonetracking.py | 7 ++++- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/test/test_devices.py b/test/test_devices.py index 628927a..05c33d5 100644 --- a/test/test_devices.py +++ b/test/test_devices.py @@ -7,6 +7,7 @@ import socket import time import tempfile import os +import select from OpenSSL import SSL, crypto from alarmdecoder.devices import USBDevice, SerialDevice, SocketDevice from alarmdecoder.util import NoDeviceError, CommError, TimeoutError @@ -292,40 +293,50 @@ class TestSocketDevice(TestCase): with patch.object(socket.socket, 'connect', return_value=None): self._device.open(no_reader_thread=True) - with patch.object(socket.socket, 'recv') as mock: - self._device.read() + with patch('socket.socket.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with patch.object(socket.socket, 'recv') as mock: + self._device.read() mock.assert_called_with(1) def test_read_exception(self): - with patch.object(self._device._device, 'recv', side_effect=socket.error): - with self.assertRaises(CommError): - self._device.read() + with patch('socket.socket.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with patch.object(self._device._device, 'recv', side_effect=socket.error): + with self.assertRaises(CommError): + self._device.read() def test_read_line(self): - with patch.object(self._device._device, 'recv', side_effect=list("testing\r\n")): - ret = None - try: - ret = self._device.read_line() - except StopIteration: - pass + with patch('socket.socket.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with patch.object(self._device._device, 'recv', side_effect=list("testing\r\n")): + ret = None + try: + ret = self._device.read_line() + except StopIteration: + pass - self.assertEquals(ret, "testing") + self.assertEquals(ret, "testing") def test_read_line_timeout(self): - with patch.object(self._device._device, 'recv', return_value='a') as mock: - with self.assertRaises(TimeoutError): - self._device.read_line(timeout=0.1) + with patch('socket.socket.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with patch.object(self._device._device, 'recv', return_value='a') as mock: + with self.assertRaises(TimeoutError): + self._device.read_line(timeout=0.1) - self.assertIn('a', self._device._buffer) + self.assertIn('a', self._device._buffer) def test_read_line_exception(self): - with patch.object(self._device._device, 'recv', side_effect=socket.error): - with self.assertRaises(CommError): - self._device.read_line() - - with self.assertRaises(CommError): - self._device.read_line() + with patch('socket.socket.fileno', return_value=1): + with patch.object(select, 'select', return_value=[[1], [], []]): + with patch.object(self._device._device, 'recv', side_effect=socket.error): + with self.assertRaises(CommError): + self._device.read_line() + + with self.assertRaises(CommError): + self._device.read_line() def test_ssl(self): ssl_key = crypto.PKey() diff --git a/test/test_zonetracking.py b/test/test_zonetracking.py index 7c2c901..a20a3df 100644 --- a/test/test_zonetracking.py +++ b/test/test_zonetracking.py @@ -1,13 +1,18 @@ from unittest import TestCase from mock import Mock, MagicMock +from alarmdecoder import AlarmDecoder +from alarmdecoder.panels import ADEMCO from alarmdecoder.messages import Message, ExpanderMessage from alarmdecoder.zonetracking import Zonetracker, Zone class TestZonetracking(TestCase): def setUp(self): - self._zonetracker = Zonetracker() + self._alarmdecoder = Mock(spec=AlarmDecoder) + + self._alarmdecoder.mode = ADEMCO + self._zonetracker = Zonetracker(self._alarmdecoder) self._zonetracker.on_fault += self.fault_event self._zonetracker.on_restore += self.restore_event From 44b03f486cfd2a80c64452bdf4096af11e325026 Mon Sep 17 00:00:00 2001 From: Scott Petersen Date: Tue, 23 Feb 2016 12:13:32 -0800 Subject: [PATCH 6/6] Firmware upload utility improvements. --- bin/ad2-firmwareupload | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/bin/ad2-firmwareupload b/bin/ad2-firmwareupload index e68e44d..9b171df 100755 --- a/bin/ad2-firmwareupload +++ b/bin/ad2-firmwareupload @@ -56,18 +56,28 @@ def main(): print "Flashing device: {0} - {2} baud\r\nFirmware: {1}".format(device, firmware, baudrate) - if ':' in device: - hostname, port = device.split(':') - dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port))) - dev.open(no_reader_thread=True) - else: - dev = alarmdecoder.devices.SerialDevice(interface=device) - dev.open(baudrate=baudrate, no_reader_thread=True) + dev = None + try: + if ':' in device: + hostname, port = device.split(':') + dev = alarmdecoder.devices.SocketDevice(interface=(hostname, int(port))) + dev.open(no_reader_thread=True) + else: + dev = alarmdecoder.devices.SerialDevice(interface=device) + dev.open(baudrate=baudrate, no_reader_thread=True) - time.sleep(3) - alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) + time.sleep(3) + alarmdecoder.util.Firmware.upload(dev, firmware, handle_firmware, debug=debug) - dev.close() + except alarmdecoder.util.NoDeviceError, ex: + print "Error: Could not find device: {0}".format(ex) + except alarmdecoder.util.UploadError, ex: + print "Error: Error uploading firmware: {0}".format(ex) + except Exception, ex: + print "Error: {0}".format(ex) + finally: + if dev is not None: + dev.close() if __name__ == "__main__": main()