diff --git a/CHANGELOG.md b/CHANGELOG.md
index fed47d4..61be70e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ SPDX-License-Identifier: EUPL-1.2
# Changelog
+## Development
+
+- `Machine.block` command to directly control block temperatures.
+
## Version 0.11.0
- Initial support for 384-well blocks, at least in data/file reading.
diff --git a/src/qslib/machine.py b/src/qslib/machine.py
index d718654..b564e5c 100644
--- a/src/qslib/machine.py
+++ b/src/qslib/machine.py
@@ -251,9 +251,7 @@ def run_command_to_ack(self, command: str | SCPICommand) -> str:
raise ConnectionError(f"Not connected to {self.host}")
loop = asyncio.get_event_loop()
try:
- return loop.run_until_complete(
- self.connection.run_command(command, just_ack=True)
- )
+ return loop.run_until_complete(self.connection.run_command(command, just_ack=True))
except CommandError as e:
e.__traceback__ = None
raise e
@@ -351,15 +349,11 @@ def list_files(
) -> list[str] | list[dict[str, Any]]:
loop = asyncio.get_event_loop()
return loop.run_until_complete(
- self.connection.list_files(
- path, leaf=leaf, verbose=verbose, recursive=recursive
- )
+ self.connection.list_files(path, leaf=leaf, verbose=verbose, recursive=recursive)
)
@_ensure_connection(AccessLevel.Observer)
- def read_file(
- self, path: str, context: str | None = None, leaf: str = "FILE"
- ) -> bytes:
+ def read_file(self, path: str, context: str | None = None, leaf: str = "FILE") -> bytes:
"""Read a file.
Parameters
@@ -375,9 +369,7 @@ def read_file(
bytes
returned file
"""
- return asyncio.get_event_loop().run_until_complete(
- self.connection.read_file(path, context, leaf)
- )
+ return asyncio.get_event_loop().run_until_complete(self.connection.read_file(path, context, leaf))
@_ensure_connection(AccessLevel.Controller)
def write_file(self, path: str, data: str | bytes) -> None:
@@ -385,11 +377,7 @@ def write_file(self, path: str, data: str | bytes) -> None:
data = data.encode()
self.run_command_bytes(
- b"FILE:WRITE "
- + path.encode()
- + b" \n"
- + base64.encodebytes(data)
- + b"\n"
+ b"FILE:WRITE " + path.encode() + b" \n" + base64.encodebytes(data) + b"\n"
)
@_ensure_connection(AccessLevel.Observer)
@@ -405,9 +393,7 @@ def list_runs_in_storage(self) -> list[str]:
"""
x = self.run_command("FILE:LIST? public_run_complete:")
a = x.split("\n")[1:-1]
- return [
- re.sub("^public_run_complete:", "", s)[:-4] for s in a if s.endswith(".eds")
- ]
+ return [re.sub("^public_run_complete:", "", s)[:-4] for s in a if s.endswith(".eds")]
@_ensure_connection(AccessLevel.Observer)
def load_run_from_storage(self, path: str) -> "Experiment": # type: ignore
@@ -418,9 +404,7 @@ def load_run_from_storage(self, path: str) -> "Experiment": # type: ignore
return Experiment.from_machine_storage(self, path)
@_ensure_connection(AccessLevel.Guest)
- def save_run_from_storage(
- self, machine_path: str, download_path: str | IO[bytes], overwrite: bool = False
- ) -> None:
+ def save_run_from_storage(self, machine_path: str, download_path: str | IO[bytes], overwrite: bool = False) -> None:
"""Download a file from run storage on the machine.
Parameters
@@ -451,9 +435,7 @@ def save_run_from_storage(
@_ensure_connection(AccessLevel.Observer)
def _get_log_from_byte(self, name: str | bytes, byte: int) -> bytes:
- logfuture: Future[
- tuple[bytes, bytes, Future[tuple[bytes, bytes, None]] | None]
- ] = asyncio.Future()
+ logfuture: Future[tuple[bytes, bytes, Future[tuple[bytes, bytes, None]] | None]] = asyncio.Future()
if self.connection is None:
raise Exception
if isinstance(name, bytes):
@@ -489,9 +471,7 @@ def machine_status(self) -> MachineStatus:
@_ensure_connection(AccessLevel.Observer)
def get_running_protocol(self) -> Protocol:
p = _unwrap_tags(self.run_command("PROT? ${Protocol}"))
- pn, svs, rm = self.run_command(
- "RET ${Protocol} ${SampleVolume} ${RunMode}"
- ).split()
+ pn, svs, rm = self.run_command("RET ${Protocol} ${SampleVolume} ${RunMode}").split()
p = f"PROT -volume={svs} -runmode={rm} {pn} " + p
return Protocol.from_scpicommand(SCPICommand.from_string(p))
@@ -509,9 +489,7 @@ def set_access_level(
" Change max_access level to continue."
)
- self.run_command(
- f"ACC -stealth={stealth} -exclusive={exclusive} {access_level}"
- )
+ self.run_command(f"ACC -stealth={stealth} -exclusive={exclusive} {access_level}")
log.debug(f"Took access level {access_level} {exclusive=} {stealth=}")
self._current_access_level = access_level
@@ -560,6 +538,42 @@ def drawer_close(self, lower_cover: bool = True, check: bool = True) -> None:
if lower_cover:
self.cover_lower(check=check, ensure_drawer=False)
+ @property
+ @_ensure_connection(AccessLevel.Observer)
+ def block(self) -> tuple[bool, float]:
+ """Returns whether the block is currently temperature-controlled, and the current block temperature setting."""
+ sbool, v = self.run_command("BLOCK?").split()
+ sbool = sbool.lower()
+ v = float(v)
+
+ if sbool == "on":
+ return True, v
+ elif sbool == "off":
+ return False, v
+ else:
+ raise ValueError(f"Block status {sbool} {v} is not understood.")
+
+ @block.setter
+ @_ensure_connection(AccessLevel.Controller)
+ def block(self, value: float | None | False | True | tuple[bool, float]):
+ """Set the block temperature control.
+
+ If a float is given, it will be set to that temperature; None or False will
+ turn off the block temperature control, and True will turn it on at the current set temperature. A tuple can be given
+ to specify both the on/off status and the temperature."""
+ if (value is None) or (value is False):
+ bcom = "OFF"
+ elif value is True:
+ bcom = "ON"
+ elif isinstance(value, tuple):
+ bcom = f"{'ON' if value[0] else 'OFF'} {float(value[1])}"
+ else:
+ try:
+ bcom = f"ON {float(value)}"
+ except ValueError:
+ raise ValueError(f"Block value {value} is not understood.")
+ self.run_command(f"BLOCK {bcom}")
+
@property
def status(self) -> RunStatus:
"""Return the current status of the run."""
@@ -693,14 +707,10 @@ def at_access(
log.debug(f"Took access level {access_level} {exclusive=} {stealth=}.")
yield self
self.set_access_level(fac, fex, fst)
- log.debug(
- f"Dropped access level {access_level}, returning to {fac} exclusive={fex} stealth={fst}."
- )
+ log.debug(f"Dropped access level {access_level}, returning to {fac} exclusive={fex} stealth={fst}.")
@contextmanager
- def ensured_connection(
- self, access_level: AccessLevel = AccessLevel.Observer
- ) -> Generator[Machine, None, None]:
+ def ensured_connection(self, access_level: AccessLevel = AccessLevel.Observer) -> Generator[Machine, None, None]:
if self.automatic:
was_connected = self.connected
if not was_connected: