@@ -104,13 +104,13 @@ class AlarmDecoder(object): | |||||
self._armed_status = None | self._armed_status = None | ||||
self._fire_status = (False, 0) | self._fire_status = (False, 0) | ||||
self._battery_status = (False, 0) | self._battery_status = (False, 0) | ||||
self._panic_status = None | |||||
self._panic_status = False | |||||
self._relay_status = {} | self._relay_status = {} | ||||
self._internal_address_mask = 0xFFFFFFFF | self._internal_address_mask = 0xFFFFFFFF | ||||
self.address = 18 | self.address = 18 | ||||
self.configbits = 0xFF00 | self.configbits = 0xFF00 | ||||
self.address_mask = 0x00000000 | |||||
self.address_mask = 0xFFFFFFF | |||||
self.emulate_zone = [False for x in range(5)] | self.emulate_zone = [False for x in range(5)] | ||||
self.emulate_relay = [False for x in range(4)] | self.emulate_relay = [False for x in range(4)] | ||||
self.emulate_lrr = False | self.emulate_lrr = False | ||||
@@ -243,7 +243,9 @@ class AlarmDecoder(object): | |||||
""" | """ | ||||
Sets configuration entries on the device. | 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 = [] | config_entries = [] | ||||
# HACK: This is ugly.. but I can't think of an elegant way of doing it. | # 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(('DEDUPLICATE', 'Y' if self.deduplicate else 'N')) | ||||
config_entries.append(('MODE', PANEL_TYPES.keys()[PANEL_TYPES.values().index(self.mode)])) | 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): | def reboot(self): | ||||
""" | """ | ||||
@@ -20,6 +20,7 @@ import threading | |||||
import serial | import serial | ||||
import serial.tools.list_ports | import serial.tools.list_ports | ||||
import socket | import socket | ||||
import select | |||||
from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError | from .util import CommError, TimeoutError, NoDeviceError, InvalidMessageError | ||||
from .event import event | from .event import event | ||||
@@ -559,6 +560,12 @@ class USBDevice(Device): | |||||
return ret | return ret | ||||
def purge(self): | |||||
""" | |||||
Purges read/write buffers. | |||||
""" | |||||
self._device.purge_buffers() | |||||
def _get_serial_number(self): | def _get_serial_number(self): | ||||
""" | """ | ||||
Retrieves the FTDI device serial number. | Retrieves the FTDI device serial number. | ||||
@@ -849,6 +856,13 @@ class SerialDevice(Device): | |||||
return ret | return ret | ||||
def purge(self): | |||||
""" | |||||
Purges read/write buffers. | |||||
""" | |||||
self._device.flushInput() | |||||
self._device.flushOutput() | |||||
class SocketDevice(Device): | class SocketDevice(Device): | ||||
""" | """ | ||||
@@ -989,7 +1003,6 @@ class SocketDevice(Device): | |||||
self._init_ssl() | self._init_ssl() | ||||
self._device.connect((self._host, self._port)) | self._device.connect((self._host, self._port)) | ||||
#self._device.setblocking(1) | |||||
if self._use_ssl: | if self._use_ssl: | ||||
while True: | while True: | ||||
@@ -1069,7 +1082,10 @@ class SocketDevice(Device): | |||||
data = None | data = None | ||||
try: | 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: | except socket.error, err: | ||||
raise CommError('Error while reading from device: {0}'.format(str(err)), err) | raise CommError('Error while reading from device: {0}'.format(str(err)), err) | ||||
@@ -1106,6 +1122,12 @@ class SocketDevice(Device): | |||||
try: | try: | ||||
while timeout_event.reading: | 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) | buf = self._device.recv(1) | ||||
if buf != '': | if buf != '': | ||||
@@ -1117,6 +1139,7 @@ class SocketDevice(Device): | |||||
if len(self._buffer) > 0: | if len(self._buffer) > 0: | ||||
got_line = True | got_line = True | ||||
break | break | ||||
else: | else: | ||||
time.sleep(0.01) | time.sleep(0.01) | ||||
@@ -1144,6 +1167,19 @@ class SocketDevice(Device): | |||||
return ret | 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): | def _init_ssl(self): | ||||
""" | """ | ||||
Initializes our device as an SSL connection. | Initializes our device as an SSL connection. | ||||
@@ -38,6 +38,20 @@ class InvalidMessageError(Exception): | |||||
pass | 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): | class Firmware(object): | ||||
""" | """ | ||||
Represents firmware for the `AlarmDecoder`_ devices. | Represents firmware for the `AlarmDecoder`_ devices. | ||||
@@ -50,10 +64,12 @@ class Firmware(object): | |||||
STAGE_LOAD = 3 | STAGE_LOAD = 3 | ||||
STAGE_UPLOADING = 4 | STAGE_UPLOADING = 4 | ||||
STAGE_DONE = 5 | STAGE_DONE = 5 | ||||
STAGE_ERROR = 98 | |||||
STAGE_DEBUG = 99 | |||||
# FIXME: Rewrite this monstrosity. | # FIXME: Rewrite this monstrosity. | ||||
@staticmethod | @staticmethod | ||||
def upload(dev, filename, progress_callback=None): | |||||
def upload(dev, filename, progress_callback=None, debug=False): | |||||
""" | """ | ||||
Uploads firmware to an `AlarmDecoder`_ device. | Uploads firmware to an `AlarmDecoder`_ device. | ||||
@@ -70,15 +86,29 @@ class Firmware(object): | |||||
Perform the actual firmware upload to the device. | Perform the actual firmware upload to the device. | ||||
""" | """ | ||||
with open(filename) as upload_file: | with open(filename) as upload_file: | ||||
line_cnt = 0 | |||||
for line in upload_file: | for line in upload_file: | ||||
line_cnt += 1 | |||||
line = line.rstrip() | line = line.rstrip() | ||||
if line[0] == ':': | if line[0] == ':': | ||||
dev.write(line + "\r") | 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) | time.sleep(0.0) | ||||
@@ -100,6 +130,8 @@ class Firmware(object): | |||||
position = 0 | position = 0 | ||||
dev.purge() | |||||
while timeout_event.reading: | while timeout_event.reading: | ||||
try: | try: | ||||
char = dev.read() | char = dev.read() | ||||
@@ -112,7 +144,7 @@ class Firmware(object): | |||||
else: | else: | ||||
position = 0 | position = 0 | ||||
except Exception: | |||||
except Exception, err: | |||||
pass | pass | ||||
if timer: | if timer: | ||||
@@ -121,10 +153,10 @@ class Firmware(object): | |||||
else: | else: | ||||
raise TimeoutError('Timeout while waiting for line terminator.') | 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.""" | """Callback to update progress for the specified stage.""" | ||||
if progress_callback is not None: | if progress_callback is not None: | ||||
progress_callback(stage) | |||||
progress_callback(stage, **kwargs) | |||||
if dev is None: | if dev is None: | ||||
raise NoDeviceError('No device specified for firmware upload.') | raise NoDeviceError('No device specified for firmware upload.') | ||||
@@ -137,21 +169,36 @@ class Firmware(object): | |||||
dev.stop_reader() | dev.stop_reader() | ||||
while dev._read_thread.is_alive(): | while dev._read_thread.is_alive(): | ||||
stage_callback(Firmware.STAGE_WAITING) | stage_callback(Firmware.STAGE_WAITING) | ||||
time.sleep(1) | |||||
time.sleep(2) | |||||
time.sleep(0.5) | |||||
# Reboot the device and wait for the boot loader. | # 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. | # 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 | #!/usr/bin/env python | ||||
import os | |||||
import sys, time | import sys, time | ||||
import alarmdecoder | import alarmdecoder | ||||
def handle_firmware(stage): | |||||
def handle_firmware(stage, **kwargs): | |||||
if stage == alarmdecoder.util.Firmware.STAGE_START: | if stage == alarmdecoder.util.Firmware.STAGE_START: | ||||
handle_firmware.wait_tick = 0 | handle_firmware.wait_tick = 0 | ||||
handle_firmware.upload_tick = 0 | handle_firmware.upload_tick = 0 | ||||
@@ -30,6 +31,10 @@ def handle_firmware(stage): | |||||
sys.stdout.flush() | sys.stdout.flush() | ||||
elif stage == alarmdecoder.util.Firmware.STAGE_DONE: | elif stage == alarmdecoder.util.Firmware.STAGE_DONE: | ||||
print "\r\nDone!" | 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(): | def main(): | ||||
device = '/dev/ttyUSB0' | device = '/dev/ttyUSB0' | ||||
@@ -47,20 +52,32 @@ def main(): | |||||
if len(sys.argv) > 3: | if len(sys.argv) > 3: | ||||
baudrate = 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) | 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__": | if __name__ == "__main__": | ||||
main() | main() |
@@ -7,6 +7,7 @@ import socket | |||||
import time | import time | ||||
import tempfile | import tempfile | ||||
import os | import os | ||||
import select | |||||
from OpenSSL import SSL, crypto | from OpenSSL import SSL, crypto | ||||
from alarmdecoder.devices import USBDevice, SerialDevice, SocketDevice | from alarmdecoder.devices import USBDevice, SerialDevice, SocketDevice | ||||
from alarmdecoder.util import NoDeviceError, CommError, TimeoutError | from alarmdecoder.util import NoDeviceError, CommError, TimeoutError | ||||
@@ -292,40 +293,50 @@ class TestSocketDevice(TestCase): | |||||
with patch.object(socket.socket, 'connect', return_value=None): | with patch.object(socket.socket, 'connect', return_value=None): | ||||
self._device.open(no_reader_thread=True) | 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) | mock.assert_called_with(1) | ||||
def test_read_exception(self): | 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): | 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): | 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): | 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): | def test_ssl(self): | ||||
ssl_key = crypto.PKey() | ssl_key = crypto.PKey() | ||||
@@ -1,13 +1,18 @@ | |||||
from unittest import TestCase | from unittest import TestCase | ||||
from mock import Mock, MagicMock | from mock import Mock, MagicMock | ||||
from alarmdecoder import AlarmDecoder | |||||
from alarmdecoder.panels import ADEMCO | |||||
from alarmdecoder.messages import Message, ExpanderMessage | from alarmdecoder.messages import Message, ExpanderMessage | ||||
from alarmdecoder.zonetracking import Zonetracker, Zone | from alarmdecoder.zonetracking import Zonetracker, Zone | ||||
class TestZonetracking(TestCase): | class TestZonetracking(TestCase): | ||||
def setUp(self): | 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_fault += self.fault_event | ||||
self._zonetracker.on_restore += self.restore_event | self._zonetracker.on_restore += self.restore_event | ||||