tobru mods

This commit is contained in:
Tobias Brunner 2021-11-28 14:15:58 +00:00
commit 73c6297ac6
21 changed files with 1419 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__
*.py[cod]
*$py.class

79
__init__.py Normal file
View File

@ -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,
})

22
actions/__init__.py Normal file
View File

@ -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()

108
actions/base.py Normal file
View File

@ -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')

86
actions/playback.py Normal file
View File

@ -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()

30
actions/shutdown.py Normal file
View File

@ -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')

41
actions/tracklist.py Normal file
View File

@ -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()

50
actions/volume.py Normal file
View File

@ -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')

2
ext.conf Normal file
View File

@ -0,0 +1,2 @@
[pummeluff]
enabled = True

46
frontend.py Normal file
View File

@ -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()

142
registry.py Normal file
View File

@ -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()

19
sound.py Normal file
View File

@ -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))

BIN
sounds/fail.wav Normal file

Binary file not shown.

BIN
sounds/success.wav Normal file

Binary file not shown.

6
threads/__init__.py Normal file
View File

@ -0,0 +1,6 @@
'''
Threads of Mopidy Pummeluff.
'''
from .gpio_handler import *
from .tag_reader import *

84
threads/gpio_handler.py Normal file
View File

@ -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

119
threads/tag_reader.py Normal file
View File

@ -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

171
web.py Normal file
View File

@ -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))

42
webui/index.html Normal file
View File

@ -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>

188
webui/script.js Normal file
View File

@ -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()

181
webui/style.css Normal file
View File

@ -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;
}