Ejemplo n.º 1
0
    def __init__(
        self,
        config_dir=None,
        initial_state=salobj.State.STANDBY,
        simulation_mode=0,
        mock_port=None,
    ):
        self.reader = None
        self.writer = None
        self.move_code = 0
        self.mock_ctrl = None  # mock controller, or None of not constructed
        self.status_interval = 0.2  # delay between short status commands (sec)

        # Amount to add to "tolerance" reported by the low-level controller
        # to set az_tolarance (deg).
        self.az_tolerance_margin = 0.5

        # Tolerance for "in position" (deg).
        # Set the initial value here, then update from the
        # "Tolerance" reported in long status.
        self.az_tolerance = 1.5
        self.homing = False

        # Task for sleeping in the status loop; cancel this to trigger
        # an immediate status update. Warning: do not cancel status_task
        # because that may be waiting for TCP/IP communication.
        self.status_sleep_task = salobj.make_done_future()
        # Task for the status loop. To trigger new status cancel
        # status_sleep_task, not status_task.
        self.status_task = salobj.make_done_future()
        # Task that waits while connecting to the TCP/IP controller.
        self.connect_task = salobj.make_done_future()
        # Task that waits while shutter doors move
        self.shutter_task = salobj.make_done_future()
        # The conditions that self.shutter_task is waiting for.
        # Must be one of: ShutterDoorState.OPENED,
        # ShutterDoorState.CLOSED or None (for don't care)
        self.desired_main_shutter_state = None
        self.desired_dropout_shutter_state = None
        self.cmd_lock = asyncio.Lock()
        self.config = None
        self.mock_port = mock_port
        self.status_event = asyncio.Event()
        super().__init__(
            "ATDome",
            index=0,
            config_schema=CONFIG_SCHEMA,
            config_dir=config_dir,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
        )

        # Homing can be slow; rather than queue up these commands
        # (which is unlikely to be what the user wants) allow multiple
        # command to run, so the CSC can reject the overlapping commands.
        self.cmd_homeAzimuth.allow_multiple_callbacks = True
        # Set but don't put azimuthState.homed=False: only homed is maintained
        # locally; we have no idea of the rest of the state.
        self.evt_azimuthState.set(homed=False)
Ejemplo n.º 2
0
    def __init__(
        self,
        config_dir=None,
        initial_state=salobj.State.STANDBY,
        simulation_mode=0,
        mock_port=None,
    ):
        schema_path = (
            pathlib.Path(__file__)
            .resolve()
            .parents[4]
            .joinpath("schema", "ATDome.yaml")
        )

        self.reader = None
        self.writer = None
        self.move_code = 0
        self.mock_ctrl = None  # mock controller, or None of not constructed
        self.status_interval = 0.2  # delay between short status commands (sec)
        # Amount to add to "tolerance" reported by the low-level controller
        # to set az_tolarance (deg).
        self.az_tolerance_margin = 0.5
        # Tolerance for "in position" (deg).
        # Set the initial value here, then update from the
        # "Tolerance" reported in long status.
        self.az_tolerance = 1.5
        # Task for sleeping in the status loop; cancel this to trigger
        # an immediate status update. Warning: do not cancel status_task
        # because that may be waiting for TCP/IP communication.
        self.status_sleep_task = salobj.make_done_future()
        # Task for the status loop. To trigger new status cancel
        # status_sleep_task, not status_task.
        self.status_task = salobj.make_done_future()
        # Task that waits while connecting to the TCP/IP controller.
        self.connect_task = salobj.make_done_future()
        # Task that waits while shutter doors move
        self.shutter_task = salobj.make_done_future()
        # The conditions that self.shutter_task is waiting for.
        # Must be one of: ShutterDoorState.OPENED,
        # ShutterDoorState.CLOSED or None (for don't care)
        self.desired_main_shutter_state = None
        self.desired_dropout_shutter_state = None
        self.cmd_lock = asyncio.Lock()
        self.config = None
        self.mock_port = mock_port
        super().__init__(
            "ATDome",
            index=0,
            schema_path=schema_path,
            config_dir=config_dir,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
        )
Ejemplo n.º 3
0
 def __init__(self, name):
     self.name = name
     self.callback = None
     self.auto_acknowledge_delay = 0
     self.auto_unacknowledge_delay = 0
     self.escalate_to = ""
     self.escalate_delay = 0
     self.auto_acknowledge_task = salobj.make_done_future()
     self.auto_unacknowledge_task = salobj.make_done_future()
     self.escalate_task = salobj.make_done_future()
     self.unmute_task = salobj.make_done_future()
     self.reset()
Ejemplo n.º 4
0
 def __init__(self, config):
     super().__init__(
         config=config,
         name=f"test.ConfiguredSeverities.{config.name}",
         remote_info_list=[],
     )
     self.run_timer = salobj.make_done_future()
Ejemplo n.º 5
0
    def __init__(self, title, cache, SAMPLE_TIME, toolBar, channels=[]):
        super().__init__(title)
        self.setObjectName(title)
        self.SAMPLE_TIME = SAMPLE_TIME
        self.toolBar = toolBar

        self.chart = AbstractChart()

        self.updateTask = make_done_future()

        self.update_after = 0

        self.cache = cache

        self.chartView = VMSChartView(self.chart, QtCharts.QLineSeries)
        self.chartView.updateMaxSensor(self.cache.sensors())
        self.chartView.axisChanged.connect(self.axisChanged)
        self.chartView.unitChanged.connect(self.unitChanged)
        for channel in channels:
            self.chartView.addSerie(str(channel[0]) + " " + channel[1])

        self.coefficient = 1
        self.unit = units[0]
        self.setupAxes = True
        self._setupAxes()

        self.setWidget(self.chartView)
Ejemplo n.º 6
0
    def __init__(
        self,
        kafka_config,
        log_level=logging.INFO,
    ):
        self.kafka_config = kafka_config

        self.log = logging.getLogger()
        if not self.log.hasHandlers():
            self.log.addHandler(logging.StreamHandler())
        self.log.setLevel(log_level)

        semaphore_filename = "SALKAFKA_PRODUCER_RUNNING"
        suffix = os.environ.get("SALKAFKA_SEMAPHORE_SUFFIX")
        if suffix is not None:
            semaphore_filename += f"_{suffix.upper()}"

        self.semaphore_file = pathlib.Path("/tmp", semaphore_filename)
        if self.semaphore_file.exists():
            self.semaphore_file.unlink()

        # A collection of ComponentProducers.
        # Set by run_producers but not run_distributed_producer.
        self.producers = []

        # A collection of producer subprocess tasks.
        # Set by run_distributed_producer but not run_producers.
        self.producer_tasks = []

        self.start_task = asyncio.Future()

        # Internal task to monitor startup of producer(s)
        # This is different than start_task in order to avoid a race condition
        # when code (typically a unit test) creates a ComponentProducerSet
        # and immediately starts waiting for start_task to be done.
        self._interruptable_start_task = salobj.make_done_future()

        self._run_producer_subprocess_task = salobj.make_done_future()

        # A task that ends when the service is interrupted.
        self._wait_forever_task = asyncio.Future()
Ejemplo n.º 7
0
 def __init__(self, config):
     remote_name, remote_index = salobj.name_to_name_index(config.name)
     remote_info = base.RemoteInfo(
         name=remote_name,
         index=remote_index,
         callback_names=["evt_heartbeat"],
         poll_names=[],
     )
     super().__init__(
         config=config,
         name=f"Heartbeat.{remote_info.name}:{remote_info.index}",
         remote_info_list=[remote_info],
     )
     self.heartbeat_timer_task = salobj.make_done_future()
Ejemplo n.º 8
0
    def __init__(
        self,
        index,
        initial_state=salobj.State.STANDBY,
        simulation_mode=0,
    ):
        """
        Initialize DSM CSC.

        Parameters
        ----------
        index : `int`
            Index for the DSM. This enables the control of multiple DSMs.
        initial_state : `lsst.ts.salobj.State`, optional
            State to place CSC in after initialization.
        simulation_mode : `int`, optional
            Flag to determine mode of operation.
        """

        self.telemetry_directory = None
        self.telemetry_loop_task = salobj.make_done_future()
        self.telemetry_watcher = aionotify.Watcher()
        self.simulated_telemetry_ui_config_written = False
        self.simulated_telemetry_loop_task = salobj.make_done_future()
        self.simulation_loop_time = None

        super().__init__(
            "DSM",
            index,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
        )

        ch = logging.StreamHandler()
        self.log.addHandler(ch)

        self.finish_csc_setup()
Ejemplo n.º 9
0
    def __init__(self, name='ATHeaderService', initial_state=mystate):

        # Where we will store the metadata
        self.clean()

        # Start the CSC with name
        super().__init__(name=name, index=0, initial_state=initial_state)
        print(f"Creating for worker for: {name}")
        print(f"Running {salobj.__version__}")
        self.atcam = salobj.Remote(domain=self.domain,
                                   name="ATCamera",
                                   index=0)
        self.atcam.evt_startIntegration.callback = self.startIntegration_callback
        self.atcam.evt_endOfImageTelemetry.callback = self.endOfImageTelemetry_callback
        self.endOfImage_timeout_task = salobj.make_done_future()
Ejemplo n.º 10
0
    def __init__(self, items, maxItems=50 * 30, updateInterval=0.1):
        super().__init__()
        self.maxItems = maxItems
        self.timeAxis = None

        self._next_update = 0
        self.updateInterval = updateInterval
        self.updateTask = make_done_future()

        self._caches = []
        for axis in items.items():
            data = [("timestamp", "f8")]
            for d in axis[1]:
                if d is None:
                    self._caches.append(TimeCache(maxItems, data))
                    data = [("timestamp", "f8")]
                else:
                    data.append((d, "f8"))
                    self._addSerie(d, axis[0])
            self._caches.append(TimeCache(maxItems, data))

        # Caveat emptor, the order here is important. Hard to find, but the order in
        # which chart, axis and series are constructed and attached should always be:
        # - construct Axis, Chart, Serie
        # - addAxis to chart
        # - attach series to axis
        # Changing the order will result in undetermined behaviour, most
        # likely the axis or even graph not shown. It's irrelevant when you
        # fill series with data. See QtChart::createDefaultAxes in QtChart
        # source code for details.
        self.timeAxis = QtCharts.QDateTimeAxis()
        self.timeAxis.setReverse(True)
        self.timeAxis.setTickCount(5)
        self.timeAxis.setFormat("h:mm:ss.zzz")
        self.timeAxis.setTitleText("Time (TAI)")
        self.timeAxis.setGridLineVisible(True)

        self.addAxis(self.timeAxis, Qt.AlignBottom)

        for serie in self.series():
            serie.attachAxis(self.timeAxis)
Ejemplo n.º 11
0
    def __init__(
        self,
        simulation_mode=0,
        initial_state: salobj.State = salobj.State.STANDBY,
        config_dir=None,
    ):

        super().__init__(
            name="CBP",
            index=0,
            config_dir=config_dir,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
            config_schema=CONFIG_SCHEMA,
        )
        self.component = component.CBPComponent(self)
        self.simulator = None
        self.telemetry_task = salobj.make_done_future()
        self.telemetry_interval = 0.5
        self.in_position_timeout = 20
        self.log.info("CBP CSC initialized")
Ejemplo n.º 12
0
    def __init__(
        self,
        index,
        simulation_mode=0,
        initial_state=salobj.State.STANDBY,
        config_dir=None,
        settings_to_apply="",
    ):

        super().__init__(
            name="PMD",
            index=index,
            config_dir=config_dir,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
            config_schema=CONFIG_SCHEMA,
            settings_to_apply=settings_to_apply,
        )
        self.telemetry_task = salobj.make_done_future()
        self.telemetry_interval = 1
        self.index = index
        self.component = None
Ejemplo n.º 13
0
    def __init__(self,
                 config_dir=None,
                 initial_state=salobj.State.OFFLINE,
                 simulation_mode=0):
        self.server = None
        self.mock_ctrl = None

        # Set this to 2 when trackStart is called, then decrement
        # when telemetry is received. If > 0 or enabled_substate is
        # SLEWING_OR_TRACKING then allow the track command.
        # This solves the problem of allowing the track command
        # immediately after the trackStart, before telemetry is received.
        self._tracking_started_telemetry_counter = 0

        # The sequential number of times the camera cable wrap following error
        # has been too large.
        self._num_ccw_following_errors = 0

        self._check_ccw_following_error_task = salobj.make_done_future()
        self._reported_ccw_following_error_issue = False
        self._faulting = False
        self._prev_flags_tracking_success = False
        self._prev_flags_tracking_lost = False

        super().__init__(
            name="MTRotator",
            index=0,
            port=constants.TELEMETRY_PORT,
            sync_pattern=constants.ROTATOR_SYNC_PATTERN,
            CommandCode=enums.CommandCode,
            ConfigClass=structs.Config,
            TelemetryClass=structs.Telemetry,
            config_dir=config_dir,
            config_schema=CONFIG_SCHEMA,
            initial_state=initial_state,
            simulation_mode=simulation_mode,
        )
        self.mtmount_remote = salobj.Remote(domain=self.domain, name="MTMount")
Ejemplo n.º 14
0
    async def make_csc(
        self,
        initial_state=salobj.State.OFFLINE,
        config_dir=None,
        simulation_mode=1,
        log_level=None,
        timeout=STD_TIMEOUT,
        run_mock_ccw=True,  # Set False to test failure to enable
        **kwargs,
    ):
        """Override make_csc

        This exists primarily because we need to start a mock
        camera cable wrap controller before we can enable the CSC.
        It also offers the opportunity to make better defaults.

        Parameters
        ----------
        name : `str`
            Name of SAL component.
        initial_state : `lsst.ts.salobj.State` or `int`, optional
            The initial state of the CSC. Defaults to STANDBY.
        config_dir : `str`, optional
            Directory of configuration files, or `None` (the default)
            for the standard configuration directory (obtained from
            `ConfigureCsc._get_default_config_dir`).
        simulation_mode : `int`, optional
            Simulation mode. Defaults to 0 because not all CSCs support
            simulation. However, tests of CSCs that support simulation
            will almost certainly want to set this nonzero.
        log_level : `int` or `None`, optional
            Logging level, such as `logging.INFO`.
            If `None` then do not set the log level, leaving the default
            behavior of `SalInfo`: increase the log level to INFO.
        timeout : `float`, optional
            Time limit for the CSC to start (seconds).
        run_mock_ccw : `bool`, optional
            If True then start a mock camera cable wrap controller.
        **kwargs : `dict`, optional
            Extra keyword arguments for `basic_make_csc`.
            For a configurable CSC this may include ``settings_to_apply``,
            especially if ``initial_state`` is DISABLED or ENABLED.

        """
        # We cannot transition the CSC to ENABLED
        # until the CCW following loop is running.
        # So if initial_state is ENABLED
        # start the CSC in DISABLED, start the CCW following loop,
        # then transition to ENABLED and swallow the DISABLED state
        # and associated controller state (make_csc reads all but the final
        # CSC summary state and controller state).
        if initial_state == salobj.State.ENABLED:
            modified_initial_state = salobj.State.DISABLED
        else:
            modified_initial_state = initial_state

        async with super().make_csc(
                initial_state=modified_initial_state,
                config_dir=config_dir,
                simulation_mode=simulation_mode,
                log_level=log_level,
                timeout=timeout,
                **kwargs,
        ), salobj.Controller(name="MTMount") as self.mtmount_controller:
            if run_mock_ccw:
                self.mock_ccw_task = asyncio.create_task(self.mock_ccw_loop())
            else:
                print("do not run mock_ccw_loop")
                self.mock_ccw_task = salobj.make_done_future()
            if initial_state != modified_initial_state:
                await self.remote.cmd_enable.start(timeout=STD_TIMEOUT)
                await self.assert_next_summary_state(salobj.State.DISABLED)
                await self.assert_next_sample(
                    topic=self.remote.evt_controllerState,
                    controllerState=ControllerState.DISABLED,
                )
            try:
                yield
            finally:
                self.enable_mock_ccw_telemetry = False
                await asyncio.wait_for(self.mock_ccw_task, timeout=STD_TIMEOUT)
Ejemplo n.º 15
0
    def __init__(self, domain, config, alarm_callback=None):
        self.domain = domain
        self.alarm_callback = alarm_callback

        self._enabled = False
        self.enable_task = salobj.make_done_future()

        # Dict of (sal_component_name, sal_index): lsst.ts.salobj.Remote
        self.remotes = dict()

        # Dict of rule_name: Rule
        self.rules = dict()

        # Convert the name of each disabled sal component from a string
        # in the form ``name`` or ``name:index`` to a tuple ``(name, index)``.
        config.disabled_sal_components = [
            salobj.name_to_name_index(name)
            for name in config.disabled_sal_components
        ]
        self.config = config

        # Make the rules.
        for ruledata in self.config.rules:
            ruleclassname = ruledata["classname"]
            ruleclass = get_rule_class(ruleclassname)
            try:
                ruleschema = ruleclass.get_schema()
                if ruleschema is None:
                    validator = None
                else:
                    validator = salobj.DefaultingValidator(ruleschema)
            except Exception as e:
                raise ValueError(
                    f"Schema for rule class {ruleclassname} not valid") from e
            for i, ruleconfig_dict in enumerate(ruledata["configs"]):
                try:
                    if validator is None:
                        if ruleconfig_dict:
                            raise ValueError("Rule config dict must be empty")
                        full_ruleconfig_dict = dict()
                    else:
                        full_ruleconfig_dict = validator.validate(
                            ruleconfig_dict)
                    ruleconfig = types.SimpleNamespace(**full_ruleconfig_dict)
                except Exception as e:
                    raise ValueError(
                        f"Config {i+1} for rule class {ruleclassname} not valid: "
                        f"config={ruleconfig_dict}") from e
                rule = ruleclass(config=ruleconfig)
                if rule.is_usable(disabled_sal_components=config.
                                  disabled_sal_components):
                    self.add_rule(rule)

        # Accumulate a list of topics that have callback functions.
        self._topics_with_callbacks = list()
        for remote in self.remotes.values():
            for name in dir(remote):
                if name[0:4] in ("evt_", "tel_"):
                    topic = getattr(remote, name)
                    if topic.callback is not None:
                        self._topics_with_callbacks.append(topic)

        # Set escalation information in the alarms.
        remaining_names = set(self.rules)
        for escalation_item in config.escalation:
            for name_glob in escalation_item["alarms"]:
                name_regex = fnmatch.translate(name_glob)
                compiled_name_regex = re.compile(name_regex, re.IGNORECASE)
                matched_names = [
                    name for name in remaining_names
                    if compiled_name_regex.match(name)
                ]
                remaining_names = remaining_names.difference(matched_names)
                for name in matched_names:
                    alarm = self.rules[name].alarm
                    alarm.escalate_to = escalation_item["to"]
                    alarm.escalate_delay = escalation_item["delay"]

        self.start_task = asyncio.ensure_future(self.start())
Ejemplo n.º 16
0
    def __init__(
        self,
        port,
        door_time=1,
        az_vel=6,
        home_az=10,
        home_az_overshoot=1,
        home_az_vel=1,
    ):
        # If port == 0 then this will be updated to the actual port
        # in `start`, right after the TCP/IP server is created.
        self.port = port
        self.door_time = door_time
        self.az_vel = az_vel
        self.high_speed = 5
        self.coast = 0.5
        self.tolerance = 1.0
        self.home_az = home_az
        self.home_az_overshoot = home_az_overshoot
        self.home_az_vel = home_az_vel
        self.encoder_counts_per_360 = 4018143232
        self.az_actuator = simactuators.CircularPointToPointActuator(
            speed=az_vel)
        self.az_move_timeout = 120
        self.watchdog_reset_time = 600
        self.dropout_timer = 5
        self.reverse_delay = 4
        self.main_door_encoder_closed = 118449181478
        self.main_door_encoder_opened = 8287616388
        self.dropout_door_encoder_closed = 5669776578
        self.dropout_door_encoder_opened = 5710996184
        self.door_move_timeout = 360
        self.door_actuators = {
            enum: simactuators.PointToPointActuator(
                min_position=0,
                max_position=100,
                start_position=0,
                speed=100 / door_time,
            )
            for enum in Door
        }

        self._homing_task = salobj.make_done_future()
        self.rain_sensor_enabled = True
        self.rain_detected = False
        self.cloud_sensor_enabled = True
        self.clouds_detected = False
        self.scb_link_ok = True
        self.auto_shutdown_enabled = False
        self.estop_active = False
        # Name of a command to report as failed once, the next time it is seen,
        # or None if no failures. Used to test CSC handling of failed commands.
        self.fail_command = None

        self.last_rot_right = None
        self.log = logging.getLogger("MockDomeController")
        self.server = None

        # Dict of command: (has_argument, function).
        # The function is called with:
        # * No arguments, if `has_argument` False.
        # * The argument as a string, if `has_argument` is True.
        self.dispatch_dict = {
            "?": (False, self.do_short_status),
            "+": (False, self.do_full_status),
            "ST": (False, self.do_stop),
            "CL": (False, functools.partial(self.do_close_doors, Door.Main)),
            "OP": (False, functools.partial(self.do_open_doors, Door.Main)),
            "UP": (False, functools.partial(self.do_close_doors,
                                            Door.Dropout)),
            "DN": (False, functools.partial(self.do_open_doors, Door.Dropout)),
            "SC": (
                False,
                functools.partial(self.do_close_doors,
                                  Door.Main | Door.Dropout),
            ),
            "SO": (
                False,
                functools.partial(self.do_open_doors,
                                  Door.Main | Door.Dropout),
            ),
            "HM": (False, self.do_home),
            "MV": (True, self.do_set_cmd_az),
        }
Ejemplo n.º 17
0
    def __init__(
        self,
        index,
        config_dir=None,
        initial_state=salobj.State.OFFLINE,
        settings_to_apply="",
        simulation_mode=0,
    ):
        index = enums.SalIndex(index)
        controller_constants = constants.IndexControllerConstants[index]

        # The maximum position limits configured in the low-level controller.
        # The limits specified by the configureLimits command must be
        # within these limits.
        self.max_pos_limits = constants.MAX_POSITION_LIMITS[index]

        # Current position limits; initialize to max limits,
        # but update from configuration reported by the low-level controller.
        self.current_pos_limits = copy.copy(self.max_pos_limits)

        # The move command that is currently running, or None.
        self.move_command = None

        # Task for the current move, if one is being commanded.
        self.move_task = salobj.make_done_future()

        # Record missing compensation inputs we have warned about,
        # to avoid duplicate warnings.
        self.missing_inputs_str = ""

        # Interval between compensation updates.
        # Set in `configure`, but we need an initial value.
        self.compensation_interval = 0.2

        # Maximum time for the longest possible move (sec) with some buffer.
        # Set in `config_callback` but we need an initial value.
        # 61.74 is the value for the mock controller as of 2021-04-15.
        self.max_move_duration = 65

        # The part of the compensation loop that is always safe to cancel.
        self.compensation_wait_task = salobj.make_done_future()

        # The whole compensation loop; to safely cancel this use::
        #
        #     async with self.write_lock:
        #        self.compensation_loop_task.cancel()
        self.compensation_loop_task = salobj.make_done_future()

        # Event set when a telemetry message is received from
        # the low-level controller, after it has been parsed.
        self.telemetry_event = asyncio.Event()

        # Count of number of telemetry samples read
        # since issuing a low-level command.
        # Maxes out at MAX_N_TELEMETRY.
        self.n_telemetry = 0

        structs.Config.FRAME_ID = controller_constants.config_frame_id
        structs.Telemetry.FRAME_ID = controller_constants.telemetry_frame_id

        super().__init__(
            name="MTHexapod",
            index=index,
            port=controller_constants.port,
            sync_pattern=controller_constants.sync_pattern,
            CommandCode=enums.CommandCode,
            ConfigClass=structs.Config,
            TelemetryClass=structs.Telemetry,
            config_schema=CONFIG_SCHEMA,
            config_dir=config_dir,
            initial_state=initial_state,
            settings_to_apply=settings_to_apply,
            simulation_mode=simulation_mode,
        )

        # TODO DM-28005: add a suitable Remote from which to get temperature;
        # perhaps something like:
        # self.eas = salobj.Remote(domain=self.domain, name="EAS", include=[?])
        self.mtmount = salobj.Remote(
            domain=self.domain, name="MTMount", include=["target"]
        )
        self.mtrotator = salobj.Remote(
            domain=self.domain, name="MTRotator", include=["target"]
        )
Ejemplo n.º 18
0
    def __init__(
        self,
        log,
        host=hexrotcomm.LOCAL_HOST,
        command_port=constants.TELEMETRY_PORT + 1,
        telemetry_port=constants.TELEMETRY_PORT,
        initial_state=ControllerState.OFFLINE,
    ):
        self.encoder_resolution = 200_000  # counts/deg; arbitrary
        # Amplitude of jitter in measured position (deg),
        # to simulate encoder jitter.
        self.position_jitter = 0.000003

        self.tracking_timed_out = False
        config = structs.Config()
        config.velocity_limit = 3  # deg/sec
        config.accel_limit = 1  # deg/sec^2
        config.pos_error_threshold = 0.1  # deg
        config.lower_pos_limit = -90  # deg
        config.upper_pos_limit = 90  # deg
        config.following_error_threshold = 0.1  # deg
        config.track_success_pos_threshold = 0.01  # deg
        config.tracking_lost_timeout = 5  # sec
        telemetry = structs.Telemetry()
        telemetry.set_pos = math.nan
        self.tracking_timer_task = salobj.make_done_future()

        self.rotator = simactuators.TrackingActuator(
            min_position=config.lower_pos_limit,
            max_position=config.upper_pos_limit,
            max_velocity=config.velocity_limit,
            max_acceleration=config.accel_limit,
            dtmax_track=0.25,
        )
        # Set True when the first TRACK_VEL_CMD command is received
        # and False when not tracking. Used to delay setting
        # telemetry.flags_slew_complete to 1 until we know
        # where we are going.
        self.track_vel_cmd_seen = False

        # Dict of command key: command
        extra_commands = {
            (
                enums.CommandCode.SET_ENABLED_SUBSTATE,
                enums.SetEnabledSubstateParam.MOVE_POINT_TO_POINT,
            ):
            self.do_move_point_to_point,
            (
                enums.CommandCode.SET_ENABLED_SUBSTATE,
                enums.SetEnabledSubstateParam.TRACK,
            ):
            self.do_track,
            (
                enums.CommandCode.SET_ENABLED_SUBSTATE,
                enums.SetEnabledSubstateParam.STOP,
            ):
            self.do_stop,
            (
                enums.CommandCode.SET_ENABLED_SUBSTATE,
                enums.SetEnabledSubstateParam.CONSTANT_VELOCITY,
            ):
            self.do_constant_velocity,
            enums.CommandCode.FAULT:
            self.do_fault,
            enums.CommandCode.POSITION_SET:
            self.do_position_set,
            enums.CommandCode.SET_CONSTANT_VEL:
            self.do_set_constant_vel,
            enums.CommandCode.CONFIG_VEL:
            self.do_config_vel,
            enums.CommandCode.CONFIG_ACCEL:
            self.do_config_accel,
            enums.CommandCode.TRACK_VEL_CMD:
            self.do_track_vel_cmd,
        }

        super().__init__(
            log=log,
            CommandCode=enums.CommandCode,
            extra_commands=extra_commands,
            config=config,
            telemetry=telemetry,
            host=host,
            command_port=command_port,
            telemetry_port=telemetry_port,
            initial_state=initial_state,
        )