Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhz committed Nov 18, 2019
0 parents commit b8b049f
Show file tree
Hide file tree
Showing 37 changed files with 1,608 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
*.deb
*.egg
*.egg-info/
*.egg/
*.ignore
*.py[co]
*.py[oc]
*.spl
*.vagrant
.DS_Store
.coverage
.eggs/
.eggs/*
.idea
.idea/
.pt
.vagrant/
RELEASE-VERSION.txt
build/
cover/
dist/
dump.rdb
flake8.log
local/
local_*
metadata/
nosetests.xml
output.xml
pylint.log
redis-server.log
redis-server/
__pycache__
*/__pycache__
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2019 Ryan Hu and Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
187 changes: 187 additions & 0 deletions osdp/bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import logging
from datetime import datetime, timedelta
import time
from queue import Queue
from threading import Lock
from uuid import UUID, uuid4

log = logging.getLogger('osdp')

'''
A group of OSDP devices sharing communications
'''
class Bus:

DRIVER_BYTE = 0xFF

def __init__(self, connection: OsdpConnection, on_reply_received):
self._connection = connection
self._on_reply_received = on_reply_received
self._configured_devices = {}
self._configured_devices_lock = Lock()
self._read_timeout = timedelta(milliseconds=200)
self.id = uuid4()
self._is_shutting_down = False

@property
def idle_line_delay(self) -> timedelta:
return timedelta(milliseconds=(1000.0/self._connection.baud_rate * 16.0))

def close(self):
self._is_shutting_down = True
self._connection.close()

def send_command(self, command: Command):
found_device = self._configured_devices.get(command.address)
if found_device is not None:
found_device.send_command(command)

def add_device(self, address: int, use_crc: bool, use_secure_channel: bool):
found_device = self._configured_devices.get(address)
self._configured_devices_lock.acquire()
if found_device is not None:
self._configured_devices.pop(address)
self._configured_devices[address] = Device(address, use_crc, use_secure_channel)
self._configured_devices_lock.release()

def remove_device(self, address: int):
found_device = self._configured_devices.get(address)
self._configured_devices_lock.acquire()
if found_device is not None:
self._configured_devices.pop(address)
self._configured_devices_lock.release()

def is_online(self, address: int) -> bool:
found_device = self._configured_devices.get(address)
if found_device is None:
return False
else:
return found_device.is_online

def run_polling_loop(self):
last_message_sent_time = datetime.min
while not self._is_shutting_down:
if not self._connection.is_open:
try:
self._connection.open()
except:
log.exception("Error while opening connection %s", self._connection)

time_difference = timedelta(milliseconds=100) - (datetime.now() - last_message_sent_time)
time.sleep(max(time_difference, timedelta(seconds=0)).total_seconds())

if not self._configured_devices:
last_message_sent_time = datetime.now()
continue

for device in list(self._configured_devices.values()):
data = bytearray([self.DRIVER_BYTE])
command = device.get_next_command_data()

last_message_sent_time = datetime.now()

reply: Reply = None
try:
reply = self.send_command_and_receive_reply(data, command, device)
except:
log.exception("Error while sending command %s and receiving reply", command)
self._connection.close()
continue

try:
self.process_reply(reply, device)
except:
log.exception("Error while processing reply %s", reply)
self._connection.close()
continue

time.sleep(self.idle_line_delay.total_seconds())

def process_reply(self, reply: Reply, device: Device):
if not reply.is_valid_reply:
return

if reply.is_secure_message:
mac = device.generate_mac(reply.message_for_mac_generation, False)
if not reply.is_valid_mac(mac):
device.reset_security()
return

if reply.type != ReplyType.Busy:
device.valid_reply_has_been_received(reply.sequence)

extract_reply_data = reply.extract_reply_data
error_code = ErrorCode(extract_reply_data[0])
if reply.type == ReplyType.Nak and (error_code==ErrorCode.DoesNotSupportSecurityBlock || error_code==ErrorCode.DoesNotSupportSecurityBlock):
device.reset_security()

if reply.type==ReplyType.CrypticData:
device.initialize_secure_channel(reply)
elif reply.type==ReplyType.InitialRMac:
device.validate_secure_channel_establishment(reply)

if self._on_reply_received is not None:
self._on_reply_received(reply)

def send_command_and_receive_reply(data: bytearray, command: Command, device: Device) -> Reply:
command_data: bytes = None
try:
command_data = command.build_command(device)
except:
log.exception("Error while building command %s", command)
raise
data.extend(command_data)

log.debug("Raw write data: %s", command_data.hex())

self._connection.write(bytes(data))

reply_buffer = bytearray()

if not self.wait_for_start_of_message(reply_buffer):
raise TimeoutError("Timeout waiting for reply message")

if not self.wait_for_message_length(reply_buffer):
raise TimeoutError("Timeout waiting for reply message length")

if not self.wait_for_rest_of_message(reply_buffer, self.extract_message_length(reply_buffer)):
raise TimeoutError("Timeout waiting for rest of reply message")

log.debug("Raw reply data: %s", reply_buffer.hex())

return Reply.parse(reply_buffer, self.id, command, device)

def extract_message_length(self, reply_buffer: bytearray) -> int:
return Message.convert_bytes_to_short(bytes(reply_buffer[2:3]))

def wait_for_rest_of_message(self, buffer: bytearray, reply_length: int):
while len(buffer) < reply_length:
bytes_read = self._connection.read(reply_length-len(buffer))
if len(bytes_read)>0:
buffer.extend(bytes_read)
else:
return False
return True

def wait_for_message_length(self, buffer: bytearray) -> bool:
while len(buffer) < 4:
bytes_read = self._connection.read(4)
if len(bytes_read)>0:
buffer.extend(bytes_read)
else:
return False
return True

def wait_for_start_of_message(self, buffer: bytearray) -> bool:
while True:
bytes_read = self._connection.read(1)
if len(bytes_read)==0:
return False

if bytes_read[0]!=Message.SOM:
continue

buffer.extend(bytes_read)
break
return True

29 changes: 29 additions & 0 deletions osdp/connections/osdp_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from abc import ABC, abstractmethod

class OsdpConnection(ABC):

@property
@abstractmethod
def baud_rate(self) -> int:
pass

@property
@abstractmethod
def is_open(self) -> bool:
pass

@abstractmethod
def open(self):
pass

@abstractmethod
def close(self):
pass

@abstractmethod
def write(self, buf: bytes):
pass

@abstractmethod
def read(self, size: int=1) -> bytes:
pass
31 changes: 31 additions & 0 deletions osdp/connections/serial_port_osdp_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import serial


class SerialPortOsdpConnection(OsdpConnection):

def __init__(self, port: str, baud_rate: int):
self._port = port
self._baud_rate = baud_rate
self.serial_port = None

@property
def baud_rate(self) -> int:
return self._baud_rate

@property
def is_open(self) -> bool:
self.serial_port is not None and self.serial_port.is_open

def open(self):
self.serial_port = serial.Serial(port=self._port, baudrate=self._baud_rate, timeout=2.0)

def close(self):
if self.serial_port is not None:
self.serial_port.close()
self.serial_port = None

def write(self, buf: bytes):
self.serial_port.write(buf)

def read(self, size: int=1) -> bytes:
return self.serial_port.read(size)
42 changes: 42 additions & 0 deletions osdp/connections/tcp_client_osdp_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import socket
import sys


class TcpClientOsdpConnection(OsdpConnection):

def __init__(self, server: str, port_number: int):
self._server = server
self._port_number = port_number
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(2)
self.is_connected = False

@property
def baud_rate(self) -> int:
return 9600

@property
def is_open(self) -> bool:
return self.is_connected

def open(self):
server_address = (self._server, self._port_number)
self.sock.connect(server_address)
self.is_connected = True

def close(self):
self.sock.close()
self.is_connected = False

def write(self, buf: bytes):
try:
self.sock.send(buf)
except socket.timeout as e:
self.is_connected = False

def read(self, size: int=1) -> bytes:
try:
return self.sock.recv(size)
except socket.timeout as e:
self.is_connected = False
return b''
42 changes: 42 additions & 0 deletions osdp/connections/tcp_server_osdp_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import socket
import sys


class TcpServerOsdpConnection(OsdpConnection):

def __init__(self, port_number: int):
self._port_number = port_number
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(2)
server_address = ('0.0.0.0', self._port_number)
self.sock.bind(server_address)
self.connection = None

@property
def baud_rate(self) -> int:
return 9600

@property
def is_open(self) -> bool:
return self.connection is not None

def open(self):
self.sock.listen(1)
self.connection, _ = self.sock.accept()

def close(self):
self.connection.close()
self.connection = None

def write(self, buf: bytes):
try:
self.connection.sendall(buf)
except socket.timeout as e:
self.close()

def read(self, size: int=1) -> bytes:
try:
return self.connection.recv(size)
except socket.timeout as e:
self.close()
return b''
Loading

0 comments on commit b8b049f

Please sign in to comment.