async def status(fps_maker: FPSWrapper, positioner_id: int): """Returns the status of a positioner with low-level initialisation.""" fps_maker.initialise = False async with fps_maker as fps: pos = Positioner(positioner_id, fps) try: await pos.update_firmware_version() print(f"Firmware: {pos.firmware}") print(f"Bootloader: {pos.is_bootloader()}") except Exception as err: raise JaegerError(f"Failed retrieving firmware: {err}") try: await pos.update_status() bit_names = ", ".join(bit.name for bit in pos.status.active_bits) print(f"Status: {pos.status.value} ({bit_names})") except Exception as err: raise JaegerError(f"Failed retrieving status: {err}") try: await pos.update_position() print(f"Position: {pos.position}") except Exception as err: raise JaegerError(f"Failed retrieving position: {err}")
def get_sjd(observatory: Optional[str] = None) -> int: """Returns the SDSS Julian Date as an integer based on the observatory. Parameters ---------- observatory The current observatory, either APO or LCO. If `None`, uses ``$OBSERVATORY``. """ if observatory is None: try: observatory = os.environ["OBSERVATORY"] except KeyError: raise JaegerError( "Observatory not passed and $OBSERVATORY is not set.") observatory = observatory.upper() if observatory not in ["APO", "LCO"]: raise JaegerError(f"Invalid observatory {observatory}.") time = Time.now() mjd = time.mjd if observatory == "APO": return int(mjd + 0.3) else: return int(mjd + 0.7)
def __init__( self, fps: FPS, *args, ieb_status_delay: float = 60.0, observatory: str | None = None, **kwargs, ): jaeger.actor_instance = self self.fps = fps # This is mostly for the miniwoks. If the schema file is not the base # one, merge them. base = os.path.join(os.path.dirname(__file__), "..") base_schema = os.path.realpath(os.path.join(base, "etc/schema.json")) schema = kwargs.get("schema", None) c_schema = os.path.realpath(os.path.join(base, schema)) if schema else None kwargs["schema"] = merge_json(base_schema, c_schema, write_temporary_file=True) # Pass the FPS instance as the second argument to each parser # command (the first argument is always the actor command). self.parser_args = [fps] # Set the observatory where the actor is running. if observatory is None: try: self.observatory = os.environ["OBSERVATORY"] except KeyError: raise JaegerError( "Observatory not passed and $OBSERVATORY is not set.") else: self.observatory = observatory super().__init__(*args, **kwargs) self.version = __version__ # Add ActorHandler to log and to the warnings logger. self.actor_handler = ActorHandler( self, level=logging.WARNING, filter_warnings=[JaegerUserWarning], ) log.addHandler(self.actor_handler) if log.warnings_logger: log.warnings_logger.addHandler(self.actor_handler) self._alive_task = asyncio.create_task(self._report_alive()) self._status_watcher_task = asyncio.create_task(self._status_watcher()) self.fps.alerts.set_actor(self) self.fps.chiller.set_actor(self)
async def wait_for_status( self, status: List[maskbits.PositionerStatus], delay=1, timeout: Optional[float] = None, ) -> bool: """Polls the status until it reaches a certain value. Parameters ---------- status The status to wait for. Can be a list in which case it will wait until all the statuses in the list have been reached. delay Time, in seconds, to wait between position updates. timeout How many seconds to wait for the status to reach the desired value before aborting. Returns ------- result Returns `True` if the status has been reached or `False` if the timeout limit was reached. """ if self.is_bootloader(): raise JaegerError( "wait_for_status cannot be scheduled in bootloader mode.") if not isinstance(status, (list, tuple)): status = [status] async def status_waiter(wait_for_status): while True: await self.update_status() # Check all statuses in the list all_reached = True for ss in wait_for_status: if ss not in self.status: all_reached = False break if all_reached: return await asyncio.sleep(delay) wait_for_status = [self.flags(int(ss)) for ss in status] try: await asyncio.wait_for(status_waiter(wait_for_status), timeout) except asyncio.TimeoutError: return False return True
async def goto( fps_maker, positioner_ids: tuple[int, ...] | list[int], alpha: float, beta: float, speed: float | None, all: bool = False, force: bool = False, relative: bool = False, use_sync: bool = True, go_cowboy: bool = False, ): """Sends positioners to a given (alpha, beta) position.""" with fps_maker as fps: if all: if not force: raise JaegerError( "Use --force to move all positioners at once.") positioner_ids = list(fps.positioners.keys()) else: positioner_ids = list(positioner_ids) if not relative: if alpha < 0 or beta < 0: raise JaegerError( "Negative angles only allowed in relative mode.") new_positions = {} for pid in positioner_ids: new_positions[pid] = (alpha, beta) try: await goto_( fps, new_positions, speed=speed, relative=relative, use_sync_line=use_sync, go_cowboy=go_cowboy, ) except (JaegerError, TrajectoryError) as err: raise JaegerError(f"Goto command failed: {err}")
async def send_to_all(self, *args, **kwargs): """Sends a command to all connected positioners. This method has been deprecated. Use `.send_command` with a list for ``positioner_ids`` instead. """ raise JaegerError( "send_to_all has been deprecated. Use send_command instead.")
def get_snapshot_async( path: str, robot_grid: Optional[RobotGridCalib] = None, data: Optional[dict] = None, highlight: int | None = None, title: str | None = None, ): """Creates an FPS snapshot and saves it to disk. To be used with an executor. Parameters ---------- path The path where to save the file. robot_grid The Kaiju ``RobotGridCalib`` instance to plot. 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. highlight Robot to highlight. title A title for the plot. """ import matplotlib.pyplot as plt 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: robot_grid = load_robot_grid(data) assert robot_grid is not None with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=".+array interface is deprecated.+") ax = robot_grid.plot_state(highlightRobot=highlight) assert ax is not None if title is not None: ax.set_title(title) plt.tight_layout() ax.figure.savefig(path) plt.close("all")
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
async def unlock(self, force=False): """Unlocks the `.FPS` if all collisions have been resolved.""" # Send STOP_TRAJECTORY. This clears the collided flags. await self.stop_trajectory(clear_flags=True) await self.update_status(timeout=2) for positioner in self.positioners.values(): if positioner.collision: self._locked = True raise JaegerError("Cannot unlock the FPS until all the " "collisions have been cleared.") self._locked = False self.locked_by = [] return True
def __new__(cls: Type[FPS_T], *args, **kwargs): if cls in cls._instance and kwargs == {}: raise JaegerError("An instance of FPS is already running. " "Use get_instance() to retrieve it.") if cls not in cls._instance or kwargs != {}: if cls not in cls._instance: new_obj = super().__new__(cls) cls._instance[cls] = new_obj else: new_obj = cls._instance[cls] dict.__init__(new_obj, {}) new_obj.initialised = False return new_obj raise RuntimeError( "Returning None in BaseFPS.__new__. This should not happen.")
async def start_can(self): """Starts the JaegerCAN interface.""" use_lock = config["fps"]["use_lock"] if use_lock and self.pid_lock is None: try: if not os.path.exists(os.path.dirname(LOCK_FILE)): os.makedirs(os.path.dirname(LOCK_FILE)) self.pid_lock = LockFile(LOCK_FILE) except Exception: raise JaegerError( f"Failed creating lock file {LOCK_FILE}. " "Probably another instance is running. " "If that is not the case, remove the lock file and retry.") if isinstance(self.can, JaegerCAN): await self.can.start() return self.can = await JaegerCAN.create(self.can, fps=self) return True
def add_positioner( self, positioner: int | Positioner, centre=(None, None), ) -> Positioner: """Adds a new positioner to the list, and checks for duplicates.""" if isinstance(positioner, self.positioner_class): positioner_id = positioner.positioner_id else: positioner_id = positioner positioner = self.positioner_class(positioner_id, None, centre=centre) if positioner_id in self.positioners: raise JaegerError(f"{self.__class__.__name__}: there is already a " f"positioner in the list with positioner_id " f"{positioner_id}.") self.positioners[positioner_id] = positioner return positioner
async def create_random_configuration( fps: FPS, seed: int | None = None, safe=True, uniform: tuple[float, ...] | None = None, collision_buffer: float | None = None, max_deadlocks: int = 6, deadlock_retries: int = 5, **kwargs, ): """Creates a random configuration using Kaiju.""" from jaeger.target.configuration import ManualConfiguration seed = seed or numpy.random.randint(0, 1000000) numpy.random.seed(seed) robot_grid = get_robot_grid(fps, seed=seed, collision_buffer=collision_buffer) alphaL, betaL = config["kaiju"]["lattice_position"] # We use Kaiju for convenience in the non-safe mode. for robot in robot_grid.robotDict.values(): if robot.isOffline: continue if uniform is not None: alpha0, alpha1, beta0, beta1 = uniform robot.setAlphaBeta( numpy.random.uniform(alpha0, alpha1), numpy.random.uniform(beta0, beta1), ) else: if safe: safe_mode = config["safe_mode"] if safe_mode is False: safe_mode = {"min_beta": 165, "max_beta": 175} robot.setAlphaBeta( numpy.random.uniform(0, 359.9), numpy.random.uniform( safe_mode["min_beta"], 175.0, ), ) else: robot.setXYUniform() robot.setDestinationAlphaBeta(alphaL, betaL) # Confirm that the configuration is valid. This should only matter # for full range random configurations. try: robot_grid, _ = await decollide_in_executor(robot_grid, simple=True) grid_data = { robot.id: (robot.alpha, robot.beta) for robot in robot_grid.robotDict.values() } except JaegerError: raise JaegerError( "Decollision failed. Cannot create random configuration.") _, _, did_fail, deadlocks = await get_path_pair_in_executor(robot_grid) # If too many deadlocks, just try a new seed. n_deadlock = len(deadlocks) if did_fail and n_deadlock > max_deadlocks: log.warning("Too many deadlocked robots. Trying new seed.") return await create_random_configuration( fps, safe=safe, uniform=uniform, collision_buffer=collision_buffer, deadlock_retries=deadlock_retries, ) if did_fail and n_deadlock > 0: # Now the fun part, if there are only a few deadlocks, try assigning them # a random position. log.warning(f"Found {n_deadlock} deadlocked robots. Trying to unlock.") for nn in range(1, deadlock_retries + 1): log.info(f"Retry {nn} out of {deadlock_retries}.") to_replace_robot = numpy.random.choice(deadlocks) robot_grid = get_robot_grid( fps, seed=seed + 1, collision_buffer=collision_buffer, ) for robot in robot_grid.robotDict.values(): if robot.isOffline: continue if robot.id == to_replace_robot: robot.setXYUniform() else: robot.setAlphaBeta(*grid_data[robot.id]) try: robot_grid, _ = await decollide_in_executor(robot_grid, simple=True) grid_data = { robot.id: (robot.alpha, robot.beta) for robot in robot_grid.robotDict.values() } except JaegerError: raise JaegerError( "Decollision failed. Cannot create random configuration.") _, _, did_fail, deadlocks = await get_path_pair_in_executor( robot_grid) if did_fail is False: log.info("Random configuration has been unlocked.") break else: log.info(f"{len(deadlocks)} deadlocks remaining.") if nn == deadlock_retries: log.warning("Failed unlocking. Trying new seed.") return await create_random_configuration( fps, seed=seed + 1, safe=safe, uniform=uniform, collision_buffer=collision_buffer, deadlock_retries=deadlock_retries, ) pT = calibration.positionerTable.copy().reset_index() # Build an assignment dictionary. data = {} for pid in grid_data: holeID = pT.loc[pT.positionerID == pid].holeID.values[0] data[holeID] = { "alpha": grid_data[pid][0], "beta": grid_data[pid][1], "fibre_type": "Metrology", } return ManualConfiguration(data, **kwargs)
async def load_firmware( fps: FPS, firmware_file: str | pathlib.Path, positioners: Optional[List[int]] = None, messages_per_positioner: Optional[int] = None, force: bool = False, show_progressbar: bool = False, progress_callback: Optional[Callable[[int, int], Any]] = None, stop_logging: bool = True, ): """Convenience function to run through the steps of loading a new firmware. This function is a coroutine and not intendend for direct use. Use the ``jaeger`` CLI instead. Parameters ---------- fps `~jaeger.fps.FPS` instance to which the commands will be sent. firmware_file Binary file containing the firmware to load. positioners A list of positioner ids whose firmware to update, or `None` to update all the positioners in ``fps``. messages_per_positioner How many messages to send to each positioner at once. This can improve the performance but also overflow the CAN bus buffer. With the default value of `None`, reverts to the configuration value ``positioner.firmware_messages_per_positioner``. force Forces the firmware load to continue even if some positioners are not responding or are not in bootloader mode. show_progressbar Whether to show a progress bar. progress_callback A function to call as data gets transferred to the positioners. The callback is called with ``(current_chunk, n_chuck)`` where ``current_chunk`` is the number of the data chunk being sent and ``n_chunk`` is the total number of chunks in the data package. stop_logging Disable logging to file for the CAN logger to improve performance. """ if show_progressbar: try: import progressbar except ImportError: warnings.warn( "progressbar2 is not installed. Cannot show a progress bar.", JaegerUserWarning, ) progressbar = None show_progressbar = False else: progressbar = None start_time = time.time() firmware_file = pathlib.Path(firmware_file) assert firmware_file.exists(), "cannot find firmware file" log.info(f"firmware file {firmware_file!s} found.") # Open firmware data as binary. firmware_data = open(firmware_file, "rb") crc32 = zlib.crc32(firmware_data.read()) filesize = os.path.getsize(firmware_file) # Check to make sure all positioners are in bootloader mode. valid_positioners = [] n_bad = 0 for positioner_id in fps.positioners: if positioners is not None and positioner_id not in positioners: continue positioner = fps.positioners[positioner_id] if (not positioner.is_bootloader() or BootloaderStatus.BOOTLOADER_INIT not in positioner.status or BootloaderStatus.UNKNOWN in positioner.status): n_bad += 1 continue valid_positioners.append(positioner) if len(valid_positioners) == 0: raise JaegerError( "no positioners found in bootloader mode or with valid status.") if n_bad > 0: msg = f"{n_bad} positioners not in bootloader mode or state is invalid." if force: warnings.warn(msg + " Proceeding becasuse force=True.", JaegerUserWarning) else: raise JaegerError(msg) log.info("stopping pollers") await fps.pollers.stop() log.info(f"upgrading firmware on {len(valid_positioners)} positioners.") start_firmware_payload = int_to_bytes(filesize) + int_to_bytes(crc32) log.info(f"CRC32: {crc32}") log.info(f"File size: {filesize} bytes") pids = [pos.positioner_id for pos in valid_positioners] cmd = await fps.send_command( CommandID.START_FIRMWARE_UPGRADE, positioner_ids=pids, data=[start_firmware_payload], ) if cmd.status.failed or cmd.status.timed_out: log.error("firmware upgrade failed.") return False # Restore pointer to start of file firmware_data.seek(0) log.info("starting data send.") if stop_logging and can_log.fh: fh_handler = can_log.handlers.pop(can_log.handlers.index(can_log.fh)) else: fh_handler = None chunk_size = 8 n_chunks = int(numpy.ceil(filesize / chunk_size)) with contextlib.ExitStack() as stack: if show_progressbar and progressbar: bar = stack.enter_context( progressbar.ProgressBar(max_value=n_chunks)) else: bar = None messages_default = config["positioner"][ "firmware_messages_per_positioner"] messages_per_positioner = messages_per_positioner or messages_default assert isinstance(messages_per_positioner, int) ii = 0 while True: cmds = [] stop = False for __ in range(messages_per_positioner): chunk = firmware_data.read(chunk_size) packetdata = bytearray(chunk) # packetdata.reverse() # IMPORTANT! no longer needed for P1 if len(packetdata) == 0: stop = True break cmds.append( fps.send_command( CommandID.SEND_FIRMWARE_DATA, positioner_ids=pids, data=[packetdata], timeout=15, )) await asyncio.gather(*cmds) if any(cmd.status.failed or cmd.status.timed_out for cmd in cmds): log.error("firmware upgrade failed.") if fh_handler: can_log.addHandler(fh_handler) return False ii += messages_per_positioner if show_progressbar and bar: if ii < n_chunks: bar.update(ii) if progress_callback: progress_callback(ii, n_chunks) if stop: break log.info("firmware upgrade complete.") if fh_handler: can_log.addHandler(fh_handler) total_time = time.time() - start_time log.info(f"upgrading firmware took {total_time:.2f} s.") return True
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
async def goto( fps: FPS, new_positions: dict[int, tuple[float, float]], speed: Optional[float] = None, relative: bool = False, use_sync_line: bool = None, go_cowboy: bool = False, force: bool = False, command: CluCommand[JaegerActor] | None = None, ): """Send positioners to a given position using a trajectory with ``kaiju`` check. Parameters ---------- fps The `.FPS` instance. new_positions The new positions as a dictionary of positioner ID to a tuple of new alpha and beta angles. Positioners not specified will be kept on the same positions. speed The speed to use. relative If `True`, ``alpha`` and ``beta`` are considered relative angles. use_sync_line Whether to use the SYNC line to start the trajectories. go_cowboy If set, does not create a ``kaiju``-safe trajectory. Use at your own risk. force If ``go_cowboy=False`` and the trajectory is deadlocked, a `.TrajectoryError` will be raised. Use ``force=True`` to apply the trajectory anyway. command A command to pass to `.send_trajectory` to output additional information. """ if fps.locked: FPSLockedError("The FPS is locked.") if fps.moving: raise JaegerError("FPS is moving. Cannot send goto.") for pid in new_positions: if pid not in fps.positioners: raise JaegerError(f"Positioner ID {pid} is not connected.") speed = float(speed or config["positioner"]["motor_speed"]) if speed < 500 or speed > 5000: raise JaegerError("Invalid speed.") positioner_ids = list(new_positions.keys()) await fps.update_position(positioner_ids=positioner_ids) trajectories = {} if go_cowboy is True: for pid in positioner_ids: pos = fps[pid] if pos.alpha is None or pos.beta is None: raise JaegerError( f"Positioner {pid}: cannot goto with unknown position.") current_alpha = pos.alpha current_beta = pos.beta if relative is True: alpha_end = current_alpha + new_positions[pid][0] beta_end = current_beta + new_positions[pid][1] else: alpha_end = new_positions[pid][0] beta_end = new_positions[pid][1] alpha_delta = abs(alpha_end - current_alpha) beta_delta = abs(beta_end - current_beta) time_end = [ get_goto_move_time(alpha_delta, speed=speed), get_goto_move_time(beta_delta, speed=speed), ] trajectories[pid] = { "alpha": [(current_alpha, 0.1), (alpha_end, time_end[0] + 0.1)], "beta": [(current_beta, 0.1), (beta_end, time_end[1] + 0.1)], } else: if relative is True: raise JaegerError("relative is not implemented for kaiju moves.") data = {"collision_buffer": None, "grid": {}} for pid, (current_alpha, current_beta) in fps.get_positions_dict().items(): if current_alpha is None or current_beta is None: raise JaegerError( f"Positioner {pid} does not know its position.") if pid in new_positions: data["grid"][int(pid)] = ( current_alpha, current_beta, new_positions[pid][0], new_positions[pid][1], fps[pid].disabled, ) else: data["grid"][int(pid)] = ( current_alpha, current_beta, current_alpha, current_beta, fps[pid].disabled, ) (to_destination, _, did_fail, deadlocks) = await run_in_executor( get_path_pair, data=data, stop_if_deadlock=force, executor="process", ignore_did_fail=force, ) if did_fail is True: if force is False: raise TrajectoryError( "Cannot execute trajectory. Found " f"{len(deadlocks)} deadlocks ({deadlocks}).") else: warnings.warn( f"Found {len(deadlocks)} deadlocks but applying trajectory.", JaegerUserWarning, ) trajectories = to_destination return await send_trajectory( fps, trajectories, use_sync_line=use_sync_line, command=command, extra_dump_data={"kaiju_trajectory": not go_cowboy}, )
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
async def initialise( self: T, start_pollers: bool | None = None, enable_low_temperature: bool = True, ) -> T: """Initialises all positioners with status and firmware version. Parameters ---------- start_pollers Whether to initialise the pollers. """ if start_pollers is None: start_pollers = config["fps"]["start_pollers"] assert isinstance(start_pollers, bool) # Clear all robots self.clear() self.positioner_to_bus = {} # Stop pollers while initialising if self.pollers.running: await self.pollers.stop() # Make sure CAN buses are connected. await self.start_can() # Test IEB connection. if isinstance(self.ieb, IEB): try: async with self.ieb: pass except BaseException as err: warnings.warn(str(err), JaegerUserWarning) assert isinstance(self.can, JaegerCAN), "CAN connection not established." if len(self.can.interfaces) == 0: warnings.warn("CAN interfaces not found.", JaegerUserWarning) return self # Get the positioner-to-bus map await self._get_positioner_bus_map() # Stop poller in case they are running await self.pollers.stop() get_fw_command = self.send_command( CommandID.GET_FIRMWARE_VERSION, positioner_ids=0, timeout=config["fps"]["initialise_timeouts"], ) assert isinstance(get_fw_command, GetFirmwareVersion) await get_fw_command if get_fw_command.status.failed: raise JaegerError("Failed retrieving firmware version.") # Loops over each reply and set the positioner status to OK. If the # positioner was not in the list, adds it. for reply in get_fw_command.replies: if reply.positioner_id not in self.positioners: if hasattr(reply.message, "interface"): interface = reply.message.interface bus = reply.message.bus else: interface = bus = None self.add_positioner(reply.positioner_id, interface=interface, bus=bus) positioner = self.positioners[reply.positioner_id] positioner.fps = self positioner.firmware = get_fw_command.get_firmware()[ reply.positioner_id] if positioner.positioner_id in config["fps"][ "disabled_positioners"]: positioner.disabled = True # Add offline robots. Offline positioners are physically in the array but # they don't reply to commands and we need to specify their position. Once # That's done they behave as normal disabled robots. if config["fps"]["offline_positioners"] is not None: for pid in config["fps"]["offline_positioners"]: off_alpha, off_beta = config["fps"]["offline_positioners"][pid] if pid not in self.positioners: positioner = self.add_positioner(pid) else: positioner = self.positioners[pid] positioner.disabled = True positioner.offline = True positioner.alpha = off_alpha positioner.beta = off_beta # Mark as initialised here although we have some more work to do. self.initialised = True pids = sorted(list(self.keys())) if len(pids) > 0: log.info(f"Found {len(pids)} connected positioners: {pids!r}.") else: warnings.warn("No positioners found.", JaegerUserWarning) return self if len(set([pos.firmware for pos in self.values()])) > 1: warnings.warn( "Found positioners with different firmware versions.", JaegerUserWarning, ) # Stop all positioners just in case. This won't clear collided flags. if not self.is_bootloader(): await self.stop_trajectory() # Initialise positioners try: disable_precise_moves = config["positioner"][ "disable_precise_moves"] # if disable_precise_moves: # warnings.warn("Disabling precise moves.", JaegerUserWarning) pos_initialise = [ positioner.initialise( disable_precise_moves=disable_precise_moves) for positioner in self.values() ] await asyncio.gather(*pos_initialise) except (JaegerError, PositionerError) as err: raise JaegerError(f"Some positioners failed to initialise: {err}") if disable_precise_moves is True and any( [self[i].precise_moves for i in self]): log.error("Unable to disable precise moves for some positioners.") n_non_initialised = len([ positioner for positioner in self.positioners.values() if (positioner.status == positioner.flags.UNKNOWN or not positioner.initialised) ]) if n_non_initialised > 0: raise JaegerError( f"{n_non_initialised} positioners failed to initialise.") if self.is_bootloader(): bootlist = [ p.positioner_id for p in self.values() if p.is_bootloader() ] warnings.warn( f"Positioners in booloader mode: {bootlist!r}.", JaegerUserWarning, ) return self # Check if any of the positioners are collided and if so lock the FPS. locked_by = [] for positioner in self.values(): if positioner.collision: locked_by.append(positioner.positioner_id) if len(locked_by) > 0: await self.lock(by=locked_by, do_warn=False, snapshot=False) warnings.warn( "The FPS was collided and has been locked.", JaegerUserWarning, ) if config.get("safe_mode", False) is not False: min_beta = MIN_BETA if isinstance(config["safe_mode"], dict): min_beta = config["safe_mode"].get("min_beta", MIN_BETA) warnings.warn( f"Safe mode enabled. Minimum beta is {min_beta} degrees.", JaegerUserWarning, ) # Disable collision detection for listed robots. disable_collision = config["fps"][ "disable_collision_detection_positioners"] disable_connected = list( set(disable_collision) & set(self.positioners.keys())) if len(disable_connected) > 0: warnings.warn( f"Disabling collision detection for positioners {disable_connected}.", JaegerUserWarning, ) await self.send_command( CommandID.ALPHA_CLOSED_LOOP_WITHOUT_COLLISION_DETECTION, positioner_ids=disable_connected, ) await self.send_command( CommandID.BETA_CLOSED_LOOP_WITHOUT_COLLISION_DETECTION, positioner_ids=disable_connected, ) # Start temperature watcher. if self.__temperature_task is not None: self.__temperature_task.cancel() if (isinstance(self.ieb, IEB) and not self.ieb.disabled and enable_low_temperature): self.__temperature_task = asyncio.create_task( self._handle_temperature()) else: self.set_status((self.status & ~FPSStatus.TEMPERATURE_NORMAL) | FPSStatus.TEMPERATURE_UNKNOWN) # Issue an update status to get the status set. await self.update_status() # Start the pollers if start_pollers and not self.is_bootloader(): self.pollers.start() # Initialise alerts and chiller bots with a bit of delay to let the actor # time to start. asyncio.create_task(self.alerts.start(delay=5)) asyncio.create_task(self.chiller.start(delay=5)) return self
def __init__( self, positioner_ids: int | List[int], timeout: Optional[float] = None, done_callback: Optional[Callable] = None, n_positioners: Optional[int] = None, data: Union[None, data_co, Dict[int, data_co]] = None, ignore_unknown: bool = True, ): global COMMAND_UID assert self.broadcastable is not None, "broadcastable not set" assert self.command_id is not None, "command_id not set" if isinstance(positioner_ids, (list, tuple)): self.positioner_ids = list(positioner_ids) if len(positioner_ids) != len(set(positioner_ids)): raise JaegerError("The list of positioner_ids must be unique.") if len(positioner_ids) > 1 and 0 in positioner_ids: raise JaegerError( "Broadcasts cannot be mixed with other positioners.") else: self.positioner_ids = [positioner_ids] if self.is_broadcast and self.broadcastable is False: raise JaegerError( f"Command {self.command_id.name} cannot be broadcast.") #: The data payload for the messages to send. if data is None: self.data = {pid: [bytearray()] for pid in self.positioner_ids} elif isinstance(data, bytearray): self.data = {pid: [data] for pid in self.positioner_ids} elif isinstance(data, (list, tuple)): self.data = {pid: data for pid in self.positioner_ids} elif isinstance(data, dict): self.data = {} for pid, value in data.items(): if value is None: self.data[pid] = [bytearray()] elif isinstance(value, (list, tuple)): self.data[pid] = value elif isinstance(value, bytearray): self.data[pid] = [value] else: raise ValueError(f"Invalid data {value!r}.") else: raise ValueError(f"Invalid data {data!r}.") if self.is_broadcast: if len(self.data[0]) > 1: raise CommandError( "Broadcasts can only include a single data packet.") # Number of replies expected unless broadcast. if self.is_broadcast: if n_positioners: self._n_replies = len(self.data[0]) * n_positioners else: self._n_replies = None else: self._n_replies = sum([len(value) for value in self.data.values()]) if timeout is None: pass else: self.timeout = timeout if self.timeout < 0 and self._n_replies is None: raise CommandError( "In a broadcast a timeout is required unless n_positioners is set." ) #: A list of messages with the responses to this command. self.replies: List[Reply] = [] # Messages sent. self.messages = [] self.message_uids = [] # Generate a UUID for this command. self.command_uid = COMMAND_UID COMMAND_UID += 1 # Starting and end time self.start_time: float | None = None self.end_time: float | None = None # If this is the first time we run this command the pool will be empty. uid_bits = config["positioner"]["uid_bits"] command_pool = UID_POOL[self.command_id] if not self.is_broadcast: for pid in self.positioner_ids: if pid not in command_pool: command_pool[pid] = set(range(1, 2**uid_bits)) else: if 0 not in command_pool: command_pool[0] = set([0]) # What interface and bus this command should be sent to. Only relevant # for multibus interfaces. To be filled by the FPS class when queueing # the command. self._interfaces: Optional[List[BusABC]] = [] self._bus: Optional[int] = None self._done_callback = done_callback self._timeout_handle = None self._ignore_unknown = ignore_unknown self.loop = asyncio.get_event_loop() StatusMixIn.__init__( self, maskbit_flags=CommandStatus, initial_status=CommandStatus.READY, callback_func=self.status_callback, ) asyncio.Future.__init__(self)
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, )
def decollide( robot_grid: Optional[RobotGridCalib] = None, data: Optional[dict] = None, simple: bool = False, decollide_grid_fallback: bool = False, priority_order: list[int] = [], ) -> tuple[RobotGridCalib | dict, list[int]]: """Decollides a potentially collided grid. Raises on fail. 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. simple Runs ``decollideGrid()`` and returns. decollide_grid_fallback If `True`, runs ``decollideGrid()`` if the positioner-by-positioner decollision fails. priority_list A sorted list of positioner IDs with the order of which positioners to try to keep at their current positions. Positioners earlier in the list will be decollided last. Ignore in case of ``simple=True``. Returns ------- grid,decollided If ``robot_grid`` is passed, returns the same grid instance after decollision. If ``data`` is passed, returns a dictionary describing the decollided grid that can be used to recreate a grid using `.load_robot_grid`. Also returns the list of robots that have been decollided. """ 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: robot_grid = load_robot_grid(data) assert robot_grid is not None if simple: collided = robot_grid.getCollidedRobotList() robot_grid.decollideGrid() if len(robot_grid.getCollidedRobotList()) > 0: raise JaegerError("Failed decolliding grid.") if data is not None: return dump_robot_grid(robot_grid), collided else: return robot_grid, collided # First pass. If collided, decollide each robot one by one. collided = robot_grid.getCollidedRobotList() # Sort collided robots by priority order. collided = sorted( collided, key=lambda x: len(priority_order) - priority_order.index(x) if x in priority_order else -1, ) decollided = [] if len(collided) > 0: for robot_id in collided: if robot_grid.isCollided(robot_id): if robot_grid.robotDict[robot_id].isOffline: continue robot_grid.decollideRobot(robot_id) decollided.append( robot_id) # Even if we failed it may have moved. if robot_grid.isCollided(robot_id): raise JaegerError( f"Failed decolliding positioner {robot_id}.") # Second pass. If still collided, try a grid decollision. if len(robot_grid.getCollidedRobotList()) > 0: if decollide_grid_fallback: warn("Grid is still colliding. Attempting full grid decollision.") robot_grid.decollideGrid() if robot_grid.getCollidedRobotList() is not False: raise JaegerError("Failed decolliding grid.") # We don't know which robots were decollided so assume all collided # robots have moved. decollided = collided else: raise JaegerError("Failed decolliding grid.") if data is not None: return dump_robot_grid(robot_grid), decollided else: return robot_grid, decollided