diff --git a/alarmdecoder/decoder.py b/alarmdecoder/decoder.py index ac2198c..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 @@ -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 e3acd8b..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 @@ -559,6 +560,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 +856,13 @@ class SerialDevice(Device): return ret + def purge(self): + """ + Purges read/write buffers. + """ + self._device.flushInput() + self._device.flushOutput() + class SocketDevice(Device): """ @@ -989,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: @@ -1069,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) @@ -1106,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 != '': @@ -1117,6 +1139,7 @@ class SocketDevice(Device): if len(self._buffer) > 0: got_line = True break + else: time.sleep(0.01) @@ -1144,6 +1167,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 23abf4a..afd7ebc 100644 --- a/alarmdecoder/util.py +++ b/alarmdecoder/util.py @@ -38,6 +38,20 @@ 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 +64,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 +86,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=10.0) + 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 - if progress_callback is not None: - progress_callback(Firmware.STAGE_UPLOADING) + else: + if progress_callback is not None: + progress_callback(Firmware.STAGE_UPLOADING) time.sleep(0.0) @@ -100,6 +130,8 @@ class Firmware(object): position = 0 + dev.purge() + while timeout_event.reading: try: char = dev.read() @@ -112,7 +144,7 @@ class Firmware(object): else: position = 0 - except Exception: + except Exception, err: pass if timer: @@ -121,10 +153,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.') @@ -137,21 +169,36 @@ 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 + 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, err: + retry -= 1 + else: + retry = 0 + found_loader = True # And finally do the upload. - do_upload() - stage_callback(Firmware.STAGE_DONE) + 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_ERROR, error="Error entering bootloader.") diff --git a/bin/ad2-firmwareupload b/bin/ad2-firmwareupload index 8223f91..9b171df 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,20 +52,32 @@ 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() - 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) + 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() 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