tobru mods
This commit is contained in:
commit
73c6297ac6
|
@ -0,0 +1,3 @@
|
|||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
|
@ -0,0 +1,79 @@
|
|||
'''
|
||||
Mopidy Pummeluff Python module.
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
import pkg_resources
|
||||
import mopidy
|
||||
|
||||
from .frontend import PummeluffFrontend
|
||||
from .web import LatestHandler, RegistryHandler, RegisterHandler, UnregisterHandler, \
|
||||
ActionClassesHandler
|
||||
|
||||
|
||||
__version__ = pkg_resources.get_distribution('Mopidy-Pummeluff').version
|
||||
|
||||
def app_factory(config, core): # pylint: disable=unused-argument
|
||||
'''
|
||||
App factory for the web apps.
|
||||
|
||||
:param mopidy.config config: The mopidy config
|
||||
:param mopidy.core.Core: The mopidy core
|
||||
|
||||
:return: The registered app request handlers
|
||||
:rtype: list
|
||||
'''
|
||||
return [
|
||||
('/latest/', LatestHandler),
|
||||
('/registry/', RegistryHandler),
|
||||
('/register/', RegisterHandler),
|
||||
('/unregister/', UnregisterHandler),
|
||||
('/action-classes/', ActionClassesHandler),
|
||||
]
|
||||
|
||||
|
||||
class Extension(mopidy.ext.Extension):
|
||||
'''
|
||||
Mopidy Pummeluff extension.
|
||||
'''
|
||||
|
||||
dist_name = 'Mopidy-Pummeluff'
|
||||
ext_name = 'pummeluff'
|
||||
version = __version__
|
||||
|
||||
def get_default_config(self): # pylint: disable=no-self-use
|
||||
'''
|
||||
Return the default config.
|
||||
|
||||
:return: The default config
|
||||
'''
|
||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
||||
return mopidy.config.read(conf_file)
|
||||
|
||||
def get_config_schema(self):
|
||||
'''
|
||||
Return the config schema.
|
||||
|
||||
:return: The config schema
|
||||
'''
|
||||
schema = super(Extension, self).get_config_schema()
|
||||
return schema
|
||||
|
||||
def setup(self, registry):
|
||||
'''
|
||||
Setup the extension.
|
||||
|
||||
:param mopidy.ext.Registry: The mopidy registry
|
||||
'''
|
||||
registry.add('frontend', PummeluffFrontend)
|
||||
|
||||
registry.add('http:static', {
|
||||
'name': self.ext_name,
|
||||
'path': os.path.join(os.path.dirname(__file__), 'webui'),
|
||||
})
|
||||
|
||||
registry.add('http:app', {
|
||||
'name': self.ext_name,
|
||||
'factory': app_factory,
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff tags.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'PlayPause',
|
||||
'Stop',
|
||||
'PreviousTrack',
|
||||
'NextTrack',
|
||||
'Shutdown',
|
||||
'Tracklist',
|
||||
'Volume',
|
||||
)
|
||||
|
||||
from .playback import PlayPause, Stop, PreviousTrack, NextTrack
|
||||
from .shutdown import Shutdown
|
||||
from .tracklist import Tracklist
|
||||
from .volume import Volume
|
||||
|
||||
ACTIONS = {}
|
||||
for action in __all__:
|
||||
ACTIONS[action] = globals()[action].__doc__.strip()
|
|
@ -0,0 +1,108 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff base action.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'Action',
|
||||
)
|
||||
|
||||
from logging import getLogger
|
||||
from inspect import getfullargspec
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Action:
|
||||
'''
|
||||
Base RFID tag class, which will implement the factory pattern in Python's
|
||||
own :py:meth:`__new__` method.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core):
|
||||
'''
|
||||
Execute the action.
|
||||
|
||||
:param mopidy.core.Core core: The mopidy core instance
|
||||
|
||||
:raises NotImplementedError: When class method is not implemented
|
||||
'''
|
||||
name = cls.__name__
|
||||
error = 'Missing execute class method in the %s class'
|
||||
LOGGER.error(error, name)
|
||||
raise NotImplementedError(error % name)
|
||||
|
||||
def __init__(self, uid, alias=None, parameter=None):
|
||||
'''
|
||||
Concstructor.
|
||||
'''
|
||||
self.uid = uid
|
||||
self.alias = alias
|
||||
self.parameter = parameter
|
||||
self.scanned = None
|
||||
|
||||
def __str__(self):
|
||||
'''
|
||||
String representation of tag.
|
||||
|
||||
:return: The alias
|
||||
:rtype: str
|
||||
'''
|
||||
return self.alias or self.uid
|
||||
|
||||
def __repr__(self):
|
||||
'''
|
||||
Instance representation of tag.
|
||||
|
||||
:return: The class name and UID
|
||||
:rtype: str
|
||||
'''
|
||||
identifier = self.alias or self.uid
|
||||
return f'<{self.__class__.__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)
|
||||
self.execute(*args)
|
||||
|
||||
def as_dict(self, include_scanned=False):
|
||||
'''
|
||||
Dict representation of the tag.
|
||||
|
||||
:param bool include_scanned: Include scanned timestamp
|
||||
|
||||
:return: The dict version of the tag
|
||||
:rtype: dict
|
||||
'''
|
||||
data = {
|
||||
'action_class': self.__class__.__name__,
|
||||
'uid': self.uid,
|
||||
'alias': self.alias or '',
|
||||
'parameter': self.parameter or '',
|
||||
}
|
||||
|
||||
if include_scanned:
|
||||
data['scanned'] = self.scanned
|
||||
|
||||
return data
|
||||
|
||||
def validate(self):
|
||||
'''
|
||||
Validate parameter.
|
||||
|
||||
:raises ValueError: When parameter is not allowed but defined
|
||||
'''
|
||||
parameterised = len(getfullargspec(self.execute).args) > 2
|
||||
|
||||
if parameterised and not self.parameter:
|
||||
raise ValueError('Parameter required for this tag')
|
||||
|
||||
if not parameterised and self.parameter:
|
||||
raise ValueError('No parameter allowed for this tag')
|
|
@ -0,0 +1,86 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff playback actions.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'PlayPause',
|
||||
'Stop',
|
||||
'PreviousTrack',
|
||||
'NextTrack',
|
||||
)
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from .base import Action
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class PlayPause(Action):
|
||||
'''
|
||||
Pauses or resumes the playback, based on the current state.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, 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()
|
||||
|
||||
|
||||
class Stop(Action):
|
||||
'''
|
||||
Stops the playback.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core):
|
||||
'''
|
||||
Stop playback.
|
||||
|
||||
:param mopidy.core.Core core: The mopidy core instance
|
||||
'''
|
||||
LOGGER.info('Stopping playback')
|
||||
core.playback.stop()
|
||||
|
||||
|
||||
class PreviousTrack(Action):
|
||||
'''
|
||||
Changes to the previous track.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core):
|
||||
'''
|
||||
Change to previous track.
|
||||
|
||||
:param mopidy.core.Core core: The mopidy core instance
|
||||
'''
|
||||
LOGGER.info('Changing to previous track')
|
||||
core.playback.previous()
|
||||
|
||||
|
||||
class NextTrack(Action):
|
||||
'''
|
||||
Changes to the next track.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core):
|
||||
'''
|
||||
Change to next track.
|
||||
|
||||
:param mopidy.core.Core core: The mopidy core instance
|
||||
'''
|
||||
LOGGER.info('Changing to next track')
|
||||
core.playback.next()
|
|
@ -0,0 +1,30 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff shutdown tag.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'Shutdown',
|
||||
)
|
||||
|
||||
from logging import getLogger
|
||||
from os import system
|
||||
|
||||
from .base import Action
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Shutdown(Action):
|
||||
'''
|
||||
Shutting down the system.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core):
|
||||
'''
|
||||
Shutdown.
|
||||
|
||||
:param mopidy.core.Core core: The mopidy core instance
|
||||
'''
|
||||
LOGGER.info('Shutting down')
|
||||
system('sudo /sbin/shutdown -h now')
|
|
@ -0,0 +1,41 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff tracklist tag.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'Tracklist',
|
||||
)
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from .base import Action
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Tracklist(Action):
|
||||
'''
|
||||
Replaces the current tracklist with the URI retreived from the tag's
|
||||
parameter.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core, uri): # pylint: disable=arguments-differ
|
||||
'''
|
||||
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)
|
||||
|
||||
playlists = [playlist.uri for playlist in core.playlists.as_list().get()]
|
||||
|
||||
if uri in playlists:
|
||||
uris = [item.uri for item in core.playlists.get_items(uri).get()]
|
||||
else:
|
||||
uris = [uri]
|
||||
|
||||
core.tracklist.clear()
|
||||
core.tracklist.add(uris=uris)
|
||||
core.playback.play()
|
|
@ -0,0 +1,50 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff volume tag.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'Volume',
|
||||
)
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from .base import Action
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Volume(Action):
|
||||
'''
|
||||
Sets the volume to the percentage value retreived from the tag's parameter.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def execute(cls, core, volume): # pylint: disable=arguments-differ
|
||||
'''
|
||||
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 validate(self):
|
||||
'''
|
||||
Validates if the parameter is an integer between 0 and 100.
|
||||
|
||||
:param mixed parameter: The parameter
|
||||
|
||||
:raises ValueError: When parameter is invalid
|
||||
'''
|
||||
super().validate()
|
||||
|
||||
try:
|
||||
number = int(self.parameter)
|
||||
assert 0 <= number <= 100
|
||||
except (ValueError, AssertionError):
|
||||
raise ValueError('Volume parameter has to be a number between 0 and 100')
|
|
@ -0,0 +1,46 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff frontend.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'PummeluffFrontend',
|
||||
)
|
||||
|
||||
from threading import Event
|
||||
from logging import getLogger
|
||||
|
||||
import pykka
|
||||
from mopidy import core as mopidy_core
|
||||
|
||||
from .threads import GPIOHandler, TagReader
|
||||
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class PummeluffFrontend(pykka.ThreadingActor, mopidy_core.CoreListener):
|
||||
'''
|
||||
Pummeluff frontend which basically reacts to GPIO button pushes and touches
|
||||
of RFID tags.
|
||||
'''
|
||||
|
||||
def __init__(self, config, core): # pylint: disable=unused-argument
|
||||
super().__init__()
|
||||
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 GPIO handler & tag reader threads.
|
||||
'''
|
||||
self.gpio_handler.start()
|
||||
self.tag_reader.start()
|
||||
|
||||
def on_stop(self):
|
||||
'''
|
||||
Set threading stop event to tell GPIO handler & tag reader threads to
|
||||
stop their operations.
|
||||
'''
|
||||
self.stop_event.set()
|
|
@ -0,0 +1,142 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff registry.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'RegistryDict',
|
||||
'REGISTRY',
|
||||
)
|
||||
|
||||
import os
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from mopidy_pummeluff import actions
|
||||
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class RegistryDict(dict):
|
||||
'''
|
||||
Class which can be used to retreive and write RFID tags to the registry.
|
||||
'''
|
||||
|
||||
registry_path = '/var/lib/mopidy/pummeluff/tags.json'
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Constructor.
|
||||
|
||||
Automatically reads the registry if it exists.
|
||||
'''
|
||||
super().__init__()
|
||||
|
||||
if os.path.exists(self.registry_path):
|
||||
self.read()
|
||||
else:
|
||||
LOGGER.warning('Registry not existing yet on "%s"', self.registry_path)
|
||||
|
||||
@classmethod
|
||||
def unserialize_item(cls, item):
|
||||
'''
|
||||
Unserialize an item from the persistent storage on filesystem to a
|
||||
native action.
|
||||
|
||||
:param tuple item: The item
|
||||
|
||||
:return: The action
|
||||
:rtype: actions.Action
|
||||
'''
|
||||
if 'tag_class' in item:
|
||||
item['action_class'] = item.pop('tag_class')
|
||||
|
||||
return item['uid'], cls.init_action(**item)
|
||||
|
||||
@classmethod
|
||||
def init_action(cls, action_class, uid, alias=None, parameter=None):
|
||||
'''
|
||||
Initialise a new action instance.
|
||||
|
||||
:param str action_class: The action class
|
||||
:param str uid: The RFID UID
|
||||
:param str alias: The alias
|
||||
:param str parameter: The parameter
|
||||
|
||||
:return: The action instance
|
||||
:rtype: actions.Action
|
||||
'''
|
||||
uid = str(uid).strip()
|
||||
action_class = getattr(actions, action_class)
|
||||
|
||||
return action_class(uid, alias, parameter)
|
||||
|
||||
def read(self):
|
||||
'''
|
||||
Read registry from disk.
|
||||
|
||||
:raises IOError: When registry file on disk is missing
|
||||
'''
|
||||
LOGGER.debug('Reading registry from %s', self.registry_path)
|
||||
|
||||
with open(self.registry_path) as f:
|
||||
data = json.load(f)
|
||||
self.clear()
|
||||
self.update((self.unserialize_item(item) for item in data))
|
||||
|
||||
def write(self):
|
||||
'''
|
||||
Write registry to disk.
|
||||
'''
|
||||
LOGGER.debug('Writing registry to %s', self.registry_path)
|
||||
|
||||
config = self.registry_path
|
||||
directory = os.path.dirname(config)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
with open(config, 'w') as f:
|
||||
json.dump([action.as_dict() for action in self.values()], f, indent=4)
|
||||
|
||||
def register(self, action_class, uid, alias=None, parameter=None):
|
||||
'''
|
||||
Register a new tag in the registry.
|
||||
|
||||
:param str action_class: The action class
|
||||
:param str uid: The UID
|
||||
:param str alias: The alias
|
||||
:param str parameter: The parameter (optional)
|
||||
|
||||
:return: The action
|
||||
:rtype: actions.Action
|
||||
'''
|
||||
LOGGER.info('Registering %s tag %s with parameter "%s"', action_class, uid, parameter)
|
||||
|
||||
action = self.init_action(
|
||||
action_class=action_class,
|
||||
uid=uid,
|
||||
alias=alias,
|
||||
parameter=parameter
|
||||
)
|
||||
|
||||
action.validate()
|
||||
|
||||
self[uid] = action
|
||||
self.write()
|
||||
|
||||
return action
|
||||
|
||||
def unregister(self, uid):
|
||||
'''
|
||||
Unregister a tag from the registry.
|
||||
|
||||
:param str uid: The UID
|
||||
'''
|
||||
LOGGER.info('Unregistering tag %s', uid)
|
||||
|
||||
del self[uid]
|
||||
self.write()
|
||||
|
||||
|
||||
REGISTRY = RegistryDict()
|
|
@ -0,0 +1,19 @@
|
|||
'''
|
||||
Python module to play sounds.
|
||||
'''
|
||||
|
||||
__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))
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
'''
|
||||
Threads of Mopidy Pummeluff.
|
||||
'''
|
||||
|
||||
from .gpio_handler import *
|
||||
from .tag_reader import *
|
|
@ -0,0 +1,84 @@
|
|||
'''
|
||||
Python module for the dedicated Mopidy Pummeluff threads.
|
||||
'''
|
||||
|
||||
__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, PlayPause, Stop, PreviousTrack, NextTrack
|
||||
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,
|
||||
#29: PlayPause,
|
||||
#31: Stop,
|
||||
#33: PreviousTrack,
|
||||
35: NextTrack,
|
||||
}
|
||||
|
||||
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().__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)
|
||||
play_sound('success.wav')
|
||||
|
||||
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].execute(self.core)
|
||||
self.timestamps[pin] = now
|
|
@ -0,0 +1,119 @@
|
|||
'''
|
||||
Python module for the dedicated Mopidy Pummeluff threads.
|
||||
'''
|
||||
|
||||
__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.registry import REGISTRY
|
||||
from mopidy_pummeluff.actions.base import Action
|
||||
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().__init__()
|
||||
self.core = core
|
||||
self.stop_event = stop_event
|
||||
#self.rfid = RFID()
|
||||
self.rfid = RFID(pin_irq=15)
|
||||
|
||||
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
|
||||
'''
|
||||
try:
|
||||
action = REGISTRY[str(uid)]
|
||||
LOGGER.info('Triggering action of registered tag')
|
||||
play_sound('success.wav')
|
||||
action(self.core)
|
||||
|
||||
except KeyError:
|
||||
LOGGER.info('Tag is not registered, thus doing nothing')
|
||||
play_sound('fail.wav')
|
||||
action = Action(uid=uid)
|
||||
|
||||
action.scanned = time()
|
||||
TagReader.latest = action
|
|
@ -0,0 +1,171 @@
|
|||
'''
|
||||
Python module for Mopidy Pummeluff web classes.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'LatestHandler',
|
||||
'RegistryHandler',
|
||||
'RegisterHandler',
|
||||
'UnregisterHandler',
|
||||
'ActionClassesHandler',
|
||||
)
|
||||
|
||||
from json import dumps
|
||||
from logging import getLogger
|
||||
|
||||
from tornado.web import RequestHandler
|
||||
|
||||
from mopidy_pummeluff.registry import REGISTRY
|
||||
from mopidy_pummeluff.actions import ACTIONS
|
||||
from mopidy_pummeluff.threads import TagReader
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class LatestHandler(RequestHandler): # pylint: disable=abstract-method
|
||||
'''
|
||||
Request handler which returns the latest scanned tag.
|
||||
'''
|
||||
|
||||
def get(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle GET request.
|
||||
'''
|
||||
tag = TagReader.latest
|
||||
|
||||
LOGGER.debug('Returning latest tag %s', tag)
|
||||
|
||||
if tag is None:
|
||||
data = {
|
||||
'success': False,
|
||||
'message': 'No tag scanned yet'
|
||||
}
|
||||
|
||||
else:
|
||||
data = {
|
||||
'success': True,
|
||||
'message': 'Scanned tag found',
|
||||
}
|
||||
|
||||
data.update(tag.as_dict(include_scanned=True))
|
||||
|
||||
self.set_header('Content-type', 'application/json')
|
||||
self.write(dumps(data))
|
||||
|
||||
|
||||
class RegistryHandler(RequestHandler): # pylint: disable=abstract-method
|
||||
'''
|
||||
Request handler which returns all registered tags.
|
||||
'''
|
||||
|
||||
def get(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle GET request.
|
||||
'''
|
||||
tags_list = []
|
||||
|
||||
for tag in REGISTRY.values():
|
||||
tags_list.append(tag.as_dict())
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'message': 'Registry successfully read',
|
||||
'tags': tags_list
|
||||
}
|
||||
|
||||
self.set_header('Content-type', 'application/json')
|
||||
self.write(dumps(data))
|
||||
|
||||
|
||||
class RegisterHandler(RequestHandler): # pylint: disable=abstract-method
|
||||
'''
|
||||
Request handler which registers an RFID tag in the registry.
|
||||
'''
|
||||
|
||||
def post(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle POST request.
|
||||
'''
|
||||
try:
|
||||
tag = REGISTRY.register(
|
||||
action_class=self.get_argument('action-class'),
|
||||
uid=self.get_argument('uid'),
|
||||
alias=self.get_argument('alias', None),
|
||||
parameter=self.get_argument('parameter', None),
|
||||
)
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'message': 'Tag successfully registered',
|
||||
}
|
||||
|
||||
data.update(tag.as_dict())
|
||||
|
||||
except ValueError as ex:
|
||||
self.set_status(400)
|
||||
data = {
|
||||
'success': False,
|
||||
'message': str(ex)
|
||||
}
|
||||
|
||||
self.set_header('Content-type', 'application/json')
|
||||
self.write(dumps(data))
|
||||
|
||||
def put(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle PUT request.
|
||||
'''
|
||||
self.post()
|
||||
|
||||
|
||||
class UnregisterHandler(RequestHandler): # pylint: disable=abstract-method
|
||||
'''
|
||||
Request handler which unregisters an RFID tag from the registry.
|
||||
'''
|
||||
|
||||
def post(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle POST request.
|
||||
'''
|
||||
try:
|
||||
REGISTRY.unregister(uid=self.get_argument('uid'))
|
||||
|
||||
data = {
|
||||
'success': True,
|
||||
'message': 'Tag successfully unregistered',
|
||||
}
|
||||
|
||||
except ValueError as ex:
|
||||
self.set_status(400)
|
||||
data = {
|
||||
'success': False,
|
||||
'message': str(ex)
|
||||
}
|
||||
|
||||
self.set_header('Content-type', 'application/json')
|
||||
self.write(dumps(data))
|
||||
|
||||
def put(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle PUT request.
|
||||
'''
|
||||
self.post()
|
||||
|
||||
|
||||
class ActionClassesHandler(RequestHandler): # pylint: disable=abstract-method
|
||||
'''
|
||||
Request handler which returns all action classes.
|
||||
'''
|
||||
|
||||
def get(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
'''
|
||||
Handle GET request.
|
||||
'''
|
||||
data = {
|
||||
'success': True,
|
||||
'message': 'Action classes successfully retreived',
|
||||
'action_classes': ACTIONS
|
||||
}
|
||||
|
||||
self.set_header('Content-type', 'application/json')
|
||||
self.write(dumps(data))
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<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 Tag Management</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Pummeluff Tag Management</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div id="register">
|
||||
<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 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="action-class">Class</label>
|
||||
<select id="action-class" name="action-class">
|
||||
</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 Tag</button>
|
||||
<button id="unregister-button" role="button">× Unregister Tag</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="registry">
|
||||
<h2>Registered Tags</h2>
|
||||
<div id="tags"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* API class which communicates with the Pummeluff REST API.
|
||||
*/
|
||||
|
||||
class API {
|
||||
|
||||
/*
|
||||
* Send AJAX request to REST API endpoint.
|
||||
*/
|
||||
|
||||
request = (endpoint, data, callback) => {
|
||||
let init = {}
|
||||
if(data)
|
||||
init = { method: 'POST', body: data }
|
||||
|
||||
fetch(endpoint, init)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(callback)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh the registry.
|
||||
*/
|
||||
|
||||
refreshRegistry = () => {
|
||||
let callback = (response) => {
|
||||
let tagsContainer = document.getElementById('tags')
|
||||
while(tagsContainer.firstChild) {
|
||||
tagsContainer.removeChild(tagsContainer.firstChild)
|
||||
}
|
||||
|
||||
for(let tag of response.tags) {
|
||||
let tagElement = document.createElement('div')
|
||||
tagElement.setAttribute('class', 'tag')
|
||||
|
||||
let args = new Array('alias', 'uid', 'action_class', 'parameter')
|
||||
for(let arg of args) {
|
||||
let spanElement = document.createElement('span')
|
||||
let value = tag[arg] ? tag[arg] : '-'
|
||||
spanElement.setAttribute('class', arg.replace('_', '-'))
|
||||
spanElement.innerHTML = value
|
||||
tagElement.appendChild(spanElement)
|
||||
}
|
||||
|
||||
tagsContainer.appendChild(tagElement)
|
||||
}
|
||||
}
|
||||
|
||||
this.request('/pummeluff/registry/', false, callback)
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh the tags.
|
||||
*/
|
||||
|
||||
refreshActionClasses = () => {
|
||||
let callback = (response) => {
|
||||
let select = document.getElementById('action-class');
|
||||
while(select.firstChild)
|
||||
select.removeChild(select.firstChild)
|
||||
|
||||
for(let action_class in response.action_classes) {
|
||||
let option = document.createElement('option')
|
||||
option.setAttribute('value', action_class)
|
||||
option.innerHTML = action_class + ' (' + response.action_classes[action_class] + ')'
|
||||
select.appendChild(option)
|
||||
}
|
||||
}
|
||||
|
||||
this.request('/pummeluff/action-classes/', false, callback)
|
||||
}
|
||||
|
||||
/*
|
||||
* Reset the form.
|
||||
*/
|
||||
|
||||
formCallback = (response) => {
|
||||
if(response.success) {
|
||||
this.refreshRegistry()
|
||||
document.getElementById('uid').value = ''
|
||||
document.getElementById('alias').value = ''
|
||||
document.getElementById('parameter').value = ''
|
||||
document.getElementById('action-class').selectIndex = 0
|
||||
} else {
|
||||
window.alert(response.message)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Register a new tag.
|
||||
*/
|
||||
|
||||
register = () => {
|
||||
let form = document.getElementById('register-form')
|
||||
let data = new FormData(form)
|
||||
this.request('/pummeluff/register/', data, this.formCallback)
|
||||
}
|
||||
|
||||
/*
|
||||
* Unregister an existing tag.
|
||||
*/
|
||||
|
||||
unregister = () => {
|
||||
let form = document.getElementById('register-form')
|
||||
let data = new FormData(form)
|
||||
this.request('/pummeluff/unregister/', data, this.formCallback)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get latest scanned tag.
|
||||
*/
|
||||
|
||||
getLatestTag = () => {
|
||||
let latest_tag = undefined
|
||||
|
||||
let uid_field = document.getElementById('uid')
|
||||
let alias_field = document.getElementById('alias')
|
||||
let parameter_field = document.getElementById('parameter')
|
||||
let action_class_select = document.getElementById('action-class')
|
||||
|
||||
uid_field.value = ''
|
||||
alias_field.value = ''
|
||||
parameter_field.value = ''
|
||||
action_class_select.selectIndex = 0
|
||||
|
||||
let link = document.getElementById('read-rfid-tag')
|
||||
link.classList.add('reading')
|
||||
|
||||
let do_request = () => {
|
||||
let callback = (response) => {
|
||||
if(latest_tag && response.success && JSON.stringify(response) != JSON.stringify(latest_tag)) {
|
||||
uid_field.value = response.uid
|
||||
|
||||
if(response.alias)
|
||||
alias_field.value = response.alias
|
||||
|
||||
if(response.parameter)
|
||||
parameter_field.value = response.parameter
|
||||
|
||||
if(response.action_class)
|
||||
action_class_select.value = response.action_class
|
||||
|
||||
link.classList.remove('reading')
|
||||
} else {
|
||||
setTimeout(() => do_request(), 1000)
|
||||
}
|
||||
|
||||
latest_tag = response
|
||||
}
|
||||
|
||||
api.request('/pummeluff/latest/', false, callback)
|
||||
}
|
||||
|
||||
do_request()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
api = new API()
|
||||
|
||||
api.refreshRegistry()
|
||||
api.refreshActionClasses()
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
let target = event.target
|
||||
let div = target.closest('div')
|
||||
|
||||
if(div && div.classList.contains('tag')) {
|
||||
for(let child of div.children) {
|
||||
document.getElementById(child.className).value = child.innerHTML.replace(/^-$/, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('register-form').onsubmit = () => {
|
||||
api.register()
|
||||
return false;
|
||||
}
|
||||
|
||||
document.getElementById('unregister-button').onclick = () => {
|
||||
api.unregister()
|
||||
return false
|
||||
}
|
||||
|
||||
document.getElementById('read-rfid-tag').onclick = () => api.getLatestTag()
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Main Elements
|
||||
*/
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body
|
||||
{
|
||||
margin : 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
background-color: #222;
|
||||
color : #eee;
|
||||
font-family : sans-serif;
|
||||
font-size : 14px;
|
||||
min-height : 100vh;
|
||||
}
|
||||
|
||||
/*
|
||||
* Headings
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2
|
||||
{
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Structural page elements
|
||||
*/
|
||||
|
||||
main
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
header
|
||||
{
|
||||
padding: 20px 20px 10px 20px;
|
||||
}
|
||||
|
||||
main > div
|
||||
{
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Register Form
|
||||
*/
|
||||
|
||||
form
|
||||
{
|
||||
background-color: #333;
|
||||
padding : 20px;
|
||||
width : 250px;
|
||||
}
|
||||
|
||||
label
|
||||
{
|
||||
display: block;
|
||||
margin : 10px 0 5px 0;
|
||||
}
|
||||
|
||||
label:first-child
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button
|
||||
{
|
||||
background-color: #222;
|
||||
border : 0;
|
||||
border-color : #333;
|
||||
border-style : solid;
|
||||
border-width : 0 0 2px 0;
|
||||
color : #eee;
|
||||
outline : none;
|
||||
padding : 10px;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
select
|
||||
{
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
input::placeholder
|
||||
{
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input:focus
|
||||
{
|
||||
border-bottom-color: #8ff;
|
||||
}
|
||||
|
||||
button
|
||||
{
|
||||
color : #eee;
|
||||
cursor : pointer;
|
||||
font-weight: bold;
|
||||
margin-top : 10px;
|
||||
}
|
||||
|
||||
button#register-button
|
||||
{
|
||||
background-color: #4a4;
|
||||
}
|
||||
|
||||
button#unregister-button
|
||||
{
|
||||
background-color: #a20;
|
||||
}
|
||||
|
||||
#read-rfid-tag
|
||||
{
|
||||
color : #8ff;
|
||||
font-size : 11px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#read-rfid-tag.reading {
|
||||
animation: blink 0.5s cubic-bezier(.5, 0, 1, 1) infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes blink { to { opacity: 0.25; } }
|
||||
|
||||
/*
|
||||
* Registered Tags
|
||||
*/
|
||||
|
||||
div.tag
|
||||
{
|
||||
background-color: #eee;
|
||||
box-shadow : 1px 1px 5px #000;
|
||||
color : #222;
|
||||
cursor : pointer;
|
||||
display : inline-block;
|
||||
line-height : 20px;
|
||||
margin : 0 20px 20px 0;
|
||||
padding : 10px;
|
||||
width : 400px;
|
||||
}
|
||||
|
||||
div.tag span.uid,
|
||||
div.tag span.action-class,
|
||||
div.tag span.parameter
|
||||
{
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
|
||||
div.tag span.action-class
|
||||
{
|
||||
background-color: #888;
|
||||
border-radius : 10px;
|
||||
color : #eee;
|
||||
display : inline-block;
|
||||
font-size : 11px;
|
||||
line-height : 11px;
|
||||
margin-left : 5px;
|
||||
padding : 3px 5px;
|
||||
}
|
||||
|
||||
div.tag span.alias,
|
||||
div.tag span.parameter
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
Loading…
Reference in New Issue