Browse Source

Merge branch 'dev'

pyserial_fix
Scott Petersen 9 years ago
parent
commit
4aced9bdb3
6 changed files with 179 additions and 63 deletions
  1. +6
    -6
      alarmdecoder/decoder.py
  2. +38
    -2
      alarmdecoder/devices.py
  3. +68
    -21
      alarmdecoder/util.py
  4. +28
    -11
      bin/ad2-firmwareupload
  5. +33
    -22
      test/test_devices.py
  6. +6
    -1
      test/test_zonetracking.py

+ 6
- 6
alarmdecoder/decoder.py View File

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


+ 38
- 2
alarmdecoder/devices.py View File

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


+ 68
- 21
alarmdecoder/util.py View File

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

+ 28
- 11
bin/ad2-firmwareupload View File

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

+ 33
- 22
test/test_devices.py View File

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


+ 6
- 1
test/test_zonetracking.py View File

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


Loading…
Cancel
Save