From 18e536d7f7c0475ba7047652f1788445059c1a09 Mon Sep 17 00:00:00 2001 From: June Tate-Gans Date: Sat, 8 May 2021 19:25:05 -0500 Subject: [PATCH] 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. --- g13gui/g13gui/applet/__init__.py | 0 g13gui/g13gui/applet/applet.py | 160 ++++++++++++++++++ g13gui/g13gui/applet/loopbackdisplaydevice.py | 24 +++ g13gui/g13gui/applet/manager.py | 115 +++++++++++++ g13gui/g13gui/applet/switcher.py | 101 +++++++++++ g13gui/g13gui/applets/__init__.py | 0 g13gui/g13gui/applets/clock.py | 41 +++++ 7 files changed, 441 insertions(+) create mode 100644 g13gui/g13gui/applet/__init__.py create mode 100644 g13gui/g13gui/applet/applet.py create mode 100644 g13gui/g13gui/applet/loopbackdisplaydevice.py create mode 100644 g13gui/g13gui/applet/manager.py create mode 100644 g13gui/g13gui/applet/switcher.py create mode 100644 g13gui/g13gui/applets/__init__.py create mode 100644 g13gui/g13gui/applets/clock.py diff --git a/g13gui/g13gui/applet/__init__.py b/g13gui/g13gui/applet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/g13gui/g13gui/applet/applet.py b/g13gui/g13gui/applet/applet.py new file mode 100644 index 0000000..dd534a4 --- /dev/null +++ b/g13gui/g13gui/applet/applet.py @@ -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() diff --git a/g13gui/g13gui/applet/loopbackdisplaydevice.py b/g13gui/g13gui/applet/loopbackdisplaydevice.py new file mode 100644 index 0000000..edb45f3 --- /dev/null +++ b/g13gui/g13gui/applet/loopbackdisplaydevice.py @@ -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 diff --git a/g13gui/g13gui/applet/manager.py b/g13gui/g13gui/applet/manager.py new file mode 100644 index 0000000..75eec1a --- /dev/null +++ b/g13gui/g13gui/applet/manager.py @@ -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 diff --git a/g13gui/g13gui/applet/switcher.py b/g13gui/g13gui/applet/switcher.py new file mode 100644 index 0000000..3882557 --- /dev/null +++ b/g13gui/g13gui/applet/switcher.py @@ -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) diff --git a/g13gui/g13gui/applets/__init__.py b/g13gui/g13gui/applets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/g13gui/g13gui/applets/clock.py b/g13gui/g13gui/applets/clock.py new file mode 100644 index 0000000..0521166 --- /dev/null +++ b/g13gui/g13gui/applets/clock.py @@ -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)