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