mxbiflow 0.1.1__tar.gz
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.
- mxbiflow-0.1.1/PKG-INFO +168 -0
- mxbiflow-0.1.1/README.md +139 -0
- mxbiflow-0.1.1/pyproject.toml +40 -0
- mxbiflow-0.1.1/src/mxbiflow/__init__.py +3 -0
- mxbiflow-0.1.1/src/mxbiflow/assets/__init__.py +5 -0
- mxbiflow-0.1.1/src/mxbiflow/assets/clicker.wav +0 -0
- mxbiflow-0.1.1/src/mxbiflow/config_store.py +68 -0
- mxbiflow-0.1.1/src/mxbiflow/data_logger.py +114 -0
- mxbiflow-0.1.1/src/mxbiflow/default/__init__.py +4 -0
- mxbiflow-0.1.1/src/mxbiflow/default/idle/assets/apple_v1.png +0 -0
- mxbiflow-0.1.1/src/mxbiflow/default/idle/idle.py +57 -0
- mxbiflow-0.1.1/src/mxbiflow/detector_bridge.py +87 -0
- mxbiflow-0.1.1/src/mxbiflow/game.py +84 -0
- mxbiflow-0.1.1/src/mxbiflow/infra/eventbus.py +31 -0
- mxbiflow-0.1.1/src/mxbiflow/main.py +106 -0
- mxbiflow-0.1.1/src/mxbiflow/models/animal.py +130 -0
- mxbiflow-0.1.1/src/mxbiflow/models/reward.py +7 -0
- mxbiflow-0.1.1/src/mxbiflow/models/session.py +145 -0
- mxbiflow-0.1.1/src/mxbiflow/mxbiflow.py +43 -0
- mxbiflow-0.1.1/src/mxbiflow/path.py +41 -0
- mxbiflow-0.1.1/src/mxbiflow/scene/__init__.py +8 -0
- mxbiflow-0.1.1/src/mxbiflow/scene/scene_manager.py +64 -0
- mxbiflow-0.1.1/src/mxbiflow/scene/scene_protocol.py +22 -0
- mxbiflow-0.1.1/src/mxbiflow/scheduler.py +90 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/models.py +70 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/config.json +116 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage.py +161 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage_models.py +65 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/config.json +70 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage.py +173 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage_models.py +80 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/config.json +83 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_models.py +58 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_stage.py +149 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/artifacts.py +13 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/detect/models.py +21 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/detect/scene.py +271 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_models.py +31 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_scene.py +336 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/touch/touch_models.py +17 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/touch/touch_scene.py +256 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/utils/targets.py +57 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/bundle_dir.py +553 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/config.py +41 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/media.py +61 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/models.py +57 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/scene.py +252 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/stage.py +218 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/trial_io.py +23 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/trial_schema.py +113 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/error_task/error_scene.py +53 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/idle_task/assets/apple_v1.png +0 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/idle_task/idle_scene.py +85 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/README.md +188 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/config.csv +7 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/config.json +67 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/initial_habituation_training_stage.py +172 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/models.py +56 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward.py +244 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward_models.py +50 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/task_protocol.py +26 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/task_table.py +29 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/assets/starter.py +27 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/models.py +68 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/config.json +118 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_models.py +41 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_stage.py +122 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_models.py +19 -0
- mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_scene.py +249 -0
- mxbiflow-0.1.1/src/mxbiflow/timer/__init__.py +3 -0
- mxbiflow-0.1.1/src/mxbiflow/timer/frame_timer.py +47 -0
- mxbiflow-0.1.1/src/mxbiflow/timer/realtime_timer.py +0 -0
- mxbiflow-0.1.1/src/mxbiflow/tmp_email.py +13 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/animal.py +87 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/baseconfig.py +68 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/card.py +18 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/__init__.py +17 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/beambreak_detector_card.py +29 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/fusion_detector.py +45 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/mock_detector_card.py +20 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/rfid_detector.py +40 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/device_card.py +67 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/rewarder/mock_rewarder_card.py +20 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/rewarder/rpi_gpio_rewarder.py +33 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/devices.py +183 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/dialog/__init__.py +3 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/dialog/add_devices_dialog.py +64 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/components/experiment_groups.py +122 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/experiment_panel.py +91 -0
- mxbiflow-0.1.1/src/mxbiflow/ui/mxbi_panel.py +152 -0
- mxbiflow-0.1.1/src/mxbiflow/utils/logger.py +19 -0
- mxbiflow-0.1.1/src/mxbiflow/utils/serial.py +10 -0
mxbiflow-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mxbiflow
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: mxbiflow is a toolkit based on pygame and pymxbi
|
|
5
|
+
Author: HuYang
|
|
6
|
+
Author-email: HuYang <huyangcommit@gmail.com>
|
|
7
|
+
Requires-Dist: gpiozero>=2.0.1
|
|
8
|
+
Requires-Dist: inflection>=0.5.1
|
|
9
|
+
Requires-Dist: keyring>=25.6.0
|
|
10
|
+
Requires-Dist: loguru>=0.7.3
|
|
11
|
+
Requires-Dist: matplotlib>=3.10.6
|
|
12
|
+
Requires-Dist: mss>=10.1.0
|
|
13
|
+
Requires-Dist: numpy>=2.3.3
|
|
14
|
+
Requires-Dist: pandas>=2.3.2
|
|
15
|
+
Requires-Dist: pillow>=11.3.0
|
|
16
|
+
Requires-Dist: pyaudio>=0.2.14
|
|
17
|
+
Requires-Dist: pydantic>=2.11.7
|
|
18
|
+
Requires-Dist: pygame-ce>=2.5.6
|
|
19
|
+
Requires-Dist: pymotego>=0.1.3
|
|
20
|
+
Requires-Dist: pymxbi>=0.1.2
|
|
21
|
+
Requires-Dist: pyserial>=3.5
|
|
22
|
+
Requires-Dist: pyside6>=6.10.1
|
|
23
|
+
Requires-Dist: qtawesome>=1.4.1
|
|
24
|
+
Requires-Dist: rich>=14.1.0
|
|
25
|
+
Requires-Dist: typer>=0.20.0
|
|
26
|
+
Requires-Dist: varname>=0.15.0
|
|
27
|
+
Requires-Python: >=3.14
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
## TODO
|
|
31
|
+
|
|
32
|
+
- [ ] session should be managed automatically
|
|
33
|
+
|
|
34
|
+
## Table of Contents
|
|
35
|
+
- [Task](https://github.com/Ccccraz/mxbi/blob/main/docs/task.md)
|
|
36
|
+
|
|
37
|
+
## Get Started 🚀
|
|
38
|
+
|
|
39
|
+
I typically use `uv` to manage a Python project, and I **highly recommend** you use `uv` to start the mxbi code as well.
|
|
40
|
+
|
|
41
|
+
```shell
|
|
42
|
+
cd # Go back to home
|
|
43
|
+
|
|
44
|
+
cd /path/your_favorite # Choose your favorite directory
|
|
45
|
+
|
|
46
|
+
git clone https://github.com/Ccccraz/mxbi.git # Clone the mxbi repository
|
|
47
|
+
|
|
48
|
+
cd mxbi # Enter the mxbi directory
|
|
49
|
+
|
|
50
|
+
uv sync # Install required dependencies
|
|
51
|
+
|
|
52
|
+
uv run mxbi # Start mxbi
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Current architecture
|
|
56
|
+
|
|
57
|
+
Currently, the entire code structure is divided into five main parts:
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
- **User GUI**: used for manually adjusting experiment settings
|
|
61
|
+
- **Task scheduler**: used for adjusting task difficulty and switching between tasks
|
|
62
|
+
- **Task**: used for executing specific tasks
|
|
63
|
+
- **Theater**: serves as the environment for task execution
|
|
64
|
+
- **Detector**: Detector, used to detect RFID tags
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
flowchart LR
|
|
68
|
+
user_gui -->|create theater| theater
|
|
69
|
+
theater -->|create scheduler| scheduler
|
|
70
|
+
scheduler -->|create task| task
|
|
71
|
+
task -->|adjust level| scheduler
|
|
72
|
+
scheduler -->|check rfid| detector
|
|
73
|
+
detector -->|control| scheduler
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Task Scheduler
|
|
77
|
+
|
|
78
|
+
There were too many different **states** in the original code, which made the code branch paths overly complex. Therefore, I decided to separate the task `scheduler`, ensuring that the tasks themselves are stateless and that their inputs and outputs are deterministic. The task scheduler will ensure that its initial state is correct.
|
|
79
|
+
|
|
80
|
+
Moreover, the `Protocol` is used to define the interface of Task, which enables the scheduler not only to adjust the difficulty but also to switch between tasks.
|
|
81
|
+
|
|
82
|
+
## Detector
|
|
83
|
+
|
|
84
|
+
### Overview
|
|
85
|
+
|
|
86
|
+
The RFID module should run in an independent thread, performing detection every **100 ms** and triggering corresponding callbacks based on the detection results.
|
|
87
|
+
|
|
88
|
+
### Detector States
|
|
89
|
+
|
|
90
|
+
- **NO_ANIMAL**: No animal detected
|
|
91
|
+
- **ANIMAL_PRESENT**: One animal detected
|
|
92
|
+
- **MORE_THAN_ONE_ANIMAL**: Multiple animals detected (defined as error behavior)
|
|
93
|
+
|
|
94
|
+
### Data Maintenance
|
|
95
|
+
|
|
96
|
+
- **current_animal**: Currently detected animal
|
|
97
|
+
- **history_animal**: Historical animal record
|
|
98
|
+
|
|
99
|
+
### State Transition Events
|
|
100
|
+
|
|
101
|
+
- **NO_ANIMAL → ANIMAL_PRESENT** and current_animal != history_animal: New animal appears
|
|
102
|
+
- **NO_ANIMAL → ANIMAL_PRESENT** and current_animal == history_animal: Animal returns
|
|
103
|
+
- **ANIMAL_PRESENT → NO_ANIMAL**: Animal leaves
|
|
104
|
+
- **ANIMAL_PRESENT → ANIMAL_PRESENT** and current_animal != history_animal: New animal appears
|
|
105
|
+
- **ANIMAL_PRESENT → MORE_THAN_ONE_ANIMAL**: Multiple animals detected (error behavior)
|
|
106
|
+
- **MORE_THAN_ONE_ANIMAL → ANIMAL_PRESENT**: Recovery
|
|
107
|
+
- **MORE_THAN_ONE_ANIMAL → NO_ANIMAL**: Recovery
|
|
108
|
+
|
|
109
|
+
### Scheduler State Regulation
|
|
110
|
+
|
|
111
|
+
- **ANIMAL_ENTERED** → Scheduler in SCHEDULE
|
|
112
|
+
- **ANIMAL_RETURNED** → Scheduler in SCHEDULE
|
|
113
|
+
- **ANIMAL_CHANGED** → Scheduler in SCHEDULE
|
|
114
|
+
- **ANIMAL_LEFT** → Scheduler in IDLE
|
|
115
|
+
- **ERROR_DETECTED** → Scheduler in ERROR
|
|
116
|
+
|
|
117
|
+
```mermaid
|
|
118
|
+
sequenceDiagram
|
|
119
|
+
participant RFID_Thread
|
|
120
|
+
participant Detector
|
|
121
|
+
participant Scheduler
|
|
122
|
+
|
|
123
|
+
loop Every 100 ms
|
|
124
|
+
RFID_Thread->>Detector: Perform detection
|
|
125
|
+
activate Detector
|
|
126
|
+
|
|
127
|
+
alt NO_ANIMAL → ANIMAL_PRESENT (current ≠ history)
|
|
128
|
+
Detector->>Detector: State transition
|
|
129
|
+
Detector->>Scheduler: ANIMAL_ENTERED
|
|
130
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
131
|
+
else NO_ANIMAL → ANIMAL_PRESENT (current = history)
|
|
132
|
+
Detector->>Detector: State transition
|
|
133
|
+
Detector->>Scheduler: ANIMAL_RETURNED
|
|
134
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
135
|
+
else ANIMAL_PRESENT → NO_ANIMAL
|
|
136
|
+
Detector->>Detector: State transition
|
|
137
|
+
Detector->>Scheduler: ANIMAL_LEFT
|
|
138
|
+
Scheduler->>Scheduler: Set to IDLE
|
|
139
|
+
else ANIMAL_PRESENT → ANIMAL_PRESENT (current ≠ history)
|
|
140
|
+
Detector->>Detector: State transition
|
|
141
|
+
Detector->>Scheduler: ANIMAL_CHANGED
|
|
142
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
143
|
+
else ANIMAL_PRESENT → MORE_THAN_ONE_ANIMAL
|
|
144
|
+
Detector->>Detector: State transition
|
|
145
|
+
Detector->>Scheduler: ERROR_DETECTED
|
|
146
|
+
Scheduler->>Scheduler: Set to ERROR
|
|
147
|
+
else MORE_THAN_ONE_ANIMAL → ANIMAL_PRESENT
|
|
148
|
+
Detector->>Detector: State transition
|
|
149
|
+
Detector->>Scheduler: Recovery event
|
|
150
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
151
|
+
else MORE_THAN_ONE_ANIMAL → NO_ANIMAL
|
|
152
|
+
Detector->>Detector: State transition
|
|
153
|
+
Detector->>Scheduler: Recovery event
|
|
154
|
+
Scheduler->>Scheduler: Set to IDLE
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
deactivate Detector
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Extract data model
|
|
162
|
+
|
|
163
|
+
The states in the original code were scattered across a large number of variables, so I tried to extract all the data models for unified management. You can find most of the data models under `src/mxbi/models`.
|
|
164
|
+
|
|
165
|
+
## TODO:
|
|
166
|
+
|
|
167
|
+
- More tasks
|
|
168
|
+
- More detailed instruction documents
|
mxbiflow-0.1.1/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
## TODO
|
|
2
|
+
|
|
3
|
+
- [ ] session should be managed automatically
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Task](https://github.com/Ccccraz/mxbi/blob/main/docs/task.md)
|
|
7
|
+
|
|
8
|
+
## Get Started 🚀
|
|
9
|
+
|
|
10
|
+
I typically use `uv` to manage a Python project, and I **highly recommend** you use `uv` to start the mxbi code as well.
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
cd # Go back to home
|
|
14
|
+
|
|
15
|
+
cd /path/your_favorite # Choose your favorite directory
|
|
16
|
+
|
|
17
|
+
git clone https://github.com/Ccccraz/mxbi.git # Clone the mxbi repository
|
|
18
|
+
|
|
19
|
+
cd mxbi # Enter the mxbi directory
|
|
20
|
+
|
|
21
|
+
uv sync # Install required dependencies
|
|
22
|
+
|
|
23
|
+
uv run mxbi # Start mxbi
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Current architecture
|
|
27
|
+
|
|
28
|
+
Currently, the entire code structure is divided into five main parts:
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
- **User GUI**: used for manually adjusting experiment settings
|
|
32
|
+
- **Task scheduler**: used for adjusting task difficulty and switching between tasks
|
|
33
|
+
- **Task**: used for executing specific tasks
|
|
34
|
+
- **Theater**: serves as the environment for task execution
|
|
35
|
+
- **Detector**: Detector, used to detect RFID tags
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
flowchart LR
|
|
39
|
+
user_gui -->|create theater| theater
|
|
40
|
+
theater -->|create scheduler| scheduler
|
|
41
|
+
scheduler -->|create task| task
|
|
42
|
+
task -->|adjust level| scheduler
|
|
43
|
+
scheduler -->|check rfid| detector
|
|
44
|
+
detector -->|control| scheduler
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Task Scheduler
|
|
48
|
+
|
|
49
|
+
There were too many different **states** in the original code, which made the code branch paths overly complex. Therefore, I decided to separate the task `scheduler`, ensuring that the tasks themselves are stateless and that their inputs and outputs are deterministic. The task scheduler will ensure that its initial state is correct.
|
|
50
|
+
|
|
51
|
+
Moreover, the `Protocol` is used to define the interface of Task, which enables the scheduler not only to adjust the difficulty but also to switch between tasks.
|
|
52
|
+
|
|
53
|
+
## Detector
|
|
54
|
+
|
|
55
|
+
### Overview
|
|
56
|
+
|
|
57
|
+
The RFID module should run in an independent thread, performing detection every **100 ms** and triggering corresponding callbacks based on the detection results.
|
|
58
|
+
|
|
59
|
+
### Detector States
|
|
60
|
+
|
|
61
|
+
- **NO_ANIMAL**: No animal detected
|
|
62
|
+
- **ANIMAL_PRESENT**: One animal detected
|
|
63
|
+
- **MORE_THAN_ONE_ANIMAL**: Multiple animals detected (defined as error behavior)
|
|
64
|
+
|
|
65
|
+
### Data Maintenance
|
|
66
|
+
|
|
67
|
+
- **current_animal**: Currently detected animal
|
|
68
|
+
- **history_animal**: Historical animal record
|
|
69
|
+
|
|
70
|
+
### State Transition Events
|
|
71
|
+
|
|
72
|
+
- **NO_ANIMAL → ANIMAL_PRESENT** and current_animal != history_animal: New animal appears
|
|
73
|
+
- **NO_ANIMAL → ANIMAL_PRESENT** and current_animal == history_animal: Animal returns
|
|
74
|
+
- **ANIMAL_PRESENT → NO_ANIMAL**: Animal leaves
|
|
75
|
+
- **ANIMAL_PRESENT → ANIMAL_PRESENT** and current_animal != history_animal: New animal appears
|
|
76
|
+
- **ANIMAL_PRESENT → MORE_THAN_ONE_ANIMAL**: Multiple animals detected (error behavior)
|
|
77
|
+
- **MORE_THAN_ONE_ANIMAL → ANIMAL_PRESENT**: Recovery
|
|
78
|
+
- **MORE_THAN_ONE_ANIMAL → NO_ANIMAL**: Recovery
|
|
79
|
+
|
|
80
|
+
### Scheduler State Regulation
|
|
81
|
+
|
|
82
|
+
- **ANIMAL_ENTERED** → Scheduler in SCHEDULE
|
|
83
|
+
- **ANIMAL_RETURNED** → Scheduler in SCHEDULE
|
|
84
|
+
- **ANIMAL_CHANGED** → Scheduler in SCHEDULE
|
|
85
|
+
- **ANIMAL_LEFT** → Scheduler in IDLE
|
|
86
|
+
- **ERROR_DETECTED** → Scheduler in ERROR
|
|
87
|
+
|
|
88
|
+
```mermaid
|
|
89
|
+
sequenceDiagram
|
|
90
|
+
participant RFID_Thread
|
|
91
|
+
participant Detector
|
|
92
|
+
participant Scheduler
|
|
93
|
+
|
|
94
|
+
loop Every 100 ms
|
|
95
|
+
RFID_Thread->>Detector: Perform detection
|
|
96
|
+
activate Detector
|
|
97
|
+
|
|
98
|
+
alt NO_ANIMAL → ANIMAL_PRESENT (current ≠ history)
|
|
99
|
+
Detector->>Detector: State transition
|
|
100
|
+
Detector->>Scheduler: ANIMAL_ENTERED
|
|
101
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
102
|
+
else NO_ANIMAL → ANIMAL_PRESENT (current = history)
|
|
103
|
+
Detector->>Detector: State transition
|
|
104
|
+
Detector->>Scheduler: ANIMAL_RETURNED
|
|
105
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
106
|
+
else ANIMAL_PRESENT → NO_ANIMAL
|
|
107
|
+
Detector->>Detector: State transition
|
|
108
|
+
Detector->>Scheduler: ANIMAL_LEFT
|
|
109
|
+
Scheduler->>Scheduler: Set to IDLE
|
|
110
|
+
else ANIMAL_PRESENT → ANIMAL_PRESENT (current ≠ history)
|
|
111
|
+
Detector->>Detector: State transition
|
|
112
|
+
Detector->>Scheduler: ANIMAL_CHANGED
|
|
113
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
114
|
+
else ANIMAL_PRESENT → MORE_THAN_ONE_ANIMAL
|
|
115
|
+
Detector->>Detector: State transition
|
|
116
|
+
Detector->>Scheduler: ERROR_DETECTED
|
|
117
|
+
Scheduler->>Scheduler: Set to ERROR
|
|
118
|
+
else MORE_THAN_ONE_ANIMAL → ANIMAL_PRESENT
|
|
119
|
+
Detector->>Detector: State transition
|
|
120
|
+
Detector->>Scheduler: Recovery event
|
|
121
|
+
Scheduler->>Scheduler: Set to SCHEDULE
|
|
122
|
+
else MORE_THAN_ONE_ANIMAL → NO_ANIMAL
|
|
123
|
+
Detector->>Detector: State transition
|
|
124
|
+
Detector->>Scheduler: Recovery event
|
|
125
|
+
Scheduler->>Scheduler: Set to IDLE
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
deactivate Detector
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Extract data model
|
|
133
|
+
|
|
134
|
+
The states in the original code were scattered across a large number of variables, so I tried to extract all the data models for unified management. You can find most of the data models under `src/mxbi/models`.
|
|
135
|
+
|
|
136
|
+
## TODO:
|
|
137
|
+
|
|
138
|
+
- More tasks
|
|
139
|
+
- More detailed instruction documents
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mxbiflow"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "mxbiflow is a toolkit based on pygame and pymxbi"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "HuYang", email = "huyangcommit@gmail.com" }]
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"gpiozero>=2.0.1",
|
|
10
|
+
"inflection>=0.5.1",
|
|
11
|
+
"keyring>=25.6.0",
|
|
12
|
+
"loguru>=0.7.3",
|
|
13
|
+
"matplotlib>=3.10.6",
|
|
14
|
+
"mss>=10.1.0",
|
|
15
|
+
"numpy>=2.3.3",
|
|
16
|
+
"pandas>=2.3.2",
|
|
17
|
+
"pillow>=11.3.0",
|
|
18
|
+
"pyaudio>=0.2.14",
|
|
19
|
+
"pydantic>=2.11.7",
|
|
20
|
+
"pygame-ce>=2.5.6",
|
|
21
|
+
"pymotego>=0.1.3",
|
|
22
|
+
"pymxbi>=0.1.2",
|
|
23
|
+
"pyserial>=3.5",
|
|
24
|
+
"pyside6>=6.10.1",
|
|
25
|
+
"qtawesome>=1.4.1",
|
|
26
|
+
"rich>=14.1.0",
|
|
27
|
+
"typer>=0.20.0",
|
|
28
|
+
"varname>=0.15.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
mxbi = "mxbi:main"
|
|
33
|
+
main = "mxbiflow.main:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.8.17,<0.9.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources]
|
|
40
|
+
pymxbi = { path = "../pymxbi/pymxbi", editable = true }
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T", bound=BaseModel)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigStore(Generic[T]):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
config_path: Path,
|
|
14
|
+
config_class: type[T],
|
|
15
|
+
*,
|
|
16
|
+
create_default: bool = True,
|
|
17
|
+
) -> None:
|
|
18
|
+
self._config_path = config_path
|
|
19
|
+
self._config_class = config_class
|
|
20
|
+
self._create_default = create_default
|
|
21
|
+
self._config = self._load_config()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def value(self) -> T:
|
|
25
|
+
return self._config
|
|
26
|
+
|
|
27
|
+
def _ensure_config_readable(self) -> None:
|
|
28
|
+
if not self._config_path.exists():
|
|
29
|
+
raise FileNotFoundError(f"Config file {self._config_path} not found")
|
|
30
|
+
|
|
31
|
+
if not os.access(self._config_path, os.R_OK):
|
|
32
|
+
raise PermissionError(f"Config file {self._config_path} is not readable")
|
|
33
|
+
|
|
34
|
+
def _create_default_config(self) -> T:
|
|
35
|
+
config = self._config_class()
|
|
36
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self._config_path.write_text(
|
|
38
|
+
config.model_dump_json(indent=4),
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
return config
|
|
42
|
+
|
|
43
|
+
def _load_config(self) -> T:
|
|
44
|
+
try:
|
|
45
|
+
self._ensure_config_readable()
|
|
46
|
+
text = self._config_path.read_text(encoding="utf-8")
|
|
47
|
+
return self._config_class.model_validate_json(text)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
if self._create_default:
|
|
50
|
+
return self._create_default_config()
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
f"Failed to load config file {self._config_path}: {e}"
|
|
53
|
+
) from e
|
|
54
|
+
|
|
55
|
+
def save(self, data: T | None = None) -> None:
|
|
56
|
+
if data is not None:
|
|
57
|
+
self._config = data
|
|
58
|
+
|
|
59
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
try:
|
|
61
|
+
self._config_path.write_text(
|
|
62
|
+
self._config.model_dump_json(indent=4),
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Failed to save config file {self._config_path}: {e}"
|
|
68
|
+
) from e
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .utils.logger import logger
|
|
9
|
+
|
|
10
|
+
now = datetime.now()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataLoggerType(StrEnum):
|
|
14
|
+
JSONL = "jsonl"
|
|
15
|
+
JSON = "json"
|
|
16
|
+
CSV = "csv"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataLogger:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
path: Path,
|
|
23
|
+
session_id: int,
|
|
24
|
+
monkey: str,
|
|
25
|
+
filename: str,
|
|
26
|
+
type: DataLoggerType = DataLoggerType.JSONL,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._path = path
|
|
29
|
+
self._session_id = session_id
|
|
30
|
+
self._monkey = monkey
|
|
31
|
+
self._filename = filename
|
|
32
|
+
self._type = type
|
|
33
|
+
|
|
34
|
+
self._data_dir = self._ensure_data_dir()
|
|
35
|
+
self._data_path = self._get_path(f".{self._type.value}")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def path(self) -> Path:
|
|
39
|
+
return self._data_path
|
|
40
|
+
|
|
41
|
+
def _ensure_data_dir(self) -> Path:
|
|
42
|
+
date_path = Path(f"{now.year}{now.month:02d}{now.day:02d}")
|
|
43
|
+
session_path = Path(f"{self._session_id}")
|
|
44
|
+
monkey_path = Path(f"{self._monkey}")
|
|
45
|
+
|
|
46
|
+
base_dir = self._path / date_path / session_path / monkey_path
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return base_dir
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"failed to create {base_dir}: {e}")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
def _get_path(self, suffix: str) -> Path:
|
|
56
|
+
return self._data_dir / f"{self._filename}{suffix}"
|
|
57
|
+
|
|
58
|
+
def save(self, data: dict) -> None:
|
|
59
|
+
match self._type:
|
|
60
|
+
case DataLoggerType.JSONL:
|
|
61
|
+
self._save_jsonl(data)
|
|
62
|
+
case DataLoggerType.JSON:
|
|
63
|
+
self._save_json(data)
|
|
64
|
+
case DataLoggerType.CSV:
|
|
65
|
+
self.save_csv_row(data)
|
|
66
|
+
|
|
67
|
+
def _save_jsonl(self, data: dict) -> None:
|
|
68
|
+
try:
|
|
69
|
+
json_line = json.dumps(data, ensure_ascii=False)
|
|
70
|
+
|
|
71
|
+
with open(self._data_path, "a", encoding="utf-8") as f:
|
|
72
|
+
f.write(json_line + "\n")
|
|
73
|
+
|
|
74
|
+
except TypeError as e:
|
|
75
|
+
logger.error(f"Data is not JSON serializable: {e}")
|
|
76
|
+
raise
|
|
77
|
+
except IOError as e:
|
|
78
|
+
logger.error(f"Failed to write to file {self._data_path}: {e}")
|
|
79
|
+
raise
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Unexpected error while writing data: {e}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
def _save_json(self, data: dict) -> None:
|
|
85
|
+
try:
|
|
86
|
+
self._data_path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
with open(self._data_path, "w", encoding="utf-8") as f:
|
|
88
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
89
|
+
except TypeError as e:
|
|
90
|
+
logger.error(f"Data is not JSON serializable: {e}")
|
|
91
|
+
raise
|
|
92
|
+
except IOError as e:
|
|
93
|
+
logger.error(f"Failed to write to file {self._data_path}: {e}")
|
|
94
|
+
raise
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Unexpected error while writing JSON data: {e}")
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
def save_csv_row(self, data: dict) -> None:
|
|
100
|
+
csv_path = self._get_path(".csv")
|
|
101
|
+
try:
|
|
102
|
+
csv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
file_exists = csv_path.exists() and csv_path.stat().st_size > 0
|
|
104
|
+
|
|
105
|
+
fieldnames = sorted(data.keys())
|
|
106
|
+
|
|
107
|
+
with csv_path.open("a", newline="", encoding="utf-8") as f:
|
|
108
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
109
|
+
if not file_exists:
|
|
110
|
+
writer.writeheader()
|
|
111
|
+
writer.writerow({k: data.get(k, "") for k in fieldnames})
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Failed to write CSV row to {csv_path}: {e}")
|
|
114
|
+
raise
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from random import choice
|
|
4
|
+
|
|
5
|
+
from pygame import Event, Rect, Surface, image, transform
|
|
6
|
+
|
|
7
|
+
from mxbiflow import get_mxbiflow
|
|
8
|
+
from mxbiflow.scene.scene_protocol import SceneProtocol
|
|
9
|
+
|
|
10
|
+
ASSETS_PATH = Path(__file__).parent / "assets"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Asset:
|
|
15
|
+
image: Surface
|
|
16
|
+
rect: Rect
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IDLE:
|
|
20
|
+
_running: bool
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._mxbiflow = get_mxbiflow()
|
|
24
|
+
|
|
25
|
+
self._screen_size = self._mxbiflow.mxbi.screen_size
|
|
26
|
+
self._pos = ((self._screen_size.width // 4) * 3, self._screen_size.height // 2)
|
|
27
|
+
self._vstimulus_size = self._screen_size.width // 2 * 0.75
|
|
28
|
+
|
|
29
|
+
self._assets = [
|
|
30
|
+
Asset(
|
|
31
|
+
asset_image := transform.scale(
|
|
32
|
+
image.load(path).convert_alpha(),
|
|
33
|
+
(self._vstimulus_size, self._vstimulus_size),
|
|
34
|
+
),
|
|
35
|
+
asset_image.get_rect(center=self._pos),
|
|
36
|
+
)
|
|
37
|
+
for path in ASSETS_PATH.glob("*.png")
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
self._asset = choice(self._assets)
|
|
41
|
+
|
|
42
|
+
def start(self) -> None:
|
|
43
|
+
self._running = True
|
|
44
|
+
|
|
45
|
+
def quit(self) -> None:
|
|
46
|
+
self._running = False
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def running(self) -> bool:
|
|
50
|
+
return self._running
|
|
51
|
+
|
|
52
|
+
def handle_event(self, event: Event) -> None: ...
|
|
53
|
+
|
|
54
|
+
def update(self, dt_s: float) -> None: ...
|
|
55
|
+
|
|
56
|
+
def draw(self, screen: Surface) -> None:
|
|
57
|
+
screen.blit(self._asset.image, self._asset.rect)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from queue import Empty, SimpleQueue
|
|
3
|
+
|
|
4
|
+
import pygame
|
|
5
|
+
from pygame import Event, event
|
|
6
|
+
from pymxbi.detector import MockDetector
|
|
7
|
+
from pymxbi.detector.detector import DetectionResult, Detector, DetectorEvent
|
|
8
|
+
|
|
9
|
+
EVT_DETECTOR = pygame.USEREVENT + 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DetectorMsg:
|
|
14
|
+
kind: DetectorEvent
|
|
15
|
+
animal: str | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DetectorBridge:
|
|
19
|
+
def __init__(self, detector: Detector) -> None:
|
|
20
|
+
self._detector = detector
|
|
21
|
+
self._q = SimpleQueue()
|
|
22
|
+
self._started = False
|
|
23
|
+
|
|
24
|
+
def start(self) -> None:
|
|
25
|
+
if self._started:
|
|
26
|
+
return
|
|
27
|
+
self._started = True
|
|
28
|
+
|
|
29
|
+
self._detector.begin()
|
|
30
|
+
self._detector.register_event(DetectorEvent.ANIMAL_ENTERED, self._emit_entered)
|
|
31
|
+
self._detector.register_event(DetectorEvent.ANIMAL_LEFT, self._emit_left)
|
|
32
|
+
self._detector.register_event(DetectorEvent.FAULT_DETECTED, self._emit_fault)
|
|
33
|
+
|
|
34
|
+
def _emit(self, kind: DetectorEvent, animal: str | None) -> None:
|
|
35
|
+
self._q.put(DetectorMsg(kind=kind, animal=animal))
|
|
36
|
+
|
|
37
|
+
def _emit_entered(self, detection_result: DetectionResult) -> None:
|
|
38
|
+
self._emit(
|
|
39
|
+
DetectorEvent.ANIMAL_ENTERED,
|
|
40
|
+
detection_result.animal_id or detection_result.animal_name,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _emit_left(self, detection_result: DetectionResult) -> None:
|
|
44
|
+
self._emit(
|
|
45
|
+
DetectorEvent.ANIMAL_LEFT,
|
|
46
|
+
detection_result.animal_id or detection_result.animal_name,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _emit_fault(self, detection_result: DetectionResult) -> None:
|
|
50
|
+
self._emit(
|
|
51
|
+
DetectorEvent.FAULT_DETECTED,
|
|
52
|
+
detection_result.animal_id or detection_result.animal_name,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def emit_pygame_event(self) -> None:
|
|
56
|
+
while True:
|
|
57
|
+
try:
|
|
58
|
+
msg = self._q.get_nowait()
|
|
59
|
+
except Empty:
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
event.post(event.Event(EVT_DETECTOR, msg=msg))
|
|
63
|
+
|
|
64
|
+
def manaul_emit(self, animal_idx: int | None = None) -> None:
|
|
65
|
+
if isinstance(self._detector, MockDetector):
|
|
66
|
+
if animal_idx is None:
|
|
67
|
+
self._detector.animal_left()
|
|
68
|
+
else:
|
|
69
|
+
self._detector.animal_present(animal_idx)
|
|
70
|
+
|
|
71
|
+
def handle_event(self, event: Event) -> None:
|
|
72
|
+
if event.type == pygame.KEYDOWN:
|
|
73
|
+
match event.key:
|
|
74
|
+
case pygame.K_0:
|
|
75
|
+
self.manaul_emit(0)
|
|
76
|
+
case pygame.K_1:
|
|
77
|
+
self.manaul_emit(1)
|
|
78
|
+
case pygame.K_2:
|
|
79
|
+
self.manaul_emit(2)
|
|
80
|
+
case pygame.K_3:
|
|
81
|
+
self.manaul_emit(3)
|
|
82
|
+
case pygame.K_4:
|
|
83
|
+
self.manaul_emit(4)
|
|
84
|
+
case pygame.K_5:
|
|
85
|
+
self.manaul_emit(5)
|
|
86
|
+
case pygame.K_l:
|
|
87
|
+
self.manaul_emit()
|