Merge pull request #6 from confirm/buttons

Buttons
This commit is contained in:
Dominique Barton 2019-03-26 21:37:45 +01:00 committed by GitHub
commit bb79136f58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 676 additions and 499 deletions

View file

@ -1,15 +1,15 @@
Mopidy Pummeluff
================
Pummeluff is a `Mopidy <http://www.mopidy.com/>`_ extension which allows you to control Mopidy via RFID cards. It is as simple as that:
Pummeluff is a `Mopidy <http://www.mopidy.com/>`_ extension which allows you to control Mopidy via RFID tags. It is as simple as that:
- Register an action to an RFID card
- Touch that card on the RFID reader and the action will be executed
- Register an action to an RFID tag
- Touch that tag on the RFID reader and the action will be executed
Thus, the Mopidy Pummeluff extension adds the following features to Mopidy:
- A radically simple web UI which can be used to manage the RFID cards
- A daemon which continuously reads RFID cards in the background and executes the assigned actions
- A radically simple web UI which can be used to manage the RFID tags
- A daemon which continuously reads RFID tags in the background and executes the assigned actions
There are several actions included, such as replacing the tracklist with a desired URI, setting the volume to a specific level or controlling the playback state.
@ -19,11 +19,18 @@ Hardware
Requirements
------------
To get the whole thing working, you need the following hardware:
To get the whole thing working, you need at least the following hardware:
- A Raspberry Pi 3 Model B
- An ``RC522`` RFID module (available on `AliExpress <https://www.aliexpress.com/wholesale?SearchText=rc522>`_ for approx. *USD 1*)
- RFID cards, keyfobs or stickers (``ISO 14443A`` and ``Mifare`` should work)
- An ``RC522`` RFID module (`RC522 on AliExpress <https://www.aliexpress.com/wholesale?SearchText=rc522>`_ for approx. *USD 1*)
- RFID tags (``ISO 14443A`` & ``Mifare`` should work, `14443A tags on AliExpress <https://www.aliexpress.com/wholesale?SearchText=14443A+lot>`_ for approx. *0.4 USD* per tag)
- Female dupont jumper wires (`female dupont jumper cables on AliExpress <https://www.aliexpress.com/wholesale?SearchText=dupont>`_ for approx. *1 USD*)
Optionally you can also add two buttons to the RPi, which can be used for power & playback control:
- Two momentary push buttons (`momentary push buttons on AliExpress <https://www.aliexpress.com/wholesale?SearchText=momentary+push+button>`_ for approx. *USD 1-2*)
Pummeluff also supports a status LED, which lights up when Pummeluff (i.e. Mopidy) is running. You can go with a separate LED, just make sure it can handle 3.3V or add a resistor. There are also push buttons with integrated LED's available, for example `these 5V momentary push buttons on AliExpress <https://www.aliexpress.com/item/16mm-Metal-brass-Push-Button-Switch-flat-round-illumination-ring-Latching-1NO-1NC-Car-press-button/32676526568.html>`_.
.. note::
@ -49,10 +56,20 @@ Please have a look at the `Raspberry Pi SPI pinout <https://pinout.xyz/pinout/sp
This connections are only valid for the RPi model ``3B`` and ``3B+``. If you want to use another RPI model, make sure you're using the correct pins.
.. important::
Connecting the buttons (optional)
---------------------------------
Some manuals in the internet mention that the ``IRQ`` pin shouldn't be connected.
However, Mopidy Pummeluff really uses the ``IRQ`` pin for the interrupt, so that less CPU cycles are used for the card reading daemon. If you don't connect the ``IRQ`` pin, Mopidy Pummeluff won't work!
You can connect two buttons to the RPi:
- ``RPi pin 5`` - Power button: Shutdown the Raspberry Pi into halt state & wake it up again from halt state
- ``RPi pin 7`` - Playback button: Pause and resume the playback
The buttons must shortcut their corresponding pins against ``GND`` (e.g. pin ``6``) when pressed. This means you want to connect one pin of the button (i.e. ``C``) to RPI's ``GND``, and the other one (i.e. ``NO``) to RPi's pin ``5`` or ``7``.
Connecting the status LED (optional)
------------------------------------
If you want to have a status LED which is turned on when the RPi is running, you can connect an LED to a ``GND`` pin (e.g. pin ``6``) & to pin ``8``.
Installation
============
@ -60,7 +77,7 @@ Installation
Prepare Raspberry Pi
--------------------
Before you can install and use Mopidy Pummeluff, you need to configure your Raspberry Pi.
Before you can install and use Mopidy Pummeluff, you need to configure your Raspberry Pi properly.
We want to enable the ``SPI`` interface and give the ``mopidy`` user access to it. This is required for the communication to the RFID module. Enter this command:
@ -76,7 +93,7 @@ After that, add your ``mopidy`` user to the ``spi`` and ``gpio`` group:
sudo usermod -a -G spi,gpio mopidy
If you're planning to use a card to shutdown the system, you also need to create a sudo rule, so that the ``mopidy`` user can shutdown the system without a password prompt:
If you're planning to use a button or RFID tag to shutdown the system, you also need to create a sudo rule, so that the ``mopidy`` user can shutdown the system without a password prompt:
.. code-block:: bash
@ -127,4 +144,4 @@ Usage
=====
Open the Mopidy Web UI (i.e. ``http://{MOPIDY_IP}:6680/``).
You should see a ``pummeluff`` web client which can be used to regsiter new RFID cards.
You should see a ``pummeluff`` web client which can be used to regsiter new RFID tags.

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
'''
Python module for Mopidy Pummeluff actions.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'replace_tracklist',
'set_volume',
'play_pause',
'stop',
'shutdown',
)
from logging import getLogger
from os import system
LOGGER = getLogger(__name__)
def replace_tracklist(core, uri):
'''
Replace tracklist and play.
:param mopidy.core.Core core: The mopidy core instance
:param str uri: An URI for the tracklist replacement
'''
LOGGER.info('Replacing tracklist with URI "%s"', uri)
core.tracklist.clear()
core.tracklist.add(uri=uri)
core.playback.play()
def set_volume(core, volume):
'''
Set volume of the mixer.
:param mopidy.core.Core core: The mopidy core instance
:param volume: The new (percentage) volume
:type volume: int|str
'''
LOGGER.info('Setting volume to %s', volume)
try:
core.mixer.set_volume(int(volume))
except ValueError as ex:
LOGGER.error(str(ex))
def play_pause(core):
'''
Pause or resume the playback.
:param mopidy.core.Core core: The mopidy core instance
'''
playback = core.playback
if playback.get_state().get() == 'playing':
LOGGER.info('Pausing the playback')
playback.pause()
else:
LOGGER.info('Resuming the playback')
playback.resume()
def stop(core):
'''
Stop playback.
:param mopidy.core.Core core: The mopidy core instance
'''
LOGGER.info('Stopping playback')
core.playback.stop()
def shutdown(core): # pylint: disable=unused-argument
'''
Shutdown.
:param mopidy.core.Core core: The mopidy core instance
'''
LOGGER.info('Shutting down')
system('sudo /sbin/shutdown -h now')

View file

@ -1,281 +0,0 @@
# -*- coding: utf-8 -*-
'''
Python module for Mopidy Pummeluff cards.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'Card',
'TracklistCard',
'VolumeCard',
'PauseCard',
'StopCard',
'ShutdownCard',
)
from os import system
from logging import getLogger
from .registry import REGISTRY
LOGGER = getLogger(__name__)
class InvalidCardType(Exception):
'''
Exception which is thrown when an invalid card type is defined.
'''
pass
class Card(object):
'''
Base RFID card class, which will implement the factory pattern in Python's
own :py:meth:`__new__` method.
'''
def __new__(cls, uid):
'''
Implement factory pattern and return correct card instance.
'''
card = REGISTRY.get(uid, {})
new_cls = cls.get_class(card.get('type', ''))
if cls is Card and cls is not new_cls:
instance = new_cls(uid=uid)
else:
instance = super(Card, cls).__new__(cls, uid=uid)
instance.registered = bool(card)
instance.alias = card.get('alias')
instance.parameter = card.get('parameter')
return instance
def __init__(self, uid):
self.uid = uid
def __str__(self):
cls_name = self.__class__.__name__
identifier = self.alias or self.uid
return '<{}: {}>'.format(cls_name, identifier)
@staticmethod
def get_class(card_type):
'''
Return class for specific card type.
:param str card_type: The card type
:return: The card class
:rtype: type
'''
try:
name = card_type.title() + 'Card'
cls = globals()[name]
assert issubclass(cls, Card)
except (KeyError, AssertionError):
raise InvalidCardType('Card class for type "{}" does\'t exist.'.format(card_type))
return cls
@classmethod
def get_type(cls, card_class=None):
'''
Return the type for a specific card class.
:param type card_class: The card class
:return: The card type
:rtype: str
'''
return (card_class or cls).__name__[0:-4].lower()
@classmethod
def all(cls):
'''
Return all registered cards in a list.
:return: Registered cards
:rtype: list[Card]
'''
return {uid: Card(uid=uid) for uid in REGISTRY}
@classmethod
def register(cls, uid, alias=None, parameter=None, card_type=None):
'''
Register card in the registry.
:param str uid: The card's UID
:param str alias: The card's alias
:param str parameter: The optional parameter
:param str card_type: The card type
'''
if card_type is None:
card_type = cls.get_type(cls)
uid = uid.strip()
if not uid:
error = 'Invalid UID defined'
LOGGER.error(error)
raise ValueError(error)
LOGGER.info('Registering %s card %s with parameter "%s"', card_type, uid, parameter)
real_cls = cls.get_class(card_type)
if real_cls == Card:
error = 'Registering cards without explicit types are not allowed. ' \
'Set card_type argument on Card.register() ' \
'or use register() method of explicit card classes.'
raise InvalidCardType(error)
if hasattr(real_cls, 'validate_parameter'):
real_cls.validate_parameter(parameter)
REGISTRY[uid] = {
'type': card_type,
'alias': alias.strip(),
'parameter': parameter.strip()
}
return Card.all().get(uid)
@property
def dict(self):
'''
Return the dict version of this card.
:return: The dict version of this card
:rtype: dict
'''
card_dict = {
'uid': self.uid,
'alias': self.alias,
'type': self.get_type(),
'parameter': self.parameter,
}
if hasattr(self, 'scanned'):
card_dict['scanned'] = self.scanned
return card_dict
def action(self, mopidy_core): # pylint: disable=unused-argument
'''
Action method which is executed when the card is detected on the RFID
reader.
:param mopidy.core.Core mopidy_core: The mopidy core instance
:raises NotImplementedError: Always raised when method not implemented
'''
cls = self.__class__.__name__
error = 'Missing action() method in the %s class'
LOGGER.error(error, cls)
raise NotImplementedError(error % cls)
class TracklistCard(Card):
'''
Replaces the current tracklist with the URI retreived from the card's
parameter.
'''
def action(self, mopidy_core):
'''
Replace tracklist and play.
:param mopidy.core.Core mopidy_core: The mopidy core instance
'''
LOGGER.info('Replacing tracklist with URI "%s"', self.parameter)
mopidy_core.tracklist.clear()
mopidy_core.tracklist.add(uri=self.parameter)
mopidy_core.playback.play()
class VolumeCard(Card):
'''
Sets the volume to the percentage value retreived from the card's parameter.
'''
@staticmethod
def validate_parameter(parameter):
'''
Validates if the parameter is an integer between 0 and 100.
:param mixed parameter: The parameter
:raises ValueError: When parameter is invalid
'''
try:
number = int(parameter)
assert number >= 0 and number <= 100
except (ValueError, AssertionError):
raise ValueError('Volume parameter has to be a number between 0 and 100')
def action(self, mopidy_core):
'''
Set volume.
:param mopidy.core.Core mopidy_core: The mopidy core instance
'''
LOGGER.info('Setting volume to %s', self.parameter)
try:
mopidy_core.mixer.set_volume(int(self.parameter))
except ValueError as ex:
LOGGER.error(str(ex))
class PauseCard(Card):
'''
Pauses or resumes the playback, based on the current state.
'''
def action(self, mopidy_core): # pylint: disable=no-self-use
'''
Pause or resume the playback.
:param mopidy.core.Core mopidy_core: The mopidy core instance
'''
playback = mopidy_core.playback
if playback.get_state().get() == 'playing':
LOGGER.info('Pausing the playback')
playback.pause()
else:
LOGGER.info('Resuming the playback')
playback.resume()
class StopCard(Card):
'''
Stops the playback.
'''
def action(self, mopidy_core): # pylint: disable=no-self-use
'''
Stop playback.
:param mopidy.core.Core mopidy_core: The mopidy core instance
'''
LOGGER.info('Stopping playback')
mopidy_core.playback.stop()
class ShutdownCard(Card):
'''
Shutting down the system.
'''
def action(self, mopidy_core): # pylint: disable=no-self-use,unused-argument
'''
Shutdown.
:param mopidy.core.Core mopidy_core: The mopidy core instance
'''
LOGGER.info('Shutting down')
system('sudo /sbin/shutdown -h now')

View file

@ -5,119 +5,45 @@ Python module for Mopidy Pummeluff frontend.
from __future__ import absolute_import, unicode_literals, print_function
from os import path, system
from threading import Thread, Event
from time import time
__all__ = (
'PummeluffFrontend',
)
from threading import Event
from logging import getLogger
import pykka
from mopidy import core as mopidy_core
from .rfid_reader import RFIDReader, ReadError
from .cards import Card
from .threads import GPIOHandler, TagReader
LOGGER = getLogger(__name__)
class CardReader(Thread):
'''
Thread which reads RFID cards from the RFID reader.
Because the RFID reader algorithm is reacting to an IRQ (interrupt), it is
blocking as long as no card is touched, even when Mopidy is exiting. Thus,
we're running the thread as daemon thread, which means it's exiting at the
same moment as the main thread (aka Mopidy core) is exiting.
'''
daemon = True
latest = None
@staticmethod
def play_sound(sound):
'''
Play sound via aplay.
:param str sound: The name of the sound file
'''
file_path = path.join(path.dirname(__file__), 'sounds', sound)
system('aplay -q {}'.format(file_path))
def __init__(self, core, stop_event):
'''
Class constructor.
:param threading.Event stop_event: The stop event
'''
super(CardReader, self).__init__()
self.core = core
self.stop_event = stop_event
def run(self):
'''
Run RFID reading loop.
'''
reader = RFIDReader()
prev_time = time()
prev_uid = ''
while not self.stop_event.is_set():
reader.wait_for_tag()
try:
now = time()
uid = reader.uid
if now - prev_time > 1 or uid != prev_uid:
LOGGER.info('Card %s read', uid)
self.handle_uid(uid)
prev_time = now
prev_uid = uid
except ReadError:
pass
reader.cleanup()
def handle_uid(self, uid):
'''
Handle the scanned card / retreived UID.
:param str uid: The UID
'''
card = Card(uid)
if card.registered:
LOGGER.info('Triggering action of registered card')
self.play_sound('success.wav')
card.action(mopidy_core=self.core)
else:
LOGGER.info('Card is not registered, thus doing nothing')
self.play_sound('fail.wav')
card.scanned = time()
CardReader.latest = card
class PummeluffFrontend(pykka.ThreadingActor, mopidy_core.CoreListener):
'''
Pummeluff frontend which basically reads cards from the RFID reader.
Pummeluff frontend which basically reacts to GPIO button pushes and touches
of RFID tags.
'''
def __init__(self, config, core): # pylint: disable=unused-argument
super(PummeluffFrontend, self).__init__()
self.core = core
self.stop_event = Event()
self.card_reader = CardReader(core=core, stop_event=self.stop_event)
self.core = core
self.stop_event = Event()
self.gpio_handler = GPIOHandler(core=core, stop_event=self.stop_event)
self.tag_reader = TagReader(core=core, stop_event=self.stop_event)
def on_start(self):
'''
Start card reader thread after the actor is started.
Start GPIO handler & tag reader threads.
'''
self.card_reader.start()
self.gpio_handler.start()
self.tag_reader.start()
def on_stop(self):
'''
Stop card reader thread before the actor stops.
Set threading stop event to tell GPIO handler & tag reader threads to
stop their operations.
'''
self.stop_event.set()

View file

@ -20,11 +20,11 @@ LOGGER = getLogger(__name__)
class RegistryDict(dict):
'''
Simple card registry based on Python's internal :py:class:`dict` class,
Simple tag registry based on Python's internal :py:class:`dict` class,
which reads and writes the registry from/to disk.
'''
registry_path = '/var/lib/mopidy/pummeluff/cards.json'
registry_path = '/var/lib/mopidy/pummeluff/tags.json'
def __init__(self):
super(RegistryDict, self).__init__(self)

View file

@ -1,48 +0,0 @@
# -*- coding: utf8 -*-
'''
Python card reader module.
'''
from __future__ import absolute_import, unicode_literals, print_function
from RPi.GPIO import cleanup # pylint: disable=no-name-in-module
from pirc522 import RFID
class ReadError(Exception):
'''
Exception which is thrown when the UID could not be read from the card.
'''
class RFIDReader(RFID):
'''
Card reader for the RC522 RFID card reader board, based on the excellent
:py:class:`pirc522.RFID` class.
'''
@staticmethod
def cleanup():
'''
Cleanup GPIO ports.
'''
cleanup()
@property
def uid(self):
'''
Return the UID from the card.
:return: The hex UID
:rtype: string
'''
error, data = self.request() # pylint: disable=unused-variable
if error:
raise ReadError('Could not read tag')
error, uid_chunks = self.anticoll()
if error:
raise ReadError('Could not read UID')
uid = '{0[0]:02X}{0[1]:02X}{0[2]:02X}{0[3]:02X}'.format(uid_chunks) # pylint: disable=invalid-format-index
return uid

22
mopidy_pummeluff/sound.py Normal file
View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
'''
Python module to play sounds.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'play_sound',
)
from os import path, system
def play_sound(sound):
'''
Play sound via aplay.
:param str sound: The name of the sound file
'''
file_path = path.join(path.dirname(__file__), 'sounds', sound)
system('aplay -q {}'.format(file_path))

246
mopidy_pummeluff/tags.py Normal file
View file

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
'''
Python module for Mopidy Pummeluff tags.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'Tag',
'TracklistTag',
'VolumeTag',
'PlayPauseTag',
'StopTag',
'ShutdownTag',
)
from logging import getLogger
from .registry import REGISTRY
from . import actions
LOGGER = getLogger(__name__)
class InvalidTagType(Exception):
'''
Exception which is thrown when an invalid tag type is defined.
'''
pass
class Tag(object):
'''
Base RFID tag class, which will implement the factory pattern in Python's
own :py:meth:`__new__` method.
'''
def __new__(cls, uid):
'''
Implement factory pattern and return correct tag instance.
'''
tag = REGISTRY.get(uid, {})
new_cls = cls.get_class(tag.get('type', ''))
if cls is Tag and cls is not new_cls:
instance = new_cls(uid=uid)
else:
instance = super(Tag, cls).__new__(cls, uid=uid)
instance.registered = bool(tag)
instance.alias = tag.get('alias')
instance.parameter = tag.get('parameter')
return instance
def __init__(self, uid):
self.uid = uid
self.scanned = None
def __str__(self):
cls_name = self.__class__.__name__
identifier = self.alias or self.uid
return '<{}: {}>'.format(cls_name, identifier)
def __call__(self, core):
'''
Action method which is called when the tag is detected on the RFID
reader.
:param mopidy.core.Core core: The mopidy core instance
'''
args = [core]
if self.parameter:
args.append(self.parameter)
getattr(actions, self.action)(*args)
@staticmethod
def get_class(tag_type):
'''
Return class for specific tag type.
:param str tag_type: The tag type
:return: The tag class
:rtype: type
'''
try:
name = tag_type.title() + 'Tag'
cls = globals()[name]
assert issubclass(cls, Tag)
except (KeyError, AssertionError):
raise InvalidTagType('Tag class for type "{}" does\'t exist.'.format(tag_type))
return cls
@classmethod
def get_type(cls, tag_class=None):
'''
Return the type for a specific tag class.
:param type tag_class: The tag class
:return: The tag type
:rtype: str
'''
return (tag_class or cls).__name__[0:-3].lower()
@classmethod
def all(cls):
'''
Return all registered tags in a list.
:return: Registered tags
:rtype: list[Tag]
'''
return {uid: Tag(uid=uid) for uid in REGISTRY}
@classmethod
def register(cls, uid, alias=None, parameter=None, tag_type=None):
'''
Register tag in the registry.
:param str uid: The tag's UID
:param str alias: The tag's alias
:param str parameter: The optional parameter
:param str tag_type: The tag type
:return: The registered tag
:rtype: Tag
'''
if tag_type is None:
tag_type = cls.get_type(cls)
uid = uid.strip()
if not uid:
error = 'Invalid UID defined'
LOGGER.error(error)
raise ValueError(error)
LOGGER.info('Registering %s tag %s with parameter "%s"', tag_type, uid, parameter)
real_cls = cls.get_class(tag_type)
if real_cls == Tag:
error = 'Registering tags without explicit types are not allowed. ' \
'Set tag_type argument on Tag.register() ' \
'or use register() method of explicit tag classes.'
raise InvalidTagType(error)
if hasattr(real_cls, 'validate_parameter'):
real_cls.validate_parameter(parameter)
REGISTRY[uid] = {
'type': tag_type,
'alias': alias.strip(),
'parameter': parameter.strip()
}
return Tag.all().get(uid)
@property
def dict(self):
'''
Return the dict version of this tag.
:return: The dict version of this tag
:rtype: dict
'''
tag_dict = {
'uid': self.uid,
'alias': self.alias,
'type': self.get_type(),
'parameter': self.parameter,
}
tag_dict['scanned'] = self.scanned
return tag_dict
@property
def action(self):
'''
Return a name of an action (function) defined in the
:py:mod:`mopidy_pummeluff.actions` Python module.
:return: An action name
:rtype: str
:raises NotImplementedError: When action property isn't defined
'''
cls = self.__class__.__name__
error = 'Missing action property in the %s class'
LOGGER.error(error, cls)
raise NotImplementedError(error % cls)
class TracklistTag(Tag):
'''
Replaces the current tracklist with the URI retreived from the tag's
parameter.
'''
action = 'replace_tracklist'
class VolumeTag(Tag):
'''
Sets the volume to the percentage value retreived from the tag's parameter.
'''
action = 'set_volume'
@staticmethod
def validate_parameter(parameter):
'''
Validates if the parameter is an integer between 0 and 100.
:param mixed parameter: The parameter
:raises ValueError: When parameter is invalid
'''
try:
number = int(parameter)
assert number >= 0 and number <= 100
except (ValueError, AssertionError):
raise ValueError('Volume parameter has to be a number between 0 and 100')
class PlayPauseTag(Tag):
'''
Pauses or resumes the playback, based on the current state.
'''
action = 'play_pause'
class StopTag(Tag):
'''
Stops the playback.
'''
action = 'stop'
class ShutdownTag(Tag):
'''
Shutting down the system.
'''
action = 'shutdown'

View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
'''
Threads of Mopidy Pummeluff.
'''
from __future__ import absolute_import, unicode_literals, print_function
from .gpio_handler import *
from .tag_reader import *

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
'''
Python module for the dedicated Mopidy Pummeluff threads.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'GPIOHandler',
)
from threading import Thread
from logging import getLogger
from time import time
import RPi.GPIO as GPIO
from mopidy_pummeluff.actions import shutdown, play_pause
from mopidy_pummeluff.sound import play_sound
LOGGER = getLogger(__name__)
class GPIOHandler(Thread):
'''
Thread which handles the GPIO ports, which basically means activating the
LED when it's started and then reacting to button presses.
'''
button_pins = {
5: shutdown,
7: play_pause,
}
led_pin = 8
def __init__(self, core, stop_event):
'''
Class constructor.
:param mopidy.core.Core core: The mopidy core instance
:param threading.Event stop_event: The stop event
'''
super(GPIOHandler, self).__init__()
self.core = core
self.stop_event = stop_event
now = time()
self.timestamps = {x: now for x in self.button_pins}
# pylint: disable=no-member
def run(self):
'''
Run the thread.
'''
GPIO.setmode(GPIO.BOARD)
for pin in self.button_pins:
LOGGER.debug('Setup pin %s as button pin', pin)
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(pin, GPIO.RISING, callback=lambda pin: self.button_push(pin)) # pylint: disable=unnecessary-lambda
LOGGER.debug('Setup pin %s as LED pin', self.led_pin)
GPIO.setup(self.led_pin, GPIO.OUT)
GPIO.output(self.led_pin, GPIO.HIGH)
self.stop_event.wait()
GPIO.cleanup() # pylint: disable=no-member
def button_push(self, pin):
'''
Callback method when a button is pushed.
:param int pin: Pin number
'''
now = time()
before = self.timestamps[pin]
if (GPIO.input(pin) == GPIO.LOW) and (now - before > 0.25):
LOGGER.debug('Button at pin %s was pushed', pin)
play_sound('success.wav')
self.button_pins[pin](self.core)
self.timestamps[pin] = now

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
'''
Python module for the dedicated Mopidy Pummeluff threads.
'''
from __future__ import absolute_import, unicode_literals, print_function
__all__ = (
'TagReader',
)
from threading import Thread
from time import time
from logging import getLogger
import RPi.GPIO as GPIO
from pirc522 import RFID
from mopidy_pummeluff.tags import Tag
from mopidy_pummeluff.sound import play_sound
LOGGER = getLogger(__name__)
class ReadError(Exception):
'''
Exception which is thrown when an RFID read error occurs.
'''
class TagReader(Thread):
'''
Thread which reads RFID tags from the RFID reader.
Because the RFID reader algorithm is reacting to an IRQ (interrupt), it is
blocking as long as no tag is touched, even when Mopidy is exiting. Thus,
we're running the thread as daemon thread, which means it's exiting at the
same moment as the main thread (aka Mopidy core) is exiting.
'''
daemon = True
latest = None
def __init__(self, core, stop_event):
'''
Class constructor.
:param mopidy.core.Core core: The mopidy core instance
:param threading.Event stop_event: The stop event
'''
super(TagReader, self).__init__()
self.core = core
self.stop_event = stop_event
self.rfid = RFID()
def run(self):
'''
Run RFID reading loop.
'''
rfid = self.rfid
prev_time = time()
prev_uid = ''
while not self.stop_event.is_set():
rfid.wait_for_tag()
try:
now = time()
uid = self.read_uid()
if now - prev_time > 1 or uid != prev_uid:
LOGGER.info('Tag %s read', uid)
self.handle_uid(uid)
prev_time = now
prev_uid = uid
except ReadError:
pass
GPIO.cleanup() # pylint: disable=no-member
def read_uid(self):
'''
Return the UID from the tag.
:return: The hex UID
:rtype: string
'''
rfid = self.rfid
error, data = rfid.request() # pylint: disable=unused-variable
if error:
raise ReadError('Could not read tag')
error, uid_chunks = rfid.anticoll()
if error:
raise ReadError('Could not read UID')
uid = '{0[0]:02X}{0[1]:02X}{0[2]:02X}{0[3]:02X}'.format(uid_chunks) # pylint: disable=invalid-format-index
return uid
def handle_uid(self, uid):
'''
Handle the scanned tag / retreived UID.
:param str uid: The UID
'''
tag = Tag(uid)
if tag.registered:
LOGGER.info('Triggering action of registered tag')
play_sound('success.wav')
tag(self.core)
else:
LOGGER.info('Tag is not registered, thus doing nothing')
play_sound('fail.wav')
tag.scanned = time()
TagReader.latest = tag

View file

@ -16,15 +16,15 @@ from logging import getLogger
from tornado.web import RequestHandler
from . import cards
from .frontend import CardReader
from . import tags
from .threads import TagReader
LOGGER = getLogger(__name__)
class LatestHandler(RequestHandler): # pylint: disable=abstract-method
'''
Request handler which returns the latest scanned card.
Request handler which returns the latest scanned tag.
'''
def initialize(self, core): # pylint: disable=arguments-differ
@ -39,23 +39,23 @@ class LatestHandler(RequestHandler): # pylint: disable=abstract-method
'''
Handle GET request.
'''
card = CardReader.latest
tag = TagReader.latest
LOGGER.debug('Returning latest card %s', card)
LOGGER.debug('Returning latest tag %s', tag)
if card is None:
if tag is None:
data = {
'success': False,
'message': 'No card scanned yet'
'message': 'No tag scanned yet'
}
else:
data = {
'success': True,
'message': 'Scanned card found',
'message': 'Scanned tag found',
}
data.update(card.dict)
data.update(tag.dict)
self.set_header('Content-type', 'application/json')
self.write(dumps(data))
@ -63,7 +63,7 @@ class LatestHandler(RequestHandler): # pylint: disable=abstract-method
class RegistryHandler(RequestHandler): # pylint: disable=abstract-method
'''
Request handler which returns all registered cards.
Request handler which returns all registered tags.
'''
def initialize(self, core): # pylint: disable=arguments-differ
@ -78,15 +78,15 @@ class RegistryHandler(RequestHandler): # pylint: disable=abstract-method
'''
Handle GET request.
'''
cards_list = []
tags_list = []
for card in cards.Card.all().values():
cards_list.append(card.dict)
for tag in tags.Tag.all().values():
tags_list.append(tag.dict)
data = {
'success': True,
'message': 'Registry successfully read',
'cards': cards_list
'tags': tags_list
}
self.set_header('Content-type', 'application/json')
@ -95,7 +95,7 @@ class RegistryHandler(RequestHandler): # pylint: disable=abstract-method
class RegisterHandler(RequestHandler): # pylint: disable=abstract-method
'''
Request handler which registers an RFID card in the registry.
Request handler which registers an RFID tag in the registry.
'''
def initialize(self, core): # pylint: disable=arguments-differ
@ -111,19 +111,19 @@ class RegisterHandler(RequestHandler): # pylint: disable=abstract-method
Handle POST request.
'''
try:
card = cards.Card.register(
tag = tags.Tag.register(
uid=self.get_argument('uid'),
alias=self.get_argument('alias', None),
parameter=self.get_argument('parameter'),
card_type=self.get_argument('type')
tag_type=self.get_argument('type')
)
data = {
'success': True,
'message': 'Card successfully registered',
'message': 'Tag successfully registered',
}
data.update(card.dict)
data.update(tag.dict)
except ValueError as ex:
self.set_status(400)
@ -144,7 +144,7 @@ class RegisterHandler(RequestHandler): # pylint: disable=abstract-method
class TypesHandler(RequestHandler): # pylint: disable=abstract-method
'''
Request handler which returns all card types.
Request handler which returns all tag types.
'''
def initialize(self, core): # pylint: disable=arguments-differ
@ -161,12 +161,12 @@ class TypesHandler(RequestHandler): # pylint: disable=abstract-method
'''
types = {}
for cls_name in cards.__all__:
card_cls = getattr(cards, cls_name)
if card_cls is not cards.Card:
card_type = cards.Card.get_type(card_cls)
card_doc = card_cls.__doc__.strip().split('.')[0]
types[card_type] = card_doc
for cls_name in tags.__all__:
tag_cls = getattr(tags, cls_name)
if tag_cls is not tags.Tag:
tag_type = tags.Tag.get_type(tag_cls)
tag_doc = tag_cls.__doc__.strip().split('.')[0]
types[tag_type] = tag_doc
data = {
'success': True,

View file

@ -5,20 +5,20 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="style.css" rel="stylesheet" type="text/css">
<title>Pummeluff Card Management</title>
<title>Pummeluff Tag Management</title>
</head>
<body>
<header>
<h1>Pummeluff Card Management</h1>
<h1>Pummeluff Tag Management</h1>
</header>
<main>
<div id="register">
<h2>Register New Card</h2>
<h2>Register New Tag</h2>
<div>
<form id="register-form">
<label for="uid">UID</label>
<input id="uid" name="uid" type="text" placeholder="The unique card ID">
<a id="read-rfid-card" href="#">Read UID from RFID card</a>
<input id="uid" name="uid" type="text" placeholder="The unique tag ID">
<a id="read-rfid-tag" href="#">Read UID from RFID tag</a>
<label for="alias">Alias</label>
<input id="alias" name="alias" type="text" placeholder="Your personal alias / identifier">
<label for="type">Type</label>
@ -26,13 +26,13 @@
</select>
<label for="parameter">Parameter</label>
<input id="parameter" name="parameter" type="text" placeholder="A type-specific parameter">
<button id="register-button" role="submit">✓ Register Card</button>
<button id="register-button" role="submit">✓ Register Tag</button>
</form>
</div>
</div>
<div id="registry">
<h2>Registered Cards</h2>
<div id="cards"></div>
<h2>Registered Tags</h2>
<div id="tags"></div>
</div>
</main>
<script src="script.js"></script>

View file

@ -31,26 +31,26 @@ class API {
{
let callback = function(response)
{
let cardsContainer = document.getElementById('cards')
while(cardsContainer.firstChild)
cardsContainer.removeChild(cardsContainer.firstChild)
let tagsContainer = document.getElementById('tags')
while(tagsContainer.firstChild)
tagsContainer.removeChild(tagsContainer.firstChild)
for(let card of response.cards)
for(let tag of response.tags)
{
let cardElement = document.createElement('div')
cardElement.setAttribute('class', 'card')
let tagElement = document.createElement('div')
tagElement.setAttribute('class', 'tag')
let args = new Array('alias', 'uid', 'type', 'parameter')
for(let arg of args)
{
let spanElement = document.createElement('span')
let value = card[arg] ? card[arg] : '-'
let value = tag[arg] ? tag[arg] : '-'
spanElement.setAttribute('class', arg)
spanElement.innerHTML = value
cardElement.appendChild(spanElement)
tagElement.appendChild(spanElement)
}
cardsContainer.appendChild(cardElement)
tagsContainer.appendChild(tagElement)
}
}
@ -58,7 +58,7 @@ class API {
}
/*
* Refresh the card types.
* Refresh the tag types.
*/
refreshTypes()
@ -82,7 +82,7 @@ class API {
}
/*
* Register a new card.
* Register a new tag.
*/
register()
@ -110,12 +110,12 @@ class API {
}
/*
* Get latest scanned card.
* Get latest scanned tag.
*/
getLatestCard()
getLatestTag()
{
let latest_card = undefined
let latest_tag = undefined
let uid_field = document.getElementById('uid')
let alias_field = document.getElementById('alias')
@ -127,14 +127,14 @@ class API {
parameter_field.value = ''
type_select.selectIndex = 0
let link = document.getElementById('read-rfid-card')
let link = document.getElementById('read-rfid-tag')
link.classList.add('reading')
let do_request = function()
{
let callback = function(response)
{
if(latest_card && response.success && JSON.stringify(response) != JSON.stringify(latest_card))
if(latest_tag && response.success && JSON.stringify(response) != JSON.stringify(latest_tag))
{
uid_field.value = response.uid
@ -154,7 +154,7 @@ class API {
setTimeout(() => do_request(), 1000)
}
latest_card = response
latest_tag = response
}
api.request('/pummeluff/latest/', false, callback)
@ -176,4 +176,4 @@ document.getElementById('register-form').onsubmit = function()
return false;
}
document.getElementById('read-rfid-card').onclick = () => api.getLatestCard()
document.getElementById('read-rfid-tag').onclick = () => api.getLatestTag()

View file

@ -114,24 +114,24 @@ button
margin-top : 10px;
}
#read-rfid-card
#read-rfid-tag
{
text-decoration: none;
color: #fa0;
font-size: 11px;
}
#read-rfid-card.reading {
#read-rfid-tag.reading {
animation: blink 0.5s cubic-bezier(.5, 0, 1, 1) infinite alternate;
}
@keyframes blink { to { opacity: 0.25; } }
/*
* Registered Cards
* Registered Tags
*/
div.card
div.tag
{
display : inline-block;
background-color: #eee;
@ -143,14 +143,14 @@ div.card
line-height : 20px;
}
div.card span.uid,
div.card span.type,
div.card span.parameter
div.tag span.uid,
div.tag span.type,
div.tag span.parameter
{
font-family: Courier New, monospace;
}
div.card span.type
div.tag span.type
{
display : inline-block;
background-color: #888;
@ -162,8 +162,8 @@ div.card span.type
border-radius : 10px;
}
div.card span.alias,
div.card span.parameter
div.tag span.alias,
div.tag span.parameter
{
display: block;
}