commit
bb79136f58
45
README.rst
45
README.rst
|
@ -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.
|
83
mopidy_pummeluff/actions.py
Normal file
83
mopidy_pummeluff/actions.py
Normal 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')
|
|
@ -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')
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
22
mopidy_pummeluff/sound.py
Normal 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
246
mopidy_pummeluff/tags.py
Normal 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'
|
9
mopidy_pummeluff/threads/__init__.py
Normal file
9
mopidy_pummeluff/threads/__init__.py
Normal 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 *
|
83
mopidy_pummeluff/threads/gpio_handler.py
Normal file
83
mopidy_pummeluff/threads/gpio_handler.py
Normal 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
|
120
mopidy_pummeluff/threads/tag_reader.py
Normal file
120
mopidy_pummeluff/threads/tag_reader.py
Normal 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
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue