Source code for sils.controller

"""離散時間 制御ロジック (Controller)

satelite/control.cpp の Python 移植。
衛星のメインループ(~200Hz)ごとに呼ばれ、サーボパルス指令を返す。
"""

import math


[docs] class ControlState: UNLOAD_TIME = 0 UNLOAD_ROTATE = 1 STABLE = 2 LOTATE = 3
def _angle_diff_rad(input_rad: float, base_rad: float) -> float: """角度差を -π〜π に正規化""" diff = input_rad - base_rad if diff > math.pi: diff -= 2 * math.pi elif diff < -math.pi: diff += 2 * math.pi return diff
[docs] class SatelliteController: """衛星の姿勢制御ステートマシン (control.cpp の移植)""" # 制御パラメータ(control.cpp と一致させる) SERVO_CENTER_US = 1520 PULSE_DELTA_US = 1000 UNLOAD_MIN_DURATION_US = 300000 # 300 ms ROTATE_DURATION_US = 200000 # 200 ms ANGLE_TOLERANCE_RAD = 10.0 / 180.0 * math.pi ANGULAR_VELOCITY_TOLERANCE_RAD_PER_S = 0.5 / 180.0 * math.pi def __init__(self): self.state = ControlState.UNLOAD_TIME self.state_start_time_us: int = 0 self.current_servo_pulse_us: int = self.SERVO_CENTER_US self.target_angle_rad: float = 0.0 self.is_target_in_angle: bool = False self.control_enabled: bool = True # 内部時間カウンタ(μs) self._time_us: int = 0
[docs] def reset(self): """状態を初期化する""" self.state = ControlState.UNLOAD_TIME self.state_start_time_us = 0 self.current_servo_pulse_us = self.SERVO_CENTER_US self.target_angle_rad = 0.0 self.is_target_in_angle = False self.control_enabled = True self._time_us = 0
[docs] def set_target(self, angle_rad: float): """目標角度を設定""" self.target_angle_rad = angle_rad
def _state_transition(self, new_state: int): self.state = new_state self.state_start_time_us = self._time_us
[docs] def set_control_enabled(self, enabled: bool): self.control_enabled = enabled if not enabled: self._state_transition(ControlState.UNLOAD_TIME) self.current_servo_pulse_us = self.SERVO_CENTER_US self.is_target_in_angle = False
[docs] def is_control_enabled(self) -> bool: return self.control_enabled
[docs] def update(self, dt_us: int, sun_angle_rad: float, angular_velocity_rad_per_s: float) -> int: """制御ループ1ステップ(衛星メインループ相当) Args: dt_us: 経過時間 [μs] sun_angle_rad: 太陽センサから推定した角度 [rad] angular_velocity_rad_per_s: IMU角速度 [rad/s] Returns: サーボパルス指令 [μs] """ self._time_us += dt_us now = self._time_us if not self.control_enabled: return self.SERVO_CENTER_US # 角度誤差(control.cpp と同じ符号規約) error = _angle_diff_rad(-self.target_angle_rad, sun_angle_rad) if self.state == ControlState.UNLOAD_TIME: self.current_servo_pulse_us = self.SERVO_CENTER_US if (now - self.state_start_time_us) > self.UNLOAD_MIN_DURATION_US: self._state_transition(ControlState.UNLOAD_ROTATE) elif self.state == ControlState.UNLOAD_ROTATE: self.current_servo_pulse_us = self.SERVO_CENTER_US if abs(angular_velocity_rad_per_s) < self.ANGULAR_VELOCITY_TOLERANCE_RAD_PER_S: self._state_transition(ControlState.STABLE) elif self.state == ControlState.STABLE: self.current_servo_pulse_us = self.SERVO_CENTER_US # TODO(姿勢制御): バンバン制御 (PULSE_DELTA_US 固定キック) のため目標近傍で # 整定せず限界サイクルに入り、|error| < ANGLE_TOLERANCE_RAD かつ STABLE が # 持続しない。結果 is_target_in_angle が安定して立たず、ミッション成功時の # 自動撮影が発火しない。修正方針は control.cpp の同 TODO を参照。 # 実機 (satelite/firmware/control.cpp) は移植関係なので同期して修正すること。 # 検証: ground/e2e/tests/test_target_capture.py::test_target_triggers_auto_capture # が現在 xfail。修正後に xfail を外す。 if abs(error) > self.ANGLE_TOLERANCE_RAD: if error < 0: self.current_servo_pulse_us = self.SERVO_CENTER_US + self.PULSE_DELTA_US else: self.current_servo_pulse_us = self.SERVO_CENTER_US - self.PULSE_DELTA_US self._state_transition(ControlState.LOTATE) self.is_target_in_angle = False else: self.is_target_in_angle = True elif self.state == ControlState.LOTATE: if (now - self.state_start_time_us) >= self.ROTATE_DURATION_US: self._state_transition(ControlState.UNLOAD_TIME) return self.current_servo_pulse_us
[docs] def get_control_state(self) -> int: return self.state
[docs] def is_control_finished(self) -> bool: return self.is_target_in_angle