psychopy 2025.1.1__py3-none-any.whl → 2025.2.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.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,13 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  from pathlib import Path
5
+ from psychopy.preferences import prefs
5
6
  from psychopy.alerts._alerts import alert
6
7
  from psychopy.experiment import Param
7
- from psychopy.experiment.plugins import PluginDevicesMixin, DeviceBackend
8
+ from psychopy.experiment.plugins import PluginDevicesMixin
8
9
  from psychopy.experiment.components import getInitVals
9
10
  from psychopy.experiment.routines import Routine, BaseValidatorRoutine
11
+ from psychopy.experiment.devices import DeviceBackend
10
12
  from psychopy.localization import _translate
11
13
 
12
14
 
@@ -23,6 +25,10 @@ class VisualValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
23
25
  )
24
26
  deviceClasses = []
25
27
  version = "2025.1.0"
28
+ legacyParams = [
29
+ # old device setup params, no longer needed as this is handled by DeviceManager
30
+ "deviceBackend"
31
+ ]
26
32
 
27
33
  def __init__(
28
34
  self,
@@ -124,24 +130,17 @@ class VisualValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
124
130
  "deviceBackend",
125
131
  "channel",
126
132
  ]
127
- self.params['deviceLabel'] = Param(
128
- deviceLabel, valType="str", inputType="single", categ="Device",
129
- label=_translate("Device name"),
130
- hint=_translate(
131
- "A name to refer to this Component's associated hardware device by. If using the "
132
- "same device for multiple components, be sure to use the same name here."
133
- )
134
- )
135
- self.params['deviceBackend'] = Param(
136
- deviceBackend, valType="code", inputType="choice", categ="Device",
137
- allowedVals=self.getBackendKeys,
138
- allowedLabels=self.getBackendLabels,
139
- label=_translate("Light sensor type"),
140
- hint=_translate(
141
- "Type of light sensor to use."
142
- ),
143
- direct=False
144
- )
133
+ # label to refer to device by
134
+ def getDeviceLabels():
135
+ # start with none
136
+ labels = []
137
+ # iterate through saved devices
138
+ for name, profile in prefs.devices.items():
139
+ # if device is the correct type, include it
140
+ if profile.get("deviceClass", None) in self.deviceClasses:
141
+ labels.append(name)
142
+
143
+ return labels
145
144
  self.params['channel'] = Param(
146
145
  channel, valType="code", inputType="single", categ="Device",
147
146
  label=_translate("Light sensor channel"),
@@ -152,8 +151,6 @@ class VisualValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
152
151
  )
153
152
  )
154
153
 
155
- self.loadBackends()
156
-
157
154
  def writeDeviceCode(self, buff):
158
155
  """
159
156
  Code to setup the CameraDevice for this component.
@@ -367,10 +364,9 @@ class ScreenBufferVisualValidatorBackend(DeviceBackend):
367
364
  example for implementing other light sensor device backends.
368
365
  """
369
366
 
370
- key = "screenbuffer"
371
- label = _translate("Screen Buffer (Debug)")
372
- component = VisualValidatorRoutine
373
- deviceClasses = ["psychopy.hardware.lightsensor.ScreenBufferSampler"]
367
+ backendLabel = "Screen Buffer Sampler (Debug)"
368
+ deviceClass = "psychopy.hardware.lightsensor.ScreenBufferSampler"
369
+ icon = "light/visual_validator.png"
374
370
 
375
371
  def getParams(self: VisualValidatorRoutine):
376
372
  # define order
@@ -397,3 +393,7 @@ class ScreenBufferVisualValidatorBackend(DeviceBackend):
397
393
  ")\n"
398
394
  )
399
395
  buff.writeOnceIndentedLines(code % inits)
396
+
397
+
398
+ # register backend with Component
399
+ VisualValidatorRoutine.registerBackend(ScreenBufferVisualValidatorBackend)
@@ -110,9 +110,9 @@ def getPhotometerByName(name):
110
110
  we were unable to find it.
111
111
 
112
112
  """
113
- for photom in getAllPhotometers():
113
+ for thisName, photom in getAllPhotometers().items():
114
114
  # longName is used from the GUI and driverFor is for coders
115
- if name.lower() in photom.driverFor or name == photom.longName:
115
+ if name.lower() == thisName:
116
116
  return photom
117
117
 
118
118
 
@@ -154,61 +154,24 @@ def findPhotometer(ports=None, device=None):
154
154
  print(photom.getSpectrum())
155
155
 
156
156
  """
157
- if isinstance(device, str):
158
- photometers = [getPhotometerByName(device)]
159
- elif isinstance(device, Iterable):
160
- # if we find a string assume it is a name, otherwise treat it like a
161
- # photometer
162
- photometers = [getPhotometerByName(d)
163
- if isinstance(d, str) else d
164
- for d in device]
165
- else:
166
- photometers = getAllPhotometers()
167
-
168
- # determine candidate ports
169
- if ports is None:
170
- ports = getSerialPorts()
171
- elif type(ports) in (int, float, str):
172
- ports = [ports] # so that we can iterate
173
-
174
- # go through each port in turn
175
- photom = None
176
- logging.info('scanning serial ports...')
177
- logging.flush()
178
- for thisPort in ports:
179
- logging.info('...{}'.format(thisPort))
180
- logging.flush()
181
- for Photometer in photometers:
182
- # Looks like we got an invalid photometer, carry on
183
- if Photometer is None:
184
- continue
185
- try:
186
- photom = Photometer(port=thisPort)
187
- except Exception as ex:
188
- msg = "Couldn't initialize photometer {0}: {1}"
189
- logging.error(msg.format(Photometer.__name__, ex))
190
- # We threw an exception so we should just skip ahead
191
- continue
192
- if photom.OK:
193
- logging.info(' ...found a %s\n' % (photom.type))
194
- logging.flush()
195
- # we're now sure that this is the correct device and that
196
- # it's configured now increase the number of attempts made
197
- # to communicate for temperamental devices!
198
- if hasattr(photom, 'setMaxAttempts'):
199
- photom.setMaxAttempts(10)
200
- # we found one so stop looking
201
- return photom
202
- else:
203
- if photom.com and photom.com.isOpen:
204
- logging.info('closing port')
205
- photom.com.close()
206
-
207
- # If we got here we didn't find one
208
- logging.info('...nope!\n\t')
209
- logging.flush()
210
-
211
- return None
157
+ from .photometer import getAllPhotometerClasses
158
+ # try each port
159
+ if isinstance(ports, str) or ports is None:
160
+ ports = [ports]
161
+ for port in ports:
162
+ # get all available devices
163
+ for cls, profiles in DeviceManager.getAvailableDevices(
164
+ list(getAllPhotometerClasses().values())
165
+ ).items():
166
+ # iterate through each
167
+ for profile in profiles:
168
+ # if port matches, or device matches and no port given, initialise from this profile
169
+ if (
170
+ port is None and device == cls.__name__
171
+ ) or (
172
+ port and "port" in profile and profile['port'] in port
173
+ ):
174
+ return DeviceManager.addDevice(**profile)
212
175
 
213
176
 
214
177
  if __name__ == "__main__":
@@ -29,6 +29,13 @@ class ButtonResponse(base.BaseResponse):
29
29
  # match an integer to channel
30
30
  if isinstance(other, int):
31
31
  return other == self.channel
32
+ # match a dict by converting it to a ButtonResponse, if possible
33
+ if isinstance(other, dict):
34
+ try:
35
+ return ButtonResponse(**other) == self
36
+ except:
37
+ # if it can't instantiate a ButtonResponse, it's not the same as this
38
+ return False
32
39
 
33
40
  return False
34
41
 
@@ -188,6 +195,9 @@ class ButtonBox:
188
195
  Builder-friendly wrapper around BaseButtonGroup.
189
196
  """
190
197
  def __init__(self, device):
198
+ # start off with None for device
199
+ self.device = None
200
+
191
201
  if isinstance(device, BaseButtonGroup):
192
202
  # if given a button group, use it
193
203
  self.device = device
@@ -198,10 +208,13 @@ class ButtonBox:
198
208
  else:
199
209
  # don't use formatted string literals in _translate()
200
210
  raise ValueError(_translate(
201
- "Could not find device named '{device}', make sure it has been set up "
211
+ "Could not find device named '{}', make sure it has been set up "
202
212
  "in DeviceManager."
203
213
  ).format(device))
204
-
214
+ # if given None, use first button group we find in DeviceManager
215
+ for name, device in DeviceManager.getInitialisedDevices(BaseButtonGroup).items():
216
+ self.device = device
217
+ break
205
218
  # starting value for status (Builder)
206
219
  self.status = constants.NOT_STARTED
207
220
  # arrays to store info (Builder)