opentrons 8.6.0a1__py3-none-any.whl → 8.6.0a2__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.

opentrons/_version.py CHANGED
@@ -1,14 +1,7 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = [
5
- "__version__",
6
- "__version_tuple__",
7
- "version",
8
- "version_tuple",
9
- "__commit_id__",
10
- "commit_id",
11
- ]
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
12
5
 
13
6
  TYPE_CHECKING = False
14
7
  if TYPE_CHECKING:
@@ -16,19 +9,13 @@ if TYPE_CHECKING:
16
9
  from typing import Union
17
10
 
18
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
12
  else:
21
13
  VERSION_TUPLE = object
22
- COMMIT_ID = object
23
14
 
24
15
  version: str
25
16
  __version__: str
26
17
  __version_tuple__: VERSION_TUPLE
27
18
  version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
30
19
 
31
- __version__ = version = '8.6.0a1'
32
- __version_tuple__ = version_tuple = (8, 6, 0, 'a1')
33
-
34
- __commit_id__ = commit_id = None
20
+ __version__ = version = '8.6.0a2'
21
+ __version_tuple__ = version_tuple = (8, 6, 0, 'a2')
@@ -0,0 +1,274 @@
1
+ """Module Firmware update script."""
2
+ import argparse
3
+ import asyncio
4
+ from glob import glob
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from typing import Dict, Final, List, Optional
10
+
11
+ from opentrons.drivers.rpi_drivers import usb
12
+ from opentrons.hardware_control.module_control import MODULE_PORT_REGEX
13
+ from opentrons.hardware_control import modules
14
+ from opentrons.hardware_control.modules.mod_abc import AbstractModule
15
+ from opentrons.hardware_control.modules.update import update_firmware
16
+ from opentrons.hardware_control.types import BoardRevision
17
+
18
+
19
+ # Constants for checking if module is back online
20
+ ONLINE_RETRIES = 3
21
+ DELAY_S = 5
22
+
23
+
24
+ MODULES: Final[Dict[str, str]] = {
25
+ "temp-deck": "tempdeck",
26
+ "mag-deck": "magdeck",
27
+ "thermocycler": "thermocycler",
28
+ "heater-shaker": "heatershaker",
29
+ "absorbance-reader": "absorbancereader",
30
+ "flex-stacker": "flexstacker",
31
+ }
32
+
33
+
34
+ def parse_version(filepath: str) -> str:
35
+ """Parse the version string from the filename."""
36
+ _, ext = os.path.splitext(os.path.basename(filepath))
37
+ ext_pattern = re.escape(ext.lstrip("."))
38
+ pattern = rf"@(v\d+(?:\.\d+)*)\.{ext_pattern}"
39
+ match = re.search(pattern, os.path.basename(filepath))
40
+ return match.group(1) if match else ""
41
+
42
+
43
+ def scan_connected_modules() -> List[modules.ModuleAtPort]:
44
+ """Scan for connected modules and return list of
45
+ tuples of serial ports and device names
46
+ """
47
+ discovered_modules = []
48
+ devices = glob("/dev/ot_module*")
49
+ for port in devices:
50
+ symlink_port = port.split("dev/")[1]
51
+ module_at_port = get_module_at_port(symlink_port)
52
+ if module_at_port:
53
+ discovered_modules.append(module_at_port)
54
+ return discovered_modules
55
+
56
+
57
+ def get_module_at_port(port: str) -> Optional[modules.ModuleAtPort]:
58
+ """Given a port, returns either a ModuleAtPort
59
+ if it is a recognized module, or None if not recognized.
60
+ """
61
+ match = MODULE_PORT_REGEX.search(port)
62
+ if match:
63
+ name = match.group(1).lower()
64
+ if name not in modules.MODULE_TYPE_BY_NAME:
65
+ print(f"Unexpected module connected: {name} on {port}")
66
+ return None
67
+ return modules.ModuleAtPort(port=f"/dev/{port}", name=name)
68
+ return None
69
+
70
+
71
+ async def build_module(
72
+ mod: modules.ModuleAtPort, loop: asyncio.AbstractEventLoop
73
+ ) -> Optional[AbstractModule]:
74
+ try:
75
+ # Get the device path and
76
+ port = subprocess.check_output(["readlink", "-f", mod.port]).decode().strip()
77
+ # remove the symlink for the device, so its freed by the robot-server
78
+ print(f"Removing symlink {mod.port}")
79
+ subprocess.check_call(["unlink", mod.port])
80
+ # wwait some time to let the device teardown
81
+ await asyncio.sleep(2)
82
+ # create an instance of the module using the device path
83
+ return await modules.build(
84
+ port=port,
85
+ usb_port=mod.usb_port,
86
+ type=modules.MODULE_TYPE_BY_NAME[mod.name],
87
+ simulating=False,
88
+ hw_control_loop=loop,
89
+ )
90
+ except Exception:
91
+ return None
92
+
93
+
94
+ async def teardown_module(module: AbstractModule) -> None:
95
+ """Tearsdown the module so it can be used again by the robot-server.."""
96
+ name = module.name()
97
+ serial = module.device_info["serial"]
98
+ port_name = module.usb_port.name
99
+ command = f"echo '{port_name}' | tee /sys/bus/usb/drivers/usb"
100
+ print(f"Removing module: {name} {serial}")
101
+ try:
102
+ # stop the poller and disconnect serial
103
+ await module._poller.stop() # type: ignore
104
+ await module._driver.disconnect() # type: ignore
105
+ # unbind the device from usb and re-bind to simulate unplug
106
+ subprocess.run(f"{command}/unbind", shell=True, capture_output=True)
107
+ await asyncio.sleep(2)
108
+ subprocess.run(f"{command}/bind", shell=True, capture_output=True)
109
+ except Exception:
110
+ pass
111
+
112
+
113
+ def enable_udev_rules(enable: bool) -> None:
114
+ """Enable/Disable creation of opentrons modules by the hardware controller.
115
+
116
+ This is done so the module is not automatically picked up by the server
117
+ while we are updating it.
118
+ """
119
+ rule = "95-opentrons-modules.rules"
120
+ original = f"/etc/udev/rules.d/{rule}"
121
+ destination = f"/var/lib/{rule}"
122
+ src = original if not enable else destination
123
+ dst = destination if not enable else original
124
+ msg = "Disabl" if not enable else "Enabl"
125
+ if not os.path.exists(src):
126
+ sys.exit(f"ERROR: Rule file not found: {src}")
127
+
128
+ try:
129
+ print(f"{msg}ing udev: Moving rule {src} -> {dst}")
130
+ subprocess.check_call(["mount", "-o", "remount,rw", "/"])
131
+ subprocess.check_call(["mv", src, dst])
132
+ except Exception as e:
133
+ sys.exit(
134
+ f"ERROR: Could not {msg}e udev rule: {rule}\n{e}",
135
+ )
136
+ finally:
137
+ subprocess.check_call(["mount", "-o", "remount,ro", "/"])
138
+ subprocess.check_call(["udevadm", "control", "--reload-rules"])
139
+
140
+
141
+ def check_dev_exist(port: str) -> bool:
142
+ """True if the device with the given port exists in /dev."""
143
+ try:
144
+ return subprocess.run(["ls", port], capture_output=True).returncode == 0
145
+ except Exception:
146
+ return False
147
+
148
+
149
+ async def main(args: argparse.Namespace) -> None: # noqa: C901
150
+ """Entry point for script."""
151
+ mod_name = MODULES[args.module]
152
+ target_version = parse_version(args.file)
153
+ if not os.path.exists(args.file):
154
+ sys.exit(f"Invalid filepath: {args.file}")
155
+ if not target_version:
156
+ sys.exit(f"Target version could not be parsed from file: {args.file}")
157
+
158
+ print("Setting up...")
159
+ loop = asyncio.get_running_loop()
160
+ usb_bus = usb.USBBus(BoardRevision.FLEX_B2) # todo: get this from the robot
161
+ print(f"Searching for {mod_name} modules in /dev/")
162
+ mods = scan_connected_modules()
163
+ mods = usb_bus.match_virtual_ports(mods) # type: ignore
164
+ if not mods:
165
+ print("No modules found")
166
+ return
167
+
168
+ # Disable udev rules so modules aren't re-created by the server when they update
169
+ teardown_modules: List[AbstractModule] = []
170
+ enable_udev_rules(False)
171
+ print("\n------------------------------------------")
172
+ for mod in mods:
173
+ if mod_name not in mod.name:
174
+ continue
175
+
176
+ # Create an instance of the opentrons module
177
+ print(f"Found mod: {mod.name} at {mod.port}")
178
+ module = await build_module(mod, loop)
179
+ if module is None:
180
+ continue
181
+
182
+ name = module.name()
183
+ version = module.device_info["version"]
184
+ serial = module.device_info["serial"]
185
+ model = module.device_info["model"]
186
+ teardown_modules.append(module)
187
+ print(f"Created module: {module.name()} {model} {serial} {version}")
188
+
189
+ # Check that the update file is for this module
190
+ file_prefix = module.firmware_prefix()
191
+ if file_prefix not in args.file:
192
+ print(f"ERROR: Target module does not match file: {mod_name} {args.file}")
193
+ continue
194
+
195
+ # Check if the module is one we care about
196
+ if args.serial and serial not in args.serial:
197
+ continue
198
+
199
+ # Check if the module needs an update
200
+ if version == target_version and not args.force:
201
+ print(f"Module {name} {serial} is up-to-date.")
202
+ continue
203
+
204
+ print(f"Updating {name} {model} {serial}: {version} -> {target_version}")
205
+ await update_firmware(module, args.file)
206
+
207
+ # wait for the device to come online
208
+ for retry in range(ONLINE_RETRIES):
209
+ if retry >= ONLINE_RETRIES:
210
+ print(f"Module {serial} failed to come back online.")
211
+ break
212
+
213
+ print(f"Checking if {name} at {module.port} is online...")
214
+ await asyncio.sleep(DELAY_S)
215
+ if not check_dev_exist(module.port):
216
+ print("Not online")
217
+ continue
218
+
219
+ # re-open serial connection
220
+ if not await module._driver.is_connected(): # type: ignore
221
+ await module._driver.connect() # type: ignore
222
+
223
+ # refresh the device info
224
+ print(f"Device {module.port} is back online, refreshing device info.")
225
+ device_info = (await module._driver.get_device_info()).to_dict() # type: ignore
226
+ success = device_info["version"] == target_version
227
+ msg = "updated successfully!" if success else "failed to update"
228
+ print(f"Device {name} {serial} {msg}")
229
+ break
230
+
231
+ print("------------------------------------------\n")
232
+ print("Tearing down")
233
+ # Enable udev rules and teardown the module so they can be pick-up by the robot-server
234
+ enable_udev_rules(True)
235
+ for module in teardown_modules:
236
+ await teardown_module(module)
237
+ print("Done")
238
+
239
+
240
+ if __name__ == "__main__":
241
+ parser = argparse.ArgumentParser(description="Module FW Update.")
242
+ parser.add_argument(
243
+ "--module",
244
+ help="The module target to be updated.",
245
+ type=str,
246
+ required=True,
247
+ choices=MODULES.keys(),
248
+ )
249
+ parser.add_argument(
250
+ "--file",
251
+ help="""Path to binary file containing the FW executable"""
252
+ """Must have format `module-name@vx.x.x.bin/hex`, ex, flex-stacker@v0.0.1.bin""",
253
+ type=str,
254
+ required=True,
255
+ )
256
+ parser.add_argument(
257
+ "--serial",
258
+ help="The specific serial numbers of the devices to update.",
259
+ type=str,
260
+ nargs="+",
261
+ )
262
+ parser.add_argument(
263
+ "--force",
264
+ help="Force install the update, even if the versions are the same.",
265
+ action="store_true",
266
+ default=False,
267
+ )
268
+ args = parser.parse_args()
269
+ try:
270
+ asyncio.run(main(args))
271
+ except Exception as e:
272
+ print("ERROR: Unhandled Exception: ", e)
273
+ # Re-enable udev rules
274
+ enable_udev_rules(True)
@@ -380,7 +380,13 @@ class CommandStore(HasState[CommandState], HandlesActions):
380
380
  prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
381
381
  and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
382
382
  ):
383
- self._state.queue_status = QueueStatus.AWAITING_RECOVERY
383
+ if (
384
+ self._state.queue_status == QueueStatus.PAUSED
385
+ or self._state.is_door_blocking
386
+ ):
387
+ self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED
388
+ else:
389
+ self._state.queue_status = QueueStatus.AWAITING_RECOVERY
384
390
  self._state.recovery_target = _RecoveryTargetInfo(
385
391
  command_id=action.command_id,
386
392
  state_update_if_false_positive=state_update_if_false_positive,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentrons
3
- Version: 8.6.0a1
3
+ Version: 8.6.0a2
4
4
  Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
5
5
  Project-URL: opentrons.com, https://www.opentrons.com
6
6
  Project-URL: Source Code On Github, https://github.com/Opentrons/opentrons/tree/edge/api
@@ -24,7 +24,7 @@ Requires-Dist: click<9,>=8.0.0
24
24
  Requires-Dist: importlib-metadata>=1.0; python_version < '3.8'
25
25
  Requires-Dist: jsonschema<4.18.0,>=3.0.1
26
26
  Requires-Dist: numpy<2,>=1.20.0
27
- Requires-Dist: opentrons-shared-data==8.6.0a1
27
+ Requires-Dist: opentrons-shared-data==8.6.0a2
28
28
  Requires-Dist: packaging>=21.0
29
29
  Requires-Dist: pydantic-settings<3,>=2
30
30
  Requires-Dist: pydantic<3,>=2.0.0
@@ -32,6 +32,6 @@ Requires-Dist: pyserial>=3.5
32
32
  Requires-Dist: pyusb==1.2.1
33
33
  Requires-Dist: typing-extensions<5,>=4.0.0
34
34
  Provides-Extra: flex-hardware
35
- Requires-Dist: opentrons-hardware[flex]==8.6.0a1; extra == 'flex-hardware'
35
+ Requires-Dist: opentrons-hardware[flex]==8.6.0a2; extra == 'flex-hardware'
36
36
  Provides-Extra: ot2-hardware
37
- Requires-Dist: opentrons-hardware==8.6.0a1; extra == 'ot2-hardware'
37
+ Requires-Dist: opentrons-hardware==8.6.0a2; extra == 'ot2-hardware'
@@ -1,5 +1,5 @@
1
1
  opentrons/__init__.py,sha256=TQ_Ca_zzAM3iLzAysWKkFkQHG8-imihxDPQbLCYrf-E,4533
2
- opentrons/_version.py,sha256=TKZI6EwgFGSALxTwZXT3sPWvwD0HNFHjfuboucfB5rI,712
2
+ opentrons/_version.py,sha256=GNMqhyBM4WyqEn46p7PGtrzujTlw5TeW__t1rYiBsMc,519
3
3
  opentrons/execute.py,sha256=Y88qICDiHWQjU0L4Ou7DI5OXXu7zZcdkUvNUYmZqIfc,29282
4
4
  opentrons/legacy_broker.py,sha256=XnuEBBlrHCThc31RFW2UR0tGqctqWZ-CZ9vSC4L9whU,1553
5
5
  opentrons/ordered_set.py,sha256=g-SB3qA14yxHu9zjGyc2wC7d2TUCBE6fKZlHAtbPzI8,4082
@@ -206,6 +206,7 @@ opentrons/hardware_control/scripts/ot3gripper,sha256=vK6wdD7MOMBURhpFDSzU_eZUfPc
206
206
  opentrons/hardware_control/scripts/ot3repl,sha256=arOMCJCqhT0jrpVaYJeS2oNNMG_jLfZwKT6_YeSL-bU,359
207
207
  opentrons/hardware_control/scripts/repl.py,sha256=RojtHjYV6sa6O4SeNEgs5SvnAK0imQK_XqoLQTKlzWU,5982
208
208
  opentrons/hardware_control/scripts/tc_control.py,sha256=V6hOzoRXL3xqIUEz8Raldd45aO2JgN5m5Hr08c1G8Ko,2741
209
+ opentrons/hardware_control/scripts/update_module_fw.py,sha256=FqX4y5_Ghs-uY1_KjbQZpVu8be9XR91MJy1owGt3LLc,9795
209
210
  opentrons/legacy_commands/__init__.py,sha256=erkaz7hc2iHsTtjpFDWrR1V5n47it3U1qxD2zL9CkuE,63
210
211
  opentrons/legacy_commands/commands.py,sha256=lgImZ0Y3gZrMKDVioaOXWqa6mMJCNKDa8p-lQhTghWk,14530
211
212
  opentrons/legacy_commands/helpers.py,sha256=Bc7mjK6V7b4h472NCx_qSwD0ojd_DM7mPg18tjo1DIQ,5228
@@ -456,7 +457,7 @@ opentrons/protocol_engine/state/_move_types.py,sha256=STLssWsXMY92F_asAQrixv10A6
456
457
  opentrons/protocol_engine/state/_well_math.py,sha256=hppbOs4G6yQ6wgvKQWNhOs2zdYA_MwFW7qKgg2bGyx0,9714
457
458
  opentrons/protocol_engine/state/addressable_areas.py,sha256=3tjP7EedkpD5_tjTXa2n0PlQRRf1ttLrh_hOl9k0ThI,28811
458
459
  opentrons/protocol_engine/state/command_history.py,sha256=cz3Nllk045OpK6r9m4ByZr2hcCYo4pTy-2uoIZtV5XE,12241
459
- opentrons/protocol_engine/state/commands.py,sha256=YNPfx5PRyogc-bUzkq7sktwSCmTzssslnzYn3nMYR9g,45367
460
+ opentrons/protocol_engine/state/commands.py,sha256=y5WE2pKmnMalgHFHEiBnBurO2TZ9wVPDMpS8Y9wMyrs,45612
460
461
  opentrons/protocol_engine/state/config.py,sha256=7jSGxC6Vqj1eA8fqZ2I3zjlxVXg8pxvcBYMztRIx9Mg,1515
461
462
  opentrons/protocol_engine/state/files.py,sha256=w8xxxg8HY0RqKKEGSfHWfrjV54Gb02O3dwtisJ-9j8E,1753
462
463
  opentrons/protocol_engine/state/fluid_stack.py,sha256=uwkf0qYk1UX5iU52xmk-e3yLPK8OG-TtMCcBqrkVFpM,5932
@@ -593,8 +594,8 @@ opentrons/util/linal.py,sha256=IlKAP9HkNBBgULeSf4YVwSKHdx9jnCjSr7nvDvlRALg,5753
593
594
  opentrons/util/logging_config.py,sha256=7et4YYuQdWdq_e50U-8vFS_QyNBRgdnqPGAQJm8qrIo,9954
594
595
  opentrons/util/logging_queue_handler.py,sha256=ZsSJwy-oV8DXwpYiZisQ1PbYwmK2cOslD46AcyJ1E4I,2484
595
596
  opentrons/util/performance_helpers.py,sha256=ew7H8XD20iS6-2TJAzbQeyzStZkkE6PzHt_Adx3wbZQ,5172
596
- opentrons-8.6.0a1.dist-info/METADATA,sha256=6KTuHgy1y5dweiTc8EtwQOvxQuJ_OB93FLPuNR9kU2A,1607
597
- opentrons-8.6.0a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
598
- opentrons-8.6.0a1.dist-info/entry_points.txt,sha256=fTa6eGCYkvOtv0ov-KVE8LLGetgb35LQLF9x85OWPVw,106
599
- opentrons-8.6.0a1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
600
- opentrons-8.6.0a1.dist-info/RECORD,,
597
+ opentrons-8.6.0a2.dist-info/METADATA,sha256=cYFFLWaqACc2IODIfrDWwlHmMo3gwbsx_Tda1r2hX9Q,1607
598
+ opentrons-8.6.0a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
599
+ opentrons-8.6.0a2.dist-info/entry_points.txt,sha256=fTa6eGCYkvOtv0ov-KVE8LLGetgb35LQLF9x85OWPVw,106
600
+ opentrons-8.6.0a2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
601
+ opentrons-8.6.0a2.dist-info/RECORD,,