auto-trainer-core 2.0.1.dev353__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.dev353.dist-info/METADATA +18 -0
- auto_trainer_core-2.0.1.dev353.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev353.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev353.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,108 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import math
|
|
3
|
+
from typing import Optional, Tuple, List
|
|
4
|
+
|
|
5
|
+
from autotrainer.api import ApiDetectorKind
|
|
6
|
+
|
|
7
|
+
from autotrainer.core.diamond_triangle_config import DiamondTriangleOffsetConfig
|
|
8
|
+
from autotrainer.core import get_perf_now, Offset3DTuple, calculate_std_dev_manual
|
|
9
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
10
|
+
from autotrainer.core.analysis.detector import BaseDetector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_verbose_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_offset_nans = Offset3DTuple(math.nan, math.nan, math.nan)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass
|
|
20
|
+
class PelletMisplacedDetectorConfiguration:
|
|
21
|
+
enabled: bool = True
|
|
22
|
+
aggregate_duration: float = 1 # how long ago to check/look at results
|
|
23
|
+
# previous results older than that are discarded before each check/update.
|
|
24
|
+
|
|
25
|
+
use_dcs_y_low_limit: bool = True
|
|
26
|
+
dcs_y_low_limit: float = 0 # lower than this -> error condition
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PelletMisplacedDetector(BaseDetector):
|
|
30
|
+
|
|
31
|
+
use_daemon = True
|
|
32
|
+
default_timer_delay = 0.25 # default delay between each check of the possible condition(s)
|
|
33
|
+
# NB: should be at least 2 times smaller than aggregate_duration, otherwise given we expire too old data
|
|
34
|
+
# before checking the condition, we could always have the data set empty.
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: PelletMisplacedDetectorConfiguration):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self._config = config
|
|
39
|
+
self._prev_data: List[Tuple[float, Offset3DTuple]] = []
|
|
40
|
+
self._dcs_config: Optional[DiamondTriangleOffsetConfig] = None
|
|
41
|
+
|
|
42
|
+
@BaseDetector.is_engaged.setter
|
|
43
|
+
def is_engaged(self, value):
|
|
44
|
+
prev = self._is_engaged
|
|
45
|
+
BaseDetector.is_engaged.fset(self, value)
|
|
46
|
+
if prev != value:
|
|
47
|
+
self.post_detector_event(ApiDetectorKind.pelletMisplaced, value)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def dcs_config(self) -> Optional[DiamondTriangleOffsetConfig]:
|
|
51
|
+
return self._dcs_config
|
|
52
|
+
|
|
53
|
+
@dcs_config.setter
|
|
54
|
+
def dcs_config(self, value: Optional[DiamondTriangleOffsetConfig]):
|
|
55
|
+
logger.verbose("got new DCS config: %s", value)
|
|
56
|
+
self._dcs_config = value
|
|
57
|
+
|
|
58
|
+
def _check_state(self) -> Optional[float]:
|
|
59
|
+
cfg = self._config
|
|
60
|
+
prev_data = self._prev_data
|
|
61
|
+
engaged = False
|
|
62
|
+
p_now = get_perf_now()
|
|
63
|
+
if p_now - self._p_started < cfg.aggregate_duration:
|
|
64
|
+
# wait at least 1 window before consider
|
|
65
|
+
return None
|
|
66
|
+
idx = 0
|
|
67
|
+
while idx < len(prev_data):
|
|
68
|
+
perf_c = prev_data[idx][0]
|
|
69
|
+
if p_now - perf_c <= cfg.aggregate_duration:
|
|
70
|
+
break
|
|
71
|
+
idx += 1
|
|
72
|
+
del prev_data[:idx]
|
|
73
|
+
if cfg.use_dcs_y_low_limit:
|
|
74
|
+
# logger.debug("Checking %s", prev_positions)
|
|
75
|
+
if len(prev_data) > 0 and all(pos.y < cfg.dcs_y_low_limit for _, pos in prev_data):
|
|
76
|
+
engaged = True
|
|
77
|
+
if not self._is_engaged and engaged:
|
|
78
|
+
if len(prev_data) < 2:
|
|
79
|
+
if len(prev_data) == 0:
|
|
80
|
+
avg_pos, stdev_pos = _offset_nans, _offset_nans
|
|
81
|
+
else:
|
|
82
|
+
avg_pos, stdev_pos = prev_data[-1][1], Offset3DTuple(0, 0, 0)
|
|
83
|
+
else:
|
|
84
|
+
avg_pos, stdev_pos = calculate_std_dev_manual([d[1] for d in prev_data])
|
|
85
|
+
logger.notice("New engaged, dcs_pos=%s stdev=%s ; all=%s",
|
|
86
|
+
avg_pos.humanize(), stdev_pos.humanize(), prev_data)
|
|
87
|
+
self.is_engaged = engaged
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def update(self, pellet_inference_3d: Optional[Offset3DTuple]):
|
|
91
|
+
dcs_cfg = self._dcs_config
|
|
92
|
+
if dcs_cfg is None or not dcs_cfg.fully_valid: # refuse to guess with not fully valid
|
|
93
|
+
return
|
|
94
|
+
if pellet_inference_3d is None:
|
|
95
|
+
# no need put None 3d loc, the check is done by timer
|
|
96
|
+
return
|
|
97
|
+
rel_to_diamond = dcs_cfg.diamond_coord - pellet_inference_3d
|
|
98
|
+
dcs_pellet_3d = dcs_cfg.inference_to_diamond(rel_to_diamond)
|
|
99
|
+
if __debug__:
|
|
100
|
+
# should not be needed with fully_valid check above.
|
|
101
|
+
if any(map(math.isnan, dcs_pellet_3d)):
|
|
102
|
+
logger.debug("filtering bad pellet3d : %s", pellet_inference_3d)
|
|
103
|
+
return
|
|
104
|
+
p_now = get_perf_now()
|
|
105
|
+
with self._lock:
|
|
106
|
+
self._prev_data.append((p_now, dcs_pellet_3d))
|
|
107
|
+
# self.check_state()
|
|
108
|
+
# no: we don't need resolution/check frequency as high as the input source
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import os
|
|
5
|
+
import math
|
|
6
|
+
import threading
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, Tuple, IO
|
|
10
|
+
|
|
11
|
+
import numpy
|
|
12
|
+
|
|
13
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
14
|
+
from autotrainer.core.project import ProjectInfo, ProjectInterval
|
|
15
|
+
from autotrainer.core.perf_monitor import PerfMonitor
|
|
16
|
+
from autotrainer.core.observable_object import ObservableObject
|
|
17
|
+
from autotrainer.core.video_detection import PresenceDetectionAttrs
|
|
18
|
+
|
|
19
|
+
from ..configuration.alarm_configuration import EmergencyAlarmConfiguration
|
|
20
|
+
from ..configuration.animal_presence_configuration import GlobalAnimalPresenceConfig
|
|
21
|
+
from ..configuration.external_doors_monitor_configuration import ExternalDoorsMonitorConfig
|
|
22
|
+
from ..configuration.system_fault_config import SystemFaultConfig
|
|
23
|
+
from ..configuration.system_maintenance_config import SystemMaintenanceConfig
|
|
24
|
+
from ..message.audio_spectrum_message import AudioSpectrumMessage
|
|
25
|
+
|
|
26
|
+
from .head_fix_measurement import HeadFixMeasurement
|
|
27
|
+
from .audio_spectrum_monitor import AudioSpectrumThrashMonitor
|
|
28
|
+
from .headbar_pressure_monitor import HeadbarPressureMonitor
|
|
29
|
+
from .load_cell_monitor import LoadCellMonitor
|
|
30
|
+
from .load_cell_tare_monitor import LoadCellTareMonitor
|
|
31
|
+
from .alarm_monitor import EmergencyAlarmMonitor
|
|
32
|
+
from .global_animal_presence_monitor import GlobalAnimalPresenceMonitor
|
|
33
|
+
from .external_doors_monitor import ExternalDoorsMonitor
|
|
34
|
+
from .pellet_position_monitor import PelletMisplacedDetector, PelletMisplacedDetectorConfiguration
|
|
35
|
+
from .auto_tunnel_fan_monitor import AutoTunnelSweepMonitor, AutoTunnelSweepConfiguration
|
|
36
|
+
from .system_maintenance_monitor import SystemMaintenanceMonitor
|
|
37
|
+
from .system_fault_monitor import SystemFaultMonitor
|
|
38
|
+
|
|
39
|
+
logger = get_verbose_logger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# TODO: Separate true analysis from data recording to file(s) for post-analysis.
|
|
43
|
+
class SensorAnalysis(ObservableObject):
|
|
44
|
+
|
|
45
|
+
def __init__(self, *, topcam_presence: Optional[PresenceDetectionAttrs] = None):
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
self._project_info: Optional[ProjectInfo] = None
|
|
49
|
+
self._interval = ProjectInterval.HOUR
|
|
50
|
+
|
|
51
|
+
self._filter_invalid_weight_started = False
|
|
52
|
+
|
|
53
|
+
# The "monitor" CSV file with the bulk of the sensor data.
|
|
54
|
+
self._record_file = None
|
|
55
|
+
self._current_record_interval = -1
|
|
56
|
+
self._had_write_error = False
|
|
57
|
+
|
|
58
|
+
# The audio spectrum file.
|
|
59
|
+
self._audio_record_file_writer: Optional[Tuple[IO[str], csv.DictWriter]] = None
|
|
60
|
+
self._current_audio_record_interval = -1
|
|
61
|
+
self._audio_had_write_error = False
|
|
62
|
+
|
|
63
|
+
self._is_load_cell_engaged = False
|
|
64
|
+
self._load_cell_monitor = LoadCellMonitor()
|
|
65
|
+
|
|
66
|
+
# The analog pressure sensor on the headbar.
|
|
67
|
+
self._is_headbar_pressure_engaged = False
|
|
68
|
+
self._headbar_pressure_monitor = HeadbarPressureMonitor()
|
|
69
|
+
|
|
70
|
+
# The digital i/o switch on the headbar.
|
|
71
|
+
self._is_headbar_switch_engaged = False
|
|
72
|
+
|
|
73
|
+
self._tare_detector = LoadCellTareMonitor()
|
|
74
|
+
self._tare_callback = None
|
|
75
|
+
|
|
76
|
+
self._audio_thrashing_monitor = AudioSpectrumThrashMonitor()
|
|
77
|
+
|
|
78
|
+
self._global_animal_presence_monitor = GlobalAnimalPresenceMonitor(
|
|
79
|
+
config=GlobalAnimalPresenceConfig(),
|
|
80
|
+
load_cell_monitor=self._load_cell_monitor,
|
|
81
|
+
topcam_presence=topcam_presence,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._external_doors_monitor = ExternalDoorsMonitor(ExternalDoorsMonitorConfig())
|
|
85
|
+
|
|
86
|
+
self._pellet_misplaced_monitor = PelletMisplacedDetector(PelletMisplacedDetectorConfiguration())
|
|
87
|
+
self._auto_tunnel_sweep_monitor = AutoTunnelSweepMonitor(
|
|
88
|
+
AutoTunnelSweepConfiguration(),
|
|
89
|
+
pellet_misplaced_detector=self._pellet_misplaced_monitor,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._system_maintenance_monitor = SystemMaintenanceMonitor(config=SystemMaintenanceConfig())
|
|
93
|
+
self._system_fault_monitor = SystemFaultMonitor(config=SystemFaultConfig())
|
|
94
|
+
|
|
95
|
+
self._alarm_monitor = EmergencyAlarmMonitor(
|
|
96
|
+
config=EmergencyAlarmConfiguration(),
|
|
97
|
+
load_cell_monitor=self._load_cell_monitor,
|
|
98
|
+
load_cell_tare_monitor=self._tare_detector,
|
|
99
|
+
audio_monitor=self._audio_thrashing_monitor,
|
|
100
|
+
external_doors_monitor=self._external_doors_monitor,
|
|
101
|
+
global_animal_presence_monitor=self._global_animal_presence_monitor,
|
|
102
|
+
system_maintenance_monitor=self._system_maintenance_monitor,
|
|
103
|
+
system_fault_monitor=self._system_fault_monitor,
|
|
104
|
+
topcam_presence_attrs=topcam_presence,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self._perf_monitor = PerfMonitor(name="<sensor-analysis>", units="mps", report_window=30)
|
|
108
|
+
|
|
109
|
+
self._detectors = [
|
|
110
|
+
self._alarm_monitor, # put first
|
|
111
|
+
self._load_cell_monitor,
|
|
112
|
+
self._tare_detector,
|
|
113
|
+
self._audio_thrashing_monitor,
|
|
114
|
+
self._external_doors_monitor,
|
|
115
|
+
self._global_animal_presence_monitor,
|
|
116
|
+
self._pellet_misplaced_monitor,
|
|
117
|
+
self._auto_tunnel_sweep_monitor,
|
|
118
|
+
self._system_maintenance_monitor,
|
|
119
|
+
self._system_fault_monitor,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def detectors(self):
|
|
124
|
+
return self._detectors
|
|
125
|
+
|
|
126
|
+
def start(self):
|
|
127
|
+
for detector in self._detectors:
|
|
128
|
+
detector.start()
|
|
129
|
+
|
|
130
|
+
def stop(self):
|
|
131
|
+
for detector in self._detectors:
|
|
132
|
+
detector.stop()
|
|
133
|
+
|
|
134
|
+
def restart(self):
|
|
135
|
+
for detector in self._detectors:
|
|
136
|
+
detector.restart()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def project_info(self) -> Optional[ProjectInfo]:
|
|
140
|
+
return self._project_info
|
|
141
|
+
|
|
142
|
+
@project_info.setter
|
|
143
|
+
def project_info(self, value: ProjectInfo) -> None:
|
|
144
|
+
self._project_info = value
|
|
145
|
+
self._update_record_file()
|
|
146
|
+
self._update_audio_file()
|
|
147
|
+
self._perf_monitor.reset()
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def interval(self) -> ProjectInterval:
|
|
151
|
+
return self._interval
|
|
152
|
+
|
|
153
|
+
@interval.setter
|
|
154
|
+
def interval(self, value: ProjectInterval) -> None:
|
|
155
|
+
self._interval = value
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def load_cell_monitor(self) -> LoadCellMonitor:
|
|
159
|
+
return self._load_cell_monitor
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def headbar_pressure_monitor(self) -> HeadbarPressureMonitor:
|
|
163
|
+
return self._headbar_pressure_monitor
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def load_cell_tare_monitor(self) -> LoadCellTareMonitor:
|
|
167
|
+
return self._tare_detector
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def audio_thrashing_monitor(self) -> AudioSpectrumThrashMonitor:
|
|
171
|
+
return self._audio_thrashing_monitor
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def emergency_alarm_monitor(self) -> EmergencyAlarmMonitor:
|
|
175
|
+
return self._alarm_monitor
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def global_animal_presence_monitor(self) -> GlobalAnimalPresenceMonitor:
|
|
179
|
+
return self._global_animal_presence_monitor
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def external_doors_monitor(self) -> ExternalDoorsMonitor:
|
|
183
|
+
return self._external_doors_monitor
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def pellet_misplaced_monitor(self) -> PelletMisplacedDetector:
|
|
187
|
+
return self._pellet_misplaced_monitor
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def auto_tunnel_sweep_monitor(self) -> AutoTunnelSweepMonitor:
|
|
191
|
+
return self._auto_tunnel_sweep_monitor
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def system_maintenance_monitor(self) -> SystemMaintenanceMonitor:
|
|
195
|
+
return self._system_maintenance_monitor
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def system_fault_monitor(self) -> SystemFaultMonitor:
|
|
199
|
+
return self._system_fault_monitor
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def is_headbar_switch_engaged(self):
|
|
203
|
+
# TODO - this signal is not currently encapsulated in an object for whatever analysis is needed like the others.
|
|
204
|
+
# If it is used in the future, this should probably be refactored similar to the other sub-components.
|
|
205
|
+
return self._is_headbar_switch_engaged
|
|
206
|
+
|
|
207
|
+
def stream_start(self):
|
|
208
|
+
logger.verbose("SensorAnalysis: stream_start")
|
|
209
|
+
self._perf_monitor.reset()
|
|
210
|
+
|
|
211
|
+
def measurements_received(
|
|
212
|
+
self,
|
|
213
|
+
measurements: List[HeadFixMeasurement]
|
|
214
|
+
) -> Tuple[List[float], List[bool], List[float], List[float], List[float]] :
|
|
215
|
+
"""Return weight_vals, switch_vals, pressure_vals, temperature_vals, humidity_vals"""
|
|
216
|
+
# logger.spam("Received %s measures", len(measurements))
|
|
217
|
+
assert len(measurements) > 0
|
|
218
|
+
|
|
219
|
+
switch_vals: List[bool] = []
|
|
220
|
+
pressure_vals: List[float] = []
|
|
221
|
+
temperature_vals: List[float] = []
|
|
222
|
+
humidity_vals: List[float] = []
|
|
223
|
+
|
|
224
|
+
fh = self._record_file
|
|
225
|
+
if fh is not None:
|
|
226
|
+
now = datetime.now()
|
|
227
|
+
needs_update = now.hour != self._current_record_interval \
|
|
228
|
+
if self._interval == ProjectInterval.HOUR \
|
|
229
|
+
else now.minute != self._current_record_interval
|
|
230
|
+
if needs_update:
|
|
231
|
+
self._update_record_file()
|
|
232
|
+
|
|
233
|
+
load_cell_mon = self._load_cell_monitor
|
|
234
|
+
|
|
235
|
+
for m in measurements:
|
|
236
|
+
# NB: save all measurements to file
|
|
237
|
+
switch_vals.append(m.switch)
|
|
238
|
+
pressure_vals.append(m.pressure)
|
|
239
|
+
temperature_vals.append(m.temperature)
|
|
240
|
+
humidity_vals.append(m.humidity)
|
|
241
|
+
|
|
242
|
+
fh = self._record_file
|
|
243
|
+
if fh is not None:
|
|
244
|
+
try:
|
|
245
|
+
fh.write(
|
|
246
|
+
f"{m.when}, {m.timestamp}, {m.weight}, {int(m.switch)}, {m.pressure}, "
|
|
247
|
+
f"{m.temperature}, {m.humidity}, {int(load_cell_mon.is_engaged)}\n")
|
|
248
|
+
except Exception as err:
|
|
249
|
+
# This could be too much if something major is wrong. Just output once per file rotation.
|
|
250
|
+
if not self._had_write_error:
|
|
251
|
+
logger.exception("<sensor-analysis>: unable to write: %s", err)
|
|
252
|
+
self._had_write_error = True
|
|
253
|
+
|
|
254
|
+
# Load cell monitor. and Auto-Tare monitor
|
|
255
|
+
weight_vals: List[float] = []
|
|
256
|
+
filtered_weight_vals: List[float] = []
|
|
257
|
+
load_cell_cfg = load_cell_mon.config
|
|
258
|
+
for m in measurements:
|
|
259
|
+
value = m.weight
|
|
260
|
+
weight_vals.append(value)
|
|
261
|
+
if not (load_cell_cfg.weight_min_filter < value < load_cell_cfg.weight_max_filter):
|
|
262
|
+
filtered_weight_vals.append(math.nan)
|
|
263
|
+
if not self._filter_invalid_weight_started:
|
|
264
|
+
self._filter_invalid_weight_started = True
|
|
265
|
+
logger.verbose(
|
|
266
|
+
"starting filter value outside accepted range: %s", value
|
|
267
|
+
)
|
|
268
|
+
continue
|
|
269
|
+
if self._filter_invalid_weight_started:
|
|
270
|
+
logger.verbose("finished filter value outside accepted range: %s", value)
|
|
271
|
+
self._filter_invalid_weight_started = False
|
|
272
|
+
#
|
|
273
|
+
filtered_weight_vals.append(value)
|
|
274
|
+
self._load_cell_monitor.update(value, m.when, m.timestamp)
|
|
275
|
+
# (Auto-)tare detection.
|
|
276
|
+
self._tare_detector.update(filtered_weight_vals)
|
|
277
|
+
|
|
278
|
+
# Headbar analog pressure monitor.
|
|
279
|
+
first_measure = measurements[0]
|
|
280
|
+
self._headbar_pressure_monitor.update(pressure_vals, first_measure.when, first_measure.timestamp)
|
|
281
|
+
|
|
282
|
+
# Headbar digital switch - no real implementation at this time.
|
|
283
|
+
self._is_headbar_switch_engaged = numpy.mean(switch_vals) > 0.5
|
|
284
|
+
|
|
285
|
+
# Performance monitoring.
|
|
286
|
+
self._perf_monitor.add_cycles(len(measurements))
|
|
287
|
+
|
|
288
|
+
return weight_vals, switch_vals, pressure_vals, temperature_vals, humidity_vals
|
|
289
|
+
|
|
290
|
+
def audio_spectrum_received(self, spectrum: AudioSpectrumMessage):
|
|
291
|
+
if spectrum is None or not spectrum.magnitudes:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
self._audio_thrashing_monitor.update(spectrum.magnitudes, spectrum.when, spectrum.index)
|
|
295
|
+
|
|
296
|
+
cur = self._audio_record_file_writer
|
|
297
|
+
if cur is not None:
|
|
298
|
+
file_timestamp = datetime.now()
|
|
299
|
+
needs_update = file_timestamp.hour != self._current_audio_record_interval \
|
|
300
|
+
if self._interval == ProjectInterval.HOUR \
|
|
301
|
+
else file_timestamp.minute != self._current_audio_record_interval
|
|
302
|
+
if needs_update:
|
|
303
|
+
self._update_audio_file()
|
|
304
|
+
|
|
305
|
+
# May or may not exist after the above.
|
|
306
|
+
cur = self._audio_record_file_writer
|
|
307
|
+
if cur is not None:
|
|
308
|
+
_, writer = cur
|
|
309
|
+
try:
|
|
310
|
+
r = dict(
|
|
311
|
+
Time=spectrum.when,
|
|
312
|
+
Index=spectrum.index,
|
|
313
|
+
**{f"Bin {i}": val for i, val in enumerate(spectrum.magnitudes)},
|
|
314
|
+
)
|
|
315
|
+
writer.writerow(r)
|
|
316
|
+
except Exception as err:
|
|
317
|
+
# This could be too much if something major is wrong. Just output once per file rotation.
|
|
318
|
+
if not self._audio_had_write_error:
|
|
319
|
+
logger.exception("<sensor-analysis>: unable to write: %s", err)
|
|
320
|
+
self._audio_had_write_error = True
|
|
321
|
+
|
|
322
|
+
def _update_record_file(self):
|
|
323
|
+
fh = self._record_file
|
|
324
|
+
if fh is not None:
|
|
325
|
+
self._record_file = None
|
|
326
|
+
try:
|
|
327
|
+
fh.flush()
|
|
328
|
+
fh.close()
|
|
329
|
+
except Exception as err:
|
|
330
|
+
logger.warning("Failure closing record file: %s", err)
|
|
331
|
+
|
|
332
|
+
if self._project_info is None:
|
|
333
|
+
return
|
|
334
|
+
interval_file_info = self._project_info.get_monitor_file(interval=self._interval, when=datetime.now())
|
|
335
|
+
if interval_file_info is None:
|
|
336
|
+
logger.error("<sensor-analysis>: unable to write to expected monitor file location")
|
|
337
|
+
return
|
|
338
|
+
dest_path = Path(interval_file_info.file)
|
|
339
|
+
try:
|
|
340
|
+
file_existed = dest_path.exists()
|
|
341
|
+
fh = dest_path.open("a")
|
|
342
|
+
if not file_existed:
|
|
343
|
+
fh.write("Time, Index, Weight, Switch, Pressure, Temperature, Humidity, LoadCellEngaged\n")
|
|
344
|
+
fh.flush()
|
|
345
|
+
self._current_record_interval = interval_file_info.current_interval
|
|
346
|
+
self._had_write_error = False
|
|
347
|
+
self._record_file = fh # last
|
|
348
|
+
except Exception as err:
|
|
349
|
+
logger.error("<sensor-analysis>: unable to write to %s: %s",dest_path, err)
|
|
350
|
+
logger.info("<sensor-analysis>: saving to %s", dest_path)
|
|
351
|
+
|
|
352
|
+
def _update_audio_file(self) -> None:
|
|
353
|
+
cur = self._audio_record_file_writer
|
|
354
|
+
if cur is not None:
|
|
355
|
+
fh, writer = cur
|
|
356
|
+
try:
|
|
357
|
+
fh.flush()
|
|
358
|
+
fh.close()
|
|
359
|
+
except Exception as err:
|
|
360
|
+
logger.warning("audio record file close failed: %s", err)
|
|
361
|
+
self._audio_record_file_writer = None
|
|
362
|
+
|
|
363
|
+
if self._project_info is not None:
|
|
364
|
+
interval_file_info = self._project_info.get_audio_spectrum_file(
|
|
365
|
+
interval=self._interval, when=datetime.now())
|
|
366
|
+
|
|
367
|
+
if interval_file_info is None:
|
|
368
|
+
logger.error("<sensor-analysis>: unable to write to expected audio file location")
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
file_existed = os.path.exists(interval_file_info.file)
|
|
373
|
+
|
|
374
|
+
fh = open(interval_file_info.file, "a")
|
|
375
|
+
writer = csv.DictWriter(
|
|
376
|
+
fh,
|
|
377
|
+
fieldnames=('Time', 'Index', *(f'Bin {i}' for i in range(64))),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if not file_existed:
|
|
381
|
+
writer.writeheader()
|
|
382
|
+
|
|
383
|
+
self._current_audio_record_interval = interval_file_info.current_interval
|
|
384
|
+
self._audio_record_file_writer = (fh, writer)
|
|
385
|
+
logger.info(f"<sensor-analysis>: saving audio spectrum to {interval_file_info.file}")
|
|
386
|
+
self._audio_had_write_error = False
|
|
387
|
+
except Exception as err:
|
|
388
|
+
logger.error("<sensor-analysis>: unable to write to %r: %s", interval_file_info.file, err)
|
|
389
|
+
|
|
390
|
+
return None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Optional, Set, List
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from .detector import BaseDetector
|
|
6
|
+
from ..configuration.persistence_configuration import PersistenceConfiguration
|
|
7
|
+
from ..configuration.system_fault_config import SystemFaultConfig
|
|
8
|
+
from ..event import post_api_detector_event_content
|
|
9
|
+
from ...api import ApiDetectorKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SystemFaultMonitor(BaseDetector):
|
|
13
|
+
|
|
14
|
+
use_daemon = True # for free disk space checks
|
|
15
|
+
default_timer_delay = 30 # secs ; check is very fast so can afford do it regularly
|
|
16
|
+
|
|
17
|
+
CONFIG = "config"
|
|
18
|
+
|
|
19
|
+
FREE_DISK_SPACE_ENGAGED = "free_disk_space_engaged"
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, config: SystemFaultConfig):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._config = config
|
|
24
|
+
self._max_pellet_loaded_engaged = False
|
|
25
|
+
self._max_consecutive_failed_load_engaged = False
|
|
26
|
+
self._free_disk_space_engaged = False
|
|
27
|
+
self._engaged_reasons: Set[str] = set()
|
|
28
|
+
self._persistence_cfg = PersistenceConfiguration()
|
|
29
|
+
|
|
30
|
+
def set_persistence_config(self, config: PersistenceConfiguration):
|
|
31
|
+
self._persistence_cfg = config
|
|
32
|
+
self._logger.verbose("Received persistence config: %s", config)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def config(self) -> SystemFaultConfig:
|
|
36
|
+
return self._config
|
|
37
|
+
|
|
38
|
+
@config.setter
|
|
39
|
+
def config(self, value: SystemFaultConfig):
|
|
40
|
+
prev, self._config = self._config, value
|
|
41
|
+
self._on_property_changed(self.CONFIG, value, prev)
|
|
42
|
+
self._logger.verbose("Received config: %s", value)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def engaged_reasons(self) -> List[str]:
|
|
46
|
+
return list(self._engaged_reasons)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def free_disk_space_engaged(self):
|
|
50
|
+
return self._free_disk_space_engaged
|
|
51
|
+
|
|
52
|
+
@free_disk_space_engaged.setter
|
|
53
|
+
def free_disk_space_engaged(self, value):
|
|
54
|
+
prev, self._free_disk_space_engaged = self._free_disk_space_engaged, value
|
|
55
|
+
self._on_property_changed(self.FREE_DISK_SPACE_ENGAGED, value, prev)
|
|
56
|
+
if value != prev:
|
|
57
|
+
self.check_state_if_not_detector_thread()
|
|
58
|
+
post_api_detector_event_content(self._event_manager, ApiDetectorKind.lowFreeDiskSpace,
|
|
59
|
+
value, self._config.use_free_disk_space)
|
|
60
|
+
|
|
61
|
+
def _check_free_disk_space(self):
|
|
62
|
+
cfg = self._config
|
|
63
|
+
usage = psutil.disk_usage(self._persistence_cfg.output_location)
|
|
64
|
+
# usage free is in bytes:
|
|
65
|
+
engaged = usage.free / 2 ** 20 < cfg.free_disk_space_min_limit_mb
|
|
66
|
+
self.free_disk_space_engaged = engaged
|
|
67
|
+
|
|
68
|
+
def _check_state(self) -> Optional[float]:
|
|
69
|
+
self._check_free_disk_space()
|
|
70
|
+
cfg = self._config
|
|
71
|
+
reasons = set()
|
|
72
|
+
for reason, use, engaged in (
|
|
73
|
+
(self.FREE_DISK_SPACE_ENGAGED, cfg.use_free_disk_space, self._free_disk_space_engaged),
|
|
74
|
+
):
|
|
75
|
+
if use and engaged:
|
|
76
|
+
reasons.add(reason)
|
|
77
|
+
self._engaged_reasons = reasons
|
|
78
|
+
prev_engaged = self._is_engaged
|
|
79
|
+
self.is_engaged = len(reasons) > 0
|
|
80
|
+
if not prev_engaged and self._is_engaged:
|
|
81
|
+
self._logger.notice("Engaging with %s", reasons)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from typing import Optional, Set, List
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from .detector import BaseDetector
|
|
6
|
+
from ..configuration.persistence_configuration import PersistenceConfiguration
|
|
7
|
+
from ..configuration.system_maintenance_config import SystemMaintenanceConfig
|
|
8
|
+
from ...api import ApiDetectorKind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SystemMaintenanceMonitor(BaseDetector):
|
|
12
|
+
|
|
13
|
+
CONFIG = "config"
|
|
14
|
+
|
|
15
|
+
MAX_PELLET_LOADED_ENGAGED = "max_pellet_loaded_engaged"
|
|
16
|
+
MAX_CONSECUTIVE_FAILED_LOAD_ENGAGED = "max_consecutive_failed_load_engaged"
|
|
17
|
+
|
|
18
|
+
def __init__(self, *, config: SystemMaintenanceConfig):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._config = config
|
|
21
|
+
self._max_pellet_loaded_engaged = False
|
|
22
|
+
self._max_consecutive_failed_load_engaged = False
|
|
23
|
+
self._free_disk_space_engaged = False
|
|
24
|
+
self._engaged_reasons: Set[str] = set()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def config(self) -> SystemMaintenanceConfig:
|
|
28
|
+
return self._config
|
|
29
|
+
|
|
30
|
+
@config.setter
|
|
31
|
+
def config(self, value):
|
|
32
|
+
prev, self._config = self._config, value
|
|
33
|
+
self._on_property_changed(self.CONFIG, value, prev)
|
|
34
|
+
self._logger.verbose("Received config: %s", value)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def engaged_reasons(self) -> List[str]:
|
|
38
|
+
return list(self._engaged_reasons)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def max_pellet_loaded_engaged(self):
|
|
42
|
+
return self._max_pellet_loaded_engaged
|
|
43
|
+
|
|
44
|
+
@max_pellet_loaded_engaged.setter
|
|
45
|
+
def max_pellet_loaded_engaged(self, value):
|
|
46
|
+
prev, self._max_pellet_loaded_engaged = self._max_pellet_loaded_engaged, value
|
|
47
|
+
self._on_property_changed(self.MAX_PELLET_LOADED_ENGAGED, value, prev)
|
|
48
|
+
if value != prev:
|
|
49
|
+
self.post_detector_event(ApiDetectorKind.pelletRefillCountExceeded, value, self._config.use_max_pellet_loaded)
|
|
50
|
+
self.check_state_if_not_detector_thread()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def max_consecutive_failed_load_engaged(self):
|
|
54
|
+
return self._max_consecutive_failed_load_engaged
|
|
55
|
+
|
|
56
|
+
@max_consecutive_failed_load_engaged.setter
|
|
57
|
+
def max_consecutive_failed_load_engaged(self, value):
|
|
58
|
+
prev, self._max_consecutive_failed_load_engaged = self._max_consecutive_failed_load_engaged, value
|
|
59
|
+
self._on_property_changed(self.MAX_CONSECUTIVE_FAILED_LOAD_ENGAGED, value, prev)
|
|
60
|
+
if value != prev:
|
|
61
|
+
self.post_detector_event(ApiDetectorKind.consectivePelletLoadFailureExceeded, value,
|
|
62
|
+
self._config.use_max_consecutive_failed_load)
|
|
63
|
+
self.check_state_if_not_detector_thread()
|
|
64
|
+
|
|
65
|
+
def _check_state(self) -> Optional[float]:
|
|
66
|
+
cfg = self._config
|
|
67
|
+
reasons = set()
|
|
68
|
+
for reason, use, engaged in (
|
|
69
|
+
(self.MAX_PELLET_LOADED_ENGAGED, cfg.use_max_pellet_loaded, self._max_pellet_loaded_engaged),
|
|
70
|
+
(self.MAX_CONSECUTIVE_FAILED_LOAD_ENGAGED, cfg.use_max_consecutive_failed_load, self._max_consecutive_failed_load_engaged),
|
|
71
|
+
):
|
|
72
|
+
if use and engaged:
|
|
73
|
+
reasons.add(reason)
|
|
74
|
+
self._engaged_reasons = reasons
|
|
75
|
+
prev_engaged = self._is_engaged
|
|
76
|
+
self.is_engaged = len(reasons) > 0
|
|
77
|
+
if not prev_engaged and self._is_engaged:
|
|
78
|
+
self._logger.notice("Engaging with %s", reasons)
|
|
79
|
+
|
|
80
|
+
def update_pellet_loaded(self, loaded: int):
|
|
81
|
+
cfg = self._config
|
|
82
|
+
engaged = loaded >= cfg.max_pellets_loaded_count
|
|
83
|
+
self.max_pellet_loaded_engaged = engaged
|
|
84
|
+
|
|
85
|
+
def update_failed_pellet_load(self, *, consecutive: int):
|
|
86
|
+
cfg = self._config
|
|
87
|
+
engaged = consecutive >= cfg.max_consecutive_failed_loaded
|
|
88
|
+
self.max_consecutive_failed_load_engaged = engaged
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .animal_subject import AnimalSubject
|