async def mcp_semaphore_ok(self, command: HALCommandType): """Returns the semaphore if the semaphore is owned by the TCC or nobody.""" mcp_model = self.actor.models["mcp"] sem = mcp_model["semaphoreOwner"] if sem is None: sem_show_cmd = await self._send_command( command, "mcp", "sem.show", time_limit=20.0, raise_on_fail=False, ) if sem_show_cmd.status.did_fail: raise HALError("Cannot get mcp semaphore. Is the MCP alive?") sem = mcp_model["semaphoreOwner"] if ((sem[0] != "TCC:0:0") and (sem[0] != "") and (sem[0] != "None") and (sem[0] is not None)): raise HALError( f"Cannot axis init: Semaphore is owned by {sem[0]}. " "If you are the owner (e.g., via MCP Menu), release it and try again. " "If you are not the owner, confirm that you can steal " "it from them, then issue: mcp sem.steal") return sem
async def _send_command( self, command: HALCommandType, target: str, cmd_str: str, raise_on_fail: bool = True, **kwargs, ): """Sends a command to a target.""" bypasses = self.actor.helpers.bypasses # If the helper is bypassed, just returns a fake done command. if (self.name and self.name in bypasses) or ("all" in bypasses): command.warning(f"Bypassing command '{target} {cmd_str}'") cmd = Command() cmd.set_status(CommandStatus.DONE) return cmd if self.actor.tron is None or self.actor.tron.connected() is False: raise HALError("Not connected to Tron. Cannot send commands.") cmd = await command.send_command(target, cmd_str, **kwargs) if raise_on_fail and cmd.status.did_fail: if cmd.status == CommandStatus.TIMEDOUT: raise HALError(f"Command '{target} {cmd_str}' timed out.") else: raise HALError(f"Command '{target} {cmd_str}' failed.") return cast(Command, cmd)
async def axis_init(self, command: HALCommandType) -> bool: """Executes TCC axis init or fails.""" status = await self._send_command( command, "tcc", "axis status", time_limit=20.0, raise_on_fail=False, ) if status.status.did_fail: raise HALError("'tcc status' failed. Is the TCC connected?") if self.check_stop_in() is True: raise HALError("Cannot tcc axis init because of bad axis status: " "Check stop buttons on Interlocks panel.") sem = await self.mcp_semaphore_ok(command) if sem is False: raise HALError("Failed getting the semaphore information.") if sem == "TCC:0:0" and self.axes_are_clear(): command.debug(text="Axes clear and TCC has semaphore. " "No axis init needed, so none sent.") return True command.debug(text="Sending tcc axis init.") axis_init_cmd_str = "axis init" if self.below_alt_limit(): command.warning( text= "Altitude below interlock limit! Only initializing altitude " "and rotator: cannot move in az.") axis_init_cmd_str += " rot,alt" axis_init_cmd = await self._send_command( command, "tcc", axis_init_cmd_str, time_limit=20.0, raise_on_fail=False, ) if axis_init_cmd.status.did_fail: command.error("Cannot slew telescope: failed tcc axis init.") raise HALError( "Cannot slew telescope: check and clear interlocks?") return True
async def set_dither_position( self, command: HALCommandType, position: str, force: bool = False, ): """Sets the dither mechanism to the commanded position.""" position = position.upper() if position not in ["A", "B"]: raise HALError(f"Invalid dither position {position}.") current_position = self.get_dither_position() if current_position is None: command.warning("Current dither position is unknown.") if current_position == position and force is False: return None dither_command = await self._send_command( command, "apogee", f"dither namedpos={position}", time_limit=config["timeouts"]["apogee_dither"], ) return dither_command
async def goto_position( self, command: HALCommandType, where: str | dict[str, float], ): """Executes the goto command. Parameters ---------- command The actor command. where The name of the goto entry in the configuration file, or a dictionary with the keys ``(alt, az, rot)`` in degrees. """ config = command.actor.config if isinstance(where, str): if where not in config["goto"]: raise HALError(f"Cannot find goto position '{where}'.") alt = config["goto"][where]["alt"] az = config["goto"][where]["az"] rot = config["goto"][where]["rot"] where = {"alt": alt, "az": az, "rot": rot} if self.is_slewing: raise HALError("TCC is already slewing.") # await self.axis_init(command) # Even if this is already checked in axis_init(), let's check again that the # axes are ok, but if alt < limit, we only check az and alt because we won't # move in altitude. result = self.axes_are_clear() if not result: raise HALError("Some axes are not clear. Cannot continue.") # Now do the actual slewing. slew_result = await self.do_slew(command, where) if slew_result is False: raise HALError(f"Failed going to position {where}.") return command.info(text=f"At position {where}.")
async def expose_dither_pair( self, command: HALCommandType, exp_time: float, dither_sequence: str | None = None, exp_type: str = "object", ): """Takes an APOGEE dither set. Parameters ---------- command The command used to interact with the APOGEE actor. exp_time The exposure time for each exposure in the dither sequence. exp_type The exposure type. Valid values are ``object``, ``dark``, and ``flat``. dither_sequence The dither sequence. If `None` the first dither will be taken at the current position and the mechanism will be switched after it. Alternatively, a string ``"AB"``, ``"BA"``, etc. """ if dither_sequence is None: current = self.get_dither_position() if current is None: raise HALError( "Cannot determine current APOGEE dither position.") dither_sequence = current.upper() dither_sequence = "AB" if dither_sequence == "A" else "BA" else: dither_sequence = dither_sequence.upper() if dither_sequence not in ["AB", "BA", "AA", "BB"]: raise HALError(f"Invalid dither sequence {dither_sequence}.") for dither_position in dither_sequence: await self.expose( command, exp_time, exp_type=exp_type, dither_position=dither_position, )
def list_status(self) -> dict[str, tuple[bool, bool, float, bool]]: """Returns a dictionary with the state of the lamps. For each lamp the first value in the returned tuple is whether the lamp has been commanded on. The second value is whether the lamps is actually on. The third value is how long since it changed states. The final value indicates whether the lamp has warmed up. """ state = {} for lamp in self.LAMPS: commanded_key = f"{lamp}LampCommandedOn" commanded_on = self.actor.models["mcp"][commanded_key][0] if commanded_on is None: raise HALError(f"Failed getting {commanded_key}.") if lamp in ["wht", "UV"]: is_on = bool(commanded_on) state[lamp] = (is_on, is_on, 0.0, is_on) else: lamp_key = f"{lamp}Lamp" lamp_state = self.actor.models["mcp"][lamp_key] if any([lv is None for lv in lamp_state]): raise HALError(f"Failed getting {lamp_key}.") last_seen = lamp_state.last_seen if sum(lamp_state.value) == 4: lamp_state = True elif sum(lamp_state.value) == 0: lamp_state = False else: raise HALError( f"Failed determining state for lamp {lamp}.") elapsed = time.time() - last_seen warmed = (elapsed >= self.WARMUP[lamp] ) if bool(commanded_on) else False state[lamp] = (bool(commanded_on), lamp_state, elapsed, warmed) return state
async def expose( self, command: HALCommandType, exp_time: float = 0.0, exp_type: str = "science", readout: bool = True, read_async: bool = False, ): """Exposes BOSS. If ``readout=False``, does not read the exposure.""" if self.readout_pending is not False: raise HALError( "Cannot expose. The camera is exposing or a readout is pending." ) timeout = (exp_time + config["timeouts"]["expose"] + config["timeouts"]["boss_flushing"]) command_parts = [f"exposure {exp_type}"] if exp_type != "bias": command_parts.append(f"itime={round(exp_time, 1)}") if readout is False or read_async is True: command_parts.append("noreadout") else: timeout += config["timeouts"]["boss_readout"] command_string = " ".join(command_parts) await self._send_command(command, "boss", command_string, time_limit=timeout) self.__readout_pending = True if readout is True and read_async is True: # We use a _send_command because readout cannot await on itself. self.__readout_task = asyncio.create_task( self._send_command( command, "boss", "exposure readout", time_limit=25.0 + config["timeouts"]["boss_readout"], )) return self.__readout_pending = not readout
async def axis_stop(self, command: HALCommandType, axis: str = "") -> bool: """Issues an axis stop to the TCC.""" axis_stop_cmd = await self._send_command( command, "tcc", f"axis stop {axis}".strip(), time_limit=30.0, raise_on_fail=False, ) if axis_stop_cmd.status.did_fail: raise HALError( "Error: failed to cleanly stop telescope via tcc axis stop.") self.is_slewing = False return True
async def readout(self, command: HALCommandType): """Reads a pending readout.""" if self.readout_pending is False: raise HALError("No pending readout.") command.debug("Reading pending BOSS exposure.") if self.readout_pending and self.__readout_task: await self.__readout_task else: await self._send_command( command, "boss", "exposure readout", time_limit=25.0 + config["timeouts"]["boss_readout"], ) self.clear_readout()
async def expose( self, command: HALCommandType, exp_time: float, exp_type: str = "dark", dither_position: str | None = None, ): """Exposes APOGEE. Parameters ---------- command The command used to interact with the APOGEE actor. exp_time The exposure time. exp_type The exposure type. Valid values are ``object``, ``dark``, ``flat``, and ``DomeFlat``. dither_position The dither position. If `None`, uses the current position. """ if exp_type.lower() not in ["object", "dark", "flat", "domeflat"]: raise HALError(f"Invalid exposure type {exp_type}.") if dither_position: await self.set_dither_position(command, dither_position) expose_command = await self._send_command( command, "apogee", f"expose time={exp_time:.1f} object={exp_type.lower()}", time_limit=exp_time + config["timeouts"]["expose"], ) return expose_command
async def do_slew( self, command, coords: dict[str, float], keep_offsets=False, offset=False, ) -> bool: """Correctly handle a slew command, given what parse_args had received.""" tcc_model = self.actor.models["tcc"] # NOTE: TBD: We should limit which offsets are kept. keep_args = "/keep=(obj,arc,gcorr,calib,bore)" if keep_offsets else "" slew_cmd = None if not offset: if "ra" in coords and "dec" in coords and "rot" in coords: ra = coords["ra"] dec = coords["dec"] rot = coords["rot"] command.info(text="Slewing to (ra, dec, rot) == " f"({ra:.4f}, {dec:.4f}, {rot:g})") if keep_args: command.warning(text="keeping all offsets") slew_cmd = self._send_command( command, "tcc", f"track {ra}, {dec} icrs /rottype=object/rotang={rot:g}" f"/rotwrap=mid {keep_args}", time_limit=config["timeouts"]["slew"], raise_on_fail=False, ) elif "az" in coords and "alt" in coords and "rot" in coords: alt = coords["alt"] az = coords["az"] rot = coords["rot"] command.info(text="Slewing to (az, alt, rot) == " f"({az:.4f}, {alt:.4f}, {rot:.4f})") slew_cmd = self._send_command( command, "tcc", f"track {az:f}, {alt:f} mount/rottype=mount/rotangle={rot:f}", time_limit=config["timeouts"]["slew"], raise_on_fail=False, ) else: raise HALError("Not enough coordinates information provided.") else: if "alt" not in coords or "az" not in coords: raise HALError("Not alt/az offsets provided.") # In arcsec alt = coords["alt"] or 0.0 az = coords["az"] or 0.0 rot = coords["rot"] or 0.0 command.info(text=f"Offseting alt={alt:.3f}, az={az:.3f}") slew_cmd = self._send_command( command, "tcc", f"offset guide {az/3600.:g},{alt/3600.:g},{rot/3600.:g} /computed", time_limit=config["timeouts"]["slew"], raise_on_fail=False, ) # "tcc track" in the new TCC is only Done successfully when all requested # axes are in the "tracking" state. All other conditions mean the command # failed, and the appropriate axisCmdState and axisErrCode will be set. # However, if an axis becomes bad during the slew, the TCC will try to # finish it anyway, so we need to explicitly check for bad bits. try: self.is_slewing = True slew_cmd = await slew_cmd finally: self.is_slewing = False if slew_cmd.status.did_fail: str_axis_state = ",".join(tcc_model["axisCmdState"].value) str_axis_code = ",".join(tcc_model["axisErrCode"].value) command.error( f"tcc track command failed with axis states: {str_axis_state} " f"and error codes: {str_axis_code}") raise HALError( "Failed to complete slew: see TCC messages for details.") if self.axes_are_clear() is False: axis_bits = self.get_bad_axis_bits() command.error("TCC slew command ended with some bad bits set: " "0x{:x},0x{:x},0x{:x}".format(*axis_bits)) raise HALError( "Failed to complete slew: see TCC messages for details.") return True
async def turn_lamp( self, command: HALCommandType, lamps: str | list[str], state: bool, turn_off_others: bool = False, force: bool = False, ): """Turns a lamp on or off. This routine always blocks until the lamps are on and warmed up. If you don't want to block, call it as a task. Parameters ---------- command The command that issues the lamp switching. lamp Name of the lamp(s) to turn on or off. turn_off_others Turn off all other lamps. force If `True`, send the on/off command regardless of status. """ if isinstance(lamps, str): lamps = [lamps] for lamp in lamps: if lamp not in self.LAMPS: raise HALError(f"Invalid lamp {lamp}.") status = self.list_status() tasks = [] turn_off_tasks = [] for ll in self.LAMPS: if ll in lamps: if force is False and status[ll][0] is state and status[ll][ -1] is True: pass else: tasks.append(self._command_one(command, ll, state)) else: if turn_off_others is True: if status[ll][0] is not False or force is True: turn_off_tasks.append( self._command_one(command, ll, False)) if len(turn_off_tasks) > 0: await asyncio.gather(*turn_off_tasks) await asyncio.gather(*tasks) done_lamps: list[bool] = [False] * len(lamps) warmed: list[bool] = [False] * len(lamps) n_iter = 0 while True: if all(done_lamps): if state is False: command.info(f"Lamp(s) {','.join(lamps)} are off.") return elif all(warmed): command.info( f"Lamp(s) {','.join(lamps)} are on and warmed up.") return new_status = self.list_status() for i, lamp in enumerate(lamps): # Don't don't anything if we're already at the state. if done_lamps[i] and (state is False or warmed[i]): continue # Index 1 is if the lamp is actually on/off, not only commanded. if new_status[lamp][1] is state: done_lamps[i] = True if state is True: # Index 2 is the elapsed time since it was completely on/off. elapsed = new_status[lamp][2] if elapsed >= self.WARMUP[lamp]: command.debug(f"Lamp {lamp}: warm-up complete.") warmed[i] = True elif (n_iter % 5) == 0: remaining = int(self.WARMUP[lamp] - elapsed) command.debug(f"Warming up lamp {lamp}: " f"{remaining} s remaining.") await asyncio.sleep(1) n_iter += 1