@@ -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 | |||