Beispiel #1
0
    async def shutdown(self):
        """Stops pollers and shuts down all remaining tasks."""

        if not self.is_bootloader:
            log.info("Stopping positioners and shutting down.")
            await self.stop_trajectory()

        log.debug("Stopping all pollers.")
        if self.pollers:
            await self.pollers.stop()

        await self.chiller.stop()
        await self.alerts.stop()

        log.debug("Cancelling all pending tasks and shutting down.")

        tasks = [
            task for task in asyncio.all_tasks(loop=self.loop)
            if task is not asyncio.current_task(loop=self.loop)
        ]
        list(map(lambda task: task.cancel(), tasks))

        await asyncio.gather(*tasks, return_exceptions=True)

        self.loop.stop()

        del self.__class__._instance[self.__class__]
Beispiel #2
0
    async def lock(
        self,
        stop_trajectories: bool = True,
        by: Optional[List[int]] = None,
        do_warn: bool = True,
        snapshot: bool = True,
    ):
        """Locks the `.FPS` and prevents commands to be sent.

        Parameters
        ----------
        stop_trajectories
            Whether to stop trajectories when locking. This will not
            clear any collided flags.

        """

        self._locked = True
        if do_warn:
            warnings.warn("Locking the FPS.", JaegerUserWarning)

        if stop_trajectories:
            await self.stop_trajectory()

        if by:
            self.locked_by += by

        await self.update_status()

        if jaeger.actor_instance:
            jaeger.actor_instance.write(
                "e",
                {
                    "locked": True,
                    "locked_by": self.locked_by
                },
            )

        if snapshot:
            if self.locked_by is not None:
                highlight = self.locked_by[0]
            else:
                highlight = None

            log.debug(
                f"Saving snapshot with highlight {highlight} ({self.locked_by})"
            )
            filename = await self.save_snapshot(highlight=highlight)
            warnings.warn(f"Snapshot for locked FPS: {filename}",
                          JaegerUserWarning)
Beispiel #3
0
def get_robot_grid(fps: FPS | None,
                   seed: int | None = None,
                   collision_buffer=None):
    """Returns a new robot grid with the destination set to the lattice position.

    If an initialised instance of the FPS is available, disabled robots will be
    set offline in the grid at their current positions.

    """

    from kaiju.robotGrid import RobotGridCalib

    if seed is None:
        t = 1000 * time.time()
        seed = int(int(t) % 2**32 / 1000)

    kaiju_config = config["kaiju"]
    ang_step = kaiju_config["ang_step"]
    collision_buffer = collision_buffer or kaiju_config["collision_buffer"]
    alpha0, beta0 = kaiju_config["lattice_position"]
    epsilon = ang_step * kaiju_config["epsilon_factor"]

    if collision_buffer < 1.5:
        raise JaegerError("Invalid collision buffer < 1.5.")

    log.debug(
        f"Creating RobotGridCalib with stepSize={ang_step}, epsilon={epsilon}."
    )

    robot_grid = RobotGridCalib(stepSize=ang_step, epsilon=epsilon, seed=seed)
    robot_grid.setCollisionBuffer(collision_buffer)

    # TODO: This is a bit hacky. Kaiju doesn't have a collisionBuffer anymore
    # as collision buffers are per robot, but I want to keep this information
    # for when I dump and reload robot grids.
    robot_grid.collisionBuffer = collision_buffer

    for robot in robot_grid.robotDict.values():
        if fps is not None and robot.id in fps and fps[
                robot.id].disabled is True:
            positioner = fps[robot.id]
            robot.setDestinationAlphaBeta(positioner.alpha, positioner.beta)
            robot.setAlphaBeta(positioner.alpha, positioner.beta)
            robot.isOffline = True
        else:
            robot.setDestinationAlphaBeta(alpha0, beta0)
            robot.setAlphaBeta(alpha0, beta0)

    return robot_grid
Beispiel #4
0
    async def start(self: T) -> T:

        self.stop()

        itype = self.interface_type

        InterfaceClass: Type[Bus_co] = INTERFACES[itype]["class"]
        self.multibus = INTERFACES[itype]["multibus"]

        if not isinstance(self.channels, (list, tuple)):
            self.channels = [self.channels]

        for channel in self.channels:
            iargs = "".join(
                [f" {k}={repr(v)}" for k, v in self.interface_args.items()])
            log.debug(
                f"creating interface {itype}, channel={channel!r}{iargs}.")
            try:
                interface = InterfaceClass(channel, **self.interface_args)
                result = await interface.open()
                if result is False:
                    raise ConnectionError()
                self.interfaces.append(interface)
            except ConnectionResetError:
                log.error(
                    f"connection to {itype}:{channel} failed. "
                    "Possibly another instance is connected to the device.")
            except (socket.timeout, ConnectionError, ConnectionRefusedError,
                    OSError):
                log.error(f"connection to {itype}:{channel} failed.")
            except Exception as ee:
                raise ee.__class__(
                    f"connection to {itype}:{channel} failed: {ee}.")

        self.command_queue = asyncio.Queue()
        self._command_queue_task = asyncio.create_task(
            self._process_command_queue())

        self.notifier = Notifier(
            listeners=[self._process_reply_queue],
            buses=self.interfaces,
        )

        self._started = True

        return self
Beispiel #5
0
def jaeger(
    ctx,
    config_file,
    profile,
    verbose,
    quiet,
    ieb,
    sextant,
    virtual,
    npositioners,
    allow_host,
    no_lock,
):
    """CLI for the SDSS-V focal plane system.

    If called without subcommand starts the actor.

    """

    if allow_host is False:
        hostname = socket.getfqdn()
        if hostname.endswith("apo.nmsu.edu") or hostname.endswith("lco.cl"):
            if not hostname.startswith("sdss5-fps"):
                raise RuntimeError(
                    "At the observatories jaeger must run on sdss5-fps.")

    if verbose > 0 and quiet:
        raise click.UsageError("--quiet and --verbose are mutually exclusive.")

    if config_file:
        config.load(config_file)

    if no_lock:
        config["fps"]["use_lock"] = False

    actor_config = config.get("actor", {})

    if verbose == 1:
        log.sh.setLevel(logging.INFO)
        actor_config["verbose"] = logging.INFO
    elif verbose == 2:
        log.sh.setLevel(logging.DEBUG)
        actor_config["verbose"] = logging.DEBUG
    elif verbose >= 3:
        log.sh.setLevel(logging.DEBUG)
        can_log.sh.setLevel(logging.DEBUG)
        actor_config["verbose"] = logging.DEBUG

    if quiet:
        log.handlers.remove(log.sh)
        warnings.simplefilter("ignore")
        actor_config["verbose"] = 100

    if sextant:
        sextant_file = os.path.join(os.path.dirname(__file__),
                                    "etc/sextant.yaml")
        config["files"]["ieb_config"] = sextant_file
        log.debug(
            f"Using internal IEB sextant onfiguration file {sextant_file}.")

    if sextant or "sextants/" in config["files"]["ieb_config"]:
        enable_low_temperature = False
    else:
        enable_low_temperature = True

    if virtual is True:
        profile = "virtual"

    ctx.obj = FPSWrapper(
        profile,
        ieb=ieb,
        npositioners=npositioners,
        enable_low_temperature=enable_low_temperature,
    )
Beispiel #6
0
    def send_command(
        self,
        command: str | int | CommandID | Command,
        positioner_ids: int | List[int] | None = None,
        data: Any = None,
        now: bool = False,
        **kwargs,
    ) -> Command:
        """Sends a command to the bus.

        Parameters
        ----------
        command
            The ID of the command, either as the integer value, a string,
            or the `.CommandID` flag. Alternatively, the `.Command` to send.
        positioner_ids
            The positioner IDs to command, or zero for broadcast. If `None`,
            sends the command to all FPS non-disabled positioners.
        data
            The bytes to send. See `.Command` for details on the format.
        interface
            The index in the interface list for the interface to use. Only
            relevant in case of a multibus interface. If `None`, the positioner
            to bus map will be used.
        bus
            The bus within the interface to be used. Only relevant in case of
            a multibus interface. If `None`, the positioner to bus map will
            be used.
        now
            If `True`, the command is sent to the CAN network immediately,
            skipping the command queue. No tracking is done for this command.
            It should only be used for emergency and shutdown commands.
        kwargs
            Extra arguments to be passed to the command.

        Returns
        -------
        command
            The command sent to the bus. The command needs to be awaited
            before it is considered done.

        """

        if not isinstance(self.can, JaegerCAN) or self.can._started is False:
            raise JaegerError("CAN connection not established.")

        if positioner_ids is None:
            positioner_ids = [p for p in self if not self[p].disabled]

        if not isinstance(command, Command):
            command_flag = CommandID(command)
            CommandClass = command_flag.get_command_class()
            assert CommandClass, "CommandClass not defined"

            command = CommandClass(positioner_ids, data=data, **kwargs)

        assert isinstance(command, Command)

        broadcast = command.is_broadcast
        pids = command.positioner_ids

        if broadcast:
            if any([self[pos].disabled for pos in self]) and not command.safe:
                raise JaegerError("Some positioners are disabled.")
        else:
            if any([self[pid].disabled for pid in pids]) and not command.safe:
                raise JaegerError("Some commanded positioners are disabled.")

        if not broadcast and not all([p in self for p in pids]):
            raise JaegerError("Some positioners are not connected.")

        # Check if we are in bootloader mode.
        in_boot = [p for p in pids if p != 0 and self[p].is_bootloader()]
        if (broadcast and self.is_bootloader()) or (not broadcast
                                                    and any(in_boot)):
            if not command.bootloader:
                raise JaegerError(
                    f"Cannot send command {command.command_id.name!r} "
                    "while in bootloader mode.")

        command_name = command.name
        command_uid = command.command_uid
        header = f"({command_name}, {command_uid}): "

        if self.locked:
            if command.safe:
                log.debug(f"FPS is locked but {command_name} is safe.")
            else:
                command.cancel(silent=True)
                raise FPSLockedError("FPS is locked.")

        elif command.move_command and self.moving:
            command.cancel(silent=True)
            raise JaegerError("Cannot send move command while the "
                              "FPS is moving. Use FPS.stop_trajectory() "
                              "to stop the FPS.")

        if command.status.is_done:
            raise JaegerError(header + "trying to send a done command.")

        if not now:
            assert self.can.command_queue
            self.can.command_queue.put_nowait(command)
            can_log.debug(header + "added command to CAN processing queue.")
        else:
            self.can.send_messages(command)
            can_log.debug(header + "sent command to CAN immediately.")

        return command
Beispiel #7
0
    def __post_init__(self):

        super().__init__()

        # Start file logger
        start_file_loggers(start_log=True, start_can=False)

        if config.CONFIG_FILE:
            log.debug(f"Using configuration from {config.CONFIG_FILE}")
        else:
            warnings.warn("Unknown configuration file.", JaegerUserWarning)

        self.loop = asyncio.get_event_loop()
        self.loop.set_exception_handler(log.asyncio_exception_handler)

        # The mapping between positioners and buses.
        self.positioner_to_bus: Dict[int, Tuple[BusABC, int | None]] = {}

        self.pid_lock: LockFile | None = None

        self._locked = False
        self.locked_by: List[int] = []

        if self.ieb is None or self.ieb is True:
            self.ieb = config["files"]["ieb_config"]

        if isinstance(self.ieb, pathlib.Path):
            self.ieb = str(self.ieb)

        if isinstance(self.ieb, (str, dict)):
            if isinstance(self.ieb, str):
                self.ieb = os.path.expanduser(os.path.expandvars(str(
                    self.ieb)))
                if not os.path.isabs(self.ieb):
                    self.ieb = os.path.join(os.path.dirname(__file__),
                                            self.ieb)
            try:
                self.ieb = IEB.from_config(self.ieb)
            except FileNotFoundError:
                warnings.warn(
                    f"IEB configuration file {self.ieb} cannot be loaded.",
                    JaegerUserWarning,
                )
                self.ieb = None
        elif self.ieb is False:
            self.ieb = None
        else:
            raise ValueError(f"Invalid input value for ieb {self.ieb!r}.")

        assert isinstance(self.ieb, IEB) or self.ieb is None

        self.__status_event = asyncio.Event()
        self.__temperature_task: asyncio.Task | None = None

        self.observatory = config["observatory"]

        self.alerts = AlertsBot(self)
        self.chiller = ChillerBot(self)

        self._configuration: BaseConfiguration | None = None
        self._previous_configurations: list[BaseConfiguration] = []
        self._preloaded_configuration: BaseConfiguration | None = None

        # Position and status pollers
        self.pollers = PollerList([
            Poller(
                "status",
                self.update_status,
                delay=config["fps"]["status_poller_delay"],
                loop=self.loop,
            ),
            Poller(
                "position",
                self.update_position,
                delay=config["fps"]["position_poller_delay"],
                loop=self.loop,
            ),
        ])
Beispiel #8
0
def get_path_pair(
    robot_grid: Optional[RobotGridCalib] = None,
    data: Optional[dict] = None,
    path_generation_mode: str = "greedy",
    ignore_did_fail: bool = False,
    explode_deg: float = 5,
    explode_positioner_id: int | None = None,
    speed=None,
    smooth_points=None,
    path_delay=None,
    collision_shrink=None,
    stop_if_deadlock: bool = False,
    ignore_initial_collisions: bool = False,
) -> tuple:
    """Runs ``pathGenGreedy`` and returns the to and from destination paths.

    Parameters
    ----------
    robot_grid
        The Kaiju ``RobotGridCalib`` instance to decollide.
    data
        A dictionary of data that can be used to reload a Kaiju robot grid
        using `.load_robot_grid`. This is useful if the function is being
        run in an executor.
    path_generation_mode
        Defines the path generation algorithm to use.
        Either ``greedy`` or ``explode`` or ``explode_one``.
    ignore_did_fail
        Generate paths even if path generation failed (i.e., deadlocks).
    explode_deg
        Degrees for ``pathGenExplode``.
    explode_positioner_id
        The positioner to explode.
    speed, smooth_points, path_delay, collision_shrink
        Kaiju parameters to pass to ``getPathPair``. Otherwise uses the default
        configuration values.
    stop_if_deadlock
        If `True`, detects deadlocks early in the path and returns shorter
        trajectories (at the risk of some false positive deadlocks).
    ignore_initial_collisions
        If `True`, does not fail if the initial state is collided. To be used
        only for offsets.

    Returns
    -------
    paths
        A tuple with the to destination path, from destination path, whether path
        generation failed, and the list of deadlocks

    """

    if robot_grid is not None and data is not None:
        raise JaegerError("robot_grid and data are mutually exclusive.")

    if data is not None:
        set_destination = False if path_generation_mode == "explode" else True
        robot_grid = load_robot_grid(data, set_destination=set_destination)

    assert robot_grid is not None

    if path_generation_mode == "explode":
        log.debug(f"Running pathGenExplode with explode_deg={explode_deg}.")
        robot_grid.pathGenExplode(explode_deg)
        deadlocks = []
    elif path_generation_mode == "explode_one":
        log.debug(f"Running pathGenExplodeOne with explode_deg={explode_deg}, "
                  f"explode_positioner_id={explode_positioner_id}.")
        robot_grid.pathGenExplodeOne(explode_deg, explode_positioner_id)
        deadlocks = []
    else:
        log.debug(
            f"Running pathGenGreedy with stopIfDeadlock={stop_if_deadlock}.")
        robot_grid.pathGenGreedy(
            stopIfDeadlock=stop_if_deadlock,
            ignoreInitialCollisions=ignore_initial_collisions,
        )

        # Check for deadlocks.
        deadlocks = robot_grid.deadlockedRobots()
        if robot_grid.didFail and ignore_did_fail is False:
            return (None, None, robot_grid.didFail, deadlocks)

    speed = speed or config["kaiju"]["speed"]
    smooth_points = smooth_points or config["kaiju"]["smooth_points"]
    collision_shrink = collision_shrink or config["kaiju"]["collision_shrink"]
    path_delay = path_delay or config["kaiju"]["path_delay"]

    log.debug(
        f"Running getPathPair with speed={speed}, smoothPoints={smooth_points}, "
        f"collisionShrink={collision_shrink}, pathDelay={path_delay}.")

    to_destination, from_destination = robot_grid.getPathPair(
        speed=speed,
        smoothPoints=smooth_points,
        collisionShrink=collision_shrink,
        pathDelay=path_delay,
    )

    return (
        to_destination,
        from_destination,
        robot_grid.didFail,
        deadlocks,
    )
Beispiel #9
0
async def calibrate_positioner(
    fps, positioner_id, motors=True, datums=True, cogging=True
):
    """Runs the calibration process and saves it to the internal memory.

    Parameters
    ----------
    fps : .FPS
        The instance of `.FPS` that will receive the trajectory.
    positioner_id : int
        The ID of the positioner to calibrate.
    motors : bool
        Whether to perform the motor calibration.
    datums : bool
        Whether to perform the datums calibration.
    cogging : bool
        Whether to perform the cogging calibration (may take more
        than one hour).

    Raises
    ------
    JaegerError
        If encounters a problem during the process.

    Examples
    --------
    ::

        >>> fps = FPS()
        >>> await fps.initialise()

        # Calibrate positioner 31.
        >>> await calibrate_positioner(fps, 31)

    """

    log.info(f"Calibrating positioner {positioner_id}.")

    if positioner_id not in fps.positioners:
        raise JaegerError(f"Positioner {positioner_id} not found.")

    positioner = fps[positioner_id]

    if fps.pollers.running:
        log.debug("Stopping pollers")
        await fps.pollers.stop()

    if motors:
        log.info("Starting motor calibration.")

        cmd = await fps.send_command(
            CommandID.START_MOTOR_CALIBRATION,
            positioner_ids=positioner_id,
        )

        if cmd.status.failed:
            raise JaegerError("Motor calibration failed.")

        await asyncio.sleep(1)
        await positioner.wait_for_status(
            [
                PS.DISPLACEMENT_COMPLETED,
                PS.MOTOR_ALPHA_CALIBRATED,
                PS.MOTOR_BETA_CALIBRATED,
            ]
        )
    else:
        log.warning("Skipping motor calibration.")

    if datums:
        log.info("Starting datum calibration.")
        cmd = await fps.send_command(
            CommandID.START_DATUM_CALIBRATION,
            positioner_ids=positioner_id,
        )
        if cmd.status.failed:
            raise JaegerError("Datum calibration failed.")

        await asyncio.sleep(1)
        await positioner.wait_for_status(
            [
                PS.DISPLACEMENT_COMPLETED,
                PS.DATUM_ALPHA_CALIBRATED,
                PS.DATUM_BETA_CALIBRATED,
            ]
        )
    else:
        log.warning("Skipping datum calibration.")

    if cogging:
        log.info("Starting cogging calibration.")
        cmd = await fps.send_command(
            CommandID.START_COGGING_CALIBRATION,
            positioner_ids=positioner_id,
        )
        if cmd.status.failed:
            raise JaegerError("Cogging calibration failed.")

        await asyncio.sleep(1)
        await positioner.wait_for_status(
            [PS.COGGING_ALPHA_CALIBRATED, PS.COGGING_BETA_CALIBRATED]
        )
    else:
        log.warning("Skipping cogging calibration.")

    if motors or datums or cogging:
        log.info("Saving calibration.")
        cmd = await fps.send_command(
            CommandID.SAVE_INTERNAL_CALIBRATION,
            positioner_ids=positioner_id,
        )
        if cmd.status.failed:
            raise JaegerError("Saving calibration failed.")

        log.info(f"Positioner {positioner_id} has been calibrated.")

    return
Beispiel #10
0
async def send_trajectory(
    fps: FPS,
    trajectories: str | pathlib.Path | TrajectoryDataType,
    use_sync_line: bool | None = None,
    send_trajectory: bool = True,
    start_trajectory: bool = True,
    command: Optional[CluCommand[JaegerActor]] = None,
    dump: bool | str = True,
    extra_dump_data: dict[str, Any] = {},
) -> Trajectory:
    """Sends a set of trajectories to the positioners.

    This is a low-level function that raises errors when a problem is
    encountered. Most users should use `.FPS.send_trajectory` instead.
    `.send_trajectory` automates `.Trajectory` by calling the different
    methods in order, but provides less control.

    Parameters
    ----------
    fps
        The instance of `.FPS` that will receive the trajectory.
    trajectories
        Either a path to a YAML file to read or a dictionary with the
        trajectories. In either case the format must be a dictionary in
        which the keys are the ``positioner_ids`` and each value is a
        dictionary containing two keys: ``alpha`` and ``beta``, each
        pointing to a list of tuples ``(position, time)``, where
        ``position`` is in degrees and ``time`` is in seconds.
    use_sync_line
        If `True`, the SYNC line will be used to synchronise the beginning of
        all trajectories. Otherwise a `.START_TRAJECTORY` command will be sent
        over the CAN network. If `None`, defaults to the configuration parameter.
    send_trajectory
        If `True`, sends the trajectory to the positioners and returns the
        `.Trajectory` instance.
    start_trajectory
        If `True`, runs the trajectory after sending it. Otherwise, returns
        the `.Trajectory` instance after sending the data. Ignored if
        `send_trajectory=False`.
    command
        A command to which to output informational messages.
    dump
        Whether to dump a JSON with the trajectory to disk. If `True`,
        the trajectory is stored in ``/data/logs/jaeger/trajectories/<MJD>`` with
        a unique sequence for each trajectory. A string can be passed with a
        custom path. The dump file is created only if `.Trajectory.start` is
        called, regardless of whether the trajectory succeeds.
    extra_dump_data
        A dictionary with additional parameters to add to the dump JSON.

    Raises
    ------
    TrajectoryError
        If encounters a problem sending the trajectory.
    FPSLockedError
        If the FPS is locked.

    Examples
    --------
    ::

        >>> fps = FPS()
        >>> await fps.initialise()

        # Send a trajectory with two points for positioner 4
        >>> await fps.send_trajectory({4: {'alpha': [(90, 0), (91, 3)],
                                           'beta': [(20, 0), (23, 4)]}})

    """

    traj = Trajectory(
        fps,
        trajectories,
        dump=dump,
        extra_dump_data=extra_dump_data,
    )

    if use_sync_line is None:
        use_sync_line = config["fps"]["use_sync_line"]

    if use_sync_line:
        if not isinstance(fps.ieb, IEB) or fps.ieb.disabled:
            raise TrajectoryError(
                "IEB is not connected. Cannot use SYNC line.", traj)
        if (await fps.ieb.get_device("sync").read())[0] == "closed":
            raise TrajectoryError("The SYNC line is on high.", traj)

    if send_trajectory is False:
        return traj

    msg = "Sending trajectory data."
    log.debug(msg)
    if command:
        command.debug(msg)

    try:
        await traj.send()
    except TrajectoryError as err:
        raise TrajectoryError(
            f"Something went wrong sending the trajectory: {err}",
            err.trajectory,
        )
    finally:
        if traj.dump_file:
            traj.dump_trajectory()
            if command:
                command.debug(trajectory_dump_file=traj.dump_file)

    msg = f"Trajectory sent in {traj.data_send_time:.1f} seconds."
    log.debug(msg)
    if command:
        command.debug(msg)

    log.info(
        f"Expected time to complete trajectory: {traj.move_time:.2f} seconds.")
    if command and traj.move_time:
        command.info(move_time=round(traj.move_time, 2))

    if start_trajectory is False:
        return traj

    msg = "Starting trajectory ..."
    log.info(msg)
    if command:
        command.info(msg)

    try:
        await traj.start(use_sync_line=use_sync_line)
    except TrajectoryError as err:

        if traj.start_time is not None and traj.end_time is not None:
            elapsed = traj.end_time - traj.start_time
            elapsed_msg = f" Trajectory failed {elapsed:.2f} seconds after start."
        else:
            elapsed_msg = ""

        raise TrajectoryError(
            f"Something went wrong during the trajectory: {err}.{elapsed_msg}",
            err.trajectory,
        )
    finally:
        if traj.dump_file and command:
            command.debug(trajectory_dump_file=traj.dump_file)

    if command:
        command.info(folded=(await fps.is_folded()))

    msg = "All positioners have reached their destinations."
    log.info(msg)
    if command:
        command.info(msg)

    return traj