3636class StingrayCoreControlNode (Node ):
3737 def __init__ (self ):
3838 super ().__init__ ('stingray_core_control_node' )
39+ self ._init_node_params ()
40+ self ._init_topics ()
41+ self ._init_axes_and_thrusters ()
42+ self ._init_mixer ()
43+ self ._init_controllers ()
44+ self ._init_state ()
45+ self ._init_axis_controllers ()
46+ self ._init_subscriptions ()
47+ self ._init_publishers ()
48+ self ._init_param_callbacks ()
49+ self ._init_timer ()
3950
40- # --- параметры ---
51+ self .get_logger ().info (
52+ f"Started at { self .rate_hz } Hz | axes={ self .axes } | thrusters={ len (self .thrusters )} "
53+ )
54+
55+ def control_loop (self ):
56+ now = time .time ()
57+ dt = now - self .last_time if self .last_time is not None else 0.0
58+ self .last_time = now
59+ self .last_dt = dt
60+
61+ # === 1. Определяем управляющие воздействия ===
62+ u : dict [str , float ] = {}
63+
64+ for axis in self .axes :
65+ if axis in self .axis_ctrl and self .control .enabled [axis ]:
66+ u [axis ] = self .axis_ctrl [axis ].compute (
67+ target = self .control .impact [axis ],
68+ dt = self .last_dt ,
69+ )
70+ else :
71+ u [axis ] = self .control .impact [axis ]
72+
73+ # === 2. Преобразуем в команды thrusters ===
74+ control_list = [u [a ] for a in self .axes ]
75+ thruster_cmds = self .mixer .mix_from_list (control_list )
76+
77+ # === 3. Публикуем thruster_cmd ===
78+ msg = UInt8MultiArray ()
79+ msg .data = thruster_cmds
80+ self .pub_thruster_cmd .publish (msg )
81+
82+ # === 4. Публикуем ориентацию ===
83+ self .pub_yaw .publish (Float64 (data = self .imu .yaw ))
84+ self .pub_pitch .publish (Float64 (data = self .imu .pitch ))
85+ self .pub_roll .publish (Float64 (data = self .imu .roll ))
86+
87+ def normalize_angle_deg (self , angle_deg ):
88+ """Нормализует угол в градусах в диапазон [-180, 180)."""
89+ a = (angle_deg + 180.0 ) % 360.0 - 180.0
90+ return a
91+
92+ def _init_node_params (self ):
4193 self .declare_parameter ('rate_hz' , 100.0 )
42- self .rate_hz = float (self .get_parameter (
43- 'rate_hz' ).get_parameter_value ().double_value )
44-
94+ self .rate_hz = float (self .get_parameter ('rate_hz' ).value )
95+
4596 self .declare_parameter ('flag_setup_feedback_speed' , False )
46- self .flag_setup_feedback_speed = self .get_parameter (
47- 'flag_setup_feedback_speed' ).get_parameter_value ().bool_value
48-
49- self .declare_parameter ('topic_imu_angular' , '/vectornav/raw/common' )
50- self .declare_parameter ('topic_imu_linear_accel' ,
51- '/vectornav/imu_accel' )
52- self .declare_parameter ('topic_imu_angular_rate' , '/vectornav/imu' )
53- self .declare_parameter ('topic_loop_flags' , '/control/loop_flags' )
54- self .declare_parameter ('topic_pressure_sensor' , '/sensors/pressure' )
55- self .declare_parameter ('topic_control_data' , '/control/data' )
56- self .declare_parameter ('vectornav_yaw_offset_deg' , 0.0 )
57-
58- self .topic_imu_angular = self .get_parameter (
59- 'topic_imu_angular' ).get_parameter_value ().string_value
60- self .topic_imu_linear_accel = self .get_parameter (
61- 'topic_imu_linear_accel' ).get_parameter_value ().string_value
62- self .topic_imu_angular_rate = self .get_parameter (
63- 'topic_imu_angular_rate' ).get_parameter_value ().string_value
64- self .topic_loop_flags = self .get_parameter (
65- 'topic_loop_flags' ).get_parameter_value ().string_value
66- self .topic_pressure_sensor = self .get_parameter (
67- 'topic_pressure_sensor' ).get_parameter_value ().string_value
68- self .topic_control_data = self .get_parameter (
69- 'topic_control_data' ).get_parameter_value ().string_value
70- self .vectornav_yaw_offset = float (self .get_parameter (
71- 'vectornav_yaw_offset_deg' ).get_parameter_value ().double_value )
72-
73- # --- yaw zeroing ---
74- self .yaw_zero_offset = 0.0
75- self .imu_yaw_raw = 0.0
97+ self .flag_setup_feedback_speed = bool (self .get_parameter ('flag_setup_feedback_speed' ).value )
7698
77- self .declare_parameter ('topic_zero_yaw' , '/imu/zero_yaw' )
78- self .topic_zero_yaw = self .get_parameter (
79- 'topic_zero_yaw' ).get_parameter_value ().string_value
99+ self .declare_parameter ('debug_publish' , False )
80100
81- self .sub_zero_yaw = self .create_subscription (
82- Bool , self .topic_zero_yaw , self .zero_yaw_callback , 10 )
83101
84-
85- if not self .has_parameter ('thrusters' ):
86- self .declare_parameter ('thrusters' , ["" ,"" ])
102+ def _init_topics (self ):
103+ defaults = {
104+ 'topic_imu_angular' : '/vectornav/raw/common' ,
105+ 'topic_imu_linear_accel' : '/vectornav/imu_accel' ,
106+ 'topic_imu_angular_rate' : '/vectornav/imu' ,
107+ 'topic_loop_flags' : '/control/loop_flags' ,
108+ 'topic_pressure_sensor' : '/sensors/pressure' ,
109+ 'topic_control_data' : '/control/data' ,
110+ 'topic_zero_yaw' : '/imu/zero_yaw' ,
111+ }
87112
88- self .thrusters = list (self .get_parameter ('thrusters' ).get_parameter_value ().string_array_value )
89-
113+ for name , default in defaults .items ():
114+ self .declare_parameter (name , default )
115+ setattr (self , name , self .get_parameter (name ).value )
116+
117+ def _init_axes_and_thrusters (self ):
90118 self .declare_parameter ('axes' , ["surge" , "sway" , "heave" , "roll" , "pitch" , "yaw" ])
91- self .axes = list (self .get_parameter ('axes' ).get_parameter_value (). string_array_value )
119+ self .axes = list (self .get_parameter ('axes' ).value )
92120
121+ self .declare_parameter ('thrusters' , [])
122+ self .thrusters = list (self .get_parameter ('thrusters' ).value )
123+
124+ def _init_mixer (self ):
93125 coeffs = {}
94- for t in self .thrusters :
95- row = []
96- for a in self .axes :
97- param_name = f"{ t } _{ a } "
98- if not self .has_parameter (param_name ):
99- self .declare_parameter (param_name , 0 )
100- val = float (self .get_parameter (param_name ).value )
101- row .append (val )
102- coeffs [t ] = row
103-
104- # create pure mixer
105- self .mixer = ThrusterMixer (self .thrusters , self .axes , coeffs )
106- self .get_logger ().info (f"ThrusterMixer initialized: thrusters={ self .thrusters } , axes={ self .axes } , coeffs= { coeffs } " )
107126
108- # subscribe to parameter changes to update coeffs dynamically
127+ for thr in self .thrusters :
128+ row = []
129+ for axis in self .axes :
130+ pname = f"{ thr } _{ axis } "
131+ if not self .has_parameter (pname ):
132+ self .declare_parameter (pname , 0.0 )
133+ row .append (float (self .get_parameter (pname ).value ))
134+ coeffs [thr ] = row
109135
110- self .controllers = {}
136+ self .mixer = ThrusterMixer ( self . thrusters , self . axes , coeffs )
111137
138+ def _init_controllers (self ):
112139 axis_class_map = {
113140 'yaw' : YawController ,
114141 'pitch' : PitchController ,
115142 'roll' : RollController ,
116143 'heave' : DepthController ,
117144 'surge' : SurgeController ,
118- 'sway' : SwayController
145+ 'sway' : SwayController ,
119146 }
120147
121-
148+ self . controllers = {}
122149 self .param_keys = ["K_p" , "K_1" , "K_2" , "K_i" , "I_min" , "I_max" , "out_sat" , "ap_K" , "ap_T" ]
123150
124- # внутри ноды
125151 for axis in self .axes :
126- # Получаем список ключей для оси
127- self .declare_parameter (axis , ["K_p" , "K_1" , "K_2" , "K_i" , "I_min" , "I_max" , "out_sat" , "ap_K" , "ap_T" ])
128-
129- # Загружаем параметры
130- params_dict = {}
152+ params = {}
131153 for key in self .param_keys :
132- param_name = f'controllers.{ axis } .{ key } '
133- self .declare_parameter (param_name , 0.0 )
134- params_dict [key ] = self .get_parameter (param_name ).value
135-
136- # Инициализируем контроллер конкретного класса
137- ctrl_class = axis_class_map [axis ] # всегда берём нужный класс
138- self .controllers [axis ] = ctrl_class (** params_dict )
154+ pname = f"controllers.{ axis } .{ key } "
155+ self .declare_parameter (pname , 0.0 )
156+ params [key ] = float (self .get_parameter (pname ).value )
139157
158+ self .controllers [axis ] = axis_class_map [axis ](** params )
159+ # self.controllers["yaw"].set_debug_hook(self.debug_cb)
160+ # self.controllers["heave"].set_debug_hook(self.debug_cb)
140161
141- self .get_logger ().info (f"Controllers initialized: { self .controllers } " )
142162
163+ def _init_state (self ):
143164 self .imu = ImuState ()
144165 self .control = ControlState .from_axes (self .axes )
145166
146- # --- Axis controllers (angular / linear split) ---
167+ self .depth = 0.0
168+
169+ # yaw zeroing
170+ self .yaw_zero_offset = 0.0
171+ self .imu_yaw_raw = 0.0
172+
173+ def _init_axis_controllers (self ):
147174 self .axis_ctrl : dict [str , AxisController ] = {
148175 "yaw" : AngularAxisController (
149176 controller = self .controllers ["yaw" ],
@@ -160,114 +187,78 @@ def __init__(self):
160187 "heave" : LinearAxisController (
161188 controller = self .controllers ["heave" ],
162189 pos_fn = lambda : self .depth ,
163- vel_fn = lambda : 0.0 , # пока нет оценки вертикальной скорости
190+ vel_fn = lambda : 0.0 ,
164191 accel_fn = lambda : 0.0 ,
165192 feedback_flag_fn = lambda : self .flag_setup_feedback_speed ,
166193 ),
167194 }
168195
196+ # fallback для остальных осей
197+ # for axis in self.axes:
198+ # if axis not in self.axis_ctrl:
199+ # self.axis_ctrl[axis] = PassthroughAxisController()
200+
201+ def _init_subscriptions (self ):
169202 qos = QoSProfile (depth = 10 )
170203
171- # --- подписки ---
172204 self .sub_imu_angular = self .create_subscription (
173- CommonGroup , "/vectornav/raw/common" , self .imu_angular_callback , qos_profile_sensor_data )
205+ CommonGroup , self .topic_imu_angular ,
206+ self .imu_angular_callback , qos_profile_sensor_data
207+ )
208+
174209 self .sub_imu_linear_accel = self .create_subscription (
175- Vector3 , self .topic_imu_linear_accel , self .imu_linear_accel_callback , qos )
176- self .sub_imu_angular_rate = self .create_subscription (
177- Imu , self .topic_imu_angular_rate , self .imu_angular_rate_callback , qos )
178-
179- # --- my orientation publishers ---
180- self .pub_yaw = self .create_publisher (Float64 , '~/orientation/yaw' , 10 )
181- self .pub_pitch = self .create_publisher (Float64 , '~/orientation/pitch' , 10 )
182- self .pub_roll = self .create_publisher (Float64 , '~/orientation/roll' , 10 )
210+ Vector3 , self .topic_imu_linear_accel ,
211+ self .imu_linear_accel_callback , qos
212+ )
183213
214+ self .sub_imu_angular_rate = self .create_subscription (
215+ Imu , self .topic_imu_angular_rate ,
216+ self .imu_angular_rate_callback , qos
217+ )
184218
185- # loop flags — используем UInt8 (битфлаги)
186219 self .sub_control_mode_flags = self .create_subscription (
187- UInt8 , self .topic_loop_flags , self .control_mode_flags_callback , qos )
220+ UInt8 , self .topic_loop_flags ,
221+ self .control_mode_flags_callback , qos
222+ )
188223
189- # pressure: обычно Float32
190224 self .sub_pressure_sensor = self .create_subscription (
191- Float32 , self .topic_pressure_sensor , self .pressure_sensor_callback , qos )
225+ Float32 , self .topic_pressure_sensor ,
226+ self .pressure_sensor_callback , qos
227+ )
192228
193- # control data (входные "impact" команды)
194229 self .sub_control_data = self .create_subscription (
195- Twist , self .topic_control_data , self .control_data_callback , qos )
230+ Twist , self .topic_control_data ,
231+ self .control_data_callback , qos
232+ )
233+
234+ self .sub_zero_yaw = self .create_subscription (
235+ Bool , self .topic_zero_yaw ,
236+ self .zero_yaw_callback , 10
237+ )
196238
197- # паблишер
239+ def _init_publishers ( self ):
198240 self .pub_thruster_cmd = self .create_publisher (
199- UInt8MultiArray , '/thruster/cmd' , qos )
241+ UInt8MultiArray , '/thruster/cmd' , 10
242+ )
200243
201- self .depth = 0.0
202- self .yaw_ctrl = self .controllers ["yaw" ]
203- self .depth_ctrl = self .controllers ["heave" ]
204- self .pitch_ctrl = self .controllers ["pitch" ]
244+ self .pub_yaw = self .create_publisher (Float64 , '~/orientation/yaw' , 10 )
245+ self .pub_pitch = self .create_publisher (Float64 , '~/orientation/pitch' , 10 )
246+ self .pub_roll = self .create_publisher (Float64 , '~/orientation/roll' , 10 )
205247
206- self .declare_parameter ('debug_publish' , False )
207- self .pub_err_position = self .create_publisher (Float64 , "~/debug/err_position" , 10 )
208- self .pub_output_pi = self .create_publisher (Float64 , "~/debug/output_pi" , 10 )
248+ self .pub_err_position = self .create_publisher (Float64 , "~/debug/err_position" , 10 )
249+ self .pub_output_pi = self .create_publisher (Float64 , "~/debug/output_pi" , 10 )
209250 self .pub_feedback_speed = self .create_publisher (Float64 , "~/debug/feedback_speed" , 10 )
210- self .pub_measurement_rate = self .create_publisher (Float64 , "~/debug/measurement_rate" , 10 )
211- self .pub_out = self .create_publisher (Float64 , "~/debug/out" , 10 )
212- self .yaw_ctrl .set_debug_hook (self .debug_cb )
213- self .depth_ctrl .set_debug_hook (self .debug_cb )
214-
215-
251+ self .pub_measurement_rate = self .create_publisher (Float64 , "~/debug/measurement_rate" , 10 )
252+ self .pub_out = self .create_publisher (Float64 , "~/debug/out" , 10 )
253+
254+ def _init_param_callbacks (self ):
216255 self .add_on_set_parameters_callback (self ._on_params_changed )
217256
218- # таймер
257+ def _init_timer ( self ):
219258 self .last_time = time .time ()
220259 self .timer = self .create_timer (1.0 / self .rate_hz , self .control_loop )
221260
222- self ._last_status_log = 0.0
223- self ._status_log_interval = 1.0 # сек
224-
225- self .get_logger ().info (
226- f"StingrayCoreControlNode started at { self .rate_hz } Hz" )
227-
228- def normalize_angle_deg (self , angle_deg ):
229- """Нормализует угол в градусах в диапазон [-180, 180)."""
230- a = (angle_deg + 180.0 ) % 360.0 - 180.0
231- return a
232-
233- def control_loop (self ):
234- now = time .time ()
235- dt = now - self .last_time if self .last_time is not None else 0.0
236- self .last_time = now
237- self .last_dt = dt
238-
239- # === 1. Определяем управляющие воздействия ===
240- u : dict [str , float ] = {}
241-
242- for axis in self .axes :
243- if axis in self .axis_ctrl and self .control .enabled [axis ]:
244- u [axis ] = self .axis_ctrl [axis ].compute (
245- target = self .control .impact [axis ],
246- dt = self .last_dt ,
247- )
248- else :
249- u [axis ] = self .control .impact [axis ]
250-
251- # === 2. Преобразуем в команды thrusters ===
252- control_list = [u [a ] for a in self .axes ]
253- thruster_cmds = self .mixer .mix_from_list (control_list )
254-
255- # === 3. Публикуем UInt8MultiArray ===
256- msg = UInt8MultiArray ()
257- msg .data = thruster_cmds
258- self .pub_thruster_cmd .publish (msg )
259-
260- # === 4. Публикуем ориентацию ===
261- self .pub_yaw .publish (Float64 (data = self .imu .yaw ))
262- self .pub_pitch .publish (Float64 (data = self .imu .pitch ))
263- self .pub_roll .publish (Float64 (data = self .imu .roll ))
264-
265261
266- # Логируем статус 1 раз в секунду, а не каждый тик
267- if now - self ._last_status_log > self ._status_log_interval :
268- # self.get_logger().info(f"Thrusters command: {thruster_cmds}")
269- # self.get_logger().debug(f"Control loop tick: dt={dt:.6f} s")
270- self ._last_status_log = now
271262
272263 # --- Колбэки ---
273264 def imu_angular_callback (self , msg : CommonGroup ):
0 commit comments