@@ -14,7 +14,7 @@ import serial.tools.list_ports | |||||
import select | import select | ||||
import sys | import sys | ||||
from .base_device import Device | from .base_device import Device | ||||
from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack | |||||
from ..util import CommError, TimeoutError, NoDeviceError, bytes_hack, filter_ad2prot_byte | |||||
class SerialDevice(Device): | class SerialDevice(Device): | ||||
@@ -141,7 +141,7 @@ class SerialDevice(Device): | |||||
def fileno(self): | def fileno(self): | ||||
""" | """ | ||||
Returns the file number associated with the device | Returns the file number associated with the device | ||||
:returns: int | :returns: int | ||||
""" | """ | ||||
return self._device.fileno() | return self._device.fileno() | ||||
@@ -178,13 +178,13 @@ class SerialDevice(Device): | |||||
:returns: character read from the device | :returns: character read from the device | ||||
:raises: :py:class:`~alarmdecoder.util.CommError` | :raises: :py:class:`~alarmdecoder.util.CommError` | ||||
""" | """ | ||||
data = '' | |||||
data = b'' | |||||
try: | try: | ||||
read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | ||||
if len(read_ready) != 0: | if len(read_ready) != 0: | ||||
data = self._device.read(1) | |||||
data = filter_ad2prot_byte(self._device.read(1)) | |||||
except serial.SerialException as err: | except serial.SerialException as err: | ||||
raise CommError('Error reading from device: {0}'.format(str(err)), err) | raise CommError('Error reading from device: {0}'.format(str(err)), err) | ||||
@@ -213,54 +213,38 @@ class SerialDevice(Device): | |||||
if purge_buffer: | if purge_buffer: | ||||
self._buffer = b'' | self._buffer = b'' | ||||
got_line, data = False, '' | |||||
got_line, ret = False, None | |||||
timer = threading.Timer(timeout, timeout_event) | timer = threading.Timer(timeout, timeout_event) | ||||
if timeout > 0: | if timeout > 0: | ||||
timer.start() | timer.start() | ||||
leftovers = b'' | |||||
try: | try: | ||||
while timeout_event.reading and not got_line: | |||||
while timeout_event.reading: | |||||
read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | read_ready, _, _ = select.select([self._device.fileno()], [], [], 0.5) | ||||
if len(read_ready) == 0: | if len(read_ready) == 0: | ||||
continue | continue | ||||
bytes_avail = 0 | |||||
if hasattr(self._device, "in_waiting"): | |||||
bytes_avail = self._device.in_waiting | |||||
else: | |||||
bytes_avail = self._device.inWaiting() | |||||
buf = self._device.read(bytes_avail) | |||||
for idx in range(len(buf)): | |||||
c = buf[idx] | |||||
buf = filter_ad2prot_byte(self._device.read(1)) | |||||
ub = bytes_hack(c) | |||||
if sys.version_info > (3,): | |||||
ub = bytes([ub]) | |||||
if buf != b'': | |||||
self._buffer += buf | |||||
# NOTE: AD2SERIAL and AD2PI apparently sends down \xFF on boot. | |||||
if ub != b'' and ub != b"\xff": | |||||
self._buffer += ub | |||||
if ub == b"\n": | |||||
self._buffer = self._buffer.strip(b"\r\n") | |||||
if len(self._buffer) > 0: | |||||
got_line = True | |||||
leftovers = buf[idx:] | |||||
break | |||||
if buf == b"\n": | |||||
self._buffer = self._buffer.rstrip(b"\r\n") | |||||
if len(self._buffer) > 0: | |||||
got_line = True | |||||
break | |||||
except (OSError, serial.SerialException) as err: | except (OSError, serial.SerialException) as err: | ||||
raise CommError('Error reading from device: {0}'.format(str(err)), err) | raise CommError('Error reading from device: {0}'.format(str(err)), err) | ||||
else: | else: | ||||
if got_line: | if got_line: | ||||
data, self._buffer = self._buffer, leftovers | |||||
ret, self._buffer = self._buffer, b'' | |||||
self.on_read(data=data) | |||||
self.on_read(data=ret) | |||||
else: | else: | ||||
raise TimeoutError('Timeout while waiting for line terminator.') | raise TimeoutError('Timeout while waiting for line terminator.') | ||||
@@ -268,7 +252,7 @@ class SerialDevice(Device): | |||||
finally: | finally: | ||||
timer.cancel() | timer.cancel() | ||||
return data.decode('utf-8') | |||||
return ret.decode('utf-8') | |||||
def purge(self): | def purge(self): | ||||
""" | """ | ||||
@@ -93,6 +93,22 @@ def bytes_hack(buf): | |||||
return ub | return ub | ||||
def filter_ad2prot_byte(buf): | |||||
""" | |||||
Return the byte sent in back if valid visible terminal characters or line terminators. | |||||
""" | |||||
if sys.version_info > (3,): | |||||
c = buf[0] | |||||
else: | |||||
c = ord(buf) | |||||
if (c == 10 or c == 13): | |||||
return buf | |||||
if (c > 31 and c < 127): | |||||
return buf | |||||
else: | |||||
return b'' | |||||
def read_firmware_file(file_path): | def read_firmware_file(file_path): | ||||
""" | """ | ||||
Reads a firmware file into a dequeue for processing. | Reads a firmware file into a dequeue for processing. | ||||
@@ -151,9 +151,7 @@ class Zonetracker(object): | |||||
status = Zone.CHECK | status = Zone.CHECK | ||||
# NOTE: Expander zone faults are handled differently than | # NOTE: Expander zone faults are handled differently than | ||||
# regular messages. We don't include them in | |||||
# self._zones_faulted because they are not reported | |||||
# by the panel in it's rolling list of faults. | |||||
# regular messages. | |||||
try: | try: | ||||
self._update_zone(zone, status=status) | self._update_zone(zone, status=status) | ||||
@@ -198,6 +196,9 @@ class Zonetracker(object): | |||||
self._update_zone(zone) | self._update_zone(zone) | ||||
self._clear_zones(zone) | self._clear_zones(zone) | ||||
# Save our spot for the next message. | |||||
self._last_zone_fault = zone | |||||
else: | else: | ||||
status = Zone.FAULT | status = Zone.FAULT | ||||
if message.check_zone: | if message.check_zone: | ||||
@@ -207,8 +208,8 @@ class Zonetracker(object): | |||||
self._zones_faulted.append(zone) | self._zones_faulted.append(zone) | ||||
self._zones_faulted.sort() | self._zones_faulted.sort() | ||||
# Save our spot for the next message. | |||||
self._last_zone_fault = zone | |||||
# A new zone fault, so it is out of sequence. | |||||
self._last_zone_fault = 0 | |||||
self._clear_expired_zones() | self._clear_expired_zones() | ||||
@@ -245,6 +246,11 @@ class Zonetracker(object): | |||||
:param zone: current zone being processed | :param zone: current zone being processed | ||||
:type zone: int | :type zone: int | ||||
""" | """ | ||||
if self._last_zone_fault == 0: | |||||
# We don't know what the last faulted zone was, nothing to do | |||||
return | |||||
cleared_zones = [] | cleared_zones = [] | ||||
found_last_faulted = found_current = at_end = False | found_last_faulted = found_current = at_end = False | ||||
@@ -296,7 +302,9 @@ class Zonetracker(object): | |||||
# Actually remove the zones and trigger the restores. | # Actually remove the zones and trigger the restores. | ||||
for z in cleared_zones: | for z in cleared_zones: | ||||
self._update_zone(z, Zone.CLEAR) | |||||
# Don't clear expander zones, expander messages will fix this | |||||
if self._zones[z].expander is False: | |||||
self._update_zone(z, Zone.CLEAR) | |||||
def _clear_expired_zones(self): | def _clear_expired_zones(self): | ||||
""" | """ | ||||
@@ -14,7 +14,7 @@ if sys.version_info < (3,): | |||||
extra_requirements.append('future>=0.14.3') | extra_requirements.append('future>=0.14.3') | ||||
setup(name='alarmdecoder', | setup(name='alarmdecoder', | ||||
version='1.13.9', | |||||
version='1.13.10', | |||||
description='Python interface for the AlarmDecoder (AD2) family ' | description='Python interface for the AlarmDecoder (AD2) family ' | ||||
'of alarm devices which includes the AD2USB, AD2SERIAL and AD2PI.', | 'of alarm devices which includes the AD2USB, AD2SERIAL and AD2PI.', | ||||
long_description=readme(), | long_description=readme(), | ||||
@@ -362,5 +362,7 @@ class TestAlarmDecoder(TestCase): | |||||
self._decoder._on_read(self, data=b'[00010001000000000A--],005,[f70000051003000008020000000000],"FAULT 05 "') | self._decoder._on_read(self, data=b'[00010001000000000A--],005,[f70000051003000008020000000000],"FAULT 05 "') | ||||
self.assertEquals(self._zone_faulted, 5) | self.assertEquals(self._zone_faulted, 5) | ||||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | |||||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 05 "') | |||||
self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | self._decoder._on_read(self, data=b'[00010001000000000A--],004,[f70000051003000008020000000000],"FAULT 04 "') | ||||
self.assertEquals(self._zone_restored, 3) | self.assertEquals(self._zone_restored, 3) |
@@ -80,8 +80,11 @@ class TestSerialDevice(TestCase): | |||||
def test_read(self): | def test_read(self): | ||||
self._device.interface = '/dev/ttyS0' | self._device.interface = '/dev/ttyS0' | ||||
self._device.open(no_reader_thread=True) | self._device.open(no_reader_thread=True) | ||||
side_effect = ["t"] | |||||
if sys.version_info > (3,): | |||||
side_effect = ["t".encode('utf-8')] | |||||
with patch.object(self._device._device, 'read') as mock: | |||||
with patch.object(self._device._device, 'read', side_effect=side_effect) as mock: | |||||
with patch('serial.Serial.fileno', return_value=1): | with patch('serial.Serial.fileno', return_value=1): | ||||
with patch.object(select, 'select', return_value=[[1], [], []]): | with patch.object(select, 'select', return_value=[[1], [], []]): | ||||
ret = self._device.read() | ret = self._device.read() | ||||