Exemplo n.º 1
0
    def send_command(
        self,
        command_string: str,
        command_id: Optional[int] = None,
        **kwargs,
    ) -> ArchonCommand:
        """Sends a command to the Archon.

        Parameters
        ----------
        command_string
            The command to send to the Archon. Will be converted to uppercase.
        command_id
            The command id to associate with this message. If not provided, a
            sequential, autogenerated one will be used.
        kwargs
            Other keyword arguments to pass to `.ArchonCommand`.
        """
        command_id = command_id or self._get_id()
        if command_id > MAX_COMMAND_ID or command_id < 0:
            raise ArchonError(
                f"Command ID must be in the range [0, {MAX_COMMAND_ID:d}]."
            )

        command = ArchonCommand(
            command_string,
            command_id,
            controller=self,
            **kwargs,
        )
        self.__running_commands[command_id] = command

        self.write(command.raw)

        return command
Exemplo n.º 2
0
 async def set_param(self, param: str, value: int) -> ArchonCommand:
     """Sets the parameter ``param`` to value ``value`` calling ``FASTLOADPARAM``."""
     cmd = await self.send_command(f"FASTLOADPARAM {param} {value}")
     if not cmd.succeeded():
         raise ArchonError(
             f"Failed setting parameter {param!r} ({cmd.status.name})."
         )
     return cmd
Exemplo n.º 3
0
    async def reset(self):
        """Cancels exposures and resets timing."""
        await self.set_param("ContinuousExposures", 0)
        await self.set_param("Exposures", 0)
        cmd = await self.send_command("RESETTIMING", timeout=1)
        if not cmd.succeeded():
            self.status = ControllerStatus.ERROR
            raise ArchonError(f"Failed sending RESETTIMING ({cmd.status.name})")

        # TODO: here we should do some more checks before we say it's IDLE.
        self.status = ControllerStatus.IDLE
Exemplo n.º 4
0
    async def integrate(self, exposure_time=1):
        """Integrates the CCD for ``exposure_time`` seconds.

        Returns immediately once the exposure has begun.
        """
        if not self.status == ControllerStatus.IDLE:
            raise ArchonError("Status must be IDLE to start integrating.")

        await self.set_param("IntMS", int(exposure_time * 1000))
        await self.set_param("Exposures", 1)

        self.status = ControllerStatus.EXPOSING
Exemplo n.º 5
0
    async def get_system(self) -> dict[str, Any]:
        """Returns a dictionary with the output of the ``SYSTEM`` command."""
        cmd = await self.send_command("SYSTEM", timeout=1)
        if not cmd.succeeded():
            raise ArchonError(f"Command finished with status {cmd.status.name!r}")

        keywords = str(cmd.replies[0].reply).split()
        system = {}
        for (key, value) in map(lambda k: k.split("="), keywords):
            system[key.lower()] = value
            if match := re.match(r"^MOD([0-9]{1,2})_TYPE", key, re.IGNORECASE):
                name_key = f"mod{match.groups()[0]}_name"
                system[name_key] = ModType(int(value)).name
Exemplo n.º 6
0
    async def get_frame(self) -> dict[str, int]:
        """Returns the frame information.

        All the returned values in the dictionary are integers in decimal
        representation.
        """
        cmd = await self.send_command("FRAME", timeout=1)
        if not cmd.succeeded():
            raise ArchonError(f"Command FRAME failed with status {cmd.status.name!r}")

        keywords = str(cmd.replies[0].reply).split()
        frame = {
            key.lower(): int(value) if "TIME" not in key else int(value, 16)
            for (key, value) in map(lambda k: k.split("="), keywords)
        }

        return frame
Exemplo n.º 7
0
    async def get_status(self) -> dict[str, Any]:
        """Returns a dictionary with the output of the ``STATUS`` command."""

        def check_int(s):
            if s[0] in ("-", "+"):
                return s[1:].isdigit()
            return s.isdigit()

        cmd = await self.send_command("STATUS", timeout=1)
        if not cmd.succeeded():
            raise ArchonError(f"Command finished with status {cmd.status.name!r}")

        keywords = str(cmd.replies[0].reply).split()
        status = {
            key.lower(): int(value) if check_int(value) else float(value)
            for (key, value) in map(lambda k: k.split("="), keywords)
        }

        return status
Exemplo n.º 8
0
    def __init__(self, raw_reply: bytes, command: ArchonCommand):
        parsed = REPLY_RE.match(raw_reply)
        if not parsed:
            raise ArchonError(f"Received unparseable reply to command "
                              f"{command.raw}: {raw_reply.decode()}")

        self.command = command
        self.raw_reply = raw_reply

        rtype, rcid, rbin, rmessage = parsed.groups()
        self.type: str = rtype.decode()
        self.command_id: int = int(rcid, 16)
        self.is_binary: bool = rbin.decode() == ":"

        self.reply: str | bytes
        if self.is_binary:
            # If the reply is binary, remove the prefixes and save the full
            # content as the reply.
            self.reply = raw_reply.replace(b"<" + rcid + b":", b"")
        else:
            self.reply = rmessage.decode().strip()
Exemplo n.º 9
0
 def __str__(self) -> str:
     if isinstance(self.reply, bytes):
         raise ArchonError(
             "The reply is binary and cannot be converted to string.")
     return self.reply
Exemplo n.º 10
0
 def _get_id(self) -> int:
     """Returns an identifier from the pool."""
     if len(self._id_pool) == 0:
         raise ArchonError("No ids reamining in the pool!")
     return self._id_pool.pop()
Exemplo n.º 11
0
    async def fetch(
        self,
        buffer_no: int = -1,
        notifier: Optional[Callable[[str], None]] = None,
    ) -> numpy.ndarray:
        """Fetches a frame buffer and returns a Numpy array.

        Parameters
        ----------
        buffer_no
            The frame buffer number to read. Use ``-1`` to read the most recently
            complete frame.
        notifier
            A callback that receives a message with the current operation. Useful when
            `.fetch` is called by the actor to report progress to the users.
        """
        notifier = notifier or (lambda x: None)
        frame_info = await self.get_frame()

        if buffer_no not in [1, 2, 3, -1]:
            raise ArchonError(f"Invalid frame buffer {buffer_no}.")

        if buffer_no == -1:
            buffers = [
                (n, frame_info[f"buf{n}timestamp"])
                for n in [1, 2, 3]
                if frame_info[f"buf{n}complete"] == 1
            ]
            if len(buffers) == 0:
                raise ArchonError("There are no buffers ready to be read.")
            sorted_buffers = sorted(buffers, key=lambda x: x[1], reverse=True)
            buffer_no = sorted_buffers[0][0]
        else:
            if frame_info[f"buf{buffer_no}complete"] == 0:
                raise ArchonError(f"Buffer frame {buffer_no} cannot be read.")

        self.status = ControllerStatus.FETCHING

        # Lock for reading
        notifier(f"Locking buffer {buffer_no}")
        await self.send_command(f"LOCK{buffer_no}")

        width = frame_info[f"buf{buffer_no}width"]
        height = frame_info[f"buf{buffer_no}height"]
        bytes_per_pixel = 2 if frame_info[f"buf{buffer_no}sample"] == 0 else 4
        n_bytes = width * height * bytes_per_pixel
        n_blocks: int = int(numpy.ceil(n_bytes / 1024.0))

        start_address = frame_info[f"buf{buffer_no}base"]

        notifier("Reading frame buffer ...")

        # Set the expected length of binary buffer to read, including the prefixes.
        self.set_binary_reply_size((1024 + 4) * n_blocks)

        cmd: ArchonCommand = await self.send_command(
            f"FETCH{start_address:08X}{n_blocks:08X}",
            timeout=None,
        )

        # Unlock all
        notifier("Frame buffer readout complete. Unlocking all buffers.")
        await self.send_command("LOCK0")

        # The full read buffer probably contains some extra bytes to complete the 1024
        # reply. We get only the bytes we know are part of the buffer.
        frame = cmd.replies[0].reply[0:n_bytes]

        # Convert to uint16 array and reshape.
        dtype = f"<u{bytes_per_pixel}"  # Buffer is little-endian
        arr = numpy.frombuffer(frame, dtype=dtype)
        arr = arr.reshape(height, width)

        self.status = ControllerStatus.IDLE

        return arr
Exemplo n.º 12
0
    async def write_config(
        self,
        path: str | os.PathLike[str],
        applyall: bool = False,
        poweron: bool = False,
        timeout: float = 1,
        notifier: Optional[Callable[[str], None]] = None,
    ):
        """Writes a configuration file to the contoller.

        Parameters
        ----------
        path
            The path to the configuration file to load. It must be in INI format with
            a section called ``[CONFIG]``.
        applyall
            Whether to run ``APPLYALL`` after successfully sending the configuration.
        poweron
            Whether to run ``POWERON`` after successfully sending the configuration.
            Requires ``applyall=True``.
        timeout
            The amount of time to wait for each command to succeed.
        notifier
            A callback that receives a message with the current operation. Useful when
            `.write_config` is called by the actor to report progress to the users.
        """
        notifier = notifier or (lambda x: None)

        notifier("Reading configuration file")

        if not os.path.exists(path):
            raise ArchonError(f"File {path} does not exist.")

        c = configparser.ConfigParser()
        c.read(path)
        if not c.has_section("CONFIG"):
            raise ArchonError("The config file does not have a CONFIG section.")

        # Undo the INI format: revert \ to / and remove quotes around values.
        config = c["CONFIG"]
        lines = list(
            map(
                lambda k: k.upper().replace("\\", "/") + "=" + config[k].strip('"'),
                config,
            )
        )

        notifier("Clearing previous configuration")
        if not (await self.send_command("CLEARCONFIG", timeout=timeout)).succeeded():
            self.status = ControllerStatus.ERROR
            raise ArchonError("Failed running CLEARCONFIG.")

        notifier("Sending configuration lines")

        cmd_strs = [f"WCONFIG{n_line:04X}{line}" for n_line, line in enumerate(lines)]
        done, failed = await self.send_many(cmd_strs, max_chunk=200, timeout=timeout)
        if len(failed) > 0:
            ff = failed[0]
            self.status = ControllerStatus.ERROR
            raise ArchonError(f"Failed sending line {ff.raw!r} ({ff.status.name})")

        notifier("Sucessfully sent config lines")

        if applyall:
            notifier("Sending APPLYALL")
            cmd = await self.send_command("APPLYALL", timeout=5)
            if not cmd.succeeded():
                self.status = ControllerStatus.ERROR
                raise ArchonError(f"Failed sending APPLYALL ({cmd.status.name})")

            if poweron:
                notifier("Sending POWERON")
                cmd = await self.send_command("POWERON", timeout=timeout)
                if not cmd.succeeded():
                    self.status = ControllerStatus.ERROR
                    raise ArchonError(f"Failed sending POWERON ({cmd.status.name})")

        self.status = ControllerStatus.IDLE
Exemplo n.º 13
0
    async def read_config(self, save: str | bool = False) -> list[str]:
        """Reads the configuration from the controller.

        Parameters
        ----------
        save
            Save the configuration to a file. If ``save=True``, the configuration will
            be saved to ``~/archon_<controller_name>.acf``, or set ``save`` to the path
            of the file to save.
        """
        key_value_re = re.compile("^(.+?)=(.*)$")

        def parse_line(line):
            k, v = key_value_re.match(line).groups()
            # It seems the GUI replaces / with \ even if that doesn't seem
            # necessary in the INI format.
            k = k.replace("/", "\\")
            if ";" in v or "=" in v or "," in v:
                v = f'"{v}"'
            return k, v

        cmd_strs = [f"RCONFIG{n_line:04X}" for n_line in range(MAX_CONFIG_LINES)]
        done, failed = await self.send_many(cmd_strs, max_chunk=200, timeout=0.5)
        if len(failed) > 0:
            ff = failed[0]
            status = ff.status.name
            raise ArchonError(f"An RCONFIG command returned with code {status!r}")

        if any([len(cmd.replies) != 1 for cmd in done]):
            raise ArchonError("Some commands did not get any reply.")

        lines = [str(cmd.replies[0]) for cmd in done]

        # Trim possible empty lines at the end.
        config = "\n".join(lines).strip().splitlines()
        if not save:
            return config

        # The GUI ACF file includes the system information, so we get it.
        system = await self.get_system()

        c = configparser.ConfigParser()
        c.optionxform = str  # Make it case-sensitive
        c.add_section("SYSTEM")
        for sk, sv in system.items():
            if "_name" in sk.lower():
                continue
            sl = f"{sk.upper()}={sv}"
            k, v = parse_line(sl)
            c.set("SYSTEM", k, v)
        c.add_section("CONFIG")
        for cl in config:
            k, v = parse_line(cl)
            c.set("CONFIG", k, v)

        if isinstance(save, str):
            path = save
        else:
            path = os.path.expanduser(f"~/archon_{self.name}.acf")
        with open(path, "w") as f:
            c.write(f, space_around_delimiters=False)

        return config
Exemplo n.º 14
0
async def _do_one_controller(
    command: Command[archon.actor.ArchonActor],
    controller: ArchonController,
    exposure_params: dict[str, Any],
    exp_no: int,
    mjd_dir: pathlib.Path,
) -> bool:
    """Does the heavy lifting of exposing and writing a single controller."""

    observatory = command.actor.observatory.lower()
    hemisphere = "n" if observatory == "apo" else "s"

    config = command.actor.config
    path: pathlib.Path = mjd_dir / config["files"]["template"]
    file_path = str(path.absolute()).format(
        exposure_no=exp_no,
        controller=controller.name,
        observatory=observatory,
        hemisphere=hemisphere,
    )

    exp_time = exposure_params["exposure_time"]

    command.debug(text=dict(
        controller=controller.name,
        text="Starting exposure sequence "
        "(flavour={flavour!r}, exp_time={exposure_time}).".format(
            **exposure_params),
    ))

    # Open shutter (placeholder)

    # Use command to access the actor and command the shutter
    shutter_cmd_open = await command.actor.send_command("osu_actor", "open")

    await shutter_cmd_open  # Block until the command is done (finished or failed)
    if shutter_cmd_open.status.did_fail:
        # Do cleanup
        return command.fail(text="Shutter failed to open")

    # Report status of the shutter
    replies_open = shutter_cmd_open.replies
    shutter_status_open = replies_open[-1].body["shutter"]
    if shutter_status_open not in ["open", "closed"]:
        return command.fail(
            text=f"Unknown shutter status {shutter_status_open!r}.")

    command.info(f"Shutter is now {shutter_status_open!r}.")

    # Start integration. _Changgon
    await controller.integrate(exposure_time=exp_time)

    # Wait until the exposure is complete.
    # TODO: Here we should take into account the network and mechanical delay in
    # opening the shutter.
    await asyncio.sleep(exp_time)

    # Close shutter (placeholder)

    # Close the shutter. Note the double await.
    shutter_cmd_close = await (await command.actor.send_command(
        "osu_actor", "close"))

    await shutter_cmd_close  # Block until the command is done (finished or failed)
    if shutter_cmd_close.status.did_fail:
        # Do cleanup
        return command.fail(text="Shutter failed to close")

    # Report status of the shutter
    replies_close = shutter_cmd_close.replies
    shutter_status_close = replies_close[-1].body["shutter"]
    if shutter_status_close not in ["closed", "open"]:
        return command.fail(
            text=f"Unknown shutter status {shutter_status_close!r}.")

    command.info(f"Shutter is now {shutter_status_close!r}.")

    # Wait a little bit and check that we are reading out to a new buffer
    await asyncio.sleep(0.1)

    # Get new frame info
    frame_info = await controller.get_frame()
    wbuf = frame_info["wbuf"]
    if frame_info[f"buf{wbuf}complete"] != 0:
        controller.status = ControllerStatus.ERROR
        raise ArchonError("Read-out failed to start.")

    controller.status = ControllerStatus.READING
    command.debug(text=dict(
        controller=controller.name,
        text=f"Reading frame into buffer {wbuf}.",
    ))
    # Wait until buffer is complete.
    elapsed = 0
    while True:
        frame_info = await controller.get_frame()
        if frame_info[f"buf{wbuf}complete"] == 1:
            break
        if elapsed > config["timeouts"]["readout_max"]:
            controller.status = ControllerStatus.ERROR
            raise ArchonError("Timed out waiting for read-out to finish.")
        await asyncio.sleep(1.0)  # Sleep for one second before asking again.
        elapsed += 1

    # Reset timing
    await controller.reset()

    # Fetch buffer data
    command.debug(text=dict(
        controller=controller.name,
        text=f"Fetching buffer {wbuf}.",
    ))
    data = await controller.fetch(wbuf)

    # Divide array into CCDs and create FITS.
    # TODO: add at least a placeholder header with some basics.
    command.debug(text=dict(
        controller=controller.name,
        text="Saving data to disk.",
    ))

    loop = asyncio.get_running_loop()
    ccd_info = config["controllers"][controller.name]["ccds"]
    fits = fitsio.FITS(file_path, "rw")
    for ccd_name in ccd_info:
        region = ccd_info[ccd_name]
        ccd_data = data[region[1]:region[3], region[0]:region[2]]
        fits_write = partial(fits.create_image_hdu, extname=ccd_name)
        await loop.run_in_executor(None, fits_write, ccd_data)
    await loop.run_in_executor(None, fits.close)

    command.info(text=f"File {os.path.basename(file_path)} written to disk.")

    return True