applets: Add in the framework for Applets!

This allows us to have separate applications from the G13 Configurator render to
the G13's display, just like the way Logitech's design worked. It even includes
an applet switcher, and an example clock applet.

There's quite a few deficiencies in this model, not the least of which is the
fact that we're using dbus-python as the main communications channel. Things
that are broken include:

  - We can't tell when an applet dies except when we fail a call.
  - Applets have to be running at the same time G13 Configurator is to handle
    the initial register call.

We can likely deal with these somehow using DBus signals.
This commit is contained in:
June Tate-Gans 2021-05-08 19:25:05 -05:00
parent 81be3f2cf9
commit 18e536d7f7
7 changed files with 441 additions and 0 deletions

View File

View File

@ -0,0 +1,160 @@
import gi
import dbus
import dbus.service
import dbus.mainloop.glib
import time
from dbus.mainloop.glib import DBusGMainLoop
from dbus.exceptions import DBusException
from dbus.types import ByteArray
from g13gui.applet.loopbackdisplaydevice import LoopbackDisplayDevice
from g13gui.bitwidgets.display import Display
from g13gui.bitwidgets.screen import Screen
gi.require_version('GLib', '2.0')
from gi.repository import GLib
class Buttons(object):
L1 = 1
L2 = 2
L3 = 3
L4 = 4
class Applet(dbus.service.Object):
BUS_INTERFACE = 'com.theonelab.g13.Applet'
BUS_PATH = '/com/theonelab/g13/Applet'
def __init__(self, name):
dbus.service.Object.__init__(self, dbus.SessionBus(),
Applet.BUS_PATH)
self._name = name
self._dd = LoopbackDisplayDevice()
self._d = Display(self._dd)
self._s = Screen(self._d)
self._s.hide()
self._registered = False
self._manager = None
def register(self):
try:
self._manager = self._bus.get_object(
'com.theonelab.g13.AppletManager',
'/com/theonelab/g13/AppletManager')
except DBusException as err:
self._manager = None
return True
self._manager.Register(self._name)
self._registered = True
GLib.timeout_add_seconds(1, self._ping)
return False
def _ping(self):
if self._manager:
try:
self._manager.Ping()
except DBusException as err:
print('Lost connection with AppletManager: %s' % err)
self._registered = False
GLib.timeout_add_seconds(1, self.register)
return False
return True
def run(self):
self._bus = dbus.SessionBus()
GLib.timeout_add_seconds(1, self.register)
loop = GLib.MainLoop()
loop.run()
@property
def name(self):
return self._name
@property
def displayDevice(self):
return self._dd
@property
def display(self):
return self._d
@property
def screen(self):
return self._s
def onKeyPressed(self, timestamp, key):
pass
def onKeyReleased(self, timestamp, key):
pass
def onShown(self, timestamp):
pass
def onHidden(self):
pass
def maybePresentScreen(self):
if self.screen.visible and self._manager:
self.screen.nextFrame()
frame = self.displayDevice.frame
frame = ByteArray(frame)
self._manager.Present(frame, byte_arrays=True)
@dbus.service.method(BUS_INTERFACE,
in_signature='d', out_signature='ay',
byte_arrays=True)
def Present(self, timestamp):
self.screen.show()
self.onShown(timestamp)
self.screen.nextFrame()
return ByteArray(self.displayDevice.frame)
@dbus.service.method(BUS_INTERFACE)
def Unpresent(self):
self.screen.hide()
self.onHidden()
def _setButtonPressed(self, state, button):
buttonIdx = button - 1
button = self._s.buttonBar.button(buttonIdx)
if button:
button.pressed = state
def onUpdateScreen(self):
pass
@dbus.service.method(BUS_INTERFACE,
in_signature='di', out_signature='ay',
byte_arrays=True)
def KeyPressed(self, timestamp, key):
self.onKeyPressed(timestamp, key)
self._setButtonPressed(True, key)
self.onUpdateScreen()
self.screen.nextFrame()
return ByteArray(self.displayDevice.frame)
@dbus.service.method(BUS_INTERFACE,
in_signature='di', out_signature='ay',
byte_arrays=True)
def KeyReleased(self, timestamp, key):
self.onKeyPressed(timestamp, key)
self._setButtonPressed(False, key)
self.onUpdateScreen()
self.screen.nextFrame()
return ByteArray(self.displayDevice.frame)
def RunApplet(cls, *args, **kwargs):
DBusGMainLoop(set_as_default=True)
applet = cls(*args, **kwargs)
applet.run()

View File

@ -0,0 +1,24 @@
from builtins import property
from g13gui.bitwidgets.displaydevice import DisplayDevice
from g13gui.g13.displaydevice import G13DisplayDevice
class LoopbackDisplayDevice(G13DisplayDevice):
"""A loopback display device for the G13 manager.
This one differs from the built-in G13DisplayDevice by preventing a direct
write to the G13Manager's setLCDbuffer. This is specifically designed for
the Applet use case, where the methods return LCD frames, rather than write
directly.
"""
def __init__(self):
pass
@property
def frame(self):
return self._frame
def _pushFrame(self, lpbm):
self._frame = lpbm

View File

@ -0,0 +1,115 @@
import gi
import dbus
import dbus.service
import dbus.mainloop.glib
import time
from builtins import property
from g13gui.observer.subject import Subject
from g13gui.observer.subject import ChangeType
from g13gui.applet.switcher import Switcher
from g13gui.g13.common import G13Keys
gi.require_version('GLib', '2.0')
from gi.repository import GLib
class AppletManager(dbus.service.Object, Subject):
INTERFACE_NAME = 'com.theonelab.g13.AppletManager'
BUS_NAME = 'com.theonelab.g13.AppletManager'
BUS_PATH = '/com/theonelab/g13/AppletManager'
def __init__(self, manager):
self._bus = dbus.SessionBus()
self._busName = dbus.service.BusName(AppletManager.BUS_NAME, self._bus)
dbus.service.Object.__init__(self, self._bus,
AppletManager.BUS_PATH)
Subject.__init__(self)
self._manager = manager
# [name] -> (sender, proxy)
self._applets = {}
self._switcher = Switcher(self)
self._activeApplet = self._switcher
self._applets['Switcher'] = (self._switcher, self._switcher)
self.addChange(ChangeType.ADD, 'applet', 'Switcher')
self.notifyChanged()
@property
def activeApplet(self):
return self._activeApplet
@activeApplet.setter
def activeApplet(self, appletName):
(name, appletProxy) = self._applets[appletName]
self._activeApplet.Unpresent()
self.setProperty('activeApplet', appletProxy)
self.onPresent()
def raiseSwitcher(self):
self._activeApplet = self._switcher
self.onPresent()
@property
def appletNames(self):
return self._applets.keys()
def _updateLCD(self, frame):
self._manager.setLCDBuffer(frame)
def onPresent(self):
frame = self._activeApplet.Present(time.time(), byte_arrays=True)
frame = bytes(frame)
self._updateLCD(frame)
def onKeyPressed(self, key):
# Swap to the switcher
if key == G13Keys.BD:
self.activeApplet = 'Switcher'
return
frame = self._activeApplet.KeyPressed(time.time(), key.value['bit'])
self._updateLCD(frame)
def onKeyReleased(self, key):
frame = self._activeApplet.KeyReleased(time.time(), key.value['bit'])
self._updateLCD(frame)
def _registerApplet(self, name, sender):
proxy = self._bus.get_object(sender, '/com/theonelab/g13/Applet')
self._applets[name] = (sender, proxy)
self.addChange(ChangeType.ADD, 'applet', name)
self.notifyChanged()
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='s', sender_keyword='sender')
def Register(self, name, sender):
if sender is None:
print('Attempt to register None as sender applet!')
return False
print('Registered applet %s as %s' % (name, sender))
GLib.idle_add(self._registerApplet, str(name), sender)
def _presentScreen(self, screen, sender):
self._updateLCD(screen)
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='ay', sender_keyword='sender',
byte_arrays=True)
def Present(self, screen, sender):
# if self._activeApplet.bus_name != sender:
# return
GLib.idle_add(self._presentScreen, screen, sender)
@dbus.service.method(dbus_interface=INTERFACE_NAME,
out_signature='b',
sender_keyword='sender')
def Ping(self, sender):
if sender not in [s[0] for s in self._applets]:
return False
return True

View File

@ -0,0 +1,101 @@
import gi
import time
import threading
from builtins import property
from g13gui.observer.observer import Observer
from g13gui.observer.subject import ChangeType
from g13gui.applet.applet import Buttons
from g13gui.applet.loopbackdisplaydevice import LoopbackDisplayDevice
from g13gui.bitwidgets.display import Display
from g13gui.bitwidgets.screen import Screen
from g13gui.bitwidgets.label import Label
from g13gui.bitwidgets.button import Button
from g13gui.bitwidgets.button import Glyphs
from g13gui.bitwidgets.listview import ListView
gi.require_version('GLib', '2.0')
from gi.repository import GLib
class Switcher(Observer):
def __init__(self, appletManager):
Observer.__init__(self)
self._appletManager = appletManager
self._applets = []
self._appletManager.registerObserver(self, {'activeApplet', 'applet'})
self.changeTrigger(self.onNewApplet,
changeType=ChangeType.ADD,
keys={'applet'})
self._initWidgets()
@property
def bus_name(self):
return self
def onNewApplet(self, subject, changeType, key, data):
self._applets = sorted(self._appletManager.appletNames)
self._lv.model = self._applets
self._lv.update()
self._s.nextFrame()
frame = self._dd.frame
self._appletManager.Present(frame, self)
def _initWidgets(self):
self._dd = LoopbackDisplayDevice()
self._d = Display(self._dd)
self._s = Screen(self._d)
self._lv = ListView(self._applets)
self._lv.showAll()
self._s.addChild(self._lv)
button = Button(Glyphs.DOWN_ARROW)
self._s.buttonBar.setButton(1, button)
button = Button(Glyphs.UP_ARROW)
self._s.buttonBar.setButton(2, button)
button = Button(Glyphs.CHECKMARK)
self._s.buttonBar.setButton(3, button)
self._s.buttonBar.showAll()
def _setButtonPressed(self, state, button):
buttonIdx = button - 1
button = self._s.buttonBar.button(buttonIdx)
if button:
button.pressed = state
def Present(self, timestamp, **kwargs):
self._s.show()
self._lv.update()
self._s.nextFrame()
frame = self._dd.frame
return frame
def Unpresent(self):
self._s.hide()
def KeyPressed(self, timestamp, key):
self._setButtonPressed(True, key)
return self.Present(timestamp)
def _setActiveApplet(self):
selectedName = self._lv.markedItem()
self._appletManager.activeApplet = selectedName
def KeyReleased(self, timestamp, key):
self._setButtonPressed(False, key)
if key == Buttons.L2: # down
self._lv.nextSelection()
elif key == Buttons.L3: # up
self._lv.prevSelection()
elif key == Buttons.L4: # select
self._lv.markSelection()
GLib.idle_add(self._setActiveApplet)
return self.Present(timestamp)

View File

View File

@ -0,0 +1,41 @@
import gi
import time
from g13gui.applet.applet import Applet
from g13gui.applet.applet import RunApplet
from g13gui.bitwidgets.label import Label
from g13gui.bitwidgets.fonts import Fonts
gi.require_version('GLib', '2.0')
from gi.repository import GLib
class ClockApplet(Applet):
NAME = 'Clock'
def __init__(self):
Applet.__init__(self, ClockApplet.NAME)
self._timeLabel = Label(43, 8, '18:54:00', font=Fonts.LARGE)
self._timeLabel.showAll()
self.screen.addChild(self._timeLabel)
self._updateTimeLabel()
def _updateTimeLabel(self):
(tm_year, tm_month, tm_mday, tm_hour,
tm_min, tm_sec, tm_wday, tm_yday, tm_isdst) = time.localtime()
self._timeLabel.text = '%d:%0.2d:%0.2d' % (tm_hour, tm_min, tm_sec)
def _pushTime(self):
self._updateTimeLabel()
self.maybePresentScreen()
return self.screen.visible
def onShown(self, timestamp):
self._updateTimeLabel()
GLib.timeout_add_seconds(1, self._pushTime)
if __name__ == '__main__':
RunApplet(ClockApplet)