auto-trainer-core 2.0.1.dev349__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
- auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
- autotrainer/core/__init__.py +319 -0
- autotrainer/core/analysis/__init__.py +6 -0
- autotrainer/core/analysis/alarm_monitor.py +484 -0
- autotrainer/core/analysis/audio_spectrum_data.py +27 -0
- autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
- autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
- autotrainer/core/analysis/detector.py +179 -0
- autotrainer/core/analysis/external_doors_monitor.py +124 -0
- autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
- autotrainer/core/analysis/head_fix_measurement.py +15 -0
- autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
- autotrainer/core/analysis/load_cell_monitor.py +377 -0
- autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
- autotrainer/core/analysis/measurement_data.py +80 -0
- autotrainer/core/analysis/pellet_position_monitor.py +108 -0
- autotrainer/core/analysis/sensor_analysis.py +390 -0
- autotrainer/core/analysis/system_fault_monitor.py +81 -0
- autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
- autotrainer/core/animal/__init__.py +1 -0
- autotrainer/core/animal/animal_subject.py +208 -0
- autotrainer/core/capture.py +25 -0
- autotrainer/core/configuration/__init__.py +88 -0
- autotrainer/core/configuration/alarm_configuration.py +55 -0
- autotrainer/core/configuration/animal_presence_configuration.py +17 -0
- autotrainer/core/configuration/behavior_configuration.py +323 -0
- autotrainer/core/configuration/camera_configuration.py +108 -0
- autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
- autotrainer/core/configuration/hardware_configuration.py +30 -0
- autotrainer/core/configuration/inference_configuration.py +26 -0
- autotrainer/core/configuration/load_cell_config.py +61 -0
- autotrainer/core/configuration/persistence_configuration.py +29 -0
- autotrainer/core/configuration/presence_detection_configuration.py +9 -0
- autotrainer/core/configuration/system_configuration.py +211 -0
- autotrainer/core/configuration/system_fault_config.py +9 -0
- autotrainer/core/configuration/system_maintenance_config.py +17 -0
- autotrainer/core/diamond_triangle_config.py +150 -0
- autotrainer/core/event/__init__.py +54 -0
- autotrainer/core/event/api_event_kind.py +5 -0
- autotrainer/core/event/api_event_plugin.py +57 -0
- autotrainer/core/event/event_info.py +53 -0
- autotrainer/core/event/event_manager.py +296 -0
- autotrainer/core/event/event_manager_plugin.py +45 -0
- autotrainer/core/event/file_event_plugin.py +91 -0
- autotrainer/core/event/logger_event_plugin.py +43 -0
- autotrainer/core/fixed_array_multiqueue.py +263 -0
- autotrainer/core/fixed_array_queue.py +124 -0
- autotrainer/core/frame_index.py +34 -0
- autotrainer/core/interfaces.py +347 -0
- autotrainer/core/logging.py +714 -0
- autotrainer/core/message/__init__.py +26 -0
- autotrainer/core/message/audio_spectrum_message.py +21 -0
- autotrainer/core/message/measurement_message.py +61 -0
- autotrainer/core/message/message_handler.py +158 -0
- autotrainer/core/message/motor_configuration_message.py +128 -0
- autotrainer/core/message/system_command_kind.py +74 -0
- autotrainer/core/message/system_message_handler.py +122 -0
- autotrainer/core/message/system_status_message.py +112 -0
- autotrainer/core/multiproc.py +50 -0
- autotrainer/core/notification/__init__.py +2 -0
- autotrainer/core/notification/notification_center.py +80 -0
- autotrainer/core/notification/trigger_notification.py +27 -0
- autotrainer/core/observable_object.py +105 -0
- autotrainer/core/perf_monitor.py +77 -0
- autotrainer/core/pose_elements.py +196 -0
- autotrainer/core/project/__init__.py +19 -0
- autotrainer/core/project/project_info.py +486 -0
- autotrainer/core/queue_util.py +60 -0
- autotrainer/core/reach_event.py +38 -0
- autotrainer/core/video_detection.py +326 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from threading import Thread
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from queue import Queue, Empty
|
|
5
|
+
from typing import Optional, List, Any
|
|
6
|
+
|
|
7
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
8
|
+
from autotrainer.core.project import ProjectInfo
|
|
9
|
+
|
|
10
|
+
from .event_info import EventInfo
|
|
11
|
+
from .event_manager_plugin import EventManagerPlugin
|
|
12
|
+
from .file_event_plugin import FileEventPlugin
|
|
13
|
+
from .logger_event_plugin import LoggerEventPlugin
|
|
14
|
+
|
|
15
|
+
logger = get_verbose_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventManager:
|
|
19
|
+
"""
|
|
20
|
+
Events, in the context of the autotrainer local application, are a set of messages that track specific changes and
|
|
21
|
+
actions of interest. They are a subset of information that is likely to be logged overall, and are made to conform
|
|
22
|
+
to a specific structure for reading in conjunction with data files during analysis.
|
|
23
|
+
|
|
24
|
+
By default, events are also sent to the default logger. It is generally not necessary to post an event _and_ send
|
|
25
|
+
the same information explicitly to the logger.
|
|
26
|
+
|
|
27
|
+
Repeat behavior: The objective with repeat events (same event without interruption of another event type) is to
|
|
28
|
+
* Immediately output the first instance of the event (as would normally happen)
|
|
29
|
+
* When a new event type is received, output the number of repeats (not counting that first instance already output)
|
|
30
|
+
* Per the first bullet, this new event type will also be immediately output
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_instance: "EventManager"
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _remove_cls_instance():
|
|
37
|
+
try:
|
|
38
|
+
del EventManager._instance
|
|
39
|
+
except AttributeError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def default() -> "EventManager":
|
|
44
|
+
"""
|
|
45
|
+
Creates (if needed) and returns the default instance.
|
|
46
|
+
"""
|
|
47
|
+
cls = EventManager
|
|
48
|
+
if not hasattr(cls, "_instance"):
|
|
49
|
+
instance = cls("EventManagerInstance")
|
|
50
|
+
instance.register_plugin(LoggerEventPlugin())
|
|
51
|
+
instance.register_plugin(FileEventPlugin())
|
|
52
|
+
cls._instance = instance
|
|
53
|
+
return cls._instance
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def try_close_default():
|
|
57
|
+
"""
|
|
58
|
+
Close the default instance if it exists. Do not spin one up just to check (e.g.,
|
|
59
|
+
EventManager.default().close() if one was never created).
|
|
60
|
+
"""
|
|
61
|
+
cls = EventManager
|
|
62
|
+
cls_inst = getattr(cls, "_instance", None)
|
|
63
|
+
if cls_inst is not None:
|
|
64
|
+
cls_inst.close()
|
|
65
|
+
cls._remove_cls_instance()
|
|
66
|
+
|
|
67
|
+
def __del__(self):
|
|
68
|
+
# in case of
|
|
69
|
+
cls_inst = getattr(EventManager, "_instance", None)
|
|
70
|
+
if cls_inst is self:
|
|
71
|
+
self.try_close_default()
|
|
72
|
+
else:
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def __init__(self, key=""):
|
|
76
|
+
if key != "EventManagerInstance":
|
|
77
|
+
raise Exception("Use EventManager.default() to access and instance.")
|
|
78
|
+
|
|
79
|
+
self._plugins: List[EventManagerPlugin] = []
|
|
80
|
+
|
|
81
|
+
self._project_info = None
|
|
82
|
+
|
|
83
|
+
self._last_event_info: Optional[EventInfo] = None
|
|
84
|
+
self._repeat_event_count = 0
|
|
85
|
+
|
|
86
|
+
# Callers should expect requests to post an event return as quickly as possible. Events are pushed to a queue
|
|
87
|
+
# so that processing can be done in a separate thread as resources allow.
|
|
88
|
+
self._write_queue = Queue()
|
|
89
|
+
self._write_thread = Thread(
|
|
90
|
+
target=self._process_queue,
|
|
91
|
+
name=f"{self.__class__.__name__}",
|
|
92
|
+
daemon=True, # allow the main thread/process to exit even if this thread is still alive
|
|
93
|
+
)
|
|
94
|
+
self._write_thread.start()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_valid(self):
|
|
98
|
+
return self._write_thread is not None and self._write_queue is not None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def project(self) -> ProjectInfo:
|
|
102
|
+
return self._project_info
|
|
103
|
+
|
|
104
|
+
@project.setter
|
|
105
|
+
def project(self, value: ProjectInfo) -> None:
|
|
106
|
+
"""
|
|
107
|
+
ProjectInfo is an optional property. If set, it is used to generate the location and name of the event file in
|
|
108
|
+
the expected format.
|
|
109
|
+
|
|
110
|
+
Args
|
|
111
|
+
value: ProjectInfo object. This is used to determine the location of the event file.
|
|
112
|
+
"""
|
|
113
|
+
self._project_info = value
|
|
114
|
+
|
|
115
|
+
for plugin in self._plugins:
|
|
116
|
+
plugin.set_project(value)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def plugins(self) -> List[EventManagerPlugin]:
|
|
120
|
+
# Do not let callers modify ths list. register/unregister are available for this. This is already dangerous
|
|
121
|
+
# enough - giving them access to the plugins themselves.
|
|
122
|
+
return self._plugins.copy()
|
|
123
|
+
|
|
124
|
+
def register_plugin(self, plugin: EventManagerPlugin) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Registers a plugin with the event manager.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
plugin: The plugin to register.
|
|
130
|
+
"""
|
|
131
|
+
if plugin not in self._plugins:
|
|
132
|
+
self._plugins.append(plugin)
|
|
133
|
+
plugin.set_project(self.project)
|
|
134
|
+
|
|
135
|
+
def unregister_plugin(self, plugin: EventManagerPlugin) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Unregisters a plugin with the event manager.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
plugin: The plugin to unregister.
|
|
141
|
+
"""
|
|
142
|
+
if plugin in self._plugins:
|
|
143
|
+
self._plugins.remove(plugin)
|
|
144
|
+
# Even though the caller must have a reference to the plugin, take responsibility to close, if needed.
|
|
145
|
+
# In the future, there may be a way to unregister by some kind of key/type/identifier that doesn't require
|
|
146
|
+
# explicit access to the plugin instance by the caller.
|
|
147
|
+
plugin.close()
|
|
148
|
+
|
|
149
|
+
def flush(self):
|
|
150
|
+
for plugin in self._plugins:
|
|
151
|
+
plugin.flush()
|
|
152
|
+
|
|
153
|
+
def close(self):
|
|
154
|
+
"""
|
|
155
|
+
Closes the event manager. This is required to stop any internal threads and allow a clean exit. This should
|
|
156
|
+
only be called when the application or script is closing or otherwise finished with the event manager as an
|
|
157
|
+
instance, including the `default` cannot be restarted.
|
|
158
|
+
"""
|
|
159
|
+
cls_inst = getattr(EventManager, "_instance", None)
|
|
160
|
+
for plugin in self._plugins:
|
|
161
|
+
plugin.set_enable(False)
|
|
162
|
+
wt = self._write_thread
|
|
163
|
+
wq = self._write_queue
|
|
164
|
+
if wt is not None:
|
|
165
|
+
if wq is not None:
|
|
166
|
+
wq.put(None)
|
|
167
|
+
wt.join()
|
|
168
|
+
self._write_thread = None
|
|
169
|
+
# queue needs be flushed so that we can join it:
|
|
170
|
+
if wq is not None:
|
|
171
|
+
self._write_queue = None # set it directly, so that no other thread can now put through this instance
|
|
172
|
+
while True:
|
|
173
|
+
try:
|
|
174
|
+
item = wq.get_nowait()
|
|
175
|
+
logger.warning("dropped unhandled %s: %s", type(item), item)
|
|
176
|
+
wq.task_done()
|
|
177
|
+
except Empty:
|
|
178
|
+
break
|
|
179
|
+
wq.join()
|
|
180
|
+
if cls_inst is self:
|
|
181
|
+
self._remove_cls_instance()
|
|
182
|
+
|
|
183
|
+
def post_event_content(self, kind: int, data: Optional[Any] = None, when: Optional[datetime] = None,
|
|
184
|
+
index: int = None):
|
|
185
|
+
"""
|
|
186
|
+
Add an event info instance to the event manager output queue. This is a convenience method that creates an
|
|
187
|
+
EventInfo object and optionally populates timestamp related fields as the time this method is called. If timing
|
|
188
|
+
information is critical, those arguments should be explicitly set, or `post_event()` should be used with a
|
|
189
|
+
preconstructed `EventInfo` instance with the desired timestamp fields.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
kind: See EventInfo.kind for a detailed description.
|
|
193
|
+
data: See EventInfo.kind for a detailed description.
|
|
194
|
+
when: See EventInfo.kind for a detailed description.
|
|
195
|
+
index: See EventInfo.kind for a detailed description.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
info = EventInfo(kind,
|
|
199
|
+
when=datetime.now() if when is None else when,
|
|
200
|
+
index=time.perf_counter_ns() if index is None else index,
|
|
201
|
+
context=data)
|
|
202
|
+
|
|
203
|
+
self.post_event(info)
|
|
204
|
+
|
|
205
|
+
def post_event(self, info: EventInfo):
|
|
206
|
+
"""
|
|
207
|
+
Posts an event info instance to the event manager.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
info:
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
if info is None:
|
|
216
|
+
# "~paranoid" check but that will prevent the non-desired stop of the work thread.
|
|
217
|
+
raise RuntimeError("post_event(None) refused")
|
|
218
|
+
wq = self._write_queue
|
|
219
|
+
if wq is None:
|
|
220
|
+
logger.debug("post_event(%s) but write queue already removed", info.kind)
|
|
221
|
+
else:
|
|
222
|
+
wq.put(info)
|
|
223
|
+
|
|
224
|
+
def has_pending(self) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
This is primarily for testing and diagnostics. It should not be relied upon absolutely as this class will not
|
|
227
|
+
guarantee that whatever implementation is used to queue events for processing will accurately report the state
|
|
228
|
+
of empty at all times.
|
|
229
|
+
|
|
230
|
+
Returns: True if there are pending events to process by the handlers. Might be accurate, might not.
|
|
231
|
+
"""
|
|
232
|
+
wq = self._write_queue
|
|
233
|
+
if wq is None:
|
|
234
|
+
return False
|
|
235
|
+
return not wq.empty()
|
|
236
|
+
|
|
237
|
+
def _process_queue(self):
|
|
238
|
+
# Because we a) use plugins and b) allow for EventInfo->is_same to be overridden, we may be given a plugin that
|
|
239
|
+
# errors on every process_event, or an EventInfo that errors on every is_same. This would bury the log if we
|
|
240
|
+
# reported it every time. It may also be a one time error for the plugin, so we don't want to just skip/remove
|
|
241
|
+
# if there is an error.
|
|
242
|
+
# This will log the first occurrence of each of the above, but not spam the log if it is a recurring issue.
|
|
243
|
+
|
|
244
|
+
is_same_error_reported = False
|
|
245
|
+
process_event_error_reported = False
|
|
246
|
+
|
|
247
|
+
while True:
|
|
248
|
+
try:
|
|
249
|
+
# Workaround or current Jetson behavior w/ queue.get(timeout=).
|
|
250
|
+
info = self._write_queue.get(timeout=0.5)
|
|
251
|
+
if info is None:
|
|
252
|
+
logger.verbose("got exit sentinel, exiting main loop")
|
|
253
|
+
self._write_queue.task_done()
|
|
254
|
+
break
|
|
255
|
+
except Empty:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if not isinstance(info, EventInfo):
|
|
259
|
+
logger.warning("unexpected event info instance")
|
|
260
|
+
self._write_queue.task_done()
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
is_same = info.is_same(self._last_event_info)
|
|
265
|
+
except Exception as err: # Possibly coming from EventInfo subclass - cannot predict type of error.
|
|
266
|
+
if not is_same_error_reported:
|
|
267
|
+
logger.error("is_same failed: %s", err)
|
|
268
|
+
is_same_error_reported = True
|
|
269
|
+
else:
|
|
270
|
+
if is_same:
|
|
271
|
+
self._repeat_event_count += 1
|
|
272
|
+
self._write_queue.task_done()
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
if self._repeat_event_count > 0:
|
|
277
|
+
self._process_event(self._last_event_info, self._repeat_event_count)
|
|
278
|
+
self._repeat_event_count = 0
|
|
279
|
+
|
|
280
|
+
self._last_event_info = info
|
|
281
|
+
self._process_event(info)
|
|
282
|
+
except Exception as err: # Coming from an arbitrary plugin process_event() - cannot predict type of error.
|
|
283
|
+
# TODO (maybe): track exceptions per plugin. After some number N exceptions, disable the plugin.
|
|
284
|
+
if not process_event_error_reported:
|
|
285
|
+
logger.exception("process queue info (%s) failed: %s", info, err)
|
|
286
|
+
process_event_error_reported = True
|
|
287
|
+
|
|
288
|
+
self._write_queue.task_done()
|
|
289
|
+
|
|
290
|
+
for plugin in self._plugins:
|
|
291
|
+
plugin.close()
|
|
292
|
+
|
|
293
|
+
def _process_event(self, info: EventInfo, repeat_count: int = 0):
|
|
294
|
+
for plugin in self._plugins:
|
|
295
|
+
logger.spam("plugin %s: processing event %s", plugin, info)
|
|
296
|
+
plugin.process_event(info, repeat_count)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Protocol, Optional
|
|
2
|
+
|
|
3
|
+
from ..project import ProjectInfo
|
|
4
|
+
from .event_info import EventInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventManagerPlugin(Protocol):
|
|
8
|
+
"""
|
|
9
|
+
Interface for event manager plugins.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def set_project(self, project: Optional[ProjectInfo]) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Called when the project changes.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
project: The new project info (or none).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def set_enable(self, enable: bool) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Called when the event manager is enabled or disabled.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
enable: True if the event manager is enabled, false otherwise.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def process_event(self, info: EventInfo, repeat_count: int) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Called when an event occurs.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
info: The event info.
|
|
34
|
+
repeat_count: Number of times this event has been repeated until a different event was received.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def flush(self):
|
|
38
|
+
"""
|
|
39
|
+
For event sinks that may buffer output, this should forcibly flush any pending output.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Called when the event manager is closed. Any pending output should be flushed, resources released, etc.
|
|
45
|
+
"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ..logging import get_verbose_logger
|
|
6
|
+
from ..project import ProjectInfo, ProjectInterval
|
|
7
|
+
from .event_info import EventInfo
|
|
8
|
+
|
|
9
|
+
from .event_manager_plugin import EventManagerPlugin
|
|
10
|
+
|
|
11
|
+
logger = get_verbose_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileEventPlugin(EventManagerPlugin):
|
|
15
|
+
"""
|
|
16
|
+
An event manager plugin directs events to a project file. This plugin will nore record an event history if
|
|
17
|
+
`project` is set to `None`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._project_info = None
|
|
22
|
+
self._event_file = None
|
|
23
|
+
self._write_active = True
|
|
24
|
+
self._current_record_interval = -1
|
|
25
|
+
self._bad_file_attempt = False
|
|
26
|
+
|
|
27
|
+
def set_project(self, project: Optional[ProjectInfo]) -> None:
|
|
28
|
+
self._project_info = project
|
|
29
|
+
self._bad_file_attempt = False
|
|
30
|
+
self._update_event_file()
|
|
31
|
+
|
|
32
|
+
def set_enable(self, enable: bool) -> None:
|
|
33
|
+
self._write_active = enable
|
|
34
|
+
|
|
35
|
+
def process_event(self, info: EventInfo, repeat_count: int):
|
|
36
|
+
if not self._write_active:
|
|
37
|
+
logger.debug("write not active, skipping %s", info)
|
|
38
|
+
return
|
|
39
|
+
file_timestamp = datetime.now()
|
|
40
|
+
needs_update = file_timestamp.hour != self._current_record_interval
|
|
41
|
+
if needs_update:
|
|
42
|
+
self._update_event_file()
|
|
43
|
+
event_file = self._event_file
|
|
44
|
+
if event_file is None:
|
|
45
|
+
logger.warning("event_file None, skipping %s", info)
|
|
46
|
+
return
|
|
47
|
+
output = f"{info.when}, {info.index}, {info.kind}, {str(info.kind)}, {str(info.context)}, {repeat_count}\n"
|
|
48
|
+
event_file.write(output)
|
|
49
|
+
|
|
50
|
+
def flush(self):
|
|
51
|
+
if self._event_file is not None:
|
|
52
|
+
self._event_file.flush()
|
|
53
|
+
|
|
54
|
+
def close(self) -> None:
|
|
55
|
+
event_file = self._event_file
|
|
56
|
+
if event_file is not None:
|
|
57
|
+
event_file.close()
|
|
58
|
+
self._event_file = None
|
|
59
|
+
|
|
60
|
+
def _update_event_file(self):
|
|
61
|
+
self.close()
|
|
62
|
+
|
|
63
|
+
if not self._write_active:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if self._project_info is not None:
|
|
67
|
+
event_file_info = self._project_info.get_monitor_file(name="events", interval=ProjectInterval.HOUR,
|
|
68
|
+
when=datetime.now())
|
|
69
|
+
if event_file_info is None:
|
|
70
|
+
logger.error("unable to write to expected event file location")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
file_path = Path(event_file_info.file)
|
|
75
|
+
file_existed = file_path.exists()
|
|
76
|
+
file_path.parent.mkdir(exist_ok=True)
|
|
77
|
+
fh = file_path.open("a")
|
|
78
|
+
if not file_existed:
|
|
79
|
+
fh.write("Time, Index, EventId, EventName, Data, Repeat\n")
|
|
80
|
+
|
|
81
|
+
self._current_record_interval = event_file_info.current_interval
|
|
82
|
+
self._event_file = fh
|
|
83
|
+
|
|
84
|
+
logger.info(f"event file opened at {event_file_info.file}")
|
|
85
|
+
except Exception as ex:
|
|
86
|
+
if not self._bad_file_attempt:
|
|
87
|
+
# Don't spam log if there are write/access issues with wherever project info is pointing. Can reset
|
|
88
|
+
# the flag the next tile the project changes.
|
|
89
|
+
logger.error(f"unable to write to {event_file_info.file}")
|
|
90
|
+
logger.error(ex)
|
|
91
|
+
self._bad_file_attempt = True
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ..logging import get_verbose_logger
|
|
5
|
+
from ..project import ProjectInfo
|
|
6
|
+
from .event_info import EventInfo
|
|
7
|
+
|
|
8
|
+
from .event_manager_plugin import EventManagerPlugin
|
|
9
|
+
|
|
10
|
+
logger = get_verbose_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoggerEventPlugin(EventManagerPlugin):
|
|
14
|
+
"""
|
|
15
|
+
An EventManager plugin that sends event to the default logger facility.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, level: int = logging.DEBUG):
|
|
19
|
+
self._level = level
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def level(self) -> int:
|
|
23
|
+
return self._level
|
|
24
|
+
|
|
25
|
+
@level.setter
|
|
26
|
+
def level(self, value: int) -> None:
|
|
27
|
+
self._level = value
|
|
28
|
+
|
|
29
|
+
def set_project(self, project: Optional[ProjectInfo]) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def set_enable(self, enable: bool) -> None:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def process_event(self, info: EventInfo, repeat_count: int) -> None:
|
|
36
|
+
output = f"{info.when}, {info.index}, {info.kind}, {str(info.kind)}, {str(info.context)}, {repeat_count}"
|
|
37
|
+
logger.log(self._level, output)
|
|
38
|
+
|
|
39
|
+
def flush(self) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
pass
|