Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions enocean/communicators/communicator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division, absolute_import
from enocean.protocol.esp2_packet import ESP2Packet
from enocean.protocol.packet import ESP3Packet
import logging
import datetime

Expand All @@ -11,6 +13,11 @@
from enocean.protocol.packet import Packet, UTETeachInPacket
from enocean.protocol.constants import PACKET, PARSE_RESULT, RETURN_CODE

from enum import Enum

class ESP_Version(Enum):
ESP2 = 2
ESP3 = 3

class Communicator(threading.Thread):
'''
Expand All @@ -19,7 +26,7 @@ class Communicator(threading.Thread):
'''
logger = logging.getLogger('enocean.communicators.Communicator')

def __init__(self, callback=None, teach_in=True):
def __init__(self, version: ESP_Version=ESP_Version.ESP3, callback=None, teach_in=True):
super(Communicator, self).__init__()
# Create an event to stop the thread
self._stop_flag = threading.Event()
Expand All @@ -35,6 +42,7 @@ def __init__(self, callback=None, teach_in=True):
# Should new messages be learned automatically? Defaults to True.
# TODO: Not sure if we should use CO_WR_LEARNMODE??
self.teach_in = teach_in
self._version = version

def _get_from_send_queue(self):
''' Get message from send queue, if one exists '''
Expand All @@ -61,11 +69,15 @@ def parse(self):
''' Parses messages and puts them to receive queue '''
# Loop while we get new messages
while True:
status, self._buffer, packet = Packet.parse_msg(self._buffer)
status, self._buffer, packet = ESP3Packet.parse_msg(self._buffer) if self._version == ESP_Version.ESP3.value else ESP2Packet.parse_msg(self._buffer)
# If message is incomplete -> break the loop
if status == PARSE_RESULT.INCOMPLETE:
return status

# if message has a crc error, ignore current buffer
if status == PARSE_RESULT.CRC_MISMATCH:
self._buffer.clear()

# If message is OK, add it to receive queue or send to the callback method
if status == PARSE_RESULT.OK and packet:
packet.received = datetime.datetime.now()
Expand Down
6 changes: 3 additions & 3 deletions enocean/communicators/serialcommunicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import serial
import time

from enocean.communicators.communicator import Communicator
from enocean.communicators.communicator import Communicator, ESP_Version


class SerialCommunicator(Communicator):
''' Serial port communicator class for EnOcean radio '''
logger = logging.getLogger('enocean.communicators.SerialCommunicator')

def __init__(self, port='/dev/ttyAMA0', callback=None):
super(SerialCommunicator, self).__init__(callback)
def __init__(self, version: ESP_Version=ESP_Version.ESP3, port='/dev/ttyAMA0', callback=None):
super(SerialCommunicator, self).__init__(version = version, callback = callback)
# Initialize serial port
self.__ser = serial.Serial(port, 57600, timeout=0.1)

Expand Down
6 changes: 3 additions & 3 deletions enocean/communicators/tcpcommunicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import logging
import socket

from enocean.communicators.communicator import Communicator
from enocean.communicators.communicator import Communicator, ESP_Version


class TCPCommunicator(Communicator):
''' Socket communicator class for EnOcean radio '''
logger = logging.getLogger('enocean.communicators.TCPCommunicator')

def __init__(self, host='', port=9637):
super(TCPCommunicator, self).__init__()
def __init__(self, version: ESP_Version=ESP_Version.ESP3, host='', port=9637):
super(TCPCommunicator, self).__init__(version = version)
self.host = host
self.port = port

Expand Down
19 changes: 19 additions & 0 deletions enocean/protocol/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ class RETURN_CODE(IntEnum):
WRONG_PARAM = 0x03
OPERATION_DENIED = 0x04

# ESP2 ORG EnOcean_Equipment_Profiles_2.0.pdf
class ORG(IntEnum):
BS4 = 0x07
BS1 = 0x06
RPS = 0x05

#ESP2 Message Status Field EnOcean_Equipment_Profiles_2.0.pdf
class MSGSTATUS(IntEnum):
#RPS definitions
T2Msg = 0x20
NMsg = 0x10
T2NMsg = 0x30
T2UMsg = 0x20
T1NMsg = 0x10
T1UMsg = 0x00
#BS4 definitions
BS4 = 0x00 #must be zero in BS4 messages with RP count of 0
#BS1 definitions
BS1 = 0x00 #must be zero in BS1 too

# EnOceanSerialProtocol3.pdf / 20
class EVENT_CODE(IntEnum):
Expand Down
9 changes: 8 additions & 1 deletion enocean/protocol/crc8.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,15 @@
0xfa, 0xfd, 0xf4, 0xf3)


def calc(msg):
def calc_ESP3(msg):
checksum = 0
for byte in msg:
checksum = CRC_TABLE[checksum & 0xFF ^ byte & 0xFF]
return checksum

def calc_ESP2(msg):
checksum=0
for byte in msg:
checksum += byte
checksum &= 0xFF
return checksum
218 changes: 218 additions & 0 deletions enocean/protocol/esp2_packet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# -*- encoding: utf-8 -*-
from __future__ import print_function, unicode_literals, division, absolute_import
from enocean.protocol.packet import EventPacket, Packet, RadioPacket, ResponsePacket, UTETeachInPacket

from enocean.protocol import crc8
from enocean.protocol.eep import EEP
from enocean.protocol.constants import ORG, PACKET, RORG, PARSE_RESULT


class ESP2Packet(Packet):
'''
Base class for ESP2 Packet.
Mainly used for for packet generation and
Packet.parse_msg(buf) for parsing message.
parse_msg() returns subclass, if one is defined for the data type.
'''

@staticmethod
def parse_msg(buf):
'''
Parses message from buffer.
returns:
- PARSE_RESULT
- remaining buffer
- Packet -object (if message was valid, else None)
'''
# If the buffer doesn't contain 0xA5 (start char)
# the message isn't needed -> ignore
# if 0xA5 not in buf:
# return PARSE_RESULT.INCOMPLETE, [], None
# else:
# if buf[list(buf).index(0xA5)+1] != 0x5A:
# return PARSE_RESULT.INCOMPLETE, [], None

if not buf:
return PARSE_RESULT.INCOMPLETE, [], None

if 0xA5 not in buf:
return PARSE_RESULT.INCOMPLETE, [], None

msg_len = 0

for index,value in enumerate(buf):
try:
if buf[index] == 0xA5 and buf[index+1] == 0x5A:
data_len = buf[index+2] & 0x1F
HSEQ = buf[index+2] >> 5
opt_len = 0
msg_len = data_len
if len(buf[index+2:]) < msg_len:
# If buffer isn't long enough, the message is incomplete
return PARSE_RESULT.INCOMPLETE, buf, None
crcval = crc8.calc_ESP2(buf[index+2:index+2+data_len])
if buf[index+2+data_len] == crcval:
buf = buf[index+2:]
break
else:
ESP2Packet.logger.error('Data CRC error!')
return PARSE_RESULT.CRC_MISMATCH, buf, None
except IndexError:
# If the fields don't exist, message is incomplete
return PARSE_RESULT.INCOMPLETE, buf, None

if msg_len == 0:
ESP2Packet.logger.error('Data Length is Zero!')
return PARSE_RESULT.INCOMPLETE, [], None

msg = buf[0:msg_len]
buf = buf[msg_len+1:]

packet_type = HSEQ
data = msg[1:data_len]
opt_data = []

#Adopt ORG to RORG for ESP2
if data[0] == ORG.BS1:
data[0] = RORG.BS1
if data[0] == ORG.BS4:
data[0] = RORG.BS4
if data[0] == ORG.RPS:
data[0] = RORG.RPS

# If we got this far, everything went ok (?)
# in ESP2 the exist only two type of packets, RadioPacket and Command
if packet_type == PACKET.RADIO or packet_type == PACKET.RESERVED or packet_type == PACKET.EVENT:
# Need to handle UTE Teach-in here, as it's a separate packet type...
if data[0] == RORG.UTE:
packet = ESP2UTETeachInPacket(packet_type, data, opt_data)
# Send a response automatically, works only if
# - communicator is set
# - communicator.teach_in == True
# packet.send_response()
else:
packet = ESP2RadioPacket(packet_type, data, opt_data)
elif packet_type == PACKET.RESPONSE:
packet = ESP2ResponsePacket(packet_type, data, opt_data)
else:
packet = ESP2Packet(packet_type, data, opt_data)

return PARSE_RESULT.OK, buf, packet

@staticmethod
def create(packet_type, rorg, rorg_func, rorg_type, direction=None, command=None,
destination=None,
sender=None,
learn=False, **kwargs):
'''
Creates an packet ready for sending.
Uses rorg, rorg_func and rorg_type to determine the values set based on EEP.
Additional arguments (**kwargs) are used for setting the values.
Currently only supports:
- PACKET.RADIO
- RORGs RPS, BS1, BS4, VLD.
TODO:
- Require sender to be set? Would force the "correct" sender to be set.
- Do we need to set telegram control bits?
Might be useful for acting as a repeater?
'''

if packet_type != PACKET.RADIO:
# At least for now, only support PACKET.RADIO.
raise ValueError('Packet type not supported by this function.')

if rorg not in [RORG.RPS, RORG.BS1, RORG.BS4]:
# At least for now, only support these RORGS.
raise ValueError('RORG not supported by this function.')

if destination is None:
destination = [0xFF, 0xFF, 0xFF, 0xFF]

# TODO: Should use the correct Base ID as default.
# Might want to change the sender to be an offset from the actual address?
if sender is None:
ESP2Packet.logger.warning('Replacing sender with default address.')
sender = [0xDE, 0xAD, 0xBE, 0xEF]

if not isinstance(destination, list) or len(destination) != 4:
raise ValueError('Destination must a list containing 4 (numeric) values.')

if not isinstance(sender, list) or len(sender) != 4:
raise ValueError('Sender must a list containing 4 (numeric) values.')

packet = ESP2Packet(packet_type, data=[], optional=[])
packet.rorg = rorg

if packet.rorg == RORG.BS1:
packet.rorg = ORG.BS1
if packet.rorg == RORG.BS4:
packet.rorg = ORG.BS4
if packet.rorg == RORG.RPS:
packet.rorg = ORG.RPS

packet.data = [packet.rorg]
# Select EEP at this point, so we know how many bits we're dealing with (for VLD).
#packet.select_eep(rorg_func, rorg_type, direction, command)

# Initialize data depending on the profile.
# if rorg in [RORG.RPS, RORG.BS1]:
# packet.data.extend([0])
#elif rorg == RORG.BS4:
packet.data.extend(command)
#else:
# packet.data.extend([0] * int(packet._profile.get('bits', '1')))
packet.data.extend(sender)
packet.data.extend([0])
# Always use sub-telegram 3, maximum dbm (as per spec, when sending),
# and no security (security not supported as per EnOcean Serial Protocol).
#packet.optional = [3] + destination + [0xFF] + [0]

#if command:
# Set CMD to command, if applicable.. Helps with VLD.
#kwargs['CMD'] = command

#packet.set_eep(kwargs)
if rorg in [RORG.BS1, RORG.BS4] and not learn:
if rorg == RORG.BS1:
packet.data[1] |= (1 << 3)
if rorg == RORG.BS4:
packet.data[4] |= (1 << 3)
packet.data[-1] = packet.status

# Parse the built packet, so it corresponds to the received packages
# For example, stuff like RadioPacket.learn should be set.
packet = ESP2Packet.parse_msg(packet.build())[2]
return packet

def build(self):
''' Build Packet for sending to EnOcean controller '''
data_length = len(self.data)+1
ords = [0xA5, 0x5A, (data_length & 0x1F | ((int(self.packet_type)&0x07)<<5))]

if self.data[0] in [RORG.RPS, RORG.BS4, RORG.BS1]:
if self.data[0] == RORG.RPS:
self.data[0] = ORG.RPS
if self.data[0] == RORG.BS1:
self.data[0] = ORG.BS1
if self.data[0] == RORG.BS4:
self.data[0] = ORG.BS4

ords.extend(self.data)
ords.append(crc8.calc_ESP2(ords[2:]))
return ords

class ESP2RadioPacket(RadioPacket):
@staticmethod
def create(rorg, rorg_func, rorg_type, direction=None, command=None,
destination=None, sender=None, learn=False, **kwargs):
return ESP2Packet.create(PACKET.RADIO_ERP1, rorg, rorg_func, rorg_type,
direction, command, destination, sender, learn, **kwargs)

class ESP2ResponsePacket(ResponsePacket, ESP2Packet):
"""ESP2 version of response package"""

class ESP2EventPacket(EventPacket, ESP2Packet):
"""ESP2 version of event package"""

class ESP2UTETeachInPacket(UTETeachInPacket, ESP2RadioPacket):
"""ESP2 version of UTE teachin package"""
Loading