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,26 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Tuple, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclasses.dataclass
|
|
6
|
+
class SystemDataArgsKwargs:
|
|
7
|
+
"""A dedicated dataclass to pass data any desired args and/or kwargs as a single value for system commands"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.args = args
|
|
12
|
+
self.kwargs = kwargs
|
|
13
|
+
|
|
14
|
+
args: Tuple[Any] = ()
|
|
15
|
+
kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .audio_spectrum_message import AudioSpectrumMessage
|
|
19
|
+
from .measurement_message import MeasurementMessageProtocol
|
|
20
|
+
from .message_handler import MessageHandler
|
|
21
|
+
from .motor_configuration_message import MotorConfigurations, ServoConfigMessage, \
|
|
22
|
+
StepperConfigMessage, Motor
|
|
23
|
+
from .system_command_kind import SystemCommandKind
|
|
24
|
+
from .system_message_handler import SystemMessageHandler
|
|
25
|
+
from .system_status_message import SystemStatusMessageKind
|
|
26
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Protocol, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AudioSpectrumMessage(Protocol):
|
|
5
|
+
@property
|
|
6
|
+
def when(self) -> float:
|
|
7
|
+
"""Value of time.time() or equivalent absolute time provided by the hardware."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def index(self) -> int:
|
|
12
|
+
"""
|
|
13
|
+
A relative time index in nanoseconds. Provides finer resolution between measurements than absolute time may
|
|
14
|
+
provide on some platforms. By default, would be provided by time.perf_counter_ns().
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def magnitudes(self) -> List[float]:
|
|
20
|
+
"""The audio spectrum magnitudes in dB."""
|
|
21
|
+
pass
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MeasurementMessageProtocol(Protocol):
|
|
5
|
+
"""
|
|
6
|
+
MeasurementMessage defines the expected interface for measurement data provided by the hardware. This is expected
|
|
7
|
+
object passed as part of a SystemStatusMessageKind.MEASUREMENTS message.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def when(self) -> float:
|
|
12
|
+
"""Value of time.time() or equivalent absolute time provided by the hardware."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
# Deprecated
|
|
16
|
+
@property
|
|
17
|
+
def timestamp(self) -> int:
|
|
18
|
+
"""
|
|
19
|
+
A relative time index in nanoseconds. Provides finer resolution between measurements than absolute time may
|
|
20
|
+
provide on some platforms. By default, would be provided by time.perf_counter_ns().
|
|
21
|
+
"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def index(self) -> int:
|
|
26
|
+
"""
|
|
27
|
+
A relative time index in nanoseconds. Provides finer resolution between measurements than absolute time may
|
|
28
|
+
provide on some platforms. By default, would be provided by time.perf_counter_ns().
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def weight(self) -> float:
|
|
34
|
+
"""The load cell value in grams."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def pressure(self) -> float:
|
|
39
|
+
"""The force detector value in the range of [0, 1024]"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# Deprecated
|
|
43
|
+
@property
|
|
44
|
+
def switch(self) -> float:
|
|
45
|
+
"""The head contact DIO value as 0 or 1."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def temperature(self) -> float:
|
|
50
|
+
"""The temperature in Celsius."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def humidity(self) -> float:
|
|
55
|
+
"""The relative humidity (%)."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def head_contact(self) -> bool:
|
|
60
|
+
"""The head contact DIO value state. True if contact is made, False otherwise."""
|
|
61
|
+
pass
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from functools import partial
|
|
6
|
+
from queue import Queue, Empty
|
|
7
|
+
from threading import Thread
|
|
8
|
+
from typing import Callable, Union, Any, Optional
|
|
9
|
+
|
|
10
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
11
|
+
from ..observable_object import ObservableObject
|
|
12
|
+
|
|
13
|
+
from .system_status_message import SystemStatusMessageKind
|
|
14
|
+
|
|
15
|
+
logger = get_verbose_logger(__name__)
|
|
16
|
+
|
|
17
|
+
TERMINATE = -1001
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MessageHandler(ObservableObject):
|
|
21
|
+
"""
|
|
22
|
+
Base class for handling messages that come from the hardware. It has two primary purposes.
|
|
23
|
+
|
|
24
|
+
The first is to ensure that any middle-management of information or data happens in a separate thread from both the
|
|
25
|
+
hardware interfacing and any downstream consumers without requiring either the hardware code or the downstream code
|
|
26
|
+
to know whether that is taken care of or not - other than knowing to call start() on this object when ready to
|
|
27
|
+
receive messages.
|
|
28
|
+
|
|
29
|
+
The second is to insulate downstream consumers from any specifics of the hardware implementation such as how
|
|
30
|
+
commands are acknowledged, what capabilities different versions of the hardware may have, etc. Subclasses for
|
|
31
|
+
elements such as pellet delivery, tunnel behavior, and sensor measurements present properties (with change
|
|
32
|
+
notification), callbacks, and other means of making the data available in a consistent format that downstream
|
|
33
|
+
consumers care about rather than as specifically implemented the hardware.
|
|
34
|
+
"""
|
|
35
|
+
FIRMWARE_VERSION_PROPERTY = "firmware_version"
|
|
36
|
+
|
|
37
|
+
HEAD_MAGNET_INTENSITY_PROPERTY = "head_magnet_intensity"
|
|
38
|
+
HEAD_GATE_PROPERTY = "gate_angle"
|
|
39
|
+
|
|
40
|
+
DEVICE_X_PROPERTY = "device_x"
|
|
41
|
+
DEVICE_Y_PROPERTY = "device_y"
|
|
42
|
+
DEVICE_Z_PROPERTY = "device_z"
|
|
43
|
+
|
|
44
|
+
STEPPER_X_PROPERTY = "stepper_x"
|
|
45
|
+
STEPPER_Y_PROPERTY = "stepper_y"
|
|
46
|
+
STEPPER_Z_PROPERTY = "stepper_z"
|
|
47
|
+
|
|
48
|
+
LOAD_ARM_ANGLE_PROPERTY = "load_angle"
|
|
49
|
+
COVER_ARM_ANGLE_PROPERTY = "cover_angle"
|
|
50
|
+
TUNNEL_FAN_PROPERTY = "tunnel_fan"
|
|
51
|
+
|
|
52
|
+
FRONT_DOOR_PROPERTY = "front_door"
|
|
53
|
+
DRAWER_DOOR_PROPERTY = "drawer_door"
|
|
54
|
+
SPARE_DOOR_PROPERTY = "spare_door"
|
|
55
|
+
EXT_BUTTON_PROPERTY = "ext_button"
|
|
56
|
+
|
|
57
|
+
STIMULI_PROPERTY = "stimuli"
|
|
58
|
+
CONFIG_PROPERTY = "config"
|
|
59
|
+
|
|
60
|
+
# type hints helper:
|
|
61
|
+
# dynamic event:
|
|
62
|
+
ack_received: Callable[[str], None]
|
|
63
|
+
|
|
64
|
+
def __init__(self, input_queue: Queue, name: str = "message-handler", event_names=()):
|
|
65
|
+
super().__init__(event_names=event_names + ("ack_received",))
|
|
66
|
+
self._input_queue = input_queue
|
|
67
|
+
self._name = name
|
|
68
|
+
self._current_thread = None
|
|
69
|
+
|
|
70
|
+
def __del__(self):
|
|
71
|
+
thread = self._current_thread
|
|
72
|
+
if thread is not None:
|
|
73
|
+
self.request_terminate()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def input_queue(self) -> Queue:
|
|
77
|
+
return self._input_queue
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
if self._current_thread is None or not self._current_thread.is_alive():
|
|
81
|
+
logger.verbose("Starting system message handler thread")
|
|
82
|
+
self._current_thread = Thread(
|
|
83
|
+
target=self.run, name=self.__class__.__name__,
|
|
84
|
+
daemon=True, # in case main thread exits: also have the current handler thread to exit
|
|
85
|
+
)
|
|
86
|
+
self._current_thread.start()
|
|
87
|
+
|
|
88
|
+
def run(self):
|
|
89
|
+
try:
|
|
90
|
+
self._run()
|
|
91
|
+
except Exception as err:
|
|
92
|
+
logger.exception("Fatal error during run: %s", err)
|
|
93
|
+
|
|
94
|
+
def _run(self):
|
|
95
|
+
logger.debug(f"<{self._name}>: entering message event loop")
|
|
96
|
+
q_get = self._input_queue.get
|
|
97
|
+
task_done = self._input_queue.task_done
|
|
98
|
+
msg_received = self.message_received
|
|
99
|
+
tot_read_count = 0
|
|
100
|
+
t_next_check_size = time.perf_counter()
|
|
101
|
+
while True:
|
|
102
|
+
msg, data = q_get()
|
|
103
|
+
if __debug__:
|
|
104
|
+
tot_read_count += 1
|
|
105
|
+
t_now = time.perf_counter()
|
|
106
|
+
if t_now > t_next_check_size:
|
|
107
|
+
logger.debug("system message handler input queue: size=%s read=%.1f / s",
|
|
108
|
+
self._input_queue.qsize(), tot_read_count / 60)
|
|
109
|
+
t_next_check_size += 60
|
|
110
|
+
tot_read_count = 0
|
|
111
|
+
if msg == TERMINATE:
|
|
112
|
+
task_done()
|
|
113
|
+
break
|
|
114
|
+
elif msg == SystemStatusMessageKind.ACKNOWLEDGE:
|
|
115
|
+
try:
|
|
116
|
+
self.ack_received(data)
|
|
117
|
+
except Exception as err:
|
|
118
|
+
logger.exception("Error during ack_received callback: %s", err)
|
|
119
|
+
elif msg == SystemStatusMessageKind.FIRMWARE_VERSION:
|
|
120
|
+
self.property_changed(MessageHandler.FIRMWARE_VERSION_PROPERTY, data, None)
|
|
121
|
+
else:
|
|
122
|
+
try:
|
|
123
|
+
msg_received(msg, data)
|
|
124
|
+
except Exception as err:
|
|
125
|
+
logger.exception("Error during msg_received callback: msg=%s err=%s ; data=%s",
|
|
126
|
+
msg, err, data)
|
|
127
|
+
task_done()
|
|
128
|
+
logger.debug(f"<{self._name}>: exiting message event loop")
|
|
129
|
+
|
|
130
|
+
def request_terminate(self):
|
|
131
|
+
"""
|
|
132
|
+
Sends a termination request to the device reader queue. The thread may have not yet terminated when this call
|
|
133
|
+
returns.
|
|
134
|
+
"""
|
|
135
|
+
if self._input_queue is not None:
|
|
136
|
+
self._input_queue.put((TERMINATE, None))
|
|
137
|
+
|
|
138
|
+
def wait_terminated(self):
|
|
139
|
+
thread = self._current_thread
|
|
140
|
+
queue = self._input_queue
|
|
141
|
+
if thread is not None:
|
|
142
|
+
self._current_thread = None
|
|
143
|
+
thread.join(5)
|
|
144
|
+
if thread.is_alive():
|
|
145
|
+
logger.warning("message handler thread still alive")
|
|
146
|
+
if queue is not None:
|
|
147
|
+
self._input_queue = None
|
|
148
|
+
while not queue.empty():
|
|
149
|
+
try:
|
|
150
|
+
obj = queue.get_nowait()
|
|
151
|
+
except Empty:
|
|
152
|
+
break
|
|
153
|
+
logger.warning("drop unhandled %s ; %s", type(obj), obj)
|
|
154
|
+
queue.task_done()
|
|
155
|
+
queue.join()
|
|
156
|
+
|
|
157
|
+
def message_received(self, msg, _data):
|
|
158
|
+
pass
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Protocol, Tuple
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Motor(IntEnum):
|
|
6
|
+
"""
|
|
7
|
+
Identifiers for the different motor types
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
NONE = 0
|
|
11
|
+
TUNNEL_MAGNET_SERVO = 1
|
|
12
|
+
PELLET_X_MOTOR = 2
|
|
13
|
+
PELLET_Y_MOTOR = 3
|
|
14
|
+
PELLET_Z_MOTOR = 4
|
|
15
|
+
PELLET_COVER_SERVO = 5
|
|
16
|
+
PELLET_LOAD_SERVO = 6
|
|
17
|
+
TUNNEL_GATE_SERVO = 7
|
|
18
|
+
TUNNEL_FAN_SERVO = 8
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StepperConfigMessage(Protocol):
|
|
22
|
+
"""
|
|
23
|
+
Protocol for the interface for obtaining stepper motor configuration entities
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def motor(self) -> Motor:
|
|
28
|
+
"""Motor associated with the configuration"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def maximum_velocity(self) -> float:
|
|
33
|
+
"""Units: turns/sec"""
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def maximum_acceleration(self) -> float:
|
|
37
|
+
"""Units: turns/sec^2"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def minimum_step_inverted(self) -> int:
|
|
41
|
+
"""Units: 1/min step"""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def steps_per_revolution(self) -> float:
|
|
45
|
+
"""Units: steps/rev"""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def flip_limit_orientation(self) -> int:
|
|
49
|
+
"""
|
|
50
|
+
0 - Do no invert motor direction
|
|
51
|
+
1 - Invert motor direction
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ServoConfigMessage(Protocol):
|
|
56
|
+
"""
|
|
57
|
+
Protocol for the interface for obtaining servo motor configuration entities
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def motor(self) -> Motor:
|
|
62
|
+
"""Motor associated with the configuration"""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def maximum_velocity(self) -> float:
|
|
66
|
+
"""Units: deg/sec"""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def maximum_acceleration(self) -> float:
|
|
70
|
+
"""Units: deg/sec^2"""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def minimum_position(self) -> float:
|
|
74
|
+
"""Units: deg"""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def maximum_position(self) -> float:
|
|
78
|
+
"""Units: deg"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def min_pwm_duration(self) -> float:
|
|
82
|
+
"""Units: µs"""
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def max_pwm_duration(self) -> float:
|
|
86
|
+
"""Units: µs"""
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def detach(self) -> bool:
|
|
90
|
+
"""Is attach/detach used"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MotorConfigurations(Protocol):
|
|
94
|
+
"""
|
|
95
|
+
Protocol for access to a full set of motor configurations
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def magnet_config(self) -> Tuple[Motor, ServoConfigMessage]:
|
|
100
|
+
"""The magnet config"""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def gate_config(self) -> Tuple[Motor, ServoConfigMessage]:
|
|
104
|
+
"""The gate config"""
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def load_config(self) -> Tuple[Motor, ServoConfigMessage]:
|
|
108
|
+
"""The pellet-load config"""
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def cover_config(self) -> Tuple[Motor, ServoConfigMessage]:
|
|
112
|
+
"""The pellet-cover config"""
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def x_config(self) -> Tuple[Motor, StepperConfigMessage]:
|
|
116
|
+
"""The pellet stepper X motor & config"""
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def y_config(self) -> Tuple[Motor, StepperConfigMessage]:
|
|
120
|
+
"""The pellet stepper Y motor & config"""
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def z_config(self) -> Tuple[Motor, StepperConfigMessage]:
|
|
124
|
+
"""The pellet stepper Z motor & config"""
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def tunnel_fan_config(self) -> Tuple[Motor, ServoConfigMessage]:
|
|
128
|
+
"""The tunnel-fan motor & config"""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SystemCommandKind(IntEnum):
|
|
5
|
+
"""
|
|
6
|
+
This class defines the commands supported by the device. If there is a response beyond acknowledgement, it is
|
|
7
|
+
defined in SystemStatusMessageKind.
|
|
8
|
+
|
|
9
|
+
Commands are sent to the device with optional data and context information. The data is specific to the command
|
|
10
|
+
and may not be applicable to all commands. The context information is provided by the caller and expected back
|
|
11
|
+
as part of the acknowledgement that the command is complete. Note that complete may mean that the action is fully
|
|
12
|
+
executed (e.g., a move has reached its final position), not simply that the command was sent to the device.
|
|
13
|
+
|
|
14
|
+
The above is currently implemented and represented by the send_message() method in the DeviceConnection class.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# System commands unrelated to actual behavior.
|
|
18
|
+
REQUEST_VERSION = 1
|
|
19
|
+
|
|
20
|
+
# Actions nominally considered part of the tunnel/head fixation unit (magnet module in some nomenclature).
|
|
21
|
+
MOVE_MAGNET_SERVO = 101
|
|
22
|
+
UPDATE_SCALE_TARE = 102
|
|
23
|
+
OPEN_TUNNEL_GATE = 103
|
|
24
|
+
CLOSE_TUNNEL_GATE = 104
|
|
25
|
+
|
|
26
|
+
# Actions nominally considered part of the pellet delivery unit.
|
|
27
|
+
READ_MOTOR_CONFIGURATION = 201
|
|
28
|
+
WRITE_MOTOR_CONFIGURATION = 202
|
|
29
|
+
SET_LOAD_PELLET_PROCEDURE = 203
|
|
30
|
+
SET_SEND_PELLET_PROCEDURE = 204
|
|
31
|
+
SET_X = 205 # sets the X position for use by SEND_FIXED_XYZ
|
|
32
|
+
SET_Y = 206 # sets the Y position for use by SEND_FIXED_XYZ
|
|
33
|
+
SET_Z = 207 # sets the Z position for use by SEND_FIXED_XYZ
|
|
34
|
+
MOVE_LOAD_SERVO = 208
|
|
35
|
+
MOVE_COVER_SERVO = 209
|
|
36
|
+
SEND_HOME = 210
|
|
37
|
+
LOAD_PELLET = 211
|
|
38
|
+
SEND_PELLET = 212
|
|
39
|
+
RELEASE_PELLET = 213
|
|
40
|
+
COVER_PELLET = 214
|
|
41
|
+
SEND_TO_LIMITS = 215
|
|
42
|
+
SET_COVER_PELLET_PROCEDURE = 216
|
|
43
|
+
SET_RELEASE_PELLET_PROCEDURE = 217
|
|
44
|
+
DELAY = 218
|
|
45
|
+
SEND_FIXED_XYZ = 219
|
|
46
|
+
MOVE_X = 220 # moves X, only
|
|
47
|
+
MOVE_Y = 221 # moves Y, only
|
|
48
|
+
MOVE_Z = 222 # moves Z, only
|
|
49
|
+
MOVE_GATE_SERVO = 223
|
|
50
|
+
|
|
51
|
+
SEND_RETRACT = 224 # make y - 15, or 0 if lower.
|
|
52
|
+
|
|
53
|
+
SET_MOTOR_DRIFT = 225
|
|
54
|
+
SET_AUTO_CORRECT_DRIFT = 226
|
|
55
|
+
|
|
56
|
+
SERVO_ATTACH = 227
|
|
57
|
+
SERVO_DETACH = 228
|
|
58
|
+
|
|
59
|
+
TUNNEL_FAN_ON = 229
|
|
60
|
+
TUNNEL_FAN_OFF = 230
|
|
61
|
+
TUNNEL_FAN_SET = 231
|
|
62
|
+
|
|
63
|
+
# General actions.
|
|
64
|
+
PLAY_TONE = 301
|
|
65
|
+
SET_RGB_LED = 302
|
|
66
|
+
SET_DIGITAL_OUTPUT = 303
|
|
67
|
+
SET_ANALOG_OUTPUT = 304
|
|
68
|
+
|
|
69
|
+
# Not deprecated so long as the original hardware is supported, but a bit esoteric. It is not necessary for
|
|
70
|
+
# new hardware to support them or for new application/scripts to support them.
|
|
71
|
+
RAW_COMMAND = 1001 # TODO only used at command line for original hardware. Remove eventually.
|
|
72
|
+
SETTINGS = 1002 # TODO Specific to original hardware head fix unit and not used in user apps. Remove eventually.
|
|
73
|
+
STREAM_START = 1003 # TODO n/a new hardware, but required for original.
|
|
74
|
+
STREAM_STOP = 1004 # TODO n/a new hardware, but required for original.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from typing import Callable, List, Tuple, Optional
|
|
6
|
+
|
|
7
|
+
from ..analysis.sensor_analysis import SensorAnalysis
|
|
8
|
+
|
|
9
|
+
from .message_handler import MessageHandler
|
|
10
|
+
from .system_status_message import SystemStatusMessageKind
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SystemMessageHandler(MessageHandler):
|
|
16
|
+
|
|
17
|
+
def __init__(self, input_queue: Queue, *, sensor_analysis: Optional[SensorAnalysis] = None):
|
|
18
|
+
super().__init__(input_queue, name="system-message-handler")
|
|
19
|
+
|
|
20
|
+
self._measurement_callback = None
|
|
21
|
+
self._audio_callback = None
|
|
22
|
+
|
|
23
|
+
self._analysis = SensorAnalysis() if sensor_analysis is None else sensor_analysis
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def measurement_callback(self):
|
|
27
|
+
return self._measurement_callback
|
|
28
|
+
|
|
29
|
+
@measurement_callback.setter
|
|
30
|
+
def measurement_callback(self, measurement_callback: Callable[[Tuple], None]) -> None:
|
|
31
|
+
self._measurement_callback = measurement_callback
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def audio_callback(self):
|
|
35
|
+
return self._audio_callback
|
|
36
|
+
|
|
37
|
+
@audio_callback.setter
|
|
38
|
+
def audio_callback(self, audio_callback: Callable[[List[float]], None]) -> None:
|
|
39
|
+
prev = self._audio_callback
|
|
40
|
+
if prev is not None:
|
|
41
|
+
logger.info("Replacing audio callback %s with %s", prev, audio_callback)
|
|
42
|
+
self._audio_callback = audio_callback
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def analysis(self):
|
|
46
|
+
return self._analysis
|
|
47
|
+
|
|
48
|
+
def message_received(self, msg, data):
|
|
49
|
+
# TODO: These are treated as if the property has changed. If the number of event listeners increases or their
|
|
50
|
+
# behaviors are complex and do not check for change themselves, this could become a bottleneck. This could be
|
|
51
|
+
# updated to store previous values and only notify listeners on change, like a typical ObservableObject
|
|
52
|
+
# implementation. Keeping things simple for the time being.
|
|
53
|
+
if msg == SystemStatusMessageKind.MEASUREMENT or msg == SystemStatusMessageKind.MEASUREMENTS:
|
|
54
|
+
measures = self._analysis.measurements_received(data)
|
|
55
|
+
if self._measurement_callback is not None and len(measures) > 0:
|
|
56
|
+
self._measurement_callback(measures)
|
|
57
|
+
|
|
58
|
+
elif msg == SystemStatusMessageKind.AUDIO_SPECTRUM:
|
|
59
|
+
self._analysis.audio_spectrum_received(data)
|
|
60
|
+
if self._audio_callback is not None:
|
|
61
|
+
self._audio_callback(data.magnitudes)
|
|
62
|
+
|
|
63
|
+
elif msg == SystemStatusMessageKind.PELLET_X:
|
|
64
|
+
self.property_changed(MessageHandler.DEVICE_X_PROPERTY, data, None)
|
|
65
|
+
|
|
66
|
+
elif msg == SystemStatusMessageKind.PELLET_Y:
|
|
67
|
+
self.property_changed(MessageHandler.DEVICE_Y_PROPERTY, data, None)
|
|
68
|
+
|
|
69
|
+
elif msg == SystemStatusMessageKind.PELLET_Z:
|
|
70
|
+
self.property_changed(MessageHandler.DEVICE_Z_PROPERTY, data, None)
|
|
71
|
+
|
|
72
|
+
elif msg == SystemStatusMessageKind.PELLET_MOTOR_X:
|
|
73
|
+
self.property_changed(MessageHandler.DEVICE_X_PROPERTY, data.position, None)
|
|
74
|
+
self.property_changed(MessageHandler.STEPPER_X_PROPERTY, data, None)
|
|
75
|
+
|
|
76
|
+
elif msg == SystemStatusMessageKind.PELLET_MOTOR_Y:
|
|
77
|
+
self.property_changed(MessageHandler.DEVICE_Y_PROPERTY, data.position, None)
|
|
78
|
+
self.property_changed(MessageHandler.STEPPER_Y_PROPERTY, data, None)
|
|
79
|
+
|
|
80
|
+
elif msg == SystemStatusMessageKind.PELLET_MOTOR_Z:
|
|
81
|
+
self.property_changed(MessageHandler.DEVICE_Z_PROPERTY, data.position, None)
|
|
82
|
+
self.property_changed(MessageHandler.STEPPER_Z_PROPERTY, data, None)
|
|
83
|
+
|
|
84
|
+
elif msg == SystemStatusMessageKind.PELLET_LOAD:
|
|
85
|
+
self.property_changed(MessageHandler.LOAD_ARM_ANGLE_PROPERTY, data, None)
|
|
86
|
+
|
|
87
|
+
elif msg == SystemStatusMessageKind.PELLET_COVER:
|
|
88
|
+
self.property_changed(MessageHandler.COVER_ARM_ANGLE_PROPERTY, data, None)
|
|
89
|
+
|
|
90
|
+
elif msg == SystemStatusMessageKind.HEAD_MAGNET:
|
|
91
|
+
self.property_changed(MessageHandler.HEAD_MAGNET_INTENSITY_PROPERTY, data, None)
|
|
92
|
+
|
|
93
|
+
elif msg == SystemStatusMessageKind.TUNNEL_GATE_SERVO:
|
|
94
|
+
self.property_changed(MessageHandler.HEAD_GATE_PROPERTY, data, None)
|
|
95
|
+
|
|
96
|
+
elif msg == SystemStatusMessageKind.TUNNEL_FAN:
|
|
97
|
+
self.property_changed(MessageHandler.TUNNEL_FAN_PROPERTY, data, None)
|
|
98
|
+
|
|
99
|
+
elif msg == SystemStatusMessageKind.FRONT_DOOR:
|
|
100
|
+
# directly update related monitor, hopefully is very fast
|
|
101
|
+
self._analysis.external_doors_monitor.update_door_state(msg, data)
|
|
102
|
+
self.property_changed(MessageHandler.FRONT_DOOR_PROPERTY, data, None)
|
|
103
|
+
|
|
104
|
+
elif msg == SystemStatusMessageKind.DRAWER_DOOR:
|
|
105
|
+
# directly update related monitor, hopefully is very fast
|
|
106
|
+
self._analysis.external_doors_monitor.update_door_state(msg, data)
|
|
107
|
+
self.property_changed(MessageHandler.DRAWER_DOOR_PROPERTY, data, None)
|
|
108
|
+
|
|
109
|
+
elif msg == SystemStatusMessageKind.SPARE_DOOR:
|
|
110
|
+
self.property_changed(MessageHandler.SPARE_DOOR_PROPERTY, data, None)
|
|
111
|
+
|
|
112
|
+
elif msg == SystemStatusMessageKind.EXT_BUTTON:
|
|
113
|
+
self.property_changed(MessageHandler.EXT_BUTTON_PROPERTY, data, None)
|
|
114
|
+
|
|
115
|
+
elif msg == SystemStatusMessageKind.STIMULUS_INPUTS:
|
|
116
|
+
self.property_changed(MessageHandler.STIMULI_PROPERTY, data, None)
|
|
117
|
+
|
|
118
|
+
elif msg == SystemStatusMessageKind.MOTOR_CONFIGURATION:
|
|
119
|
+
self._on_property_changed("config", data, None)
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
logger.warning("unhandled msg %s", msg)
|