2018-01-22 04:28:35 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
|
|
|
|
from gi.repository import GObject, Gio, GLib
|
|
|
|
import sys
|
|
|
|
import argparse
|
2018-01-26 11:20:36 +01:00
|
|
|
import cmd
|
2018-01-24 12:06:16 +01:00
|
|
|
import os
|
2018-01-23 04:36:29 +01:00
|
|
|
import json
|
2018-01-22 04:28:35 +01:00
|
|
|
import logging
|
2018-01-26 17:05:50 +01:00
|
|
|
import re
|
2018-01-26 12:33:37 +01:00
|
|
|
import readline
|
2018-01-22 04:28:35 +01:00
|
|
|
import select
|
2018-01-26 11:20:36 +01:00
|
|
|
import threading
|
2018-01-23 04:36:29 +01:00
|
|
|
import time
|
2018-01-25 05:52:56 +01:00
|
|
|
import svgwrite
|
2018-01-22 04:28:35 +01:00
|
|
|
|
2018-01-23 18:18:05 +01:00
|
|
|
|
|
|
|
log_format = '%(levelname)s: %(message)s'
|
|
|
|
logger_handler = logging.StreamHandler()
|
|
|
|
logger_handler.setFormatter(logging.Formatter(log_format))
|
2018-01-22 04:28:35 +01:00
|
|
|
logger = logging.getLogger('tuhi-kete')
|
2018-01-23 18:18:05 +01:00
|
|
|
logger.addHandler(logger_handler)
|
|
|
|
logger.setLevel(logging.INFO)
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
TUHI_DBUS_NAME = 'org.freedesktop.tuhi1'
|
|
|
|
ORG_FREEDESKTOP_TUHI1_MANAGER = 'org.freedesktop.tuhi1.Manager'
|
|
|
|
ORG_FREEDESKTOP_TUHI1_DEVICE = 'org.freedesktop.tuhi1.Device'
|
|
|
|
ROOT_PATH = '/org/freedesktop/tuhi1'
|
|
|
|
|
|
|
|
|
|
|
|
class DBusError(Exception):
|
|
|
|
def __init__(self, message):
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
class _DBusObject(GObject.Object):
|
|
|
|
_connection = None
|
|
|
|
|
|
|
|
def __init__(self, name, interface, objpath):
|
|
|
|
GObject.GObject.__init__(self)
|
|
|
|
|
|
|
|
if _DBusObject._connection is None:
|
|
|
|
self._connect_to_session()
|
|
|
|
|
|
|
|
self.interface = interface
|
|
|
|
self.objpath = objpath
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.proxy = Gio.DBusProxy.new_sync(_DBusObject._connection,
|
|
|
|
Gio.DBusProxyFlags.NONE, None,
|
|
|
|
name, objpath, interface, None)
|
|
|
|
except GLib.Error as e:
|
|
|
|
if (e.domain == 'g-io-error-quark' and
|
|
|
|
e.code == Gio.IOErrorEnum.DBUS_ERROR):
|
|
|
|
raise DBusError(e.message)
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
|
|
|
|
if self.proxy.get_name_owner() is None:
|
2018-01-24 23:47:47 +01:00
|
|
|
raise DBusError('No-one is handling {}, is the daemon running?'.format(name))
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
self.proxy.connect('g-properties-changed', self._on_properties_changed)
|
|
|
|
self.proxy.connect('g-signal', self._on_signal_received)
|
|
|
|
|
|
|
|
def _connect_to_session(self):
|
|
|
|
try:
|
|
|
|
_DBusObject._connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
|
|
|
except GLib.Error as e:
|
|
|
|
if (e.domain == 'g-io-error-quark' and
|
|
|
|
e.code == Gio.IOErrorEnum.DBUS_ERROR):
|
|
|
|
raise DBusError(e.message)
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
|
|
|
|
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
|
|
|
|
# Implement this in derived classes to respond to property changes
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _on_signal_received(self, proxy, sender, signal, parameters):
|
|
|
|
# Implement this in derived classes to respond to signals
|
|
|
|
pass
|
|
|
|
|
|
|
|
def property(self, name):
|
|
|
|
p = self.proxy.get_cached_property(name)
|
|
|
|
if p is not None:
|
|
|
|
return p.unpack()
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
class TuhiKeteDevice(_DBusObject):
|
|
|
|
def __init__(self, manager, objpath):
|
|
|
|
_DBusObject.__init__(self, TUHI_DBUS_NAME,
|
|
|
|
ORG_FREEDESKTOP_TUHI1_DEVICE,
|
|
|
|
objpath)
|
|
|
|
self.manager = manager
|
|
|
|
self.is_pairing = False
|
|
|
|
|
2018-01-26 17:05:50 +01:00
|
|
|
@classmethod
|
|
|
|
def is_device_address(cls, string):
|
|
|
|
if re.match(r"[0-9a-f]{2}(:[0-9a-f]{2}){5}$", string.lower()):
|
|
|
|
return string
|
|
|
|
raise argparse.ArgumentTypeError(f'"{string}" is not a valid device address')
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
@GObject.Property
|
|
|
|
def address(self):
|
|
|
|
return self.property('Address')
|
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def name(self):
|
|
|
|
return self.property('Name')
|
|
|
|
|
2018-01-23 02:29:22 +01:00
|
|
|
@GObject.Property
|
|
|
|
def listening(self):
|
|
|
|
return self.property('Listening')
|
|
|
|
|
2018-01-23 02:34:22 +01:00
|
|
|
@GObject.Property
|
|
|
|
def drawings_available(self):
|
|
|
|
return self.property('DrawingsAvailable')
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def pair(self):
|
|
|
|
logger.debug('{}: Pairing'.format(self))
|
|
|
|
# FIXME: Pair() doesn't return anything useful yet, so we wait until
|
|
|
|
# the device is in the Manager's Devices property
|
|
|
|
self.manager.connect('notify::devices', self._on_mgr_devices_updated)
|
|
|
|
self.is_pairing = True
|
|
|
|
self.proxy.Pair()
|
|
|
|
|
2018-01-23 02:29:22 +01:00
|
|
|
def start_listening(self):
|
|
|
|
self.proxy.StartListening()
|
|
|
|
|
|
|
|
def stop_listening(self):
|
|
|
|
self.proxy.StopListening()
|
|
|
|
|
2018-01-23 04:36:29 +01:00
|
|
|
def json(self, index):
|
|
|
|
return self.proxy.GetJSONData('(u)', index)
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def _on_signal_received(self, proxy, sender, signal, parameters):
|
|
|
|
if signal == 'ButtonPressRequired':
|
2018-01-26 13:22:06 +01:00
|
|
|
logger.info(f'{self}: Press button on device now')
|
2018-01-23 02:29:22 +01:00
|
|
|
elif signal == 'ListeningStopped':
|
2018-01-24 12:06:16 +01:00
|
|
|
err = parameters[0]
|
|
|
|
if err < 0:
|
2018-01-26 13:22:06 +01:00
|
|
|
logger.error(f'{self}: an error occured: {os.strerror(err)}')
|
2018-01-23 02:29:22 +01:00
|
|
|
self.notify('listening')
|
2018-01-22 04:28:35 +01:00
|
|
|
|
2018-01-23 02:34:22 +01:00
|
|
|
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
|
|
|
|
if changed_props is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
changed_props = changed_props.unpack()
|
|
|
|
|
|
|
|
if 'DrawingsAvailable' in changed_props:
|
|
|
|
self.notify('drawings-available')
|
2018-01-25 06:44:02 +01:00
|
|
|
elif 'Listening' in changed_props:
|
|
|
|
self.notify('listening')
|
2018-01-23 02:34:22 +01:00
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def __repr__(self):
|
|
|
|
return '{} - {}'.format(self.address, self.name)
|
|
|
|
|
|
|
|
def _on_mgr_devices_updated(self, manager, pspec):
|
|
|
|
if not self.is_pairing:
|
|
|
|
return
|
|
|
|
|
|
|
|
for d in manager.devices:
|
|
|
|
if d.address == self.address:
|
|
|
|
self.is_pairing = False
|
2018-01-26 13:22:06 +01:00
|
|
|
logger.info(f'{self}: Pairing successful')
|
2018-01-23 05:20:37 +01:00
|
|
|
self.manager.quit()
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
class TuhiKeteManager(_DBusObject):
|
|
|
|
__gsignals__ = {
|
|
|
|
"pairable-device":
|
|
|
|
(GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
_DBusObject.__init__(self, TUHI_DBUS_NAME,
|
|
|
|
ORG_FREEDESKTOP_TUHI1_MANAGER,
|
|
|
|
ROOT_PATH)
|
|
|
|
|
2018-01-23 03:32:39 +01:00
|
|
|
Gio.bus_watch_name(Gio.BusType.SESSION,
|
|
|
|
TUHI_DBUS_NAME,
|
|
|
|
Gio.BusNameWatcherFlags.NONE,
|
|
|
|
None,
|
|
|
|
self._on_name_vanished)
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
self.mainloop = None
|
2018-01-22 04:28:35 +01:00
|
|
|
self._devices = {}
|
2018-01-22 08:13:12 +01:00
|
|
|
self._pairable_devices = {}
|
2018-01-22 04:28:35 +01:00
|
|
|
for objpath in self.property('Devices'):
|
|
|
|
device = TuhiKeteDevice(self, objpath)
|
|
|
|
self._devices[device.address] = device
|
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def devices(self):
|
|
|
|
return [v for k, v in self._devices.items()]
|
|
|
|
|
|
|
|
@GObject.Property
|
|
|
|
def searching(self):
|
2018-01-24 03:46:43 +01:00
|
|
|
return self.proxy.get_cached_property('Searching')
|
2018-01-23 04:44:46 +01:00
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def start_search(self):
|
2018-01-22 08:13:12 +01:00
|
|
|
self._pairable_devices = {}
|
2018-01-22 04:28:35 +01:00
|
|
|
self.proxy.StartSearch()
|
|
|
|
|
|
|
|
def stop_search(self):
|
|
|
|
self.proxy.StopSearch()
|
2018-01-22 08:13:12 +01:00
|
|
|
self._pairable_devices = {}
|
|
|
|
|
2018-01-23 03:32:39 +01:00
|
|
|
def run(self):
|
2018-01-26 11:20:36 +01:00
|
|
|
if self.mainloop is None:
|
|
|
|
self.mainloop = GObject.MainLoop()
|
|
|
|
|
2018-01-23 03:32:39 +01:00
|
|
|
try:
|
|
|
|
self.mainloop.run()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print('\r', end='') # to remove the ^C
|
|
|
|
self.mainloop.quit()
|
|
|
|
|
|
|
|
def quit(self):
|
2018-01-26 11:20:36 +01:00
|
|
|
if self.mainloop is not None:
|
|
|
|
self.mainloop.quit()
|
2018-01-23 03:32:39 +01:00
|
|
|
|
2018-01-22 08:13:12 +01:00
|
|
|
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
|
|
|
|
if changed_props is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
changed_props = changed_props.unpack()
|
|
|
|
|
|
|
|
if 'Devices' in changed_props:
|
|
|
|
objpaths = changed_props['Devices']
|
|
|
|
for objpath in objpaths:
|
2018-01-23 05:21:32 +01:00
|
|
|
try:
|
|
|
|
d = self._pairable_devices[objpath]
|
|
|
|
self._devices[d.address] = d
|
|
|
|
del self._pairable_devices[objpath]
|
|
|
|
except KeyError:
|
|
|
|
# if we called Pair() on an existing device it's not in
|
|
|
|
# pairable devices
|
|
|
|
pass
|
2018-01-22 08:13:12 +01:00
|
|
|
self.notify('devices')
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
def _on_signal_received(self, proxy, sender, signal, parameters):
|
|
|
|
if signal == 'SearchStopped':
|
|
|
|
self.notify('searching')
|
|
|
|
elif signal == 'PairableDevice':
|
|
|
|
objpath = parameters[0]
|
|
|
|
device = TuhiKeteDevice(self, objpath)
|
2018-01-22 08:13:12 +01:00
|
|
|
self._pairable_devices[objpath] = device
|
2018-01-22 04:28:35 +01:00
|
|
|
logger.debug('Found pairable device: {}'.format(device))
|
|
|
|
self.emit('pairable-device', device)
|
|
|
|
|
2018-01-23 03:32:39 +01:00
|
|
|
def _on_name_vanished(self, connection, name):
|
|
|
|
logger.error('Tuhi daemon went away')
|
|
|
|
self.mainloop.quit()
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def __getitem__(self, btaddr):
|
|
|
|
return self._devices[btaddr]
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
2018-01-23 00:03:10 +01:00
|
|
|
pass
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
class Args(object):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
class Worker(GObject.Object):
|
|
|
|
"""Implements a command to be executed.
|
|
|
|
Subclasses need to overwrite run() that will be executed
|
|
|
|
to setup the command (before the mainloop).
|
|
|
|
Subclass can also implement the stop() method which
|
|
|
|
will be executed to terminate the command, once the
|
|
|
|
mainloop has finished.
|
|
|
|
|
|
|
|
The variable need_mainloop needs to be set from the
|
|
|
|
subclass if the command requires the mainloop to be
|
|
|
|
run from an undetermined amount of time."""
|
|
|
|
|
|
|
|
need_mainloop = False
|
|
|
|
|
|
|
|
def __init__(self, manager, args=None):
|
2018-01-22 04:28:35 +01:00
|
|
|
GObject.GObject.__init__(self)
|
|
|
|
self.manager = manager
|
2018-01-25 15:55:56 +01:00
|
|
|
self._run = self.run
|
|
|
|
self._stop = self.stop
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self._run()
|
|
|
|
|
|
|
|
if self.need_mainloop:
|
|
|
|
self.manager.run()
|
|
|
|
|
|
|
|
self._stop()
|
|
|
|
|
|
|
|
|
|
|
|
class Searcher(Worker):
|
|
|
|
need_mainloop = True
|
2018-01-26 13:50:14 +01:00
|
|
|
interactive = True
|
2018-01-25 15:55:56 +01:00
|
|
|
|
|
|
|
def __init__(self, manager, args):
|
|
|
|
super(Searcher, self).__init__(manager)
|
|
|
|
self.address = args.address
|
2018-01-22 04:28:35 +01:00
|
|
|
self.is_pairing = False
|
|
|
|
|
|
|
|
def run(self):
|
2018-01-24 03:46:43 +01:00
|
|
|
if self.manager.searching:
|
|
|
|
logger.error('Another client is already searching')
|
|
|
|
return
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
self.s1 = self.manager.connect('notify::searching', self._on_notify_search)
|
|
|
|
self.s2 = self.manager.connect('pairable-device', self._on_pairable_device)
|
2018-01-22 04:28:35 +01:00
|
|
|
self.manager.start_search()
|
|
|
|
logger.debug('Started searching')
|
2018-01-23 05:21:32 +01:00
|
|
|
|
|
|
|
for d in self.manager.devices:
|
|
|
|
self._on_pairable_device(self.manager, d)
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
def stop(self):
|
2018-01-22 04:28:35 +01:00
|
|
|
if self.manager.searching:
|
|
|
|
logger.debug('Stopping search')
|
|
|
|
self.manager.stop_search()
|
2018-01-26 13:49:12 +01:00
|
|
|
self.manager.disconnect(self.s1)
|
|
|
|
self.manager.disconnect(self.s2)
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
def _on_notify_search(self, manager, pspec):
|
2018-01-23 04:44:46 +01:00
|
|
|
if not manager.searching:
|
2018-01-24 03:46:43 +01:00
|
|
|
logger.info('Search cancelled')
|
2018-01-26 13:50:14 +01:00
|
|
|
if not self.is_pairing and self.interactive:
|
2018-01-25 15:55:56 +01:00
|
|
|
self.stop()
|
2018-01-22 04:28:35 +01:00
|
|
|
|
2018-01-26 13:50:14 +01:00
|
|
|
def _on_pairable_device_interactive(self, manager, device):
|
2018-01-22 04:28:35 +01:00
|
|
|
if self.address is None:
|
|
|
|
print('Connect to device? [y/N] ', end='')
|
|
|
|
sys.stdout.flush()
|
|
|
|
i, o, e = select.select([sys.stdin], [], [], 5)
|
|
|
|
if i:
|
|
|
|
answer = sys.stdin.readline().strip()
|
|
|
|
if answer.lower() == 'y':
|
|
|
|
self.address = device.address
|
|
|
|
else:
|
|
|
|
print('timed out')
|
|
|
|
|
|
|
|
if device.address == self.address:
|
|
|
|
self.is_pairing = True
|
|
|
|
device.pair()
|
|
|
|
|
2018-01-26 13:50:14 +01:00
|
|
|
def _on_pairable_device(self, manager, device):
|
|
|
|
logger.info('Pairable device: {}'.format(device))
|
|
|
|
|
|
|
|
if self.interactive:
|
|
|
|
self._on_pairable_device_interactive(manager, device)
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
class Listener(Worker):
|
|
|
|
need_mainloop = True
|
|
|
|
|
|
|
|
def __init__(self, manager, args):
|
|
|
|
super(Listener, self).__init__(manager)
|
2018-01-23 02:29:22 +01:00
|
|
|
|
|
|
|
self.device = None
|
|
|
|
for d in manager.devices:
|
2018-01-25 15:55:56 +01:00
|
|
|
if d.address == args.address:
|
2018-01-23 02:29:22 +01:00
|
|
|
self.device = d
|
|
|
|
break
|
|
|
|
else:
|
2018-01-25 15:55:56 +01:00
|
|
|
logger.error("{}: device not found".format(args.address))
|
|
|
|
# FIXME: this should be an exception
|
2018-01-23 02:29:22 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
if self.device is None:
|
|
|
|
return
|
|
|
|
|
2018-01-24 07:12:31 +01:00
|
|
|
if self.device.drawings_available:
|
|
|
|
self._log_drawings_available(self.device)
|
2018-01-23 02:34:22 +01:00
|
|
|
|
2018-01-23 02:29:22 +01:00
|
|
|
if self.device.listening:
|
|
|
|
logger.info("{}: device already listening".format(self.device))
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug("{}: starting listening".format(self.device))
|
2018-01-25 15:55:56 +01:00
|
|
|
self.s1 = self.device.connect('notify::listening', self._on_device_listening)
|
|
|
|
self.s2 = self.device.connect('notify::drawings-available', self._on_drawings_available)
|
2018-01-23 02:29:22 +01:00
|
|
|
self.device.start_listening()
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
def stop(self):
|
2018-01-23 03:32:39 +01:00
|
|
|
logger.debug("{}: stopping listening".format(self.device))
|
2018-01-24 12:51:41 +01:00
|
|
|
try:
|
|
|
|
self.device.stop_listening()
|
2018-01-25 15:55:56 +01:00
|
|
|
self.device.disconnect(self.s1)
|
|
|
|
self.device.disconnect(self.s2)
|
2018-01-24 12:51:41 +01:00
|
|
|
except GLib.Error as e:
|
|
|
|
if (e.domain != 'g-dbus-error-quark' or
|
|
|
|
e.code != Gio.IOErrorEnum.EXISTS or
|
|
|
|
Gio.dbus_error_get_remote_error(e) != 'org.freedesktop.DBus.Error.ServiceUnknown'):
|
|
|
|
raise e
|
2018-01-23 02:29:22 +01:00
|
|
|
|
|
|
|
def _on_device_listening(self, device, pspec):
|
2018-01-25 06:44:02 +01:00
|
|
|
if self.device.listening:
|
|
|
|
return
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
logger.info('{}: Listening stopped'.format(device))
|
2018-01-23 02:29:22 +01:00
|
|
|
|
2018-01-23 02:34:22 +01:00
|
|
|
def _on_drawings_available(self, device, pspec):
|
2018-01-24 07:12:31 +01:00
|
|
|
self._log_drawings_available(device)
|
|
|
|
|
|
|
|
def _log_drawings_available(self, device):
|
|
|
|
s = ", ".join(["{}".format(t) for t in device.drawings_available])
|
|
|
|
logger.info('{}: drawings available: {}'.format(device, s))
|
2018-01-23 02:34:22 +01:00
|
|
|
|
2018-01-23 02:29:22 +01:00
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
class Fetcher(Worker):
|
|
|
|
def __init__(self, manager, args):
|
|
|
|
super(Fetcher, self).__init__(manager)
|
2018-01-23 04:36:29 +01:00
|
|
|
self.device = None
|
|
|
|
self.indices = None
|
2018-01-25 15:55:56 +01:00
|
|
|
address = args.address
|
|
|
|
index = args.index
|
2018-01-23 04:36:29 +01:00
|
|
|
|
|
|
|
for d in manager.devices:
|
|
|
|
if d.address == address:
|
|
|
|
self.device = d
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
logger.error("{}: device not found".format(address))
|
|
|
|
return
|
|
|
|
|
|
|
|
if index != 'all':
|
|
|
|
try:
|
2018-01-24 07:12:31 +01:00
|
|
|
index = int(index)
|
|
|
|
if index not in self.device.drawings_available:
|
2018-01-23 04:36:29 +01:00
|
|
|
raise ValueError()
|
2018-01-24 07:12:31 +01:00
|
|
|
self.indices = [index]
|
2018-01-23 04:36:29 +01:00
|
|
|
except ValueError:
|
|
|
|
logger.error("Invalid index {}".format(index))
|
|
|
|
return
|
|
|
|
else:
|
2018-01-24 07:12:31 +01:00
|
|
|
self.indices = self.device.drawings_available
|
2018-01-23 04:36:29 +01:00
|
|
|
|
|
|
|
def run(self):
|
|
|
|
if self.device is None or self.indices is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
for idx in self.indices:
|
|
|
|
jsondata = self.device.json(idx)
|
|
|
|
data = json.loads(jsondata)
|
2018-01-25 05:52:56 +01:00
|
|
|
t = time.gmtime(data['timestamp'])
|
|
|
|
t = time.strftime('%Y-%m-%d-%H-%M', t)
|
|
|
|
path = f'{data["devicename"]}-{t}.svg'
|
|
|
|
self.json_to_svg(data, path)
|
|
|
|
logger.info(f'{data["devicename"]}: saved file "{path}"')
|
|
|
|
|
|
|
|
def json_to_svg(self, js, filename):
|
|
|
|
dimensions = js['dimensions']
|
|
|
|
if dimensions == [0, 0]:
|
|
|
|
dimensions = 100, 100
|
|
|
|
svg = svgwrite.Drawing(filename=filename, size=dimensions)
|
|
|
|
g = svgwrite.container.Group(id='layer0')
|
|
|
|
for s in js['strokes']:
|
|
|
|
svgpoints = []
|
|
|
|
mode = 'M'
|
|
|
|
for p in s['points']:
|
|
|
|
x, y = p['position']
|
|
|
|
svgpoints.append((mode, x, y))
|
|
|
|
mode = 'L'
|
|
|
|
path = svgwrite.path.Path(d=svgpoints,
|
|
|
|
style="fill:none;stroke:black;stroke-width:5")
|
|
|
|
g.add(path)
|
|
|
|
|
|
|
|
svg.add(g)
|
|
|
|
svg.save()
|
2018-01-23 04:36:29 +01:00
|
|
|
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
class Printer(Worker):
|
|
|
|
def run(self):
|
|
|
|
logger.debug('Listing available devices:')
|
|
|
|
for d in self.manager.devices:
|
|
|
|
print(d)
|
2018-01-23 04:36:29 +01:00
|
|
|
|
|
|
|
|
2018-01-23 18:18:05 +01:00
|
|
|
class TuhiKeteShellLogHandler(logging.StreamHandler):
|
|
|
|
def __init__(self):
|
|
|
|
super(TuhiKeteShellLogHandler, self).__init__(sys.stdout)
|
|
|
|
self.setFormatter(logging.Formatter(log_format))
|
2018-01-26 12:33:37 +01:00
|
|
|
self._prompt = ''
|
|
|
|
|
|
|
|
def emit(self, record):
|
|
|
|
self.terminator = f'\n{self._prompt}{readline.get_line_buffer()}'
|
|
|
|
super(TuhiKeteShellLogHandler, self).emit(record)
|
2018-01-23 18:18:05 +01:00
|
|
|
|
|
|
|
def set_normal_mode(self):
|
|
|
|
self.acquire()
|
|
|
|
self.setFormatter(logging.Formatter(log_format))
|
|
|
|
self.terminator = '\n'
|
2018-01-26 12:33:37 +01:00
|
|
|
self._prompt = ''
|
2018-01-23 18:18:05 +01:00
|
|
|
self.release()
|
|
|
|
|
|
|
|
def set_prompt_mode(self, prompt):
|
|
|
|
self.acquire()
|
2018-01-26 12:33:37 +01:00
|
|
|
# '\x1b[2K\r' clears the current line and start again from the beginning
|
|
|
|
self.setFormatter(logging.Formatter(f'\x1b[2K\r{log_format}'))
|
|
|
|
self._prompt = prompt
|
2018-01-23 18:18:05 +01:00
|
|
|
self.release()
|
|
|
|
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
class TuhiKeteShell(cmd.Cmd):
|
|
|
|
intro = 'Tuhi shell control'
|
|
|
|
prompt = 'tuhi> '
|
|
|
|
|
|
|
|
def __init__(self, manager, completekey='tab', stdin=None, stdout=None):
|
|
|
|
super(TuhiKeteShell, self).__init__(completekey, stdin, stdout)
|
|
|
|
self._manager = manager
|
|
|
|
self._workers = []
|
2018-01-23 18:18:05 +01:00
|
|
|
self._log_handler = TuhiKeteShellLogHandler()
|
|
|
|
logger.removeHandler(logger_handler)
|
|
|
|
logger.addHandler(self._log_handler)
|
2018-01-26 12:55:52 +01:00
|
|
|
# patching get_names to hide some functions we do not want in the help
|
|
|
|
self.get_names = self._filtered_get_names
|
|
|
|
|
|
|
|
def _filtered_get_names(self):
|
|
|
|
names = super(TuhiKeteShell, self).get_names()
|
|
|
|
names.remove('do_EOF')
|
|
|
|
return names
|
2018-01-26 11:20:36 +01:00
|
|
|
|
|
|
|
def emptyline(self):
|
|
|
|
# make sure we do not re-enter the last typed command
|
|
|
|
pass
|
|
|
|
|
|
|
|
def do_EOF(self, arg):
|
|
|
|
print('\n\r', end='') # to remove the appended weird char
|
|
|
|
return self.do_exit(arg)
|
|
|
|
|
|
|
|
def do_exit(self, args):
|
|
|
|
'''leave the shell'''
|
|
|
|
for worker in self._workers:
|
|
|
|
worker.stop()
|
|
|
|
return True
|
|
|
|
|
2018-01-23 18:18:05 +01:00
|
|
|
def precmd(self, line):
|
|
|
|
# Restore the logger facility to something sane:
|
|
|
|
self._log_handler.set_normal_mode()
|
|
|
|
return line
|
|
|
|
|
|
|
|
def postcmd(self, stop, line):
|
|
|
|
# overwrite the logger facility to remove the current prompt and append
|
|
|
|
# a new one
|
|
|
|
self._log_handler.set_prompt_mode(self.prompt)
|
|
|
|
return stop
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
def run(self, init=None):
|
|
|
|
try:
|
|
|
|
self.cmdloop(init)
|
|
|
|
except KeyboardInterrupt as e:
|
|
|
|
print("^C")
|
|
|
|
self.run('')
|
|
|
|
|
|
|
|
def start_worker(self, worker_class, args=None):
|
|
|
|
worker = worker_class(self._manager, args)
|
|
|
|
worker.run()
|
|
|
|
self._workers.append(worker)
|
|
|
|
|
|
|
|
def do_list(self, arg):
|
|
|
|
'''list known devices'''
|
|
|
|
self.start_worker(Printer)
|
|
|
|
|
2018-01-26 16:00:30 +01:00
|
|
|
def help_listen(self):
|
|
|
|
self.do_listen('-h')
|
2018-01-26 11:20:36 +01:00
|
|
|
|
2018-01-26 16:00:30 +01:00
|
|
|
def do_listen(self, args):
|
|
|
|
'''Listen to a specific device'''
|
|
|
|
parser = argparse.ArgumentParser(prog='listen',
|
|
|
|
description='Listen to a specific device',
|
|
|
|
add_help=False)
|
|
|
|
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
|
|
|
default=None,
|
|
|
|
help='the address of the device to listen to')
|
|
|
|
parser.add_argument('mode', choices=['on', 'off'], nargs='?',
|
|
|
|
const='on', default='on')
|
2018-01-26 11:20:36 +01:00
|
|
|
try:
|
2018-01-26 16:00:30 +01:00
|
|
|
parsed_args = parser.parse_args(args.split())
|
|
|
|
except SystemExit:
|
2018-01-26 11:20:36 +01:00
|
|
|
return
|
|
|
|
|
2018-01-26 16:00:30 +01:00
|
|
|
address = parsed_args.address
|
|
|
|
mode = parsed_args.mode
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
for d in self._manager.devices:
|
|
|
|
if d.address == address:
|
|
|
|
if mode == 'on' and d.listening:
|
|
|
|
print(f'Already listening on {address}')
|
|
|
|
return
|
|
|
|
elif mode == 'off' and not d.listening:
|
|
|
|
print(f'Not listening on {address}')
|
|
|
|
return
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
print(f'Device {address} not found')
|
|
|
|
return
|
|
|
|
|
|
|
|
if mode == 'off':
|
|
|
|
for worker in [w for w in self._workers if isinstance(w, Listener)]:
|
|
|
|
if worker.device.address == address:
|
|
|
|
worker.stop()
|
|
|
|
self._workers.remove(worker)
|
|
|
|
break
|
|
|
|
return
|
|
|
|
|
|
|
|
wargs = Args()
|
|
|
|
wargs.address = address
|
|
|
|
self.start_worker(Listener, wargs)
|
|
|
|
|
2018-01-26 13:05:15 +01:00
|
|
|
def help_fetch(self):
|
|
|
|
self.do_fetch('-h')
|
|
|
|
|
|
|
|
def do_fetch(self, args):
|
|
|
|
'''Fetches one or all drawing(s) from a specific device.'''
|
|
|
|
|
|
|
|
def is_index_or_all(string):
|
|
|
|
try:
|
|
|
|
n = int(string)
|
|
|
|
except ValueError:
|
|
|
|
if string == 'all':
|
|
|
|
return string
|
|
|
|
raise argparse.ArgumentTypeError(f'"{string}" is neither a timestamp nor "all"')
|
|
|
|
else:
|
|
|
|
return n
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog='fetch',
|
|
|
|
description='Fetches a drawing or all drawings from a specific device.',
|
|
|
|
add_help=False)
|
|
|
|
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
|
|
|
default=None,
|
|
|
|
help='the address of the device to fetch drawing from')
|
|
|
|
parser.add_argument('index', metavar='{<index>|all}',
|
|
|
|
type=is_index_or_all,
|
|
|
|
const='all', nargs='?', default='all',
|
|
|
|
help='the index of the drawing to fetch or a literal "all"')
|
|
|
|
|
|
|
|
try:
|
|
|
|
parsed_args = parser.parse_args(args.split())
|
|
|
|
except SystemExit:
|
|
|
|
return
|
|
|
|
|
|
|
|
address = parsed_args.address
|
|
|
|
index = parsed_args.index
|
|
|
|
|
|
|
|
if index != 'all':
|
|
|
|
try:
|
|
|
|
int(index)
|
|
|
|
except ValueError:
|
|
|
|
print(self._fetch_usage)
|
|
|
|
return
|
|
|
|
|
|
|
|
wargs = Args()
|
|
|
|
wargs.address = address
|
|
|
|
wargs.index = index
|
|
|
|
self.start_worker(Fetcher, wargs)
|
|
|
|
|
2018-01-26 13:50:14 +01:00
|
|
|
def help_search(self):
|
|
|
|
self.do_search('-h')
|
|
|
|
|
|
|
|
def do_search(self, args):
|
|
|
|
'''Start/Stop listening for devices in pairable mode'''
|
|
|
|
parser = argparse.ArgumentParser(prog='search',
|
|
|
|
description='Start/Stop listening for devices in pairable mode.',
|
|
|
|
add_help=False)
|
|
|
|
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('mode', choices=['on', 'off'], nargs='?',
|
|
|
|
const='on', default='on')
|
|
|
|
|
|
|
|
try:
|
|
|
|
parsed_args = parser.parse_args(args.split())
|
|
|
|
except SystemExit:
|
|
|
|
return
|
|
|
|
|
|
|
|
if parsed_args.mode == 'off':
|
|
|
|
self._manager.stop_search()
|
|
|
|
return
|
|
|
|
|
|
|
|
Searcher.interactive = False
|
|
|
|
wargs = Args()
|
|
|
|
wargs.address = None
|
|
|
|
self.start_worker(Searcher, wargs)
|
|
|
|
|
|
|
|
def help_pair(self):
|
|
|
|
self.do_pair('-h')
|
|
|
|
|
|
|
|
def do_pair(self, args):
|
|
|
|
'''Pair a specific device in pairable mode'''
|
|
|
|
if not self._manager.searching and '-h' not in args.split():
|
|
|
|
print("please call search first")
|
|
|
|
return
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog='pair',
|
|
|
|
description='Pair a specific device in pairable mode.',
|
|
|
|
add_help=False)
|
|
|
|
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
|
|
|
default=None,
|
|
|
|
help='the address of the device to pair')
|
|
|
|
|
|
|
|
try:
|
|
|
|
parsed_args = parser.parse_args(args.split())
|
|
|
|
except SystemExit:
|
|
|
|
return
|
|
|
|
|
|
|
|
address = parsed_args.address
|
|
|
|
|
|
|
|
device = None
|
|
|
|
|
|
|
|
for d in self._manager.devices:
|
|
|
|
if d.address == address:
|
|
|
|
device = d
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
logger.error("{}: device not found".format(address))
|
|
|
|
return
|
|
|
|
|
|
|
|
device.pair()
|
|
|
|
|
2018-01-26 17:23:30 +01:00
|
|
|
def help_info(self):
|
|
|
|
self.do_info('-h')
|
|
|
|
|
|
|
|
def do_info(self, args):
|
|
|
|
'''Show some informations about a given device or all of them'''
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog='info',
|
|
|
|
description='Show some informations about a given device or all of them',
|
|
|
|
add_help=False)
|
|
|
|
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
|
|
|
default=None, nargs='?',
|
|
|
|
help='the address of the device to listen to')
|
|
|
|
|
|
|
|
try:
|
|
|
|
parsed_args = parser.parse_args(args.split())
|
|
|
|
except SystemExit:
|
|
|
|
return
|
|
|
|
|
|
|
|
for device in self._manager.devices:
|
|
|
|
if parsed_args.address is None or parsed_args.address == device.address:
|
|
|
|
print(device)
|
|
|
|
print('\tAvailable drawings:')
|
|
|
|
for d in device.drawings_available:
|
|
|
|
t = time.localtime(d)
|
|
|
|
t = time.strftime('%Y-%m-%d at %H:%M', t)
|
|
|
|
print(f'\t\t* {d}: drawn on the {t}')
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
|
|
|
|
class TuhiKeteShellWorker(Worker):
|
|
|
|
def __init__(self, manager, args):
|
|
|
|
super(TuhiKeteShellWorker, self).__init__(manager)
|
|
|
|
|
|
|
|
def start_mainloop(self):
|
|
|
|
# we can not call GLib.MainLoop() here or it will install a unix signal
|
|
|
|
# handler for SIGINT, and we will not be able to catch
|
|
|
|
# KeyboardInterrupt in cmdloop()
|
|
|
|
mainloop = GLib.MainLoop.new(None, False)
|
|
|
|
|
|
|
|
mainloop.run()
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self._glib_thread = threading.Thread(target=self.start_mainloop)
|
|
|
|
self._glib_thread.daemon = True
|
|
|
|
self._glib_thread.start()
|
|
|
|
|
|
|
|
self.run()
|
|
|
|
|
|
|
|
self.stop()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self._shell = TuhiKeteShell(self.manager)
|
|
|
|
self._shell.run()
|
|
|
|
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def parse_list(parser):
|
|
|
|
sub = parser.add_parser('list', help='list known devices')
|
2018-01-25 15:55:56 +01:00
|
|
|
sub.set_defaults(worker=Printer)
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
def parse_pair(parser):
|
|
|
|
sub = parser.add_parser('pair', help='pair a new device')
|
2018-01-26 17:05:50 +01:00
|
|
|
sub.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
2018-01-22 04:28:35 +01:00
|
|
|
nargs='?', default=None,
|
|
|
|
help='the address of the device to pair')
|
2018-01-25 15:55:56 +01:00
|
|
|
sub.set_defaults(worker=Searcher)
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
|
2018-01-23 02:29:22 +01:00
|
|
|
def parse_listen(parser):
|
|
|
|
sub = parser.add_parser('listen', help='listen to events from a device')
|
2018-01-26 17:05:50 +01:00
|
|
|
sub.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
2018-01-23 02:29:22 +01:00
|
|
|
default=None,
|
|
|
|
help='the address of the device to listen to')
|
2018-01-25 15:55:56 +01:00
|
|
|
sub.set_defaults(worker=Listener)
|
2018-01-23 02:29:22 +01:00
|
|
|
|
|
|
|
|
2018-01-23 04:36:29 +01:00
|
|
|
def parse_fetch(parser):
|
2018-01-25 05:52:56 +01:00
|
|
|
sub = parser.add_parser('fetch', help='download a drawing from a device and save as svg in $PWD')
|
2018-01-26 17:05:50 +01:00
|
|
|
sub.add_argument('address', metavar='12:34:56:AB:CD:EF',
|
|
|
|
type=TuhiKeteDevice.is_device_address,
|
2018-01-23 04:36:29 +01:00
|
|
|
default=None,
|
|
|
|
help='the address of the device to fetch from')
|
|
|
|
sub.add_argument('index', metavar='[<index>|all]', type=str,
|
|
|
|
default=None,
|
|
|
|
help='the index of the drawing to fetch or a literal "all"')
|
2018-01-25 15:55:56 +01:00
|
|
|
sub.set_defaults(worker=Fetcher)
|
2018-01-23 04:36:29 +01:00
|
|
|
|
|
|
|
|
2018-01-26 11:20:36 +01:00
|
|
|
def parse_shell(parser):
|
|
|
|
sub = parser.add_parser('shell', help='run a bash-like shell')
|
|
|
|
sub.set_defaults(worker=TuhiKeteShellWorker)
|
|
|
|
|
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
def parse(args):
|
|
|
|
desc = 'Commandline client to the Tuhi DBus daemon'
|
|
|
|
parser = argparse.ArgumentParser(description=desc)
|
|
|
|
parser.add_argument('-v', '--verbose',
|
|
|
|
help='Show some debugging informations',
|
|
|
|
action='store_true',
|
|
|
|
default=False)
|
|
|
|
|
|
|
|
subparser = parser.add_subparsers(help='Available commands')
|
|
|
|
parse_list(subparser)
|
|
|
|
parse_pair(subparser)
|
2018-01-23 02:29:22 +01:00
|
|
|
parse_listen(subparser)
|
2018-01-23 04:36:29 +01:00
|
|
|
parse_fetch(subparser)
|
2018-01-26 11:20:36 +01:00
|
|
|
parse_shell(subparser)
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
return parser.parse_args(args[1:])
|
|
|
|
|
|
|
|
|
|
|
|
def main(args):
|
|
|
|
args = parse(args)
|
|
|
|
if args.verbose:
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
2018-01-25 15:55:56 +01:00
|
|
|
if not hasattr(args, 'worker'):
|
2018-01-26 11:20:36 +01:00
|
|
|
args.worker = TuhiKeteShellWorker
|
2018-01-25 15:55:56 +01:00
|
|
|
|
2018-01-22 04:28:35 +01:00
|
|
|
try:
|
|
|
|
with TuhiKeteManager() as mgr:
|
2018-01-25 15:55:56 +01:00
|
|
|
worker = args.worker(mgr, args)
|
|
|
|
worker.start()
|
2018-01-22 04:28:35 +01:00
|
|
|
|
|
|
|
except DBusError as e:
|
|
|
|
logger.error(e.message)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main(sys.argv)
|