async def read_command_status(self, counter): """Wait for a command status message whose counter matches a specified value. Returns ------- duration : `float` If the command succeeds. Raises ------ salobj.ExpectedError If the command failed, the command status code is not recognized, or the command stream disconnects. """ header = structs.Header() command_status = structs.CommandStatus() try: while True: if not self.command_connected: raise salobj.ExpectedError( "Command socket disconnected while running the command" ) await tcpip.read_into(self.command_reader, header) if header.frame_id != structs.CommandStatus.FRAME_ID: self.log.warning( f"Ignoring message with unexpected frame_id: " f"{header.frame_id} != {structs.CommandStatus.FRAME_ID}" ) continue if not self.command_connected: raise salobj.ExpectedError( "Command socket disconnected while running the command" ) await tcpip.read_into(self.command_reader, command_status) if header.counter != counter: self.log.warning( "Ignoring command status for wrong command; " f"status.counter={header.counter} " f"!= expected value {counter}") continue break except ConnectionError: self.log.warning("Command stream disconnected; closing client") await self.basic_close() raise salobj.ExpectedError( "Command socket disconnected while running the command") if command_status.status == CommandStatusCode.ACK: return command_status.duration elif command_status.status == CommandStatusCode.NO_ACK: reason = command_status.reason.decode() raise salobj.ExpectedError(reason) else: raise salobj.ExpectedError( f"Unknown command status {command_status.status}; " "low-level command assumed to have failed")
async def do_track(self, data): """Specify a position, velocity, TAI time tracking update.""" if self.summary_state != salobj.State.ENABLED: raise salobj.ExpectedError("Not enabled") if (self.server.telemetry.enabled_substate != EnabledSubstate.SLEWING_OR_TRACKING): if self._tracking_started_telemetry_counter <= 0: raise salobj.ExpectedError( "Low-level controller in substate " f"{self.server.telemetry.enabled_substate} " f"instead of {EnabledSubstate.SLEWING_OR_TRACKING}") dt = data.tai - salobj.current_tai() curr_pos = data.angle + data.velocity * dt if (not self.server.config.lower_pos_limit <= curr_pos <= self.server.config.upper_pos_limit): raise salobj.ExpectedError( f"current position {curr_pos} not in range " f"[{self.server.config.lower_pos_limit}, " f"{self.server.config.upper_pos_limit}]") if not abs(data.velocity) <= self.server.config.velocity_limit: raise salobj.ExpectedError(f"abs(velocity={data.velocity}) > " f"[{self.server.config.velocity_limit}") await self.run_command( code=enums.CommandCode.TRACK_VEL_CMD, param1=data.tai, param2=data.angle, param3=data.velocity, ) self.evt_target.set_put(position=data.angle, velocity=data.velocity, tai=data.tai, force_output=True)
async def powerLightOn(self): """ Signals the Horiba device to power light on. We always set the brightness to self.startupWattage for a moment (self.startupTime), then step it back down to the default. Parameters ---------- None Returns ------- None """ if not self.cooldown_task.done(): # are we in the cooldown period? elapsed = time.time() - self.off_time remaining = self.cooldownPeriod - elapsed description = "Can't power on bulb during cool-off period. Please wait " raise salobj.ExpectedError(description + str(remaining) + " seconds.") if self.bulb_on: raise salobj.ExpectedError("Can't power on when we're already powered on.") self.component.setLightPower(self.startupWattage) self.on_time = time.time() self.warmup_task = asyncio.ensure_future(asyncio.sleep(self.warmupPeriod)) self.bulb_on = True self.on_task = asyncio.ensure_future(asyncio.sleep(self.startupTime)) await self.on_task self.component.setLightPower(self.defaultWattage)
def do_setInstrumentPort(self, data): self.assert_enabled("setInstrumentPort") if self._tracking_enabled: raise salobj.ExpectedError( "Cannot setInstrumentPort while tracking is enabled" ) port = data.port try: m3_port_positions_ind = self._port_info_dict[port][0] except IndexError: raise salobj.ExpectedError(f"Invalid port={port}") try: m3_port_positions = self.m3_port_positions[m3_port_positions_ind] except IndexError: raise RuntimeError( f"Bug! invalid m3_port_positions_ind={m3_port_positions_ind} for port={port}" ) self.evt_m3PortSelected.set_put(selected=port) m3actuator = self.actuators[Axis.M3] if ( m3actuator.target.position == m3_port_positions and self.evt_m3InPosition.data.inPosition ): # already there; don't do anything return self.actuators[Axis.M3].set_target( tai=utils.current_tai(), position=m3_port_positions, velocity=0 ) self._axis_enabled[Axis.NA1] = False self._axis_enabled[Axis.NA2] = False self.update_events()
async def enable_controller(self): """Enable the low-level controller. Returns ------- states : `list` [`ControllerState`] A list of the initial controller state, and all controller states this function transitioned the low-level controller through, ending with the ControllerState.ENABLED. """ self.assert_commandable() # Desired controller state controller_state = ControllerState.ENABLED states = [] if self.client.telemetry.state == ControllerState.FAULT: states.append(self.client.telemetry.state) # Start by issuing the clearError command. # This must be done twice, with a delay between. self.log.info("Clearing low-level controller fault state") await self.run_command(code=self.CommandCode.SET_STATE, param1=SetStateParam.CLEAR_ERROR) await asyncio.sleep(0.9) await self.run_command(code=self.CommandCode.SET_STATE, param1=SetStateParam.CLEAR_ERROR) # Give the controller time to reset # then check the resulting state await asyncio.sleep(1) if self.client.telemetry.state == ControllerState.FAULT: raise salobj.ExpectedError( "Cannot clear low-level controller fault state") current_state = self.client.telemetry.state states.append(current_state) if current_state == controller_state: # we are already in the desired controller_state return states command_state_list = _STATE_TRANSITION_DICT[(current_state, controller_state)] for command_param, resulting_state in command_state_list: self.assert_commandable() try: self.log.debug( f"Issue SET_STATE command with param1={command_param!r}") await self.run_command(code=self.CommandCode.SET_STATE, param1=command_param) await self.wait_controller_state(resulting_state) current_state = resulting_state except Exception as e: raise salobj.ExpectedError( f"SET_STATE command with param1={command_param!r} failed: {e!r}" ) from e states.append(resulting_state) return states
async def do_expose(self, data): """Take an exposure with the connected spectrograph. Parameters ---------- data : `DataType` Command data Raises ------ asyncio.CancelledError Raised if the exposure is cancelled before it completes. lsst.ts.salobj.ExpectedError Raised if an error occurs while taking the exposure. """ self.assert_enabled() msg = self.device.check_expose_ok(data.duration) if msg is not None: raise salobj.ExpectedError(msg) try: date_begin = utils.astropy_time_from_tai_unix(utils.current_tai()) task = asyncio.create_task(self.device.expose(data.duration)) await self.evt_exposureState.set_write(status=ExposureState.INTEGRATING) wavelength, spectrum = await task date_end = utils.astropy_time_from_tai_unix(utils.current_tai()) await self.evt_exposureState.set_write(status=ExposureState.DONE) temperature = self.tel_temperature.data.temperature * u.deg_C setpoint = self.tel_temperature.data.setpoint * u.deg_C n_pixels = self.evt_deviceInfo.data.npixels spec_data = dataManager.SpectrographData( wavelength=wavelength, spectrum=spectrum, duration=data.duration, date_begin=date_begin, date_end=date_end, type=data.type, source=data.source, temperature=temperature, temperature_setpoint=setpoint, n_pixels=n_pixels, ) except asyncio.TimeoutError as e: await self.evt_exposureState.set_write(status=ExposureState.TIMEDOUT) msg = f"Timeout waiting for exposure: {repr(e)}" await self.fault(code=20, report=msg) raise salobj.ExpectedError(msg) except asyncio.CancelledError: await self.evt_exposureState.set_write(status=ExposureState.CANCELLED) raise except Exception as e: await self.evt_exposureState.set_write(status=ExposureState.FAILED) msg = f"Failed to take exposure with fiber spectrograph: {repr(e)}" await self.fault(code=20, report=msg) raise salobj.ExpectedError(msg) await self.save_data(spec_data)
async def run_command(self, cmd): """Send a command to the TCP/IP controller and process its replies. Parameters ---------- cmd : `str` The command to send, e.g. "5.0 MV", "SO" or "+". Raises ------ salobj.ExpectedError If communication fails. Also an exception is logged, the CSC disconnects from the low level controller, and goes into FAULT state. If the wrong number of lines is read. Also a warning is logged. """ if not self.connected: if self.disabled_or_enabled and not self.connect_task.done(): await self.connect_task else: raise RuntimeError("Not connected and not trying to connect") async with self.cmd_lock: self.writer.write(f"{cmd}\r\n".encode()) await self.writer.drain() if cmd == "?": # Turn short status into long status cmd = "+" expected_lines = {"+": 25}.get(cmd, 0) # excluding final ">" line try: read_bytes = await asyncio.wait_for( self.reader.readuntil(b">"), timeout=self.config.read_timeout ) except Exception as e: if isinstance(e, asyncio.streams.IncompleteReadError): err_msg = "TCP/IP controller exited" else: err_msg = "TCP/IP read failed" self.log.exception(err_msg) await self.disconnect() self.fault(code=2, report=f"{err_msg}: {e}") raise salobj.ExpectedError(err_msg) data = read_bytes.decode() lines = data.split("\n")[:-1] # strip final > line lines = [elt.strip() for elt in lines] if len(lines) != expected_lines: err_msg = ( f"Command {cmd} returned {data!r}; expected {expected_lines} lines" ) self.log.error(err_msg) raise salobj.ExpectedError(err_msg) elif cmd == "+": self.handle_status(lines)
def do_startTracking(self, data): self.assert_enabled("startTracking") if not self.evt_m3InPosition.data.inPosition: raise salobj.ExpectedError( "Cannot startTracking until M3 is at a known position" ) if not self._stop_tracking_task.done(): raise salobj.ExpectedError("stopTracking not finished yet") self._tracking_enabled = True self.update_events() self._set_tracking_timer(restart=True)
async def setLightPower(self, watts): """ Sets the brightness (in watts) on the white light source. There are lots of constraints. 1 When we turn the bulb on (>800w) we aren't allowed to turn it off for 15m because the Hg inside needs to fully evaporate But we can overrride. 2 When we turn the bulb off, we need to wait 15m for the Hg to recondense before we can turn it on again. 3 When we turn on, we go to maximum brightness for 2 seconds and then drop back down. 4 1200w is the maximum brightness, and requesting higher will produce an error. 5 800w is the minimum, and requesting lower is the same as requesting to power off. Parameters ---------- watts : int or float Should be <= 1200. Values under 800 power the light off entirely. Returns ------- None """ # TODO: report watts as telemetry (or events?) if watts > 1200: raise salobj.ExpectedError(f"Wattage {watts} too high (over 1200)") if watts < 800: # turn bulb off if not self.warmup_task.done(): description = "Can't power off bulb during warm-up period. Please wait " elapsed = time.time() - self.on_time remaining = self.warmupPeriod - elapsed raise salobj.ExpectedError(description + str(remaining) + " seconds.") if self.bulb_on: self.cooldown_task = asyncio.ensure_future(asyncio.sleep(self.cooldownPeriod)) self.component.setLightPower(0) self.bulb_on = False self.off_time = time.time() else: raise salobj.ExpectedError("Bulb is already off") else: # this executes when watts are inside the 800-1200 range if not self.bulb_on: raise salobj.ExpectedError("You must turn the light on before setting light power.") if not self.on_task.done(): await self.on_task self.component.setLightPower(watts) else: self.component.setLightPower(watts) self.bulb_on = True
async def do_homeAzimuth(self, data): """Implement the ``homeAzimuth`` command.""" self.assert_enabled() put_final_azimuth_commanded_state = False if self.evt_azimuthState.data.homing or self.homing: raise salobj.ExpectedError("Already homing") try: self.homing = True await self.wait_n_status(n=2) if bool(self.evt_moveCode.data.code & MoveCode.AZIMUTH_HOMING): raise salobj.ExpectedError("Already homing") if not self.evt_azimuthState.data.state == AzimuthState.NOTINMOTION: raise salobj.ExpectedError("Azimuth is moving") # Homing is allowed; do it! self.cmd_homeAzimuth.ack_in_progress(data=data, timeout=HOME_AZIMUTH_TIMEOUT) self.evt_azimuthCommandedState.set_put( commandedState=AzimuthCommandedState.HOME, azimuth=math.nan, force_output=True, ) put_final_azimuth_commanded_state = True await self.run_command("HM") self.status_sleep_task.cancel() # Check status until move code is no longer homing. # Skip one status to start with. n_to_await = 2 while self.homing: await self.wait_n_status(n=n_to_await) n_to_await = 1 # Do not use self.evt_azimuthState.data.homing because # it includes self.homing, which we have set true. self.homing = bool(self.evt_moveCode.data.code & MoveCode.AZIMUTH_HOMING) self.evt_azimuthState.set_put(homed=True, homing=False, force_output=True) except Exception: self.log.exception("homing failed") finally: self.homing = False if put_final_azimuth_commanded_state: self.evt_azimuthCommandedState.set_put( commandedState=AzimuthCommandedState.STOP, force_output=True, )
def assert_summary_state(self, state, isbefore=None): """Assert that the current summary state is as specified. First check that CSC can command the low-level controller. Used in do_xxx methods to check that a command is allowed. Parameters ---------- state : `lsst.ts.salobj.State` Expected summary state. isbefore : `bool`, optional Deprecated. The only allowed values are False (which raises a deprecation warning) and None. """ if isbefore: raise ValueError( f"isbefore={isbefore}; this deprecated argument must be None or False" ) elif isbefore is False: warnings.warn(f"isbefore={isbefore} is deprecated", DeprecationWarning) state = salobj.State(state) self.assert_connected() if self.summary_state != state: raise salobj.ExpectedError( f"Rejected: initial state is {self.summary_state!r} instead of {state!r}" )
def convert_values(name, values, nval): out = np.array(values, dtype=float) if out.shape != (nval,): raise salobj.ExpectedError( f"Could not format {name}={values!r} as {nval} floats" ) return out
async def do_moveShutterMainDoor(self, data): """Implement the ``moveShutterMainDoor`` command.""" self.assert_enabled() if data.open: self.evt_mainDoorCommandedState.set_put( commandedState=ShutterDoorCommandedState.OPENED, force_output=True) await self.run_command("OP") else: if self.evt_dropoutDoorState.data.state not in ( ShutterDoorState.CLOSED, ShutterDoorState.OPENED, ): raise salobj.ExpectedError( "Cannot close the main door " "until the dropout door is fully closed or fully open.") self.evt_mainDoorCommandedState.set_put( commandedState=ShutterDoorCommandedState.CLOSED, force_output=True) await self.run_command("CL") await self.wait_for_shutter( dropout_state=None, main_state=ShutterDoorState.OPENED if data.open else ShutterDoorState.CLOSED, )
async def do_abort_job(self, data): """Implement the ``abort_job`` command. Parameters ---------- data: types.SimpleNamespace Must contain job_id attribute. """ self.assert_enabled("abort_job") self.log.info(f"abort_job command with {data}") if self.simulation_mode == 0: self.log.info(f"DELETE: {data.job_id}") result = self.connection.delete( f"{self.config.url}/job/{data.job_id}") result.raise_for_status() self.log.info(f"Abort result: {result.text}") self.evt_job_result.set_put(job_id=data.job_id, exit_code=255, result=result.text) else: if data.job_id in self.simulated_jobs: self.simulated_jobs.remove(data.job_id) payload = json.dumps(dict(abort_time=salobj.current_tai())) self.evt_job_result.set_put(job_id=data.job_id, exit_code=255, result=payload) self.log.info(f"Abort result: {payload}") else: raise salobj.ExpectedError("No such job id: {data.job_id}")
async def handle_summary_state(self): # disabled: connect and send telemetry, but no commands allowed. if self.summary_state in (salobj.State.ENABLED, salobj.State.DISABLED): if self.s3bucket is None: domock = self.simulation_mode & constants.SimulationMode.S3Server != 0 self.s3bucket = salobj.AsyncS3Bucket( name=self.s3bucket_name, domock=domock, create=domock ) if self.device is None: try: self.device = AvsFiberSpectrograph( serial_number=self.serial_number, log=self.log ) except Exception as e: msg = "Failed to connect to fiber spectrograph." await self.fault(code=1, report=f"{msg}: {repr(e)}") raise salobj.ExpectedError(msg) if self.telemetry_loop_task.done(): self.telemetry_loop_task = asyncio.create_task(self.telemetry_loop()) status = self.device.get_status() await self.evt_deviceInfo.set_write( npixels=status.n_pixels, fpgaVersion=status.fpga_version, firmwareVersion=status.firmware_version, libraryVersion=status.library_version, ) else: self.telemetry_loop_task.cancel() if self.device is not None: self.device.disconnect() self.device = None if self.s3bucket is not None: self.s3bucket.stop_mock() self.s3bucket = None
async def do_stop(self, data): """Halt tracking or any other motion.""" if self.summary_state != salobj.State.ENABLED: raise salobj.ExpectedError("Not enabled") await self.run_command( code=enums.CommandCode.SET_ENABLED_SUBSTATE, param1=enums.SetEnabledSubstateParam.STOP, )
def assert_commandable(self): """Assert that CSC is connected to the low-level controller and can command it. """ self.assert_connected() if not self.evt_commandableByDDS.data.state: raise salobj.ExpectedError("Controller has CSC commands disabled; " "use the EUI to enable CSC commands")
async def do_moveAzimuth(self, data): """Implement the ``moveAzimuth`` command.""" self.assert_enabled("moveAzimuth") if self.evt_azimuthState.data.homing: raise salobj.ExpectedError("Cannot move azimuth while homing") azimuth = data.azimuth if azimuth < 0 or azimuth >= 360: raise salobj.ExpectedError( f"azimuth={azimuth} deg; must be in range [0, 360)" ) await self.run_command(f"{azimuth:0.3f} MV") self.evt_azimuthCommandedState.set_put( commandedState=AzimuthCommandedState.GOTOPOSITION, azimuth=azimuth, force_output=True, ) self.status_sleep_task.cancel()
async def run(self) -> None: self.log.info("Run started") await self.checkpoint("start") if self.config.fail_run: raise salobj.ExpectedError( f"Failed in run after wait: fail_run={self.config.fail_run}") await asyncio.sleep(self.config.wait_time) await self.checkpoint("end") self.log.info("Run succeeded")
async def do_configureVelocity(self, data): """Specify the velocity limit.""" self.assert_enabled_substate(EnabledSubstate.STATIONARY) if not 0 < data.vlimit <= constants.MAX_VEL_LIMIT: raise salobj.ExpectedError( f"vlimit={data.vlimit} must be > 0 and <= {constants.MAX_VEL_LIMIT}" ) await self.run_command(code=enums.CommandCode.CONFIG_VEL, param1=data.vlimit)
async def begin_start(self, data): if data.configurationOverride == "logLevel": self.is_log_level = True elif data.configurationOverride == "arrays": self.is_log_level = False else: raise salobj.ExpectedError( f"data.configurationOverride={data.configurationOverride} must be logLevel or arrays" ) print(f"TopicWriter: writing {data.configurationOverride}")
def assert_connected(self): """Assert that the CSC is connected to the low-level controller. Raises ------ lsst.ts.salobj.ExpectedError If one or both streams is disconnected. """ if not self.connected: raise salobj.ExpectedError( "Not connected to the low-level controller.")
async def do_move(self, data): """Move a script within the queue.""" self.assert_enabled("move") try: await self.model.move( sal_index=data.salIndex, location=data.location, location_sal_index=data.locationSalIndex, ) except ValueError as e: raise salobj.ExpectedError(str(e))
async def do_stopTracking(self, data): self.assert_enabled("stopTracking") if not self._stop_tracking_task.done(): raise salobj.ExpectedError("Already stopping") self._set_tracking_timer(restart=False) self._tracking_enabled = False for axis in MainAxes: self.actuators[axis].stop() self._stop_tracking_task.cancel() self._stop_tracking_task = asyncio.ensure_future(self._finish_stop_tracking()) self.update_events()
async def emergencyPowerLightOff(self): """Signals the device to power off immediately, ignoring the 15m warmup period. The manufacturer warns that this can significantly reduce the life of the bulb. """ if self.bulb_on: self.cooldown_task = asyncio.ensure_future(asyncio.sleep(self.cooldownPeriod)) self.component.setLightPower(0) self.bulb_on = False self.off_time = time.time() else: raise salobj.ExpectedError("Bulb is already off")
async def do_move(self, data): """Specify a position.""" self.assert_enabled_substate(EnabledSubstate.STATIONARY) if (not self.client.config.min_position <= data.position <= self.client.config.max_position): raise salobj.ExpectedError( f"position {data.position} not in range " f"[{self.client.config.min_position}, " f"{self.client.config.max_position}]") await self.run_command( code=simple_mock_controller.SimpleCommandCode.MOVE, param1=data.position)
async def do_homeAzimuth(self, data): """Implement the ``homeAzimuth`` command.""" self.assert_enabled("homeAzimuth") if self.evt_azimuthState.data.homing: raise salobj.ExpectedError("Already homing") self.evt_azimuthCommandedState.set_put( commandedState=AzimuthCommandedState.HOME, azimuth=math.nan, force_output=True, ) await self.run_command("HM") self.status_sleep_task.cancel()
async def do_requeue(self, data): """Put a script back on the queue with the same configuration.""" self.assert_enabled("requeue") try: await self.model.requeue( sal_index=data.salIndex, seq_num=data.private_seqNum, location=data.location, location_sal_index=data.locationSalIndex, ) except ValueError as e: raise salobj.ExpectedError(str(e))
def assert_ready(self) -> None: """Assert summary state is enabled and detailed state is READY. Raises ------ ExpectedError If summary_state is not ENABLED or detailed state is not READY. """ self.assert_enabled() if self.detailed_state != DetailedState.READY: raise salobj.ExpectedError( f"Detailed state={self.detailed_state!r} not READY")
def configure(self, *, max_delta_azimuth=5): """Configure the algorithm. Parameters ---------- max_delta_azimuth : `float` Maximum allowed difference between dome commanded azimuth and telescope_target target azimuth. """ if max_delta_azimuth < 0: raise salobj.ExpectedError( f"max_delta_azimuth={max_delta_azimuth} must not be negative") self.max_delta_azimuth = max_delta_azimuth