diff options
-rwxr-xr-x | client.py | 3 | ||||
-rw-r--r-- | consumer.py | 143 | ||||
-rw-r--r-- | interfaces/configuration.py | 14 | ||||
-rwxr-xr-x | interfaces/endpoint.py | 5 | ||||
-rwxr-xr-x | macropad.pyw | 64 | ||||
-rw-r--r-- | socketserver.py | 11 | ||||
-rw-r--r-- | win32.py | 8 | ||||
-rw-r--r-- | xlib.py | 9 |
8 files changed, 186 insertions, 71 deletions
@@ -40,7 +40,6 @@ s = component.queryAdapter(conn, endpoint.IEndpoint) if args.layer is not None:
print(args.layer)
- s.queue = Queue()
s.connect()
with open(args.layer, "r") as json_file:
json_data = json_file.read()
@@ -57,7 +56,7 @@ class Conn(Thread): Thread.__init__(self)
self.queue = queue
self.in_queue = Queue()
- s.queue = self.in_queue
+ #s.queue = self.in_queue
s.connect()
def run(self):
diff --git a/consumer.py b/consumer.py new file mode 100644 index 0000000..9e36dd0 --- /dev/null +++ b/consumer.py @@ -0,0 +1,143 @@ +from typing import Dict
+
+from zope import component, interface
+
+from interfaces.message import ISocketMessage, Debug
+
+@interface.implementer(ISocketMessage)
+class Mapping(object):
+ """ Send an event requesting a layer change to the endpoint.
+ """
+
+ def __init__(self, message):
+ self.content = {"layer": message}
+
+import abc
+from threading import Thread
+from queue import Queue, Empty, Full
+
+class EventConsummer(Thread, metaclass=abc.ABCMeta):
+ """ Thread processing messages. This class does nothing and is intended to
+ be inherited.
+
+ The method `process` need to be overriden.
+ """
+
+ def __init__(self, timeout = None):
+ super().__init__()
+ self._queue = Queue()
+ self.timeout = timeout
+ self.daemon = True
+
+ def run(self):
+ """ Read and process the messages from the queue, and call the
+ handler.
+ """
+
+ while True:
+ # Block the thread for at most timeout seconds
+ try:
+ message = self._queue.get(timeout=self.timeout)
+ self._process(message)
+ self._queue.task_done()
+ except Empty:
+ self._process(None)
+
+ @abc.abstractmethod
+ def _process(self, message) -> None:
+ raise NotImplementedError()
+
+ def handle(self, message) -> None:
+ """ Register a new event in the queue.
+ This method is intended to be public.
+ """
+
+ # Try to add the message in the queue, if the queue is full, discard
+ # the firsts elements and try again.
+ try:
+ self._queue.put(message)
+ except Full:
+ while self._queue.full():
+ _ = self._queue.get()
+ # Discard another one more, just for sure.
+ try:
+ self._queue.put(message)
+ except Full:
+ component.handle(Debug("Ignoring event: queue is full"))
+
+import time
+from interfaces import endpoint, configuration
+from interfaces.configuration import IConfiguration
+
+class SocketMessageConsumer(EventConsummer):
+ """ Provide a handler consuming events and sending them to the endpoint.
+ It’s not an adapter as it’s not transforming the endpoint into
+ something else; instead, it requires an existing endpoint to
+ be declared in the registry.
+
+ The class register a handler_ for SocketEvents. They are queued and
+ processed when as the endpoint is connected. Events can be send as
+ usual using the component registry:
+
+ component.handle(Mapping(…))
+
+ .. _handler: https://zopecomponent.readthedocs.io/en/latest/narr.html#handlers
+ """
+
+ def __init__(self):
+ super().__init__(timeout = 2)
+ self._endpoint = component.getUtility(endpoint.IEndpoint)
+ self._last_mapping = None
+ component.provideHandler(self.handle, [ISocketMessage])
+
+ # Register the functions to associate for each kind of event.
+ self.function_table = {
+ "layer": self._process_layer,
+ }
+
+ def _process(self, message) -> None:
+
+ # We are here either because we have a message to process, or because
+ # we didn’t got any message before the timeout. We check if we are
+ # still connected to the endpoint.
+ while not self._endpoint.isConnected():
+ # Loop until we get the connection back. During this time, the
+ # message will stack in the queue.
+ component.handle(Debug("Reconnecting …"))
+ self._endpoint.state = self._endpoint.STATE_CONNECTING
+ self._endpoint.connect()
+ time.sleep(2)
+ # Also check if we have something to read from the server.
+ self._endpoint.fetch()
+
+ # The endpoint is connected and we have no message the process.
+ if message is None:
+ return
+
+ # Dispatch the event to the appropriate function.
+ for key, value in message.content.items():
+ self.function_table[key](value)
+
+ def _process_layer(self, message) -> None:
+
+ mapping, name = message
+ if mapping == self._last_mapping:
+ return
+ self._last_mapping = mapping
+
+ configuration = component.getUtility(IConfiguration)
+ if isinstance(mapping, dict):
+ self._endpoint.send(mapping)
+ elif isinstance(data, str):
+ # TODO Query the json from the configuration
+ layer = configuration.get(mapping, None)
+ if layer is not None:
+ self._endpoint.send(layer)
+
+ if name is not None:
+ # We received a name to associate the configuration with.
+ # Register the element in the configuration
+ # TODO use an event
+ configuration[name] = mapping
+ for key in mapping.keys():
+ component.handle(Debug("Associating %s with %s" % (name, key)))
diff --git a/interfaces/configuration.py b/interfaces/configuration.py new file mode 100644 index 0000000..9449f41 --- /dev/null +++ b/interfaces/configuration.py @@ -0,0 +1,14 @@ +from zope import interface +from zope.interface import Attribute + +from typing import Dict + +class IConfiguration(interface.Interface): + + def get(self, key : str, default) -> Dict: + """ Load the mapping for a given key + """ + + def __setitem__(self, key : str, value : Dict) -> None: + """ Set a value + """ diff --git a/interfaces/endpoint.py b/interfaces/endpoint.py index 2d77b66..4da7c57 100755 --- a/interfaces/endpoint.py +++ b/interfaces/endpoint.py @@ -47,6 +47,7 @@ from zope import component import json
from interfaces.message import IMessage, Debug
+from consumer import Mapping
@component.adapter(IConnection)
@interface.implementer(IEndpoint)
@@ -74,7 +75,7 @@ class EndPoint(object): # This is the first connection
# Otherwise the state should be STATE_CONNECTING
# Initialize with the default layer
- self.queue.put ( ("default", None) )
+ component.handle(Mapping(("default", None)))
except Exception as e:
print(e)
@@ -103,7 +104,7 @@ class EndPoint(object): title = desktop.getForegroundWindowTitle()
else:
title = None
- self.queue.put((layout, title))
+ component.handle(Mapping((layout, title)))
def send(self, data: list[dict[str, str]]):
""" Send the data to the endpoint. The data must be the representation
diff --git a/macropad.pyw b/macropad.pyw index f73cd1c..c7e8770 100755 --- a/macropad.pyw +++ b/macropad.pyw @@ -14,7 +14,7 @@ import sys from zope import component import interfaces -from interfaces import endpoint +from interfaces import endpoint, configuration from interfaces.message import IMessage, Debug import configparser @@ -36,6 +36,7 @@ from queue import Queue q = Queue() component.provideAdapter(interfaces.endpoint.EndPoint) +component.provideUtility(mapping, interfaces.configuration.IConfiguration) # # Guess the platform and the load the corresponding event listener @@ -46,28 +47,26 @@ component.provideAdapter(interfaces.endpoint.EndPoint) # if config.has_section("connection.serial"): - from serial_conn import SerialConnection endpoint = component.queryAdapter(SerialConnection(config["connection.serial"]), endpoint.IEndpoint) - endpoint.queue = q - endpoint.connect() - component.provideUtility(endpoint, interfaces.endpoint.IEndpoint) elif config.has_section("connection.socket"): - from socket_conn import SocketConnection endpoint = component.queryAdapter(SocketConnection(config["connection.socket"]), endpoint.IEndpoint) - endpoint.queue = q - component.provideUtility(endpoint, interfaces.endpoint.IEndpoint) - endpoint.connect() + +component.provideUtility(endpoint, interfaces.endpoint.IEndpoint) +endpoint.connect() if config.has_section("socket.serve"): import socketserver - server = socketserver.Handler(config["socket.serve"], q) + server = socketserver.Handler(config["socket.serve"]) else: server = None +import consumer +handler = consumer.SocketMessageConsumer() +handler.start() class Icon(object): @@ -147,10 +146,10 @@ class Application(object): component.handle(Debug(platform)) if platform == "win32": import win32 - listener = win32.Listener(mapping, q) + listener = win32.Listener(mapping) elif platform == 'linux': import xlib - listener = xlib.Listener(mapping, q) + listener = xlib.Listener(mapping) component.handle(Debug("Starting xlib")) component.provideUtility(listener, interfaces.desktopEvent.IDesktop) listener.start() @@ -202,46 +201,10 @@ class Application(object): except Exception as e: print(e) - - def send(self, data: str): - """ Send the configuration to the device. - The configuration can be either - - a dictionnary, and will be send as is - - a string, and will be load in a file - If the content is the same, ignore the message and return. - """ - if data == self.last_layout: - return - # Merge the new layout with the previous one, ignoring all the null. - self.last_layout = data - - conn = component.queryUtility(interfaces.endpoint.IEndpoint) - if isinstance(data, dict): - conn.send(data) - elif isinstance(data, str): - layer = mapping.get(data, None) - if layer is not None: - conn.send(layer) - - def associate(self, layout: Dict, name: str): - mapping[name] = layout - for key in layout.keys(): - component.handle(Debug("Associating %s with %s" % (name, key))) - def exec(self): try: self.update() if server is not None: server.update() - conn = component.queryUtility(interfaces.endpoint.IEndpoint) - if not conn.isConnected(): - component.handle(Debug("Reconnecting…")) - - conn.state = conn.STATE_CONNECTING - self.window.after(1000, conn.connect) - else: - # Check if we have something to read from the server, and by - # the by if the server is still active. - conn.fetch() except BaseException as e: component.handle(Debug( str(e) )) print(e) @@ -251,11 +214,6 @@ class Application(object): if app.running: self.window.after(200, self.exec) - while not q.empty(): - last_layout, app_ = q.get(False) - self.send(last_layout) - if app_ is not None: self.associate(last_layout, app_) - if __name__ == '__main__': # Start the main application, Initializing the message listener before # listening desktop events diff --git a/socketserver.py b/socketserver.py index 74f0940..eb7d4fc 100644 --- a/socketserver.py +++ b/socketserver.py @@ -34,14 +34,14 @@ actions = { "layout" : lambda x : x,
}
+from consumer import Mapping # Event used to send the mapping change request
+
class Handler(object):
""" Listen the incomming connexions and dispatch them into the connexion
object """
- def __init__(self, configuration, layout_queue):
+ def __init__(self, configuration):
""" Initialize the the socket server
-
- The layout_queue will be populated with all the elements received from the socket.
"""
super().__init__()
self.sel = selectors.DefaultSelector()
@@ -66,7 +66,6 @@ class Handler(object): self.connexions = []
- self.layout_queue = layout_queue
@component.adapter(ISocketMessage)
def sendMessage(self, content:ISocketMessage) -> None:
@@ -130,11 +129,11 @@ class Handler(object): # As we have a name in the configuration to associate with, use
# this name as a reference
for title in self.application_name:
- self.layout_queue.put((js, title))
+ component.handle(Mapping((js, title)))
else:
# Associate the layout with the current window
title = component.queryUtility(desktopEvent.IDesktop).getForegroundWindowTitle()
- self.layout_queue.put((js, title))
+ component.handle(Mapping((js, title)))
def _send(self:object, conn:socket, mask:int, text) -> None:
""" Internal method used to dispatch the message to the socket. """
@@ -9,6 +9,7 @@ import win32gui import win32con
from zope import component
from interfaces.message import IMessage, Debug
+from consumer import Mapping
# This code tries to hook the Focus changes events with the callback method.
@@ -54,10 +55,9 @@ def setHook(WinEventProc, eventType): @interface.implementer(desktopEvent.IDesktop)
class Listener(object):
- def __init__(self, mapping: Dict[str, str], queue):
+ def __init__(self, mapping: Dict[str, str]):
self.WinEventProc = WinEventProcType(self.callback)
self.mapping = mapping
- self.queue = queue
def getForegroundWindowTitle(self) -> Optional[str]:
""" Get the window title name.
@@ -100,7 +100,7 @@ class Listener(object): if pattern == "default":
continue
if pattern in title:
- self.queue.put ( (code, None) )
+ component.handle(Mapping((code, None)))
return
# Get the default mapping to apply if there is any defined
# This only applies when the window raising the event is the main
@@ -114,7 +114,7 @@ class Listener(object): # called with the GIL held, but the GIL is released (the current
# Python thread state is NULL)
print("Mapping '%s' to default" % title)
- self.queue.put ( ("default", None) )
+ component.handle(Mapping((default, None)))
def start(self) -> None:
self.hookIDs = [setHook(self.WinEventProc, et) for et in eventTypes.keys()]
@@ -8,12 +8,13 @@ from threading import Thread from interfaces.message import IMessage, Debug
from zope import component
+from consumer import Mapping
+
@interface.implementer(desktopEvent.IDesktop)
class Listener(Thread):
- def __init__(self, mapping, queue):
+ def __init__(self, mapping):
Thread.__init__(self)
- self.queue = queue
self.mapping = mapping
self.active_window = None
self.last_code = None
@@ -80,7 +81,7 @@ class Listener(Thread): if code != self.last_code:
component.handle(Debug("Switching to '%s' for '%s'" % (pattern, window_name)))
- self.queue.put ( (code, None) )
+ component.handle(Mapping((code, None)))
self.last_code = code
# We found a matching configuration. Even if the match is the
# same as the current one, we break the loop in order to
@@ -91,7 +92,7 @@ class Listener(Thread): if default is None:
return
- self.queue.put ( (default, None) )
+ component.handle(Mapping((default, None)))
self.last_code = "default"
|