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.
Files changed (92) hide show
  1. mxbiflow-0.1.1/PKG-INFO +168 -0
  2. mxbiflow-0.1.1/README.md +139 -0
  3. mxbiflow-0.1.1/pyproject.toml +40 -0
  4. mxbiflow-0.1.1/src/mxbiflow/__init__.py +3 -0
  5. mxbiflow-0.1.1/src/mxbiflow/assets/__init__.py +5 -0
  6. mxbiflow-0.1.1/src/mxbiflow/assets/clicker.wav +0 -0
  7. mxbiflow-0.1.1/src/mxbiflow/config_store.py +68 -0
  8. mxbiflow-0.1.1/src/mxbiflow/data_logger.py +114 -0
  9. mxbiflow-0.1.1/src/mxbiflow/default/__init__.py +4 -0
  10. mxbiflow-0.1.1/src/mxbiflow/default/idle/assets/apple_v1.png +0 -0
  11. mxbiflow-0.1.1/src/mxbiflow/default/idle/idle.py +57 -0
  12. mxbiflow-0.1.1/src/mxbiflow/detector_bridge.py +87 -0
  13. mxbiflow-0.1.1/src/mxbiflow/game.py +84 -0
  14. mxbiflow-0.1.1/src/mxbiflow/infra/eventbus.py +31 -0
  15. mxbiflow-0.1.1/src/mxbiflow/main.py +106 -0
  16. mxbiflow-0.1.1/src/mxbiflow/models/animal.py +130 -0
  17. mxbiflow-0.1.1/src/mxbiflow/models/reward.py +7 -0
  18. mxbiflow-0.1.1/src/mxbiflow/models/session.py +145 -0
  19. mxbiflow-0.1.1/src/mxbiflow/mxbiflow.py +43 -0
  20. mxbiflow-0.1.1/src/mxbiflow/path.py +41 -0
  21. mxbiflow-0.1.1/src/mxbiflow/scene/__init__.py +8 -0
  22. mxbiflow-0.1.1/src/mxbiflow/scene/scene_manager.py +64 -0
  23. mxbiflow-0.1.1/src/mxbiflow/scene/scene_protocol.py +22 -0
  24. mxbiflow-0.1.1/src/mxbiflow/scheduler.py +90 -0
  25. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/models.py +70 -0
  26. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/config.json +116 -0
  27. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage.py +161 -0
  28. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage_models.py +65 -0
  29. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/config.json +70 -0
  30. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage.py +173 -0
  31. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage_models.py +80 -0
  32. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/config.json +83 -0
  33. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_models.py +58 -0
  34. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_stage.py +149 -0
  35. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/artifacts.py +13 -0
  36. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/detect/models.py +21 -0
  37. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/detect/scene.py +271 -0
  38. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_models.py +31 -0
  39. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_scene.py +336 -0
  40. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/touch/touch_models.py +17 -0
  41. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/touch/touch_scene.py +256 -0
  42. mxbiflow-0.1.1/src/mxbiflow/tasks/GNGSiD/tasks/utils/targets.py +57 -0
  43. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/bundle_dir.py +553 -0
  44. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/config.py +41 -0
  45. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/media.py +61 -0
  46. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/models.py +57 -0
  47. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/scene.py +252 -0
  48. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/stage.py +218 -0
  49. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/trial_io.py +23 -0
  50. mxbiflow-0.1.1/src/mxbiflow/tasks/cross_modal/trial_schema.py +113 -0
  51. mxbiflow-0.1.1/src/mxbiflow/tasks/default/error_task/error_scene.py +53 -0
  52. mxbiflow-0.1.1/src/mxbiflow/tasks/default/idle_task/assets/apple_v1.png +0 -0
  53. mxbiflow-0.1.1/src/mxbiflow/tasks/default/idle_task/idle_scene.py +85 -0
  54. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/README.md +188 -0
  55. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/config.csv +7 -0
  56. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/config.json +67 -0
  57. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/initial_habituation_training_stage.py +172 -0
  58. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/stages/models.py +56 -0
  59. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward.py +244 -0
  60. mxbiflow-0.1.1/src/mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward_models.py +50 -0
  61. mxbiflow-0.1.1/src/mxbiflow/tasks/task_protocol.py +26 -0
  62. mxbiflow-0.1.1/src/mxbiflow/tasks/task_table.py +29 -0
  63. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/assets/starter.py +27 -0
  64. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/models.py +68 -0
  65. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/config.json +118 -0
  66. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_models.py +41 -0
  67. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_stage.py +122 -0
  68. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_models.py +19 -0
  69. mxbiflow-0.1.1/src/mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_scene.py +249 -0
  70. mxbiflow-0.1.1/src/mxbiflow/timer/__init__.py +3 -0
  71. mxbiflow-0.1.1/src/mxbiflow/timer/frame_timer.py +47 -0
  72. mxbiflow-0.1.1/src/mxbiflow/timer/realtime_timer.py +0 -0
  73. mxbiflow-0.1.1/src/mxbiflow/tmp_email.py +13 -0
  74. mxbiflow-0.1.1/src/mxbiflow/ui/components/animal.py +87 -0
  75. mxbiflow-0.1.1/src/mxbiflow/ui/components/baseconfig.py +68 -0
  76. mxbiflow-0.1.1/src/mxbiflow/ui/components/card.py +18 -0
  77. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/__init__.py +17 -0
  78. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/beambreak_detector_card.py +29 -0
  79. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/fusion_detector.py +45 -0
  80. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/mock_detector_card.py +20 -0
  81. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/detector/rfid_detector.py +40 -0
  82. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/device_card.py +67 -0
  83. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/rewarder/mock_rewarder_card.py +20 -0
  84. mxbiflow-0.1.1/src/mxbiflow/ui/components/device_card/rewarder/rpi_gpio_rewarder.py +33 -0
  85. mxbiflow-0.1.1/src/mxbiflow/ui/components/devices.py +183 -0
  86. mxbiflow-0.1.1/src/mxbiflow/ui/components/dialog/__init__.py +3 -0
  87. mxbiflow-0.1.1/src/mxbiflow/ui/components/dialog/add_devices_dialog.py +64 -0
  88. mxbiflow-0.1.1/src/mxbiflow/ui/components/experiment_groups.py +122 -0
  89. mxbiflow-0.1.1/src/mxbiflow/ui/experiment_panel.py +91 -0
  90. mxbiflow-0.1.1/src/mxbiflow/ui/mxbi_panel.py +152 -0
  91. mxbiflow-0.1.1/src/mxbiflow/utils/logger.py +19 -0
  92. mxbiflow-0.1.1/src/mxbiflow/utils/serial.py +10 -0
@@ -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
@@ -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 }
@@ -0,0 +1,3 @@
1
+ from .mxbiflow import MXBIFlow, get_mxbiflow
2
+
3
+ __all__ = ["MXBIFlow", "get_mxbiflow"]
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ ROOT = Path(__file__).parent
4
+
5
+ ASSET_CLICKER_PATH = ROOT / "clicker.wav"
@@ -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
@@ -0,0 +1,4 @@
1
+ from .habituation.habituarion import Habituarion
2
+ from .idle.idle import IDLE
3
+
4
+ __all__ = ["Habituarion", "IDLE"]
@@ -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()