opentrons 8.3.2__py2.py3-none-any.whl → 8.4.0__py2.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 opentrons might be problematic. Click here for more details.

Files changed (196) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +8 -4
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +102 -5
  39. opentrons/legacy_commands/helpers.py +74 -1
  40. opentrons/legacy_commands/types.py +33 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1356 -107
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
  52. opentrons/protocol_api/core/engine/protocol.py +253 -11
  53. opentrons/protocol_api/core/engine/stringify.py +19 -8
  54. opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
  55. opentrons/protocol_api/core/engine/well.py +73 -5
  56. opentrons/protocol_api/core/instrument.py +71 -21
  57. opentrons/protocol_api/core/labware.py +6 -2
  58. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  59. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
  60. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  61. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  62. opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
  63. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  64. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  65. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  66. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
  67. opentrons/protocol_api/core/module.py +43 -0
  68. opentrons/protocol_api/core/protocol.py +33 -0
  69. opentrons/protocol_api/core/well.py +23 -2
  70. opentrons/protocol_api/instrument_context.py +454 -150
  71. opentrons/protocol_api/labware.py +98 -50
  72. opentrons/protocol_api/module_contexts.py +140 -0
  73. opentrons/protocol_api/protocol_context.py +163 -19
  74. opentrons/protocol_api/validation.py +51 -41
  75. opentrons/protocol_engine/__init__.py +21 -2
  76. opentrons/protocol_engine/actions/actions.py +5 -5
  77. opentrons/protocol_engine/clients/sync_client.py +6 -0
  78. opentrons/protocol_engine/commands/__init__.py +66 -36
  79. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  80. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  81. opentrons/protocol_engine/commands/aspirate.py +6 -2
  82. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  83. opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
  84. opentrons/protocol_engine/commands/blow_out.py +2 -0
  85. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  86. opentrons/protocol_engine/commands/command_unions.py +102 -33
  87. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  88. opentrons/protocol_engine/commands/dispense.py +3 -1
  89. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  90. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  91. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  92. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  93. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  94. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  95. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  96. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  97. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  98. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  99. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  100. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  101. opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
  102. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  103. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  104. opentrons/protocol_engine/commands/liquid_probe.py +27 -13
  105. opentrons/protocol_engine/commands/load_labware.py +42 -39
  106. opentrons/protocol_engine/commands/load_lid.py +21 -13
  107. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  108. opentrons/protocol_engine/commands/load_module.py +18 -17
  109. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  110. opentrons/protocol_engine/commands/move_labware.py +139 -20
  111. opentrons/protocol_engine/commands/move_to_well.py +5 -11
  112. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  113. opentrons/protocol_engine/commands/pipetting_common.py +159 -8
  114. opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
  115. opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
  118. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  119. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  120. opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
  121. opentrons/protocol_engine/errors/__init__.py +10 -0
  122. opentrons/protocol_engine/errors/exceptions.py +62 -0
  123. opentrons/protocol_engine/execution/equipment.py +123 -106
  124. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  125. opentrons/protocol_engine/execution/pipetting.py +235 -25
  126. opentrons/protocol_engine/execution/tip_handler.py +82 -32
  127. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  128. opentrons/protocol_engine/protocol_engine.py +22 -13
  129. opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
  130. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  131. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  132. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  133. opentrons/protocol_engine/slot_standardization.py +11 -23
  134. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  135. opentrons/protocol_engine/state/frustum_helpers.py +36 -14
  136. opentrons/protocol_engine/state/geometry.py +892 -227
  137. opentrons/protocol_engine/state/labware.py +252 -55
  138. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  139. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  140. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  141. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  142. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  143. opentrons/protocol_engine/state/modules.py +210 -67
  144. opentrons/protocol_engine/state/pipettes.py +54 -0
  145. opentrons/protocol_engine/state/state.py +1 -1
  146. opentrons/protocol_engine/state/tips.py +14 -0
  147. opentrons/protocol_engine/state/update_types.py +180 -25
  148. opentrons/protocol_engine/state/wells.py +55 -9
  149. opentrons/protocol_engine/types/__init__.py +300 -0
  150. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  151. opentrons/protocol_engine/types/command_annotations.py +53 -0
  152. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  153. opentrons/protocol_engine/types/execution.py +96 -0
  154. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  155. opentrons/protocol_engine/types/instrument.py +47 -0
  156. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  157. opentrons/protocol_engine/types/labware.py +111 -0
  158. opentrons/protocol_engine/types/labware_movement.py +22 -0
  159. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  160. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  161. opentrons/protocol_engine/types/liquid.py +40 -0
  162. opentrons/protocol_engine/types/liquid_class.py +59 -0
  163. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  164. opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
  165. opentrons/protocol_engine/types/location.py +194 -0
  166. opentrons/protocol_engine/types/module.py +301 -0
  167. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  168. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  169. opentrons/protocol_engine/types/tip.py +18 -0
  170. opentrons/protocol_engine/types/util.py +21 -0
  171. opentrons/protocol_engine/types/well_position.py +124 -0
  172. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  173. opentrons/protocol_reader/file_format_validator.py +5 -3
  174. opentrons/protocol_runner/json_translator.py +4 -2
  175. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  176. opentrons/protocol_runner/run_orchestrator.py +4 -1
  177. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/instrument.py +16 -3
  181. opentrons/protocols/labware.py +27 -23
  182. opentrons/protocols/models/__init__.py +0 -21
  183. opentrons/simulate.py +4 -2
  184. opentrons/types.py +20 -7
  185. opentrons/util/logging_config.py +94 -25
  186. opentrons/util/logging_queue_handler.py +61 -0
  187. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
  189. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  190. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  191. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  192. opentrons/protocol_engine/types.py +0 -1311
  193. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -1,40 +1,138 @@
1
1
  import asyncio
2
2
  import re
3
- from typing import Optional
3
+ import base64
4
+ from typing import List, Optional
4
5
 
6
+ from opentrons.drivers.asyncio.communication.errors import NoResponse
5
7
  from opentrons.drivers.command_builder import CommandBuilder
6
8
  from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
7
9
 
8
- from .abstract import AbstractStackerDriver
10
+ from .abstract import AbstractFlexStackerDriver
11
+ from .errors import StackerErrorCodes, MotorStallDetected
9
12
  from .types import (
10
13
  GCODE,
14
+ ActiveRange,
15
+ LEDPattern,
16
+ MeasurementKind,
17
+ MoveResult,
18
+ SpadMapID,
11
19
  StackerAxis,
12
20
  PlatformStatus,
13
21
  Direction,
14
22
  StackerInfo,
15
23
  HardwareRevision,
16
24
  MoveParams,
25
+ AxisParams,
17
26
  LimitSwitchStatus,
18
27
  LEDColor,
28
+ StallGuardParams,
29
+ TOFConfiguration,
30
+ TOFMeasurement,
31
+ TOFMeasurementFrame,
32
+ TOFMeasurementResult,
33
+ TOFSensor,
34
+ TOFSensorMode,
35
+ TOFSensorState,
36
+ TOFSensorStatus,
19
37
  )
38
+ from .utils import validate_histogram_frame
20
39
 
21
40
 
22
41
  FS_BAUDRATE = 115200
23
- DEFAULT_FS_TIMEOUT = 40
42
+ DEFAULT_FS_TIMEOUT = 1
43
+ FS_MOVE_TIMEOUT = 20
44
+ FS_TOF_TIMEOUT = 20
24
45
  FS_ACK = "OK\n"
25
46
  FS_ERROR_KEYWORD = "err"
26
47
  FS_ASYNC_ERROR_ACK = "async"
27
48
  DEFAULT_COMMAND_RETRIES = 0
28
49
  GCODE_ROUNDING_PRECISION = 2
29
50
 
51
+ # LED animation range values
52
+ MIN_DURATION_MS = 25 # 25ms
53
+ MAX_DURATION_MS = 10000 # 10s
54
+ MAX_REPS = 10
55
+
56
+ # TOF Sensor
57
+ TOF_FRAME_RETRIES = 1
58
+ NUMBER_OF_BINS = 128
59
+
60
+ # Stallguard defaults
61
+ STALLGUARD_CONFIG = {
62
+ StackerAxis.X: StallGuardParams(StackerAxis.X, True, 2),
63
+ StackerAxis.Z: StallGuardParams(StackerAxis.Z, True, 2),
64
+ }
65
+
66
+ STACKER_MOTION_CONFIG = {
67
+ StackerAxis.X: {
68
+ "home": AxisParams(
69
+ run_current=1.5, # mAmps
70
+ hold_current=0.75,
71
+ move_params=MoveParams(
72
+ max_speed=10.0, # mm/s
73
+ acceleration=100.0, # mm/s^2
74
+ max_speed_discont=40.0, # mm/s
75
+ ),
76
+ ),
77
+ "move": AxisParams(
78
+ run_current=1.0,
79
+ hold_current=0.75,
80
+ move_params=MoveParams(
81
+ max_speed=200.0,
82
+ acceleration=1500.0,
83
+ max_speed_discont=40.0,
84
+ ),
85
+ ),
86
+ },
87
+ StackerAxis.Z: {
88
+ "home": AxisParams(
89
+ run_current=1.5,
90
+ hold_current=1.8,
91
+ move_params=MoveParams(
92
+ max_speed=10.0,
93
+ acceleration=100.0,
94
+ max_speed_discont=25.0,
95
+ ),
96
+ ),
97
+ "move": AxisParams(
98
+ run_current=1.5,
99
+ hold_current=0.5,
100
+ move_params=MoveParams(
101
+ max_speed=150.0,
102
+ acceleration=500.0,
103
+ max_speed_discont=25.0,
104
+ ),
105
+ ),
106
+ },
107
+ StackerAxis.L: {
108
+ "home": AxisParams(
109
+ run_current=0.8,
110
+ hold_current=0.15,
111
+ move_params=MoveParams(
112
+ max_speed=100.0,
113
+ acceleration=800.0,
114
+ max_speed_discont=40.0,
115
+ ),
116
+ ),
117
+ "move": AxisParams(
118
+ run_current=0.6,
119
+ hold_current=0.15,
120
+ move_params=MoveParams(
121
+ max_speed=100.0,
122
+ acceleration=800.0,
123
+ max_speed_discont=40.0,
124
+ ),
125
+ ),
126
+ },
127
+ }
128
+
30
129
 
31
- class FlexStackerDriver(AbstractStackerDriver):
130
+ class FlexStackerDriver(AbstractFlexStackerDriver):
32
131
  """FLEX Stacker driver."""
33
132
 
34
133
  @classmethod
35
134
  def parse_device_info(cls, response: str) -> StackerInfo:
36
135
  """Parse stacker info."""
37
- # TODO: Validate serial number format once established
38
136
  _RE = re.compile(
39
137
  f"^{GCODE.DEVICE_INFO} FW:(?P<fw>\\S+) HW:Opentrons-flex-stacker-(?P<hw>\\S+) SerialNo:(?P<sn>\\S+)$"
40
138
  )
@@ -45,6 +143,15 @@ class FlexStackerDriver(AbstractStackerDriver):
45
143
  m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
46
144
  )
47
145
 
146
+ @classmethod
147
+ def parse_reset_reason(cls, response: str) -> int:
148
+ """Parse the reset reason"""
149
+ _RE = re.compile(rf"^{GCODE.GET_RESET_REASON} R:(?P<R>\d)$")
150
+ match = _RE.match(response)
151
+ if not match:
152
+ raise ValueError(f"Incorrect Response for reset reason: {response}")
153
+ return int(match.group("R"))
154
+
48
155
  @classmethod
49
156
  def parse_limit_switch_status(cls, response: str) -> LimitSwitchStatus:
50
157
  """Parse limit switch statuses."""
@@ -70,26 +177,154 @@ class FlexStackerDriver(AbstractStackerDriver):
70
177
  @classmethod
71
178
  def parse_door_closed(cls, response: str) -> bool:
72
179
  """Parse door closed."""
73
- _RE = re.compile(r"^M122 D:(\d)$")
180
+ _RE = re.compile(rf"^{GCODE.GET_DOOR_SWITCH} D:(\d)$")
74
181
  match = _RE.match(response)
75
182
  if not match:
76
183
  raise ValueError(f"Incorrect Response for door closed: {response}")
77
184
  return bool(int(match.group(1)))
78
185
 
186
+ @classmethod
187
+ def parse_installation_detected(cls, response: str) -> bool:
188
+ """Parse install detection."""
189
+ _RE = re.compile(rf"^{GCODE.GET_INSTALL_DETECTED} I:(\d)$")
190
+ match = _RE.match(response)
191
+ if not match:
192
+ raise ValueError(
193
+ f"Incorrect Response for installation detected: {response}"
194
+ )
195
+ return bool(int(match.group(1)))
196
+
197
+ @classmethod
198
+ def parse_move_params(cls, response: str) -> MoveParams:
199
+ """Parse move params."""
200
+ field_names = MoveParams.get_fields()
201
+ pattern = r"\s".join(rf"{f}:(?P<{f}>(\d*\.)?\d+)" for f in field_names)
202
+ _RE = re.compile(rf"^{GCODE.GET_MOVE_PARAMS} M:([XZL]) {pattern}$")
203
+ m = _RE.match(response)
204
+ if not m:
205
+ raise ValueError(f"Incorrect Response for move params: {response}")
206
+ return MoveParams(
207
+ max_speed=float(m.group("V")),
208
+ acceleration=float(m.group("A")),
209
+ max_speed_discont=float(m.group("D")),
210
+ )
211
+
212
+ @classmethod
213
+ def parse_stallguard_params(cls, response: str) -> StallGuardParams:
214
+ """Parse stallguard params."""
215
+ pattern = r"(?P<M>[XZL]):(?P<E>\d) T:(?P<T>\d+)"
216
+ _RE = re.compile(f"^{GCODE.GET_STALLGUARD_THRESHOLD} {pattern}$")
217
+ m = _RE.match(response)
218
+ if not m:
219
+ raise ValueError(f"Incorrect Response for stallfguard params: {response}")
220
+ return StallGuardParams(
221
+ axis=StackerAxis(m.group("M")),
222
+ enabled=bool(int(m.group("E"))),
223
+ threshold=int(m.group("T")),
224
+ )
225
+
226
+ @classmethod
227
+ def parse_get_motor_register(cls, response: str) -> int:
228
+ """Parse get motor register value."""
229
+ pattern = r"(?P<M>[XZL]):(?P<R>\d+) V:(?P<V>\d+)"
230
+ _RE = re.compile(f"^{GCODE.GET_MOTOR_DRIVER_REGISTER} {pattern}$")
231
+ m = _RE.match(response)
232
+ if not m:
233
+ raise ValueError(
234
+ f"Incorrect Response for get motor driver register: {response}"
235
+ )
236
+ return int(m.group("V"))
237
+
238
+ @classmethod
239
+ def parse_get_tof_sensor_register(cls, response: str) -> int:
240
+ """Parse get tof sensor register value."""
241
+ pattern = r"(?P<S>[XZ]):(?P<R>\d+) V:(?P<V>\d+)"
242
+ _RE = re.compile(f"^{GCODE.GET_TOF_DRIVER_REGISTER} {pattern}$")
243
+ m = _RE.match(response)
244
+ if not m:
245
+ raise ValueError(
246
+ f"Incorrect Response for get tof sensor driver register: {response}"
247
+ )
248
+ return int(m.group("V"))
249
+
250
+ @classmethod
251
+ def parse_tof_sensor_status(cls, response: str) -> TOFSensorStatus:
252
+ """Parse get tof sensor status response."""
253
+ pattern = r"(?P<S>[XZ]):(?P<O>\d) T:(?P<T>\d) M:(?P<M>\d)"
254
+ _RE = re.compile(f"^{GCODE.GET_TOF_SENSOR_STATUS} {pattern}$")
255
+ m = _RE.match(response)
256
+ if not m:
257
+ raise ValueError(
258
+ f"Incorrect Response for get tof sensor status: {response}"
259
+ )
260
+ return TOFSensorStatus(
261
+ sensor=TOFSensor(m.group("S")),
262
+ state=TOFSensorState(int(m.group("T"))),
263
+ mode=TOFSensorMode(int(m.group("M"))),
264
+ ok=bool(int(m.group("O"))),
265
+ )
266
+
267
+ @classmethod
268
+ def parse_manage_tof_measurement(cls, response: str) -> TOFMeasurement:
269
+ """Parse manage tof measurement response."""
270
+ pattern = r"(?P<S>[XZ]) K:(?P<K>\d+) C:(?P<C>\d) L:(?P<L>\d+)"
271
+ _RE = re.compile(f"^{GCODE.MANAGE_TOF_MEASUREMENT} {pattern}$")
272
+ m = _RE.match(response)
273
+ if not m:
274
+ raise ValueError(
275
+ f"Incorrect Response for manage tof measurements: {response}"
276
+ )
277
+ return TOFMeasurement(
278
+ sensor=TOFSensor(m.group("S")),
279
+ kind=MeasurementKind(int(m.group("K"))),
280
+ cancelled=bool(int(m.group("C"))),
281
+ total_bytes=int(m.group("L")),
282
+ )
283
+
284
+ @classmethod
285
+ def parse_get_tof_measurement(cls, response: str) -> TOFMeasurementFrame:
286
+ """Parse get tof measurement response frame."""
287
+ pattern = r"(?P<S>[XZ]) I:(?P<I>\d+) D:(?P<D>.+)"
288
+ _RE = re.compile(f"^{GCODE.GET_TOF_MEASUREMENT} {pattern}$")
289
+ m = _RE.match(response)
290
+ if not m:
291
+ raise ValueError(
292
+ f"Incorrect Response for get tof measurement frame: {response}"
293
+ )
294
+ return TOFMeasurementFrame(
295
+ sensor=TOFSensor(m.group("S")),
296
+ frame_id=int(m.group("I")),
297
+ data=base64.b64decode(m.group("D")),
298
+ )
299
+
300
+ @classmethod
301
+ def parse_get_tof_configuration(cls, response: str) -> TOFConfiguration:
302
+ """Parse get tof sensor configuration response."""
303
+ pattern = r"(?P<S>[XZ]) I:(?P<I>\d+) A:(?P<A>\d+) K:(?P<K>\d+) P:(?P<P>\d+) H:(?P<H>\d)"
304
+ _RE = re.compile(f"^{GCODE.GET_TOF_CONFIGURATION} {pattern}$")
305
+ m = _RE.match(response)
306
+ if not m:
307
+ raise ValueError(
308
+ f"Incorrect Response for get tof sensor configuration: {response}"
309
+ )
310
+ return TOFConfiguration(
311
+ sensor=TOFSensor(m.group("S")),
312
+ spad_map_id=SpadMapID(int(m.group("I"))),
313
+ active_range=ActiveRange(int(m.group("A"))),
314
+ kilo_iterations=int(m.group("K")),
315
+ report_period_ms=int(m.group("P")),
316
+ histogram_dump=bool(m.group("H")),
317
+ )
318
+
79
319
  @classmethod
80
320
  def append_move_params(
81
321
  cls, command: CommandBuilder, params: MoveParams | None
82
322
  ) -> CommandBuilder:
83
323
  """Append move params."""
84
324
  if params is not None:
85
- if params.max_speed is not None:
86
- command.add_float("V", params.max_speed, GCODE_ROUNDING_PRECISION)
87
- if params.acceleration is not None:
88
- command.add_float("A", params.acceleration, GCODE_ROUNDING_PRECISION)
89
- if params.max_speed_discont is not None:
90
- command.add_float(
91
- "D", params.max_speed_discont, GCODE_ROUNDING_PRECISION
92
- )
325
+ command.add_float("V", params.max_speed, GCODE_ROUNDING_PRECISION)
326
+ command.add_float("A", params.acceleration, GCODE_ROUNDING_PRECISION)
327
+ command.add_float("D", params.max_speed_discont, GCODE_ROUNDING_PRECISION)
93
328
  return command
94
329
 
95
330
  @classmethod
@@ -106,6 +341,8 @@ class FlexStackerDriver(AbstractStackerDriver):
106
341
  loop=loop,
107
342
  error_keyword=FS_ERROR_KEYWORD,
108
343
  async_error_ack=FS_ASYNC_ERROR_ACK,
344
+ reset_buffer_before_write=True,
345
+ error_codes=StackerErrorCodes,
109
346
  )
110
347
  return cls(connection)
111
348
 
@@ -135,25 +372,292 @@ class FlexStackerDriver(AbstractStackerDriver):
135
372
  response = await self._connection.send_command(
136
373
  GCODE.DEVICE_INFO.build_command()
137
374
  )
138
- await self._connection.send_command(GCODE.GET_RESET_REASON.build_command())
139
- return self.parse_device_info(response)
375
+ device_info = self.parse_device_info(response)
376
+ reason_resp = await self._connection.send_command(
377
+ GCODE.GET_RESET_REASON.build_command()
378
+ )
379
+ reason = self.parse_reset_reason(reason_resp)
380
+ device_info.rr = reason
381
+ return device_info
140
382
 
141
- async def set_serial_number(self, sn: str) -> bool:
383
+ async def set_serial_number(self, sn: str) -> None:
142
384
  """Set Serial Number."""
143
- # TODO: validate the serial number format
385
+ if not re.match(r"^FST[\w]{1}[\d]{2}[\d]{8}[\d]+$", sn):
386
+ raise ValueError(
387
+ f"Invalid serial number: ({sn}) expected format: FSTA1020250119001"
388
+ )
144
389
  resp = await self._connection.send_command(
145
390
  GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn)
146
391
  )
147
392
  if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp):
148
393
  raise ValueError(f"Incorrect Response for set serial number: {resp}")
149
- return True
150
394
 
151
- async def stop_motors(self) -> bool:
395
+ async def enable_motors(self, axis: List[StackerAxis]) -> None:
396
+ """Enables the axis motor if present, disables it otherwise."""
397
+ command = GCODE.ENABLE_MOTORS.build_command()
398
+ for a in axis:
399
+ command.add_element(a.name)
400
+ resp = await self._connection.send_command(command)
401
+ if not re.match(rf"^{GCODE.ENABLE_MOTORS}$", resp):
402
+ raise ValueError(f"Incorrect Response for enable motors: {resp}")
403
+
404
+ async def stop_motors(self) -> None:
152
405
  """Stop all motor movement."""
153
406
  resp = await self._connection.send_command(GCODE.STOP_MOTORS.build_command())
154
407
  if not re.match(rf"^{GCODE.STOP_MOTORS}$", resp):
155
408
  raise ValueError(f"Incorrect Response for stop motors: {resp}")
156
- return True
409
+
410
+ async def set_run_current(self, axis: StackerAxis, current: float) -> None:
411
+ """Set axis peak run current in amps."""
412
+ resp = await self._connection.send_command(
413
+ GCODE.SET_RUN_CURRENT.build_command().add_float(axis.name, current)
414
+ )
415
+ if not re.match(rf"^{GCODE.SET_RUN_CURRENT}$", resp):
416
+ raise ValueError(f"Incorrect Response for set run current: {resp}")
417
+
418
+ async def set_ihold_current(self, axis: StackerAxis, current: float) -> None:
419
+ """Set axis hold current in amps."""
420
+ resp = await self._connection.send_command(
421
+ GCODE.SET_IHOLD_CURRENT.build_command().add_float(axis.name, current)
422
+ )
423
+ if not re.match(rf"^{GCODE.SET_IHOLD_CURRENT}$", resp):
424
+ raise ValueError(f"Incorrect Response for set ihold current: {resp}")
425
+
426
+ async def set_stallguard_threshold(
427
+ self, axis: StackerAxis, enable: bool, threshold: int
428
+ ) -> None:
429
+ """Enables and sets the stallguard threshold for the given axis motor."""
430
+ assert axis != StackerAxis.L, "Stallguard not supported for L axis"
431
+ if not -64 < threshold < 63:
432
+ raise ValueError(
433
+ f"Threshold value ({threshold}) should be between -64 and 63."
434
+ )
435
+ resp = await self._connection.send_command(
436
+ GCODE.SET_STALLGUARD.build_command()
437
+ .add_int(axis.name, int(enable))
438
+ .add_int("T", threshold)
439
+ )
440
+ if not re.match(rf"^{GCODE.SET_STALLGUARD}$", resp):
441
+ raise ValueError(f"Incorrect Response for set stallguard threshold: {resp}")
442
+
443
+ async def enable_tof_sensor(self, sensor: TOFSensor, enable: bool) -> None:
444
+ """Enable or disable the TOF sensor."""
445
+ # Enabling the TOF sensor takes a while, so give extra timeout.
446
+ timeout = FS_TOF_TIMEOUT if enable else DEFAULT_FS_TIMEOUT
447
+ resp = await self._connection.send_command(
448
+ GCODE.ENABLE_TOF_SENSOR.build_command().add_int(sensor.name, int(enable)),
449
+ timeout=timeout,
450
+ )
451
+ if not re.match(rf"^{GCODE.ENABLE_TOF_SENSOR}$", resp):
452
+ raise ValueError(f"Incorrect Response for enable TOF sensor: {resp}")
453
+
454
+ async def set_tof_configuration(
455
+ self,
456
+ sensor: TOFSensor,
457
+ spad_map_id: SpadMapID,
458
+ active_range: Optional[ActiveRange] = None,
459
+ kilo_iterations: Optional[int] = None,
460
+ report_period_ms: Optional[int] = None,
461
+ histogram_dump: Optional[bool] = None,
462
+ ) -> None:
463
+ """Set the configuration of the TOF sensor.
464
+
465
+ :param sensor: The TOF sensor to configure.
466
+ :param spad_map_id: The pre-defined SPAD map which sets the fov and focus area (14 default).
467
+ :active_range: The operating mode Short-range high-accuracy (default) or long range.
468
+ :kilo_iterations: The Measurement iterations times 1024 (4000 default).
469
+ :report_period_ms: The reporting period before each measurement (500 default).
470
+ :histogram_dump: Enables/Disables histogram measurements (True default).
471
+ :return: None
472
+ """
473
+ command = (
474
+ GCODE.SET_TOF_CONFIGURATION.build_command()
475
+ .add_element(sensor.name)
476
+ .add_int("I", spad_map_id.value)
477
+ )
478
+ if active_range:
479
+ command.add_int("A", active_range.value)
480
+ if kilo_iterations:
481
+ command.add_int("K", kilo_iterations)
482
+ if report_period_ms:
483
+ command.add_int("P", report_period_ms)
484
+ if histogram_dump:
485
+ command.add_int("H", int(histogram_dump))
486
+ resp = await self._connection.send_command(command)
487
+ if not re.match(rf"^{GCODE.SET_TOF_CONFIGURATION}$", resp):
488
+ raise ValueError(f"Incorrect Response for set TOF configuration: {resp}")
489
+
490
+ async def get_tof_configuration(self, sensor: TOFSensor) -> TOFConfiguration:
491
+ """Get the configuration of the TOF sensor."""
492
+ resp = await self._connection.send_command(
493
+ GCODE.GET_TOF_CONFIGURATION.build_command().add_element(sensor.value)
494
+ )
495
+ return self.parse_get_tof_configuration(resp)
496
+
497
+ async def manage_tof_measurement(
498
+ self,
499
+ sensor: TOFSensor,
500
+ kind: MeasurementKind = MeasurementKind.HISTOGRAM,
501
+ start: bool = True,
502
+ ) -> TOFMeasurement:
503
+ """Start or stop Measurements from the TOF sensor."""
504
+ command = (
505
+ GCODE.MANAGE_TOF_MEASUREMENT.build_command()
506
+ .add_element(sensor.name)
507
+ .add_int("K", kind.value)
508
+ )
509
+ if not start:
510
+ command.add_element("C")
511
+ resp = await self._connection.send_command(command)
512
+ return self.parse_manage_tof_measurement(resp)
513
+
514
+ async def _get_tof_histogram_frame(
515
+ self, sensor: TOFSensor, resend: bool = False
516
+ ) -> TOFMeasurementFrame:
517
+ """Get the next measurement frame from TOF sensor or resend previous."""
518
+ command = GCODE.GET_TOF_MEASUREMENT.build_command().add_element(sensor.name)
519
+ if resend:
520
+ command.add_element("R")
521
+ resp = await self._connection.send_command(command)
522
+ return self.parse_get_tof_measurement(resp)
523
+
524
+ async def get_tof_histogram(self, sensor: TOFSensor) -> TOFMeasurementResult:
525
+ """Get the full histogram measurement from the TOF sensor."""
526
+ data = []
527
+ data_len = 0
528
+ next_frame_id = 0
529
+ resend = False
530
+ retries = TOF_FRAME_RETRIES
531
+
532
+ # Cancel any ongoing measurements
533
+ status = await self.get_tof_sensor_status(sensor)
534
+ if status.state == TOFSensorState.MEASURING:
535
+ await self.manage_tof_measurement(sensor, start=False)
536
+
537
+ # Put the TOF sensor into histogram measurement mode
538
+ start = await self.manage_tof_measurement(
539
+ sensor, MeasurementKind.HISTOGRAM, start=True
540
+ )
541
+ # Request frames until the full histogram is transfered
542
+ while data_len < start.total_bytes:
543
+ try:
544
+ # Request next histogram frame
545
+ frame = await self._get_tof_histogram_frame(sensor, resend=resend)
546
+ # Validate histogram frame
547
+ validate_histogram_frame(frame.data, next_frame_id)
548
+ # append frame data
549
+ channel = frame.data[7:]
550
+ data.append(channel)
551
+ data_len += len(channel)
552
+ assert (
553
+ not data_len > start.total_bytes
554
+ ), f"Invalid number of bytes, expected {start.total_bytes} got {data_len}."
555
+ retries = TOF_FRAME_RETRIES
556
+ next_frame_id += 1
557
+ resend = False
558
+ except (ValueError, NoResponse):
559
+ # There was a timeout or timing error, request previous frame.
560
+ if retries <= 0:
561
+ # Cancel the measurement
562
+ await self.manage_tof_measurement(sensor, start.kind, start=False)
563
+ raise RuntimeError(f"Exceded frame {next_frame_id} retries")
564
+ retries -= 1
565
+ resend = True
566
+ continue
567
+ except Exception as e:
568
+ # Cancel the histogram request
569
+ await self.manage_tof_measurement(sensor, start.kind, start=False)
570
+ raise RuntimeError(f"Could not get {sensor} Histogram", e)
571
+
572
+ # Parse channel bin measurements from data
573
+ #
574
+ # There are 10 channels (0 - 9), each channel is comprised of 3 * 128 bytes.
575
+ # The data starts with all the 128 LSB bytes, then all the 128 MID bytes, and
576
+ # finally all the 128 MSB bytes for all the channels as seen below.
577
+ #
578
+ # ch0 lsb = data[0], mid = data[10], msb = data[20]
579
+ # ch1 lsb = data[1], mid = data[11], msb = data[21]
580
+ # ch2 lsb = data[2], mid = data[12], msb = data[22]
581
+ # ...
582
+ # ch9 lsb = data[9], mid = data[19], msb = data[29]
583
+ #
584
+ # Each channel has 128 bins comprised of the lsb, mid, and msb payload from above.
585
+ # We can get the bins for each channel by combining the lsb, mid, and msb payload
586
+ # for that specific channel.
587
+ bins = {}
588
+ for ch in range(10): # 0-9 channels
589
+ # Get the lsb, mid, and msb payload for the ch
590
+ lsb = data[ch]
591
+ mid = data[ch + 10]
592
+ msb = data[ch + 20]
593
+ # combine lsb, mid, and msb bytes to generate bin count for the ch
594
+ bins[ch] = [
595
+ (msb[b] << 16) | (mid[b] << 8) | lsb[b] for b in range(NUMBER_OF_BINS)
596
+ ]
597
+ return TOFMeasurementResult(start.sensor, start.kind, bins)
598
+
599
+ async def set_motor_driver_register(
600
+ self, axis: StackerAxis, reg: int, value: int
601
+ ) -> None:
602
+ """Set the register of the given motor axis driver to the given value."""
603
+ resp = await self._connection.send_command(
604
+ GCODE.SET_MOTOR_DRIVER_REGISTER.build_command()
605
+ .add_int(axis.name, reg)
606
+ .add_element(str(value))
607
+ )
608
+ if not re.match(rf"^{GCODE.SET_MOTOR_DRIVER_REGISTER}$", resp):
609
+ raise ValueError(
610
+ f"Incorrect Response for set motor driver register: {resp}"
611
+ )
612
+
613
+ async def get_motor_driver_register(self, axis: StackerAxis, reg: int) -> int:
614
+ """Gets the register value of the given motor axis driver."""
615
+ response = await self._connection.send_command(
616
+ GCODE.GET_MOTOR_DRIVER_REGISTER.build_command().add_int(axis.name, reg)
617
+ )
618
+ return self.parse_get_motor_register(response)
619
+
620
+ async def set_tof_driver_register(
621
+ self, sensor: TOFSensor, reg: int, value: int
622
+ ) -> None:
623
+ """Set the register of the given tof sensor driver to the given value."""
624
+ resp = await self._connection.send_command(
625
+ GCODE.SET_TOF_DRIVER_REGISTER.build_command()
626
+ .add_int(sensor.name, reg)
627
+ .add_element(str(value))
628
+ )
629
+ if not re.match(rf"^{GCODE.SET_TOF_DRIVER_REGISTER}$", resp):
630
+ raise ValueError(
631
+ f"Incorrect Response for set tof sensor driver register: {resp}"
632
+ )
633
+
634
+ async def get_tof_driver_register(self, sensor: TOFSensor, reg: int) -> int:
635
+ """Gets the register value of the given tof sensor driver."""
636
+ response = await self._connection.send_command(
637
+ GCODE.GET_TOF_DRIVER_REGISTER.build_command().add_int(sensor.name, reg)
638
+ )
639
+ return self.parse_get_tof_sensor_register(response)
640
+
641
+ async def get_tof_sensor_status(self, sensor: TOFSensor) -> TOFSensorStatus:
642
+ """Get the status of the tof sensor."""
643
+ response = await self._connection.send_command(
644
+ GCODE.GET_TOF_SENSOR_STATUS.build_command().add_element(sensor.name)
645
+ )
646
+ return self.parse_tof_sensor_status(response)
647
+
648
+ async def get_motion_params(self, axis: StackerAxis) -> MoveParams:
649
+ """Get the motion parameters used by the given axis motor."""
650
+ response = await self._connection.send_command(
651
+ GCODE.GET_MOVE_PARAMS.build_command().add_element(axis.name)
652
+ )
653
+ return self.parse_move_params(response)
654
+
655
+ async def get_stallguard_threshold(self, axis: StackerAxis) -> StallGuardParams:
656
+ """Get the stallguard parameters by the given axis motor."""
657
+ response = await self._connection.send_command(
658
+ GCODE.GET_STALLGUARD_THRESHOLD.build_command().add_element(axis.name)
659
+ )
660
+ return self.parse_stallguard_params(response)
157
661
 
158
662
  async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool:
159
663
  """Get limit switch status.
@@ -195,51 +699,81 @@ class FlexStackerDriver(AbstractStackerDriver):
195
699
  )
196
700
  return self.parse_door_closed(response)
197
701
 
702
+ async def get_installation_detected(self) -> bool:
703
+ """Get whether or not installation is detected.
704
+
705
+ :return: True if installation is detected, False otherwise
706
+ """
707
+ response = await self._connection.send_command(
708
+ GCODE.GET_INSTALL_DETECTED.build_command()
709
+ )
710
+ return self.parse_installation_detected(response)
711
+
198
712
  async def move_in_mm(
199
713
  self, axis: StackerAxis, distance: float, params: MoveParams | None = None
200
- ) -> bool:
201
- """Move axis."""
714
+ ) -> MoveResult:
715
+ """Move axis by the given distance in mm."""
202
716
  command = self.append_move_params(
203
717
  GCODE.MOVE_TO.build_command().add_float(
204
718
  axis.name, distance, GCODE_ROUNDING_PRECISION
205
719
  ),
206
720
  params,
207
721
  )
208
- resp = await self._connection.send_command(command)
209
- if not re.match(rf"^{GCODE.MOVE_TO}$", resp):
210
- raise ValueError(f"Incorrect Response for move to: {resp}")
211
- return True
722
+ try:
723
+ resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT)
724
+ if not re.match(rf"^{GCODE.MOVE_TO}$", resp):
725
+ raise ValueError(f"Incorrect Response for move to: {resp}")
726
+ except MotorStallDetected:
727
+ self.reset_serial_buffers()
728
+ return MoveResult.STALL_ERROR
729
+ return MoveResult.NO_ERROR
212
730
 
213
731
  async def move_to_limit_switch(
214
732
  self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None
215
- ) -> bool:
733
+ ) -> MoveResult:
216
734
  """Move until limit switch is triggered."""
217
735
  command = self.append_move_params(
218
736
  GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value),
219
737
  params,
220
738
  )
221
- resp = await self._connection.send_command(command)
222
- if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp):
223
- raise ValueError(f"Incorrect Response for move to switch: {resp}")
224
- return True
225
-
226
- async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool:
739
+ try:
740
+ resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT)
741
+ if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp):
742
+ raise ValueError(f"Incorrect Response for move to switch: {resp}")
743
+ except MotorStallDetected:
744
+ self.reset_serial_buffers()
745
+ return MoveResult.STALL_ERROR
746
+ return MoveResult.NO_ERROR
747
+
748
+ async def home_axis(self, axis: StackerAxis, direction: Direction) -> MoveResult:
227
749
  """Home axis."""
228
- resp = await self._connection.send_command(
229
- GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value)
230
- )
750
+ command = GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value)
751
+ try:
752
+ resp = await self._connection.send_command(command, timeout=FS_MOVE_TIMEOUT)
753
+ except MotorStallDetected:
754
+ self.reset_serial_buffers()
755
+ return MoveResult.STALL_ERROR
231
756
  if not re.match(rf"^{GCODE.HOME_AXIS}$", resp):
232
757
  raise ValueError(f"Incorrect Response for home axis: {resp}")
233
- return True
758
+ return MoveResult.NO_ERROR
234
759
 
235
760
  async def set_led(
236
- self, power: float, color: LEDColor | None = None, external: bool | None = None
237
- ) -> bool:
238
- """Set LED color.
761
+ self,
762
+ power: float,
763
+ color: Optional[LEDColor] = None,
764
+ external: Optional[bool] = None,
765
+ pattern: Optional[LEDPattern] = None,
766
+ duration: Optional[int] = None,
767
+ reps: Optional[int] = None,
768
+ ) -> None:
769
+ """Set LED Status bar color and pattern.
239
770
 
240
771
  :param power: Power of the LED (0-1.0), 0 is off, 1 is full power
241
772
  :param color: Color of the LED
242
773
  :param external: True if external LED, False if internal LED
774
+ :param pattern: Animation pattern of the LED status bar
775
+ :param duration: Animation duration in milliseconds (25-10000), 10s max
776
+ :param reps: Number of times to repeat the animation (-1 - 10), -1 is forever.
243
777
  """
244
778
  power = max(0, min(power, 1.0))
245
779
  command = GCODE.SET_LED.build_command().add_float(
@@ -248,13 +782,25 @@ class FlexStackerDriver(AbstractStackerDriver):
248
782
  if color is not None:
249
783
  command.add_int("C", color.value)
250
784
  if external is not None:
251
- command.add_int("E", external)
785
+ command.add_int("K", int(external))
786
+ if pattern is not None:
787
+ command.add_int("A", pattern.value)
788
+ if duration is not None:
789
+ duration = max(MIN_DURATION_MS, min(duration, MAX_DURATION_MS))
790
+ command.add_int("D", duration)
791
+ if reps is not None:
792
+ command.add_int("R", max(-1, min(reps, MAX_REPS)))
252
793
  resp = await self._connection.send_command(command)
253
794
  if not re.match(rf"^{GCODE.SET_LED}$", resp):
254
795
  raise ValueError(f"Incorrect Response for set led: {resp}")
255
- return True
256
796
 
257
- async def update_firmware(self, firmware_file_path: str) -> None:
258
- """Updates the firmware on the device."""
259
- # TODO: Implement firmware update
260
- pass
797
+ async def enter_programming_mode(self) -> None:
798
+ """Reboot into programming mode"""
799
+ command = GCODE.ENTER_BOOTLOADER.build_command()
800
+ await self._connection.send_dfu_command(command)
801
+ await self._connection.close()
802
+
803
+ def reset_serial_buffers(self) -> None:
804
+ """Reset the input and output serial buffers."""
805
+ self._connection._serial.reset_input_buffer()
806
+ self._connection._serial.reset_output_buffer()