forked from platypush/platypush
Added NFC backend
This commit is contained in:
parent
0a97bb2345
commit
24d3810e44
6 changed files with 317 additions and 47 deletions
|
@ -207,6 +207,8 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'soundfile',
|
||||
'numpy',
|
||||
'cv2',
|
||||
'nfc',
|
||||
'ndef',
|
||||
]
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
|
187
platypush/backend/nfc.py
Normal file
187
platypush/backend/nfc.py
Normal 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:
|
80
platypush/message/event/nfc.py
Normal file
80
platypush/message/event/nfc.py
Normal 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:
|
|
@ -5,7 +5,6 @@ import logging
|
|||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from threading import Thread
|
||||
|
||||
|
@ -37,36 +36,31 @@ class Request(Message):
|
|||
|
||||
super().__init__(timestamp=timestamp)
|
||||
|
||||
self.id = id if id else self._generate_id()
|
||||
self.target = target
|
||||
self.action = action
|
||||
self.origin = origin
|
||||
self.args = args if args else {}
|
||||
self.id = id if id else self._generate_id()
|
||||
self.target = target
|
||||
self.action = action
|
||||
self.origin = origin
|
||||
self.args = args if args else {}
|
||||
self.backend = backend
|
||||
self.token = token
|
||||
self.token = token
|
||||
|
||||
@classmethod
|
||||
def build(cls, msg):
|
||||
msg = super().parse(msg)
|
||||
args = {
|
||||
'target' : msg.get('target', Config.get('device_id')),
|
||||
'action' : msg['action'],
|
||||
'args' : msg['args'] if 'args' in msg else {},
|
||||
}
|
||||
args = {'target': msg.get('target', Config.get('device_id')), 'action': msg['action'],
|
||||
'args': msg.get('args', {}), 'id': msg['id'] if 'id' in msg else cls._generate_id(),
|
||||
'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time()}
|
||||
|
||||
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 'token' in msg: args['token'] = msg['token']
|
||||
return cls(**args)
|
||||
|
||||
@staticmethod
|
||||
def _generate_id():
|
||||
id = ''
|
||||
for i in range(0,16):
|
||||
id += '%.2x' % random.randint(0, 255)
|
||||
return id
|
||||
|
||||
_id = ''
|
||||
for i in range(0, 16):
|
||||
_id += '%.2x' % random.randint(0, 255)
|
||||
return _id
|
||||
|
||||
def _execute_procedure(self, *args, **kwargs):
|
||||
from platypush.config import Config
|
||||
|
@ -81,7 +75,6 @@ class Request(Message):
|
|||
|
||||
return proc.execute(*args, **kwargs)
|
||||
|
||||
|
||||
def _expand_context(self, event_args=None, **context):
|
||||
from platypush.config import Config
|
||||
|
||||
|
@ -110,7 +103,6 @@ class Request(Message):
|
|||
|
||||
return event_args
|
||||
|
||||
|
||||
@classmethod
|
||||
def expand_value_from_context(cls, _value, **context):
|
||||
for (k, v) in context.items():
|
||||
|
@ -130,10 +122,12 @@ class Request(Message):
|
|||
parsed_value = _value
|
||||
|
||||
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('\\'):
|
||||
prefix = m.group(1); expr = m.group(2);
|
||||
inner_expr = m.group(3); _value = m.group(4)
|
||||
prefix = m.group(1)
|
||||
expr = m.group(2)
|
||||
inner_expr = m.group(3)
|
||||
_value = m.group(4)
|
||||
|
||||
try:
|
||||
context_value = eval(inner_expr)
|
||||
|
@ -155,9 +149,10 @@ class Request(Message):
|
|||
parsed_value += _value
|
||||
_value = ''
|
||||
|
||||
try: return json.loads(parsed_value)
|
||||
except Exception as e: return parsed_value
|
||||
|
||||
try:
|
||||
return json.loads(parsed_value)
|
||||
except:
|
||||
return parsed_value
|
||||
|
||||
def _send_response(self, response):
|
||||
response = Response.build(response)
|
||||
|
@ -174,7 +169,6 @@ class Request(Message):
|
|||
redis.send_message(queue_name, response)
|
||||
redis.expire(queue_name, 60)
|
||||
|
||||
|
||||
def execute(self, n_tries=1, _async=True, **context):
|
||||
"""
|
||||
Execute this request and returns a Response object
|
||||
|
@ -190,44 +184,48 @@ class Request(Message):
|
|||
- 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.'):
|
||||
context['n_tries'] = n_tries
|
||||
context['n_tries'] = _n_tries
|
||||
response = self._execute_procedure(**context)
|
||||
if response is not None:
|
||||
self._send_response(response)
|
||||
return response
|
||||
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)
|
||||
|
||||
try:
|
||||
# Run the action
|
||||
args = self._expand_context(**context)
|
||||
args = self.expand_value_from_context(args, **context)
|
||||
response = plugin.run(method=method_name, **args)
|
||||
|
||||
if response and response.is_error():
|
||||
logger.warning(('Response processed with errors from ' +
|
||||
'action {}: {}').format(
|
||||
self.action, str(response)))
|
||||
action, str(response)))
|
||||
elif not response.disable_logging:
|
||||
logger.info('Processed response from action {}: {}'.
|
||||
format(self.action, str(response)))
|
||||
format(action, str(response)))
|
||||
except Exception as e:
|
||||
# Retry mechanism
|
||||
plugin.logger.exception(e)
|
||||
logger.warning(('Uncaught exception while processing response ' +
|
||||
'from action [{}]: {}').format(self.action, str(e)))
|
||||
'from action [{}]: {}').format(action, str(e)))
|
||||
|
||||
errors = errors or []
|
||||
if str(e) not in errors:
|
||||
errors.append(str(e))
|
||||
|
||||
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))
|
||||
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:
|
||||
self._send_response(response)
|
||||
return response
|
||||
|
@ -243,7 +241,6 @@ class Request(Message):
|
|||
else:
|
||||
return _thread_func(n_tries)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Overrides the str() operator and converts
|
||||
|
@ -251,16 +248,15 @@ class Request(Message):
|
|||
"""
|
||||
|
||||
return json.dumps({
|
||||
'type' : 'request',
|
||||
'target' : self.target,
|
||||
'action' : self.action,
|
||||
'args' : self.args,
|
||||
'origin' : self.origin if hasattr(self, 'origin') else None,
|
||||
'id' : self.id if hasattr(self, 'id') else None,
|
||||
'token' : self.token if hasattr(self, 'token') else None,
|
||||
'_timestamp' : self.timestamp,
|
||||
'type': 'request',
|
||||
'target': self.target,
|
||||
'action': self.action,
|
||||
'args': self.args,
|
||||
'origin': self.origin if hasattr(self, 'origin') else None,
|
||||
'id': self.id if hasattr(self, 'id') else None,
|
||||
'token': self.token if hasattr(self, 'token') else None,
|
||||
'_timestamp': self.timestamp,
|
||||
})
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -133,8 +133,12 @@ inputs
|
|||
websocket-client
|
||||
|
||||
# mpv player plugin
|
||||
python-mpv
|
||||
# python-mpv
|
||||
|
||||
# SCSS/SASS to CSS compiler for web pages style
|
||||
pyScss
|
||||
|
||||
# Support for NFC tags
|
||||
# nfcpy >= 1.0
|
||||
# ndef
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -169,6 +169,7 @@ setup(
|
|||
'Support for mopidy backend': ['websocket-client'],
|
||||
'Support for mpv player plugin': ['python-mpv'],
|
||||
'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 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']
|
||||
|
|
Loading…
Reference in a new issue