| @@ -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): | |||
| """ | |||
| @@ -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. | |||
| @@ -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.") | |||
| @@ -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() | |||
| @@ -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() | |||
| @@ -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 | |||