Skip to content

Commit 0b5bf9f

Browse files
committed
Add the EventProcessor and make print_stat_doubler use it
1 parent 8d36425 commit 0b5bf9f

File tree

9 files changed

+631
-56
lines changed

9 files changed

+631
-56
lines changed

prusa/link/const.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
PrusaLink
44
"""
55
import uuid
6+
from enum import Enum
67
from importlib.resources import files # type: ignore
78
from os import path
89
from typing import List
@@ -68,6 +69,7 @@
6869
TELEMETRY_SLEEP_AFTER = 3 * 60
6970
TELEMETRY_REFRESH_INTERVAL = 5 * 60 # full telemetry re-send
7071

72+
EVENT_TICK_INTERVAL = 0.2
7173
FAST_POLL_INTERVAL = 1
7274
SLOW_POLL_INTERVAL = 10 # for values, that aren't that important
7375
VERY_SLOW_POLL_INTERVAL = 30
@@ -242,3 +244,10 @@ class LimitsMK3S(LimitsFDM):
242244
SUPPORTED_PRINTERS = {
243245
"2c99": {"0001", "0002"},
244246
}
247+
248+
249+
class InputEventName(Enum):
250+
"""Event names for input events of the EventProcessor"""
251+
TICK = "TICK"
252+
OK_RECEIVED = "OK_RECEIVED"
253+
PRINT_INFO_RECEIVED = "PRINT_STATS_RECEIVED"
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""
2+
The idea behind these:
3+
4+
State observers take many events and generate concise actionable ones
5+
- They observe events
6+
- They accumulate state
7+
- Their state is private
8+
- They generate events
9+
10+
This should simplify the global state and move some of the workarounds
11+
into smaller pieces of code.
12+
13+
The state watchers have one thread that processes incoming events.
14+
Included is an output event handling thread to notify the rest of the app
15+
"""
16+
from enum import Enum
17+
from queue import Queue
18+
from threading import Event, Thread
19+
from typing import Any, Callable, Dict, List, Optional, Set, Union
20+
21+
22+
class ProcessorEvent:
23+
"""An event object either enqueued for processing or output from the
24+
state watchers"""
25+
26+
def __init__(self, name, *args, **kwargs):
27+
self.name = name
28+
self.args = args
29+
self.kwargs = kwargs
30+
31+
32+
InputHandlerType = Callable[[ProcessorEvent],
33+
Optional[List[ProcessorEvent]]]
34+
RawHandlerType = Callable[[ProcessorEvent],
35+
Union[None,
36+
ProcessorEvent,
37+
List[ProcessorEvent]]]
38+
OutputHandlerType = Callable[[ProcessorEvent], None]
39+
40+
41+
class EventInfo:
42+
"""An object to hold the event info and how to register a callback
43+
that would generate such event. Optionally a callback to de-register it
44+
to save on CPU power when the event is not being consumed by anything"""
45+
46+
def __init__(self,
47+
name,
48+
registration,
49+
deregistration=None):
50+
self.name = name
51+
self.registration = registration
52+
self.deregistration = deregistration
53+
self.registered = False
54+
self.callback = None
55+
56+
self.users = set()
57+
58+
def add_watcher(self, user):
59+
"""Add an EventWatcher consumer to the event"""
60+
self.users.add(user)
61+
if len(self.users) == 1 and not self.registered:
62+
self.registration(self._handler)
63+
self.registered = True
64+
65+
def remove_watcher(self, user):
66+
"""Remove an EventWatcher consumer from the event"""
67+
if user not in self.users:
68+
return
69+
self.users.remove(user)
70+
if len(self.users) == 0 and self.deregistration is not None:
71+
self.deregistration(self._handler)
72+
self.registered = False
73+
74+
def set_callback(self, callback: InputHandlerType):
75+
"""Set the callback that will be called when the event is generated
76+
This should be called only by the event processor when registering
77+
the event for the first time"""
78+
if self.callback is not None:
79+
raise ValueError("Callback already set")
80+
self.callback = callback
81+
82+
def _handler(self, *args, **kwargs):
83+
"""Adapts the callback call to generate the event that is then
84+
enqueued for processing"""
85+
self.callback(ProcessorEvent(self.name, *args, **kwargs))
86+
87+
88+
class StateWatcher:
89+
"""State watchers try to decrease the number of events that influence the
90+
global state. This is their root class"""
91+
92+
class OutputEvent(Enum):
93+
"""Events that are generated by the state watcher"""
94+
95+
def __init__(self):
96+
# Maps the event names to handlers
97+
self.event_handlers: Dict[Enum, RawHandlerType] = {}
98+
# Gets filled after the watcher is registered with the event processor
99+
# Allows the event watcher to stop watching for selected events
100+
self.tracked_events: Dict[Enum, EventInfo] = {}
101+
102+
def register(self, tracked_events: Dict[Any, EventInfo]):
103+
"""Watcher has been registered with the event processor,
104+
here is the event info it's been missing"""
105+
self.tracked_events = tracked_events
106+
for event_info in self.tracked_events.values():
107+
event_info.add_watcher(self)
108+
self._after_register()
109+
110+
def _after_register(self):
111+
"""Called after the watcher has been registered with the event
112+
processor"""
113+
114+
def process_event(self, small_event: ProcessorEvent):
115+
"""Process an event, return a list of output events for later
116+
handling"""
117+
handler = self.event_handlers.get(small_event.name)
118+
if handler:
119+
output = handler(*small_event.args, **small_event.kwargs)
120+
if output is None:
121+
return None
122+
if isinstance(output, ProcessorEvent):
123+
return [output]
124+
if isinstance(output, list):
125+
return output
126+
raise ValueError("The state watcher handler should return None, "
127+
"a ProcessorEvent or a list of ProcessorEvents")
128+
return None
129+
130+
def _get_event_info(self, event_name):
131+
"""Get the event info for the event name, throw an exception if its
132+
not tracked"""
133+
try:
134+
return self.tracked_events[event_name]
135+
except KeyError as error:
136+
raise ValueError(f"Event {event_name} not tracked by this "
137+
f"watcher") from error
138+
139+
def _stop_watching(self, event_name: Enum):
140+
"""Stop watching for an event"""
141+
self._get_event_info(event_name).remove_watcher(self)
142+
143+
def _watch(self, event_name: Enum):
144+
"""Start watching for an event"""
145+
self._get_event_info(event_name).add_watcher(self)
146+
147+
148+
class EventProcessor:
149+
"""Allows you to register StateWatchers and their handlers
150+
Idea - make the state watchers be able to decide what events to disable
151+
for themselves"""
152+
153+
def __init__(self):
154+
self.input_queue: Queue[Optional[ProcessorEvent]] = Queue()
155+
self.output_queue: Queue[Optional[ProcessorEvent]] = Queue()
156+
self.quit_evt = Event()
157+
self.tracked_events: Dict[Enum, EventInfo] = {}
158+
self.watcher_output_events: Dict[Enum, StateWatcher] = {}
159+
self.output_handlers: Dict[Enum, OutputHandlerType] = {}
160+
self.watchers: Dict[EventInfo, Set[StateWatcher]] = {}
161+
162+
self.ingest_thread = Thread(target=self._ingest_events,
163+
name="SPIngest",
164+
daemon=True)
165+
self.output_thread = Thread(target=self._process_output_events,
166+
name="SPOutput",
167+
daemon=True)
168+
self.ingest_thread.start()
169+
self.output_thread.start()
170+
171+
def track_event(self, event_info):
172+
"""Adds the event to the tracked ones"""
173+
event_info.set_callback(self.input_queue.put)
174+
self.tracked_events[event_info.name] = event_info
175+
176+
def _get_event_info(self, event_name):
177+
"""Get the event info for the event name, throw an exception if its
178+
not tracked"""
179+
try:
180+
return self.tracked_events[event_name]
181+
except KeyError as error:
182+
raise ValueError(f"Event {event_name} not tracked") from error
183+
184+
def add_watcher(self, watcher: StateWatcher):
185+
"""Add a watcher to the list of watchers"""
186+
watcher_event_info_dict = {}
187+
for event_name in watcher.event_handlers:
188+
event_info = self._get_event_info(event_name)
189+
self.watchers.setdefault(
190+
event_info, set()).add(watcher)
191+
watcher_event_info_dict[event_name] = event_info
192+
193+
for output_event in watcher.OutputEvent:
194+
if output_event in self.watcher_output_events:
195+
raise ValueError("Watchers have to have unique output events")
196+
self.watcher_output_events[output_event] = watcher
197+
198+
watcher.register(watcher_event_info_dict)
199+
200+
def add_output_event_handler(self, event_name, handler):
201+
"""Add a handler for an output event"""
202+
if event_name in self.output_handlers:
203+
raise ValueError(f"Event {event_name} already has a handler. "
204+
f"If you need multiple handlers, call them "
205+
f"manually in the controller")
206+
self.output_handlers[event_name] = handler
207+
208+
def _ingest_events(self):
209+
"""Process events in the queue"""
210+
while not self.quit_evt.is_set():
211+
event = self.input_queue.get()
212+
# Ignore Nones, used for stopping
213+
if event is None:
214+
continue
215+
event_info = self._get_event_info(event.name)
216+
for watcher in self.watchers[event_info]:
217+
output_events = watcher.process_event(event)
218+
if output_events is None:
219+
continue
220+
for output_event in output_events:
221+
self.output_queue.put(output_event)
222+
223+
def _process_output_events(self):
224+
"""Call handlers of output events"""
225+
while not self.quit_evt.is_set():
226+
event = self.output_queue.get()
227+
# Ignore Nones, used for stopping
228+
if event is None:
229+
continue
230+
if event.name in self.output_handlers:
231+
handler = self.output_handlers[event.name]
232+
handler(*event.args, **event.kwargs)
233+
234+
def stop(self):
235+
"""Stop the thread"""
236+
self.quit_evt.set()
237+
# Unblock the queues
238+
self.input_queue.put(None)
239+
self.output_queue.put(None)
240+
241+
def wait_stopped(self):
242+
"""Wait for the thread to stop"""
243+
self.ingest_thread.join()
244+
self.output_thread.join()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Utilities for the event processor"""
2+
from threading import Event, Thread
3+
from time import monotonic
4+
5+
from .event_processor import EventInfo
6+
7+
8+
class SerialEventInfoFactory:
9+
"""A factory to create EventInfo objects that generate Events from
10+
the serial signals"""
11+
def __init__(self, serial_parser):
12+
self.serial_parser = serial_parser
13+
14+
def create(self, name, regexp, priority=0):
15+
"""Creates the EventInfo object"""
16+
17+
def registration(handler):
18+
"""Registers the handler to the serial parser"""
19+
self.serial_parser.add_handler(
20+
regexp=regexp,
21+
handler=handler,
22+
priority=priority)
23+
24+
def deregistration(handler):
25+
"""De-registers the handler from the serial parser"""
26+
self.serial_parser.remove_handler(
27+
regexp=regexp,
28+
handler=handler)
29+
30+
return EventInfo(
31+
name=name,
32+
registration=registration,
33+
deregistration=deregistration,
34+
)
35+
36+
37+
class Ticker:
38+
"""A class that calls a callback every interval seconds"""
39+
40+
def __init__(self, interval=0.2):
41+
self.last_tick = monotonic()
42+
self.interval = interval # seconds
43+
self.quit_evt = Event()
44+
self.callback = None
45+
self.thread = Thread(target=self.ticker, name="Ticker", daemon=True)
46+
self.thread.start()
47+
48+
def ticker(self):
49+
"""Ticks every interval seconds, calls the callback"""
50+
while not self.quit_evt.is_set():
51+
self.last_tick = monotonic()
52+
if self.callback is not None:
53+
self.callback()
54+
55+
wait_amount = self.interval - (monotonic() - self.last_tick)
56+
if wait_amount > 0:
57+
self.quit_evt.wait(wait_amount)
58+
59+
def set_handler(self, handler):
60+
"""Sets the callback"""
61+
self.callback = handler
62+
63+
def stop(self):
64+
"""Stops the ticker"""
65+
self.quit_evt.set()
66+
67+
def wait_stopped(self):
68+
"""Waits for the ticker thread to stop"""
69+
self.thread.join()

prusa/link/printer_adapter/print_stat_doubler.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)