mxbiflow 0.1.1__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.
Files changed (93) hide show
  1. mxbiflow/__init__.py +3 -0
  2. mxbiflow/assets/__init__.py +5 -0
  3. mxbiflow/assets/clicker.wav +0 -0
  4. mxbiflow/config_store.py +68 -0
  5. mxbiflow/data_logger.py +114 -0
  6. mxbiflow/default/__init__.py +4 -0
  7. mxbiflow/default/idle/assets/apple_v1.png +0 -0
  8. mxbiflow/default/idle/idle.py +57 -0
  9. mxbiflow/detector_bridge.py +87 -0
  10. mxbiflow/game.py +84 -0
  11. mxbiflow/infra/eventbus.py +31 -0
  12. mxbiflow/main.py +106 -0
  13. mxbiflow/models/animal.py +130 -0
  14. mxbiflow/models/reward.py +7 -0
  15. mxbiflow/models/session.py +145 -0
  16. mxbiflow/mxbiflow.py +43 -0
  17. mxbiflow/path.py +41 -0
  18. mxbiflow/scene/__init__.py +8 -0
  19. mxbiflow/scene/scene_manager.py +64 -0
  20. mxbiflow/scene/scene_protocol.py +22 -0
  21. mxbiflow/scheduler.py +90 -0
  22. mxbiflow/tasks/GNGSiD/models.py +70 -0
  23. mxbiflow/tasks/GNGSiD/stages/detect_stage/config.json +116 -0
  24. mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage.py +161 -0
  25. mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage_models.py +65 -0
  26. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/config.json +70 -0
  27. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage.py +173 -0
  28. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage_models.py +80 -0
  29. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/config.json +83 -0
  30. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_models.py +58 -0
  31. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_stage.py +149 -0
  32. mxbiflow/tasks/GNGSiD/tasks/artifacts.py +13 -0
  33. mxbiflow/tasks/GNGSiD/tasks/detect/models.py +21 -0
  34. mxbiflow/tasks/GNGSiD/tasks/detect/scene.py +271 -0
  35. mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_models.py +31 -0
  36. mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_scene.py +336 -0
  37. mxbiflow/tasks/GNGSiD/tasks/touch/touch_models.py +17 -0
  38. mxbiflow/tasks/GNGSiD/tasks/touch/touch_scene.py +256 -0
  39. mxbiflow/tasks/GNGSiD/tasks/utils/targets.py +57 -0
  40. mxbiflow/tasks/cross_modal/bundle_dir.py +553 -0
  41. mxbiflow/tasks/cross_modal/config.py +41 -0
  42. mxbiflow/tasks/cross_modal/media.py +61 -0
  43. mxbiflow/tasks/cross_modal/models.py +57 -0
  44. mxbiflow/tasks/cross_modal/scene.py +252 -0
  45. mxbiflow/tasks/cross_modal/stage.py +218 -0
  46. mxbiflow/tasks/cross_modal/trial_io.py +23 -0
  47. mxbiflow/tasks/cross_modal/trial_schema.py +113 -0
  48. mxbiflow/tasks/default/error_task/error_scene.py +53 -0
  49. mxbiflow/tasks/default/idle_task/assets/apple_v1.png +0 -0
  50. mxbiflow/tasks/default/idle_task/idle_scene.py +85 -0
  51. mxbiflow/tasks/default/initial_habituation_training/README.md +188 -0
  52. mxbiflow/tasks/default/initial_habituation_training/stages/config.csv +7 -0
  53. mxbiflow/tasks/default/initial_habituation_training/stages/config.json +67 -0
  54. mxbiflow/tasks/default/initial_habituation_training/stages/initial_habituation_training_stage.py +172 -0
  55. mxbiflow/tasks/default/initial_habituation_training/stages/models.py +56 -0
  56. mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward.py +244 -0
  57. mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward_models.py +50 -0
  58. mxbiflow/tasks/task_protocol.py +26 -0
  59. mxbiflow/tasks/task_table.py +29 -0
  60. mxbiflow/tasks/two_alternative_choice/assets/starter.py +27 -0
  61. mxbiflow/tasks/two_alternative_choice/models.py +68 -0
  62. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/config.json +118 -0
  63. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_models.py +41 -0
  64. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_stage.py +122 -0
  65. mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_models.py +19 -0
  66. mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_scene.py +249 -0
  67. mxbiflow/timer/__init__.py +3 -0
  68. mxbiflow/timer/frame_timer.py +47 -0
  69. mxbiflow/timer/realtime_timer.py +0 -0
  70. mxbiflow/tmp_email.py +13 -0
  71. mxbiflow/ui/components/animal.py +87 -0
  72. mxbiflow/ui/components/baseconfig.py +68 -0
  73. mxbiflow/ui/components/card.py +18 -0
  74. mxbiflow/ui/components/device_card/__init__.py +17 -0
  75. mxbiflow/ui/components/device_card/detector/beambreak_detector_card.py +29 -0
  76. mxbiflow/ui/components/device_card/detector/fusion_detector.py +45 -0
  77. mxbiflow/ui/components/device_card/detector/mock_detector_card.py +20 -0
  78. mxbiflow/ui/components/device_card/detector/rfid_detector.py +40 -0
  79. mxbiflow/ui/components/device_card/device_card.py +67 -0
  80. mxbiflow/ui/components/device_card/rewarder/mock_rewarder_card.py +20 -0
  81. mxbiflow/ui/components/device_card/rewarder/rpi_gpio_rewarder.py +33 -0
  82. mxbiflow/ui/components/devices.py +183 -0
  83. mxbiflow/ui/components/dialog/__init__.py +3 -0
  84. mxbiflow/ui/components/dialog/add_devices_dialog.py +64 -0
  85. mxbiflow/ui/components/experiment_groups.py +122 -0
  86. mxbiflow/ui/experiment_panel.py +91 -0
  87. mxbiflow/ui/mxbi_panel.py +152 -0
  88. mxbiflow/utils/logger.py +19 -0
  89. mxbiflow/utils/serial.py +10 -0
  90. mxbiflow-0.1.1.dist-info/METADATA +168 -0
  91. mxbiflow-0.1.1.dist-info/RECORD +93 -0
  92. mxbiflow-0.1.1.dist-info/WHEEL +4 -0
  93. mxbiflow-0.1.1.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,122 @@
1
+ from typing import TYPE_CHECKING, Final
2
+
3
+ from mxbi.data_logger import DataLogger, DataLoggerType
4
+ from mxbi.tasks.two_alternative_choice.models import PersistentData, Result
5
+ from mxbi.tasks.two_alternative_choice.stages.size_reduction_stage.size_reduction_models import (
6
+ config,
7
+ )
8
+ from mxbi.tasks.two_alternative_choice.tasks.touch.touch_scene import TwoACTouchScene
9
+ from mxbi.utils.logger import logger
10
+
11
+ if TYPE_CHECKING:
12
+ from mxbi.models.animal import AnimalState, ScheduleCondition
13
+ from mxbi.models.session import SessionState
14
+ from mxbi.models.task import Feedback
15
+ from mxbi.tasks.two_alternative_choice.stages.size_reduction_stage.size_reduction_models import (
16
+ SizeReductionStageConfig,
17
+ )
18
+ from mxbi.theater import Theater
19
+
20
+ _presistent_data: dict[str, PersistentData] = {}
21
+
22
+
23
+ class TWOACSizeReductionStage:
24
+ STAGE_NAME: Final[str] = "twoac_size_reduction_stage"
25
+
26
+ def __init__(
27
+ self,
28
+ theater: "Theater",
29
+ session_state: "SessionState",
30
+ animal_state: "AnimalState",
31
+ ) -> None:
32
+ self._theater = theater
33
+ self._session_state = session_state
34
+ self._animal_state = animal_state
35
+
36
+ self._stage_config = self._load_stage_config(animal_state.name)
37
+
38
+ _levels_config = self._stage_config.levels_table[animal_state.level]
39
+ _config = self._stage_config.trial_config
40
+
41
+ _config.level = _levels_config.level
42
+ _config.stimulation_size = _levels_config.stimulation_size
43
+
44
+ self._data_logger = DataLogger(
45
+ self._session_state,
46
+ self._animal_state.name,
47
+ self.STAGE_NAME,
48
+ DataLoggerType.JSONL,
49
+ )
50
+
51
+ self._presistent_data = _presistent_data.get(self._animal_state.name)
52
+
53
+ if self._presistent_data is None:
54
+ self._presistent_data = PersistentData(
55
+ rewards=0,
56
+ correct=0,
57
+ incorrect=0,
58
+ timeout=0,
59
+ )
60
+ _presistent_data[self._animal_state.name] = self._presistent_data
61
+
62
+ self._task = TwoACTouchScene(
63
+ theater,
64
+ session_state.session_config,
65
+ animal_state,
66
+ session_state.session_config.screen_type,
67
+ _config,
68
+ self._presistent_data,
69
+ )
70
+
71
+ def start(self) -> "Feedback":
72
+ trial_data = self._task.start()
73
+ self._data_logger.save(trial_data.model_dump())
74
+
75
+ feedback = self._handle_result(trial_data.result)
76
+ logger.debug(
77
+ f"{self.STAGE_NAME}: "
78
+ f"session_id={self._session_state.session_id}, "
79
+ f"animal_name={self._animal_state.name}, "
80
+ f"animal_level={self._animal_state.level}, "
81
+ f"state_name={self.STAGE_NAME}, "
82
+ f"result={trial_data}, "
83
+ f"feedback={feedback}"
84
+ )
85
+
86
+ return feedback
87
+
88
+ def _load_stage_config(self, monkey: str) -> "SizeReductionStageConfig":
89
+ stage_config = config.root.get(monkey) or config.root.get("default")
90
+ if stage_config is None:
91
+ raise ValueError("No default stage config found")
92
+ return stage_config
93
+
94
+ def _handle_result(self, result: "Result") -> "Feedback":
95
+ feedback = False
96
+ match result:
97
+ case Result.CORRECT:
98
+ _presistent_data[self._animal_state.name].correct += 1
99
+ feedback = True
100
+ case Result.INCORRECT:
101
+ _presistent_data[self._animal_state.name].incorrect += 1
102
+ feedback = False
103
+ case Result.TIMEOUT:
104
+ _presistent_data[self._animal_state.name].timeout += 1
105
+ feedback = False
106
+ case Result.CANCEL:
107
+ feedback = False
108
+
109
+ return feedback
110
+
111
+ def quit(self) -> None:
112
+ self._task.cancle()
113
+
114
+ def on_idle(self) -> None:
115
+ self._task.cancle()
116
+
117
+ def on_return(self) -> None:
118
+ self._task.cancle()
119
+
120
+ @property
121
+ def condition(self) -> "ScheduleCondition | None":
122
+ return self._stage_config.condition
@@ -0,0 +1,19 @@
1
+ from mxbi.tasks.two_alternative_choice.models import (
2
+ BaseDataToShow,
3
+ BaseTrialConfig,
4
+ BaseTrialData,
5
+ )
6
+
7
+
8
+ class TrialConfig(BaseTrialConfig):
9
+ stimulus_freq: int
10
+ stimulus_freq_duration: int
11
+
12
+ stimulus_interval: int
13
+
14
+
15
+ class TrialData(BaseTrialData):
16
+ trial_config: TrialConfig
17
+
18
+
19
+ class DataToShow(BaseDataToShow): ...
@@ -0,0 +1,249 @@
1
+ from concurrent.futures import Future
2
+ from datetime import datetime
3
+ from math import ceil
4
+ from tkinter import CENTER, Canvas, Event
5
+ from typing import TYPE_CHECKING, Final
6
+
7
+ from mxbi.tasks.two_alternative_choice.assets.starter import Starter
8
+ from mxbi.tasks.two_alternative_choice.models import Result, TouchEvent
9
+ from mxbi.tasks.two_alternative_choice.tasks.touch.touch_models import (
10
+ DataToShow,
11
+ TrialData,
12
+ )
13
+ from mxbi.utils.aplayer import ToneConfig
14
+ from mxbi.utils.tkinter.components.canvas_with_border import CanvasWithInnerBorder
15
+ from mxbi.utils.tkinter.components.showdata_widget import ShowDataWidget
16
+ from numpy import int16
17
+ from numpy.typing import NDArray
18
+
19
+ if TYPE_CHECKING:
20
+ from mxbi.models.animal import AnimalState
21
+ from mxbi.models.session import ScreenConfig, SessionConfig
22
+ from mxbi.tasks.two_alternative_choice.models import PersistentData
23
+ from mxbi.tasks.two_alternative_choice.tasks.touch.touch_models import TrialConfig
24
+ from mxbi.theater import Theater
25
+
26
+
27
+ class TwoACTouchScene:
28
+ def __init__(
29
+ self,
30
+ theater: "Theater",
31
+ session: "SessionConfig",
32
+ animal_state: "AnimalState",
33
+ screen_type: "ScreenConfig",
34
+ trial_config: "TrialConfig",
35
+ persistent_data: "PersistentData",
36
+ ) -> None:
37
+ self._theater: "Final[Theater]" = theater
38
+ self._animal_state: "Final[AnimalState]" = animal_state
39
+ self._screen_type: "Final[ScreenConfig]" = screen_type
40
+ self._trial_config: "Final[TrialConfig]" = trial_config
41
+ self._persistent_data: "Final[PersistentData]" = persistent_data
42
+
43
+ self._tone = self._prepare_stimulus()
44
+ self._standard_reward_stimulus = self._theater.new_standard_reward_stimulus(
45
+ self._trial_config.stimulus_duration
46
+ )
47
+
48
+ self._on_trial_start()
49
+
50
+ # region public api
51
+ def start(self) -> "TrialData":
52
+ self._theater.mainloop()
53
+
54
+ return self._data
55
+
56
+ def cancle(self) -> None:
57
+ self._data.result = Result.CANCEL
58
+ self._theater.aplayer.stop()
59
+ self._on_trial_end()
60
+
61
+ # endregion
62
+
63
+ # region lifecycle
64
+ def _on_trial_start(self) -> None:
65
+ self._create_view()
66
+ self._init_data()
67
+ self._bind_events()
68
+
69
+ def _on_inter_trial(self) -> None:
70
+ self._background.after(
71
+ self._trial_config.inter_trial_interval, self._on_trial_end
72
+ )
73
+
74
+ def _on_trial_end(self) -> None:
75
+ self._background.destroy()
76
+ self._theater.root.quit()
77
+
78
+ # endregion
79
+
80
+ # region views
81
+ def _create_view(self) -> None:
82
+ self._create_background()
83
+ self._create_show_data_widget()
84
+ self._create_target()
85
+
86
+ def _create_background(self) -> None:
87
+ self._background = CanvasWithInnerBorder(
88
+ master=self._theater.root,
89
+ bg="black",
90
+ width=self._screen_type.width,
91
+ height=self._screen_type.height,
92
+ border_width=40,
93
+ )
94
+
95
+ self._background.place(relx=0.5, rely=0.5, anchor="center")
96
+
97
+ def _create_show_data_widget(self) -> None:
98
+ self._show_data_widget = ShowDataWidget(self._background)
99
+ self._show_data_widget.place(relx=0, rely=1, anchor="sw")
100
+ data = DataToShow(
101
+ name=self._animal_state.name,
102
+ id=self._animal_state.trial_id,
103
+ level_id=self._animal_state.current_level_trial_id,
104
+ level=self._trial_config.level,
105
+ rewards=self._persistent_data.rewards,
106
+ correct=self._animal_state.correct_trial,
107
+ incorrect=self._persistent_data.incorrect,
108
+ timeout=self._persistent_data.timeout,
109
+ )
110
+ self._show_data_widget.show_data(data.model_dump())
111
+
112
+ def _create_target(self) -> None:
113
+ xshift = 240
114
+ xcenter = self._screen_type.width * 0.5 + xshift
115
+ ycenter = self._screen_type.height * 0.5
116
+
117
+ self._trigger_canvas = Starter(
118
+ self._background, self._trial_config.stimulation_size
119
+ )
120
+ self._trigger_canvas.place(x=xcenter, y=ycenter, anchor="center")
121
+
122
+ def _create_wrong_view(self) -> None:
123
+ self._trigger_canvas = Canvas(
124
+ self._background,
125
+ bg="grey",
126
+ width=self._screen_type.width,
127
+ height=self._screen_type.height,
128
+ )
129
+ self._trigger_canvas.place(relx=0.5, rely=0.5, anchor=CENTER)
130
+
131
+ # endregion
132
+
133
+ # region event binding
134
+ def _bind_events(self) -> None:
135
+ # Manual reward
136
+ self._background.focus_set()
137
+ self._background.bind("<r>", lambda e: self._give_standard_stimulus())
138
+ self._background.bind("<s>", lambda e: self._theater.caputre(self._background))
139
+
140
+ # Trigger event
141
+ self._background.bind("<ButtonPress>", self._on_background_touched)
142
+ self._trigger_canvas.bind("<ButtonPress>", self._on_touched)
143
+
144
+ # Timeout event
145
+ self._trigger_canvas.after(self._trial_config.time_out, self._on_timeout)
146
+
147
+ # endregion
148
+
149
+ # region event handlers
150
+ def _on_touched(self, event: Event) -> None:
151
+ self._data.touch_events.append(
152
+ TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
153
+ )
154
+ self._background.unbind("<ButtonPress>")
155
+ self._trigger_canvas.destroy()
156
+
157
+ future = self._give_stimulus()
158
+ future.add_done_callback(self._on_stimulus_complete)
159
+
160
+ def _on_background_touched(self, event: Event) -> None:
161
+ self._data.touch_events.append(
162
+ TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
163
+ )
164
+ self._background.unbind("<ButtonPress>")
165
+ self._trigger_canvas.destroy()
166
+
167
+ self._on_incorrect()
168
+
169
+ def _on_correct(self) -> None:
170
+ self._give_reward()
171
+ self._data.result = Result.CORRECT
172
+ self._data.correct_rate = (self._animal_state.correct_trial + 1) / (
173
+ self._animal_state.current_level_trial_id + 1
174
+ )
175
+
176
+ self._on_inter_trial()
177
+
178
+ def _on_incorrect(self) -> None:
179
+ self._data.result = Result.INCORRECT
180
+ self._data.correct_rate = self._animal_state.correct_trial / (
181
+ self._animal_state.current_level_trial_id + 1
182
+ )
183
+
184
+ self._create_wrong_view()
185
+ self._on_inter_trial()
186
+
187
+ def _on_timeout(self) -> None:
188
+ self._data.result = Result.TIMEOUT
189
+ self._data.correct_rate = self._animal_state.correct_trial / (
190
+ self._animal_state.current_level_trial_id + 1
191
+ )
192
+
193
+ self._create_wrong_view()
194
+ self._on_inter_trial()
195
+
196
+ # endregion
197
+
198
+ # region sitimulus and reward
199
+ def _prepare_stimulus(self) -> "NDArray[int16]":
200
+ unit_duration = (
201
+ self._trial_config.stimulus_freq_duration
202
+ + self._trial_config.stimulus_freq_duration
203
+ )
204
+
205
+ times = ceil(self._trial_config.stimulus_duration / unit_duration)
206
+ times = max(times, 1)
207
+
208
+ freq_1 = ToneConfig(
209
+ frequency=self._trial_config.stimulus_freq,
210
+ duration=self._trial_config.stimulus_freq_duration,
211
+ )
212
+ freq_2 = ToneConfig(frequency=0, duration=self._trial_config.stimulus_interval)
213
+
214
+ return self._theater.aplayer.generate_stimulus([freq_1, freq_2], times)
215
+
216
+ def _give_stimulus(self) -> "Future[bool]":
217
+ return self._theater.aplayer.play_stimulus(self._tone)
218
+
219
+ def _on_stimulus_complete(self, future: "Future[bool]") -> None:
220
+ if future.result():
221
+ self._trigger_canvas.after(
222
+ self._trial_config.reward_delay,
223
+ lambda: self._on_correct(),
224
+ )
225
+
226
+ def _give_reward(self) -> None:
227
+ self._persistent_data.rewards += 1
228
+ self._theater.reward.give_reward(self._trial_config.reward_duration)
229
+
230
+ def _give_standard_stimulus(self) -> None:
231
+ self._standard_reward_stimulus.play(self._trial_config.reward_duration)
232
+
233
+ # endregion
234
+
235
+ # region data
236
+ def _init_data(self) -> None:
237
+ self._data = TrialData(
238
+ animal=self._animal_state.name,
239
+ trial_id=self._animal_state.trial_id,
240
+ current_level_trial_id=self._animal_state.current_level_trial_id,
241
+ trial_config=self._trial_config,
242
+ trial_start_time=datetime.now().timestamp(),
243
+ trial_end_time=0,
244
+ result=Result.TIMEOUT,
245
+ correct_rate=0,
246
+ touch_events=[],
247
+ )
248
+
249
+ # endregion
@@ -0,0 +1,3 @@
1
+ from .frame_timer import FrameTimer
2
+
3
+ __all__ = ["FrameTimer"]
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable
3
+
4
+ from pygame import time
5
+
6
+
7
+ @dataclass
8
+ class _Task:
9
+ interval_ms: int
10
+ callback: Callable[[], None]
11
+ repeat: bool
12
+ next_fire_ms: int
13
+ cancelled: bool = False
14
+
15
+
16
+ class FrameTimer:
17
+ def __init__(self) -> None:
18
+ self._tasks: list[_Task] = []
19
+
20
+ def after(self, delay_ms: int, callback: Callable[[], None]) -> _Task:
21
+ now = time.get_ticks()
22
+ task = _Task(delay_ms, callback, False, now + delay_ms)
23
+ self._tasks.append(task)
24
+ return task
25
+
26
+ def every(self, interval_ms: int, callback: Callable[[], None]) -> _Task:
27
+ now = time.get_ticks()
28
+ task = _Task(interval_ms, callback, True, now + interval_ms)
29
+ self._tasks.append(task)
30
+ return task
31
+
32
+ def cancel(self, task: _Task) -> None:
33
+ task.cancelled = True
34
+
35
+ def update(self) -> None:
36
+ now = time.get_ticks()
37
+ for task in self._tasks:
38
+ if task.cancelled:
39
+ continue
40
+ if now >= task.next_fire_ms:
41
+ task.callback()
42
+ if task.repeat and not task.cancelled:
43
+ task.next_fire_ms += task.interval_ms
44
+ else:
45
+ task.cancelled = True
46
+
47
+ self._tasks = [task for task in self._tasks if not task.cancelled]
File without changes
mxbiflow/tmp_email.py ADDED
@@ -0,0 +1,13 @@
1
+ from mxbi.utils.logger import logger
2
+ from pymotego.send_email import EmailAttachment, EmailClient
3
+
4
+
5
+ def send_email(subject: str, body: str, attachments: list[EmailAttachment]) -> None:
6
+ with EmailClient() as client:
7
+ response = client.send(
8
+ subject=subject,
9
+ html_body=body,
10
+ attachments=attachments,
11
+ )
12
+ logger.info(f"Status: {response.status_code}")
13
+ logger.info(f"Body: {response.text}")
@@ -0,0 +1,87 @@
1
+ from PySide6.QtCore import Qt, Signal
2
+ from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QLineEdit, QMenu
3
+
4
+ from ...config_store import ConfigStore
5
+ from ...models.animal import AnimalConfig
6
+ from ...path import STAGE_PATH
7
+ from ...scene import Scenes
8
+ from .card import CardFrame
9
+
10
+
11
+ class AnimalCard(CardFrame):
12
+ remove_requested = Signal()
13
+
14
+ def __init__(self, parent, animals: dict[str, str]):
15
+ super().__init__(parent=parent, object_name="card")
16
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
17
+ self.customContextMenuRequested.connect(self._on_context_menu)
18
+
19
+ items = list(animals.items())
20
+ self._animal_ids = [animal_id for animal_id, _name in items]
21
+ self._animal_names = [name for _animal_id, name in items]
22
+
23
+ layout = QFormLayout(self)
24
+ layout.setContentsMargins(8, 8, 8, 8)
25
+
26
+ label_animal_name = QLabel("animal", self)
27
+ self.combo_animal_name = QComboBox(self)
28
+ self.combo_animal_name.addItems(self._animal_names)
29
+ self.combo_animal_name.currentIndexChanged.connect(self._on_animal_changed)
30
+ layout.addRow(label_animal_name, self.combo_animal_name)
31
+
32
+ lable_animal_id = QLabel("id", self)
33
+ self.line_animal_id = QLineEdit(self)
34
+ self.line_animal_id.setReadOnly(True)
35
+ self.line_animal_id.setFocusPolicy(Qt.FocusPolicy.NoFocus)
36
+ layout.addRow(lable_animal_id, self.line_animal_id)
37
+
38
+ label_stage = QLabel("stage", self)
39
+ items = ConfigStore(STAGE_PATH, Scenes).value
40
+ self.combo_stage = QComboBox(self)
41
+ self.combo_stage.addItems([i for i in items.root])
42
+ self.combo_stage.setCurrentText("idle")
43
+ layout.addRow(label_stage, self.combo_stage)
44
+
45
+ label_level = QLabel("level", self)
46
+ self.combo_level = QComboBox(self)
47
+ self.combo_level.addItems(["0", "1"])
48
+ self.combo_level.setCurrentText("0")
49
+ layout.addRow(label_level, self.combo_level)
50
+
51
+ self._sync_animal_id()
52
+
53
+ def _on_animal_changed(self, _index: int) -> None:
54
+ self._sync_animal_id()
55
+
56
+ def _sync_animal_id(self) -> None:
57
+ index = self.combo_animal_name.currentIndex()
58
+ if index < 0 or index >= len(self._animal_ids):
59
+ self.line_animal_id.setText("")
60
+ return
61
+ self.line_animal_id.setText(self._animal_ids[index])
62
+
63
+ def animal_id(self) -> str:
64
+ return self.line_animal_id.text()
65
+
66
+ def animal_name(self) -> str:
67
+ return self.combo_animal_name.currentText()
68
+
69
+ def _on_context_menu(self, pos):
70
+ menu = QMenu(self)
71
+ action = menu.addAction("Remove")
72
+ action.triggered.connect(lambda _checked=False: self.remove_requested.emit())
73
+ menu.exec(self.mapToGlobal(pos))
74
+
75
+ def load_config(self, config: AnimalConfig) -> None:
76
+ self.combo_animal_name.setCurrentText(config.name)
77
+ self.combo_stage.setCurrentText(config.stage)
78
+ self.combo_level.setCurrentText(str(config.level))
79
+
80
+ @property
81
+ def result(self) -> AnimalConfig:
82
+ return AnimalConfig(
83
+ rfid_id=self.animal_id(),
84
+ name=self.animal_name(),
85
+ stage=self.combo_stage.currentText(),
86
+ level=int(self.combo_level.currentText()),
87
+ )
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from pymxbi import MXBIModel
4
+ from pymxbi.platform import PlatformEnum
5
+ from pymxbi.screen import get_screen_size
6
+ from PySide6.QtCore import Signal
7
+ from PySide6.QtWidgets import QComboBox, QFormLayout, QGroupBox, QLabel
8
+
9
+
10
+ class BaseConfig(QGroupBox):
11
+ changed = Signal(str)
12
+
13
+ def __init__(self, parent, mxbi_options: list[str]):
14
+ super().__init__(parent)
15
+
16
+ self.setTitle("Base config")
17
+
18
+ self._layout = QFormLayout()
19
+ self.setLayout(self._layout)
20
+
21
+ self._label_mxbi = QLabel("mxbi id:")
22
+ self._combo_mxbi = QComboBox()
23
+ self._combo_mxbi.addItems(mxbi_options)
24
+ self._layout.addRow(self._label_mxbi, self._combo_mxbi)
25
+
26
+ self._label_platform = QLabel("platform:")
27
+ self._combo_platform = QComboBox()
28
+ self._combo_platform.addItems([platform.value for platform in PlatformEnum])
29
+ self._layout.addRow(self._label_platform, self._combo_platform)
30
+
31
+ self._label_screen = QLabel("screen:")
32
+ self._combo_screen = QComboBox()
33
+
34
+ for screen in get_screen_size():
35
+ self._combo_screen.addItem(
36
+ f"{screen.width} * {screen.height}", (screen.width, screen.height)
37
+ )
38
+ self._layout.addRow(self._label_screen, self._combo_screen)
39
+
40
+ self._bind_events()
41
+
42
+ def _emit_changed(self, msg: str) -> None:
43
+ self.changed.emit(msg)
44
+
45
+ def _bind_events(self) -> None:
46
+ self._combo_mxbi.currentTextChanged.connect(self._emit_changed)
47
+ self._combo_platform.currentTextChanged.connect(self._emit_changed)
48
+
49
+ @property
50
+ def mxbi_id(self) -> str:
51
+ return self._combo_mxbi.currentText()
52
+
53
+ @property
54
+ def platform(self) -> str:
55
+ return self._combo_platform.currentText()
56
+
57
+ def load_from_model(self, model: MXBIModel) -> None:
58
+ self._combo_mxbi.setCurrentText(str(model.mxbi_id))
59
+ self._combo_platform.setCurrentText(str(model.platform))
60
+
61
+ def apply_to_model(self, model: MXBIModel) -> None:
62
+ mxbi_id_text = self._combo_mxbi.currentText().strip()
63
+ try:
64
+ model.mxbi_id = int(mxbi_id_text) if mxbi_id_text else 0
65
+ except ValueError:
66
+ model.mxbi_id = 0
67
+
68
+ model.platform = PlatformEnum(self._combo_platform.currentText())
@@ -0,0 +1,18 @@
1
+ from PySide6.QtWidgets import QFrame
2
+
3
+
4
+ class CardFrame(QFrame):
5
+ def __init__(self, parent=None, object_name: str = "card"):
6
+ super().__init__(parent=parent)
7
+
8
+ self.setObjectName(object_name)
9
+ self.setFrameShape(QFrame.Shape.Box)
10
+ self.setLineWidth(1)
11
+ self.setStyleSheet(
12
+ f"""
13
+ QFrame#{object_name} {{
14
+ border: 1px solid #C8C8C8;
15
+ border-radius: 6px;
16
+ }}
17
+ """
18
+ )
@@ -0,0 +1,17 @@
1
+ from .detector.beambreak_detector_card import BeambreakDetectorCard
2
+ from .detector.fusion_detector import FusionDetectorCard
3
+ from .detector.mock_detector_card import MockDetectorCard
4
+ from .detector.rfid_detector import RFIDDetectorCard
5
+ from .device_card import DeviceCard
6
+ from .rewarder.mock_rewarder_card import MockRewarderCard
7
+ from .rewarder.rpi_gpio_rewarder import RPIGpioPumpCard
8
+
9
+ __all__ = [
10
+ "DeviceCard",
11
+ "MockRewarderCard",
12
+ "RPIGpioPumpCard",
13
+ "MockDetectorCard",
14
+ "RFIDDetectorCard",
15
+ "BeambreakDetectorCard",
16
+ "FusionDetectorCard",
17
+ ]
@@ -0,0 +1,29 @@
1
+ from pymxbi.detector import FusionContinuousDetectorModel
2
+ from PySide6.QtWidgets import QLabel, QLineEdit
3
+
4
+ from ..device_card import DeviceCard
5
+
6
+
7
+ class BeambreakDetectorCard(DeviceCard[FusionContinuousDetectorModel]):
8
+ def __init__(self):
9
+ super().__init__()
10
+
11
+ self.set_title("Beambreak Detector")
12
+
13
+ label_pin = QLabel("Pin")
14
+ self._line_pin = QLineEdit()
15
+ self._line_pin.setPlaceholderText("Pin")
16
+ self.layout_config.addRow(label_pin, self._line_pin)
17
+
18
+ def load_config(self, model: FusionContinuousDetectorModel) -> None:
19
+ self.checkbox_enabled.setChecked(model.enabled)
20
+ self.line_device_id.setText(str(model.id))
21
+ self._line_pin.setText(str(model.pin))
22
+
23
+ @property
24
+ def result(self) -> FusionContinuousDetectorModel:
25
+ return FusionContinuousDetectorModel(
26
+ enabled=self.checkbox_enabled.isChecked(),
27
+ id=int(self.line_device_id.text()),
28
+ pin=int(self._line_pin.text()),
29
+ )