Added NFC backend

This commit is contained in:
Fabio Manganiello 2019-07-09 01:44:31 +02:00
parent 0a97bb2345
commit 24d3810e44
6 changed files with 317 additions and 47 deletions

View file

@ -207,6 +207,8 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'soundfile', 'soundfile',
'numpy', 'numpy',
'cv2', 'cv2',
'nfc',
'ndef',
] ]
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))

187
platypush/backend/nfc.py Normal file
View file

@ -0,0 +1,187 @@
import base64
import json
import nfc
import ndef
from platypush.backend import Backend
from platypush.message.event.nfc import NFCTagDetectedEvent, NFCTagRemovedEvent, NFCDeviceConnectedEvent, \
NFCDeviceDisconnectedEvent
class NfcBackend(Backend):
"""
Backend to detect NFC card events from a compatible reader.
Triggers:
* :class:`platypush.message.event.nfc.NFCDeviceConnectedEvent` when an NFC reader/writer is connected
* :class:`platypush.message.event.nfc.NFCDeviceDisconnectedEvent` when an NFC reader/writer is disconnected
* :class:`platypush.message.event.nfc.NFCTagDetectedEvent` when an NFC tag is detected
* :class:`platypush.message.event.nfc.NFCTagRemovedEvent` when an NFC tag is removed
Requires:
* **nfcpy** >= 1.0 (``pip install nfcpy``)
"""
def __init__(self, device='usb', *args, **kwargs):
"""
:param device: Address or ID of the device to be opened. Examples:
* `'usb:003:009'` opens device 9 on bus 3
* `'usb:003'` opens the first available device on bus 3
* `'usb'` opens the first available USB device (default)
"""
super().__init__(*args, **kwargs)
self.device_id = device
self._clf = None
def _get_clf(self):
if not self._clf:
self._clf = nfc.ContactlessFrontend()
self._clf.open(self.device_id)
self.bus.post(NFCDeviceConnectedEvent(reader=self._get_device_str()))
self.logger.info('Initialized NFC reader backend on device {}'.format(self._get_device_str()))
return self._clf
def _get_device_str(self):
return str(self._clf.device)
def close(self):
if self._clf:
self._clf.close()
self._clf = None
self.bus.post(NFCDeviceDisconnectedEvent(reader=self._get_device_str()))
@staticmethod
def _parse_records(tag):
records = []
for record in tag.ndef.records:
r = {
'record_type': record.type,
'record_name': record.name,
}
if isinstance(record, ndef.TextRecord):
try:
r = {
**r,
'type': 'json',
'value': json.loads(record.text),
}
except ValueError:
r = {
**r,
'type': 'text',
'text': record.text,
}
elif isinstance(record, ndef.UriRecord):
r = {
**r,
'type': 'uri',
'uri': record.uri,
'iri': record.iri,
}
elif isinstance(record, ndef.SmartposterRecord):
r = {
**r,
'type': 'smartposter',
**{attr: getattr(record, attr) for attr in ['resource', 'titles', 'title', 'action', 'icon',
'icons', 'resource_size', 'resource_type']},
}
elif isinstance(record, ndef.DeviceInformationRecord):
r = {
**r,
'type': 'device_info',
**{attr: getattr(record, attr) for attr in ['vendor_name', 'model_name', 'unique_name',
'uuid_string', 'version_string']},
}
elif isinstance(record, ndef.WifiSimpleConfigRecord):
r = {
**r,
'type': 'wifi_simple_config',
**{attr: record[attr] for attr in record.attribute_names()}
}
elif isinstance(record, ndef.WifiPeerToPeerRecord):
r = {
**r,
'type': 'wifi_peer_to_peer',
**{attr: record[attr] for attr in record.attribute_names()}
}
elif isinstance(record, ndef.BluetoothEasyPairingRecord):
r = {
**r,
'type': 'bluetooth_easy_pairing',
**{attr: getattr(record, attr) for attr in ['device_address', 'device_name', 'device_class']},
}
elif isinstance(record, ndef.BluetoothLowEnergyRecord):
r = {
**r,
'type': 'bluetooth_low_energy',
**{attr: getattr(record, attr) for attr in ['device_address', 'device_name', 'role_capabilities',
'appearance', 'flags', 'security_manager_tk_value',
'secure_connections_confirmation_value',
'secure_connections_random_value']},
}
elif isinstance(record, ndef.SignatureRecord):
r = {
**r,
'type': 'signature',
**{attr: getattr(record, attr) for attr in ['version', 'signature_type', 'hash_type', 'signature',
'signature_uri', 'certificate_format',
'certificate_store', 'certificate_uri',
'secure_connections_random_value']},
}
else:
r = {
**r,
'type': 'binary',
'data': base64.encodebytes(record.data).decode(),
}
records.append(r)
return records
@staticmethod
def _parse_id(tag):
return ''.join([('%02X' % c) for c in tag.identifier])
def _on_connect(self):
def callback(tag):
if not tag:
return False
tag_id = self._parse_id(tag)
records = self._parse_records(tag)
self.bus.post(NFCTagDetectedEvent(reader=self._get_device_str(), tag_id=tag_id, records=records))
return True
return callback
def _on_release(self):
def callback(tag):
tag_id = self._parse_id(tag)
self.bus.post(NFCTagRemovedEvent(reader=self._get_device_str(), tag_id=tag_id))
return callback
def run(self):
super().run()
while not self.should_stop():
try:
clf = self._get_clf()
clf.connect(rdwr={
'on-connect': self._on_connect(),
'on-release': self._on_release(),
})
finally:
self.close()
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1,80 @@
import json
from platypush.message.event import Event
class NFCEvent(Event):
"""
Generic class for NFC events
"""
def __init__(self, reader=None, tag_id=None, *args, **kwargs):
super().__init__(reader=reader, tag_id=tag_id, *args, **kwargs)
class NFCDeviceConnectedEvent(NFCEvent):
"""
Event triggered when an NFC reader/writer devices is connected
"""
def __init__(self, reader=None, *args, **kwargs):
"""
:param reader: Name or address of the reader that fired the event
:type reader: str
"""
super().__init__(reader=reader, *args, **kwargs)
class NFCDeviceDisconnectedEvent(NFCEvent):
"""
Event triggered when an NFC reader/writer devices is disconnected
"""
def __init__(self, reader=None, *args, **kwargs):
"""
:param reader: Name or address of the reader that fired the event
:type reader: str
"""
super().__init__(reader=reader, *args, **kwargs)
class NFCTagDetectedEvent(NFCEvent):
"""
Event triggered when an NFC tag is connected
"""
def __init__(self, reader=None, tag_id=None, records=None, *args, **kwargs):
"""
:param reader: Name or address of the reader that fired the event
:type reader: str
:param tag_id: ID of the NFC tag
:type tag_id: str
:param records: Optional, list of records read from the tag. If the tag contains JSON-serializable data then it
will be cast by the backend into the appropriate object
:type records: str, bytes or JSON-serializable object
"""
if not records:
records = []
super().__init__(reader=reader, tag_id=tag_id, records=records, *args, **kwargs)
class NFCTagRemovedEvent(NFCEvent):
"""
Event triggered when a NFC card is removed/disconnected
"""
def __init__(self, reader=None, tag_id=None, *args, **kwargs):
"""
:param reader: Name or address of the reader that fired the event
:type reader: str
:param tag_id: ID of the NFC tag
:type tag_id: str
"""
super().__init__(reader=reader, tag_id=tag_id, *args, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -5,7 +5,6 @@ import logging
import random import random
import re import re
import time import time
import traceback
from threading import Thread from threading import Thread
@ -48,25 +47,20 @@ class Request(Message):
@classmethod @classmethod
def build(cls, msg): def build(cls, msg):
msg = super().parse(msg) msg = super().parse(msg)
args = { args = {'target': msg.get('target', Config.get('device_id')), 'action': msg['action'],
'target' : msg.get('target', Config.get('device_id')), 'args': msg.get('args', {}), 'id': msg['id'] if 'id' in msg else cls._generate_id(),
'action' : msg['action'], 'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time()}
'args' : msg['args'] if 'args' in msg else {},
}
args['id'] = msg['id'] if 'id' in msg else cls._generate_id()
args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time()
if 'origin' in msg: args['origin'] = msg['origin'] if 'origin' in msg: args['origin'] = msg['origin']
if 'token' in msg: args['token'] = msg['token'] if 'token' in msg: args['token'] = msg['token']
return cls(**args) return cls(**args)
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
id = '' _id = ''
for i in range(0,16): for i in range(0, 16):
id += '%.2x' % random.randint(0, 255) _id += '%.2x' % random.randint(0, 255)
return id return _id
def _execute_procedure(self, *args, **kwargs): def _execute_procedure(self, *args, **kwargs):
from platypush.config import Config from platypush.config import Config
@ -81,7 +75,6 @@ class Request(Message):
return proc.execute(*args, **kwargs) return proc.execute(*args, **kwargs)
def _expand_context(self, event_args=None, **context): def _expand_context(self, event_args=None, **context):
from platypush.config import Config from platypush.config import Config
@ -110,7 +103,6 @@ class Request(Message):
return event_args return event_args
@classmethod @classmethod
def expand_value_from_context(cls, _value, **context): def expand_value_from_context(cls, _value, **context):
for (k, v) in context.items(): for (k, v) in context.items():
@ -130,10 +122,12 @@ class Request(Message):
parsed_value = _value parsed_value = _value
while _value and isinstance(_value, str): while _value and isinstance(_value, str):
m = re.match('([^\$]*)(\${\s*(.+?)\s*})(.*)', _value) m = re.match('([^$]*)(\${\s*(.+?)\s*})(.*)', _value)
if m and not m.group(1).endswith('\\'): if m and not m.group(1).endswith('\\'):
prefix = m.group(1); expr = m.group(2); prefix = m.group(1)
inner_expr = m.group(3); _value = m.group(4) expr = m.group(2)
inner_expr = m.group(3)
_value = m.group(4)
try: try:
context_value = eval(inner_expr) context_value = eval(inner_expr)
@ -155,9 +149,10 @@ class Request(Message):
parsed_value += _value parsed_value += _value
_value = '' _value = ''
try: return json.loads(parsed_value) try:
except Exception as e: return parsed_value return json.loads(parsed_value)
except:
return parsed_value
def _send_response(self, response): def _send_response(self, response):
response = Response.build(response) response = Response.build(response)
@ -174,7 +169,6 @@ class Request(Message):
redis.send_message(queue_name, response) redis.send_message(queue_name, response)
redis.expire(queue_name, 60) redis.expire(queue_name, 60)
def execute(self, n_tries=1, _async=True, **context): def execute(self, n_tries=1, _async=True, **context):
""" """
Execute this request and returns a Response object Execute this request and returns a Response object
@ -190,44 +184,48 @@ class Request(Message):
- group: ${group_name} # will be expanded as "Kitchen lights") - group: ${group_name} # will be expanded as "Kitchen lights")
""" """
def _thread_func(n_tries, errors=None): def _thread_func(_n_tries, errors=None):
response = None
if self.action.startswith('procedure.'): if self.action.startswith('procedure.'):
context['n_tries'] = n_tries context['n_tries'] = _n_tries
response = self._execute_procedure(**context) response = self._execute_procedure(**context)
if response is not None: if response is not None:
self._send_response(response) self._send_response(response)
return response return response
else: else:
(module_name, method_name) = get_module_and_method_from_action(self.action) action = self.expand_value_from_context(self.action, **context)
(module_name, method_name) = get_module_and_method_from_action(action)
plugin = get_plugin(module_name) plugin = get_plugin(module_name)
try: try:
# Run the action # Run the action
args = self._expand_context(**context) args = self._expand_context(**context)
args = self.expand_value_from_context(args, **context)
response = plugin.run(method=method_name, **args) response = plugin.run(method=method_name, **args)
if response and response.is_error(): if response and response.is_error():
logger.warning(('Response processed with errors from ' + logger.warning(('Response processed with errors from ' +
'action {}: {}').format( 'action {}: {}').format(
self.action, str(response))) action, str(response)))
elif not response.disable_logging: elif not response.disable_logging:
logger.info('Processed response from action {}: {}'. logger.info('Processed response from action {}: {}'.
format(self.action, str(response))) format(action, str(response)))
except Exception as e: except Exception as e:
# Retry mechanism # Retry mechanism
plugin.logger.exception(e) plugin.logger.exception(e)
logger.warning(('Uncaught exception while processing response ' + logger.warning(('Uncaught exception while processing response ' +
'from action [{}]: {}').format(self.action, str(e))) 'from action [{}]: {}').format(action, str(e)))
errors = errors or [] errors = errors or []
if str(e) not in errors: if str(e) not in errors:
errors.append(str(e)) errors.append(str(e))
response = Response(output=None, errors=errors) response = Response(output=None, errors=errors)
if n_tries-1 > 0: if _n_tries-1 > 0:
logger.info('Reloading plugin {} and retrying'.format(module_name)) logger.info('Reloading plugin {} and retrying'.format(module_name))
get_plugin(module_name, reload=True) get_plugin(module_name, reload=True)
response = _thread_func(n_tries=n_tries-1, errors=errors) response = _thread_func(_n_tries=_n_tries - 1, errors=errors)
finally: finally:
self._send_response(response) self._send_response(response)
return response return response
@ -243,7 +241,6 @@ class Request(Message):
else: else:
return _thread_func(n_tries) return _thread_func(n_tries)
def __str__(self): def __str__(self):
""" """
Overrides the str() operator and converts Overrides the str() operator and converts
@ -251,16 +248,15 @@ class Request(Message):
""" """
return json.dumps({ return json.dumps({
'type' : 'request', 'type': 'request',
'target' : self.target, 'target': self.target,
'action' : self.action, 'action': self.action,
'args' : self.args, 'args': self.args,
'origin' : self.origin if hasattr(self, 'origin') else None, 'origin': self.origin if hasattr(self, 'origin') else None,
'id' : self.id if hasattr(self, 'id') else None, 'id': self.id if hasattr(self, 'id') else None,
'token' : self.token if hasattr(self, 'token') else None, 'token': self.token if hasattr(self, 'token') else None,
'_timestamp' : self.timestamp, '_timestamp': self.timestamp,
}) })
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -133,8 +133,12 @@ inputs
websocket-client websocket-client
# mpv player plugin # mpv player plugin
python-mpv # python-mpv
# SCSS/SASS to CSS compiler for web pages style # SCSS/SASS to CSS compiler for web pages style
pyScss pyScss
# Support for NFC tags
# nfcpy >= 1.0
# ndef

View file

@ -169,6 +169,7 @@ setup(
'Support for mopidy backend': ['websocket-client'], 'Support for mopidy backend': ['websocket-client'],
'Support for mpv player plugin': ['python-mpv'], 'Support for mpv player plugin': ['python-mpv'],
'Support for compiling SASS/SCSS styles to CSS': ['pyScss'], 'Support for compiling SASS/SCSS styles to CSS': ['pyScss'],
'Support for NFC tags': ['nfcpy>=1.0', 'ndef'],
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'], # 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git'] # 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
# 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles'] # 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']