1use crate::comm::messages::{
2 enums::{OffboardControlIgnore, OffboardControlMode},
3 messages::OffboardControlMsg,
4};
5use crate::params::{ParamId, ParamValue, Params};
6use crate::rc::{Rc, Stick, Switch};
7use crate::state_machine::{ErrorFlag, Event, StateManager};
8
9pub mod service;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum ControlType {
13 Rate, Angle, Throttle, Passthrough, }
18
19pub(crate) const ATTITUDE_RATE_MODE: i32 = 0;
21#[cfg(test)]
22const ATTITUDE_ANGLE_MODE: i32 = 1;
23
24pub const OVERRIDE_NO_OVERRIDE: u16 = 0x0;
25pub const OVERRIDE_ATT_SWITCH: u16 = 0x1;
26pub const OVERRIDE_THR_SWITCH: u16 = 0x2;
27pub const OVERRIDE_X: u16 = 0x4;
28pub const OVERRIDE_Y: u16 = 0x8;
29pub const OVERRIDE_Z: u16 = 0x10;
30pub const OVERRIDE_T: u16 = 0x20;
31pub const OVERRIDE_OFFBOARD_X_INACTIVE: u16 = 0x40;
32pub const OVERRIDE_OFFBOARD_Y_INACTIVE: u16 = 0x80;
33pub const OVERRIDE_OFFBOARD_Z_INACTIVE: u16 = 0x100;
34pub const OVERRIDE_OFFBOARD_T_INACTIVE: u16 = 0x200;
35
36#[derive(Clone, Copy, Debug)]
37pub struct ControlChannel {
38 pub active: bool,
39 pub control_type: ControlType,
40 pub value: f32,
41}
42
43impl Default for ControlChannel {
44 fn default() -> Self {
45 Self {
46 active: false,
47 control_type: ControlType::Rate,
48 value: 0.0,
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Default)]
54pub struct CombinedControl {
55 pub stamp_ms: u32,
56 pub qx: ControlChannel,
57 pub qy: ControlChannel,
58 pub qz: ControlChannel,
59 pub fx: ControlChannel,
60 pub fy: ControlChannel,
61 pub fz: ControlChannel,
62 pub passthrough: [ControlChannel; 4],
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66enum RcAngleModeLockoutState {
67 #[default]
68 Inactive,
69 SwitchResetRequired {
70 estimator_recovered: bool,
71 rate_wait_logged: bool,
72 },
73 ParamRateUntilExplicitAngle,
74}
75
76#[derive(Clone, Copy, Debug, Default)]
77struct RcAngleModeLockoutMachine {
78 state: RcAngleModeLockoutState,
79 denial_active: bool,
80}
81
82#[derive(Clone, Copy, Debug, Default)]
83struct RcAngleModeLockoutInput {
84 fixed_wing: bool,
85 estimator_unhealthy: bool,
86 offboard_angle_requested: bool,
87 rc_angle_requested: bool,
88 rc_attitude_source_requested: bool,
89 att_type_switch_mapped: bool,
90 att_type_switch_rate: bool,
91 param_mode_rate: bool,
92}
93
94#[derive(Clone, Copy, Debug, Default)]
95struct RcAngleModeLockoutOutput {
96 force_rc_attitude: bool,
97 force_rc_rate: bool,
98 force_param_rate: bool,
99}
100
101impl RcAngleModeLockoutMachine {
102 fn reset(&mut self) {
103 self.state = RcAngleModeLockoutState::Inactive;
104 self.denial_active = false;
105 }
106
107 fn step(&mut self, input: RcAngleModeLockoutInput) -> RcAngleModeLockoutOutput {
108 if input.fixed_wing {
109 self.reset();
110 return RcAngleModeLockoutOutput::default();
111 }
112
113 self.advance(input);
114
115 let lockout_active = !matches!(self.state, RcAngleModeLockoutState::Inactive);
116 let unsafe_angle_requested = input.estimator_unhealthy
117 && (input.offboard_angle_requested
118 || (input.rc_attitude_source_requested && input.rc_angle_requested));
119
120 if unsafe_angle_requested {
121 self.enter(input);
122 } else {
123 self.denial_active = false;
124 }
125
126 RcAngleModeLockoutOutput {
127 force_rc_attitude: input.estimator_unhealthy && input.offboard_angle_requested,
128 force_rc_rate: lockout_active || unsafe_angle_requested,
129 force_param_rate: !input.att_type_switch_mapped
130 && unsafe_angle_requested
131 && !input.param_mode_rate,
132 }
133 }
134
135 fn advance(&mut self, input: RcAngleModeLockoutInput) {
136 match self.state {
137 RcAngleModeLockoutState::Inactive => {}
138 RcAngleModeLockoutState::SwitchResetRequired {
139 estimator_recovered,
140 rate_wait_logged,
141 } => {
142 let recovered = estimator_recovered || !input.estimator_unhealthy;
143 if recovered && input.att_type_switch_rate {
144 self.state = RcAngleModeLockoutState::Inactive;
145 if !estimator_recovered {
146 crate::log_info!(
147 "Firmware Lockout: Estimator healthy: angle mode can be re-enabled"
148 );
149 }
150 } else {
151 if !estimator_recovered && recovered {
152 crate::log_info!(
153 "Firmware Lockout: Estimator healthy: angle mode can be re-enabled"
154 );
155 }
156 if input.att_type_switch_rate && !recovered && !rate_wait_logged {
157 crate::log_info!(
158 "Firmware Lockout: Angle mode switch will be available once estimator reacquires"
159 );
160 }
161 self.state = RcAngleModeLockoutState::SwitchResetRequired {
162 estimator_recovered: recovered,
163 rate_wait_logged: rate_wait_logged
164 || (input.att_type_switch_rate && !recovered),
165 };
166 }
167 }
168 RcAngleModeLockoutState::ParamRateUntilExplicitAngle => {
169 if !input.estimator_unhealthy && input.param_mode_rate {
170 self.state = RcAngleModeLockoutState::Inactive;
171 crate::log_info!(
172 "Firmware Lockout: Estimator healthy: angle mode can be re-enabled"
173 );
174 }
175 }
176 }
177 }
178
179 fn enter(&mut self, input: RcAngleModeLockoutInput) {
180 if self.denial_active {
181 return;
182 }
183 self.denial_active = true;
184
185 crate::log_error!("Firmware Lockout: Unhealthy estimator: forced RC rate mode");
186 if input.att_type_switch_mapped {
187 if !matches!(
188 self.state,
189 RcAngleModeLockoutState::SwitchResetRequired { .. }
190 ) {
191 self.state = RcAngleModeLockoutState::SwitchResetRequired {
192 estimator_recovered: false,
193 rate_wait_logged: false,
194 };
195 }
196 crate::log_error!(
197 "Firmware Lockout: Move RC attitude switch to rate before angle mode can be re-enabled"
198 );
199 } else {
200 self.state = RcAngleModeLockoutState::ParamRateUntilExplicitAngle;
201 if !input.param_mode_rate {
202 crate::log_error!(
203 "Firmware Lockout: RC_ATT_MODE set to rate; set RC_ATT_MODE to angle after estimator recovery to re-enable angle mode"
204 );
205 }
206 }
207 }
208}
209
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
211pub struct CommandRunResult {
212 pub force_rc_attitude_mode_rate: bool,
213}
214
215#[derive(Clone, Copy, Debug, Default)]
216struct EstimatorModeSafety {
217 force_rc_attitude: bool,
218 force_rc_rate: bool,
219 force_param_rate: bool,
220}
221
222#[derive(Default)]
223pub struct CommandManager {
224 rc_command: CombinedControl,
226 offboard_command: CombinedControl,
227 combined_command: CombinedControl,
228 multirotor_failsafe_command: CombinedControl,
229 fixedwing_failsafe_command: CombinedControl,
230 last_offboard_command_us: u64,
231 last_stick_override_time: [u32; 3], rc_throttle_override: bool,
235 rc_attitude_override: bool,
236 rc_override: u16,
237 rc_angle_mode_lockout: RcAngleModeLockoutMachine,
238}
239
240impl CommandManager {
241 pub fn new() -> Self {
243 Self {
244 multirotor_failsafe_command: CombinedControl {
245 qx: ControlChannel {
246 active: true,
247 control_type: ControlType::Angle,
248 value: 0.0,
249 },
250 qy: ControlChannel {
251 active: true,
252 control_type: ControlType::Angle,
253 value: 0.0,
254 },
255 qz: ControlChannel {
256 active: true,
257 control_type: ControlType::Rate,
258 value: 0.0,
259 },
260 fx: ControlChannel {
261 active: true,
262 control_type: ControlType::Throttle,
263 value: 0.0,
264 },
265 fy: ControlChannel {
266 active: true,
267 control_type: ControlType::Throttle,
268 value: 0.0,
269 },
270 fz: ControlChannel {
271 active: true,
272 control_type: ControlType::Throttle,
273 value: 0.0,
274 },
275 ..Default::default()
276 },
277 fixedwing_failsafe_command: CombinedControl {
278 qx: ControlChannel {
279 active: true,
280 control_type: ControlType::Passthrough,
281 value: 0.0,
282 },
283 qy: ControlChannel {
284 active: true,
285 control_type: ControlType::Passthrough,
286 value: 0.0,
287 },
288 qz: ControlChannel {
289 active: true,
290 control_type: ControlType::Passthrough,
291 value: 0.0,
292 },
293 fx: ControlChannel {
294 active: true,
295 control_type: ControlType::Passthrough,
296 value: 0.0,
297 },
298 fy: ControlChannel {
299 active: true,
300 control_type: ControlType::Passthrough,
301 value: 0.0,
302 },
303 fz: ControlChannel {
304 active: true,
305 control_type: ControlType::Passthrough,
306 value: 0.0,
307 },
308 ..Default::default()
309 },
310 ..Default::default()
311 }
312 }
313
314 pub fn init(&mut self, params: &Params, state_manager: &mut StateManager) {
315 self.update_failsafe_config(params, state_manager);
316 }
317
318 pub fn update_failsafe_config(&mut self, params: &Params, state_manager: &mut StateManager) {
319 let mut failsafe_throttle = match params.get_by_id(ParamId::PARAM_FAILSAFE_THROTTLE) {
320 ParamValue::Float(val) => val,
321 _ => 0.0f32,
322 };
323
324 let is_fixed_wing = match params.get_by_id(ParamId::PARAM_FIXED_WING) {
325 ParamValue::Int(val) => val != 0,
326 _ => false,
327 };
328
329 if !is_fixed_wing && (failsafe_throttle < 0.0 || failsafe_throttle > 1.0) {
330 state_manager.update(Event::ERROR_OCCURRED(ErrorFlag::INVALID_FAILSAFE), params);
331 failsafe_throttle = 0.0f32;
332 } else {
333 state_manager.update(Event::ERROR_CLEARED(ErrorFlag::INVALID_FAILSAFE), params);
334 }
335
336 self.multirotor_failsafe_command.fx.value = 0.0;
337 self.multirotor_failsafe_command.fy.value = 0.0;
338 self.multirotor_failsafe_command.fz.value = 0.0;
339
340 match params.get_by_id(ParamId::PARAM_RC_F_AXIS) {
341 ParamValue::Int(axis) => {
343 match axis {
344 0 => self.multirotor_failsafe_command.fx.value = failsafe_throttle,
346 1 => self.multirotor_failsafe_command.fy.value = failsafe_throttle,
347 _ => self.multirotor_failsafe_command.fz.value = failsafe_throttle,
348 }
349 }
350 _ => {
353 self.multirotor_failsafe_command.fz.value = failsafe_throttle;
354 }
355 }
356
357 self.fixedwing_failsafe_command.fx.value = 0.0;
358 self.fixedwing_failsafe_command.fy.value = 0.0;
359 self.fixedwing_failsafe_command.fz.value = 0.0;
360 }
361
362 pub fn run(
363 &mut self,
364 now_ms: u32,
365 params: &Params,
366 rc: &mut Rc,
367 state_manager: &mut StateManager, ) -> CommandRunResult {
369 let now_us = now_ms as u64 * 1000;
370 let mut result = CommandRunResult {
371 force_rc_attitude_mode_rate: false,
372 };
373
374 if !rc.check_rc_health(now_us, params) {
375 state_manager.update(Event::ERROR_OCCURRED(ErrorFlag::RC_LOST), params);
376 }
377
378 if state_manager.is_in_failsafe() {
381 let is_fixed_wing = match params.get_by_id(ParamId::PARAM_FIXED_WING) {
382 ParamValue::Int(val) => val != 0,
383 _ => false,
384 };
385 self.combined_command = if is_fixed_wing {
386 self.fixedwing_failsafe_command
387 } else {
388 self.multirotor_failsafe_command
389 };
390 return result; }
392
393 self.update_offboard_timeout(now_us, params);
395
396 let safety = self.estimator_mode_safety(params, rc, state_manager);
397 result.force_rc_attitude_mode_rate = safety.force_param_rate;
398
399 let has_new_rc = rc.new_command();
403 if has_new_rc || safety.force_rc_rate {
404 self.interpret_rc(
405 rc,
406 params,
407 safety.force_rc_rate.then_some(ControlType::Rate),
408 );
409 }
410
411 self.do_muxing(params, rc, now_ms, safety.force_rc_attitude);
413
414 result
415 }
416
417 fn update_offboard_timeout(&mut self, now_us: u64, params: &Params) {
418 let timeout_ms = match params.get_by_id(ParamId::PARAM_OFFBOARD_TIMEOUT) {
419 ParamValue::Int(val) => val as u32,
420 _ => 100,
421 };
422
423 if now_us <= self.last_offboard_command_us + (timeout_ms as u64 * 1000) {
424 return;
425 }
426
427 self.offboard_command.qx.active = false;
428 self.offboard_command.qy.active = false;
429 self.offboard_command.qz.active = false;
430 self.offboard_command.fx.active = false;
431 self.offboard_command.fy.active = false;
432 self.offboard_command.fz.active = false;
433 for channel in &mut self.offboard_command.passthrough {
434 channel.active = false;
435 }
436 }
437
438 pub fn set_new_offboard_command(
441 &mut self,
442 now_us: u64,
443 msg: &OffboardControlMsg,
444 _params: &Params,
445 ) {
446 self.last_offboard_command_us = now_us;
448 self.offboard_command.stamp_ms = (now_us / 1000) as u32;
449
450 self.offboard_command.qx.value = msg.qx;
451 self.offboard_command.qy.value = msg.qy;
452 self.offboard_command.qz.value = msg.qz;
453 self.offboard_command.fx.value = msg.fx;
454 self.offboard_command.fy.value = msg.fy;
455 self.offboard_command.fz.value = msg.fz;
456 for (channel, value) in self
457 .offboard_command
458 .passthrough
459 .iter_mut()
460 .zip(msg.passthrough.iter())
461 {
462 channel.value = *value;
463 channel.control_type = ControlType::Passthrough;
464 }
465
466 self.offboard_command.qx.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_QX);
467 self.offboard_command.qy.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_QY);
468 self.offboard_command.qz.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_QZ);
469 self.offboard_command.fx.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_FX);
470 self.offboard_command.fy.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_FY);
471 self.offboard_command.fz.active = !msg.ignore.contains(OffboardControlIgnore::IGNORE_FZ);
472 self.offboard_command.passthrough[0].active =
473 !msg.ignore.contains(OffboardControlIgnore::IGNORE_PASS_0);
474 self.offboard_command.passthrough[1].active =
475 !msg.ignore.contains(OffboardControlIgnore::IGNORE_PASS_1);
476 self.offboard_command.passthrough[2].active =
477 !msg.ignore.contains(OffboardControlIgnore::IGNORE_PASS_2);
478 self.offboard_command.passthrough[3].active =
479 !msg.ignore.contains(OffboardControlIgnore::IGNORE_PASS_3);
480
481 match msg.mode {
482 OffboardControlMode::ModePassThrough => {
483 self.offboard_command.qx.control_type = ControlType::Passthrough;
484 self.offboard_command.qy.control_type = ControlType::Passthrough;
485 self.offboard_command.qz.control_type = ControlType::Passthrough;
486 self.offboard_command.fx.control_type = ControlType::Passthrough;
487 self.offboard_command.fy.control_type = ControlType::Passthrough;
488 self.offboard_command.fz.control_type = ControlType::Passthrough;
489 }
490 OffboardControlMode::ModeRollratePitchrateYawrateThrottle => {
491 self.offboard_command.qx.control_type = ControlType::Rate;
492 self.offboard_command.qy.control_type = ControlType::Rate;
493 self.offboard_command.qz.control_type = ControlType::Rate;
494 self.offboard_command.fx.control_type = ControlType::Throttle;
495 self.offboard_command.fy.control_type = ControlType::Throttle;
496 self.offboard_command.fz.control_type = ControlType::Throttle;
497 }
498 OffboardControlMode::ModeRollPitchYawrateThrottle => {
499 self.offboard_command.qx.control_type = ControlType::Angle;
500 self.offboard_command.qy.control_type = ControlType::Angle;
501 self.offboard_command.qz.control_type = ControlType::Rate;
502 self.offboard_command.fx.control_type = ControlType::Throttle;
503 self.offboard_command.fy.control_type = ControlType::Throttle;
504 self.offboard_command.fz.control_type = ControlType::Throttle;
505 }
506 }
507 }
508
509 fn interpret_rc(
511 &mut self,
512 rc: &Rc,
513 params: &Params,
514 forced_roll_pitch_type: Option<ControlType>,
515 ) {
516 self.rc_command.qx.value = rc.stick(Stick::X);
518 self.rc_command.qy.value = rc.stick(Stick::Y);
519 self.rc_command.qz.value = rc.stick(Stick::Z);
520 let f_stick_value = rc.stick(Stick::F);
521
522 match params.get_by_id(ParamId::PARAM_RC_F_AXIS) {
524 ParamValue::Int(axis) => {
526 match axis {
527 0 => {
528 self.rc_command.fx.value = f_stick_value;
530 self.rc_command.fy.value = 0.0;
531 self.rc_command.fz.value = 0.0;
532 }
533 1 => {
534 self.rc_command.fx.value = 0.0;
536 self.rc_command.fy.value = f_stick_value;
537 self.rc_command.fz.value = 0.0;
538 }
539 _ => {
540 self.rc_command.fx.value = 0.0;
542 self.rc_command.fy.value = 0.0;
543 self.rc_command.fz.value = f_stick_value;
544 }
545 }
546 }
547 _ => {
549 self.rc_command.fx.value = 0.0;
551 self.rc_command.fy.value = 0.0;
552 self.rc_command.fz.value = f_stick_value;
553 }
554 }
555
556 self.rc_command.qx.active = true;
558 self.rc_command.qy.active = true;
559 self.rc_command.qz.active = true;
560 self.rc_command.fx.active = true;
561 self.rc_command.fy.active = true;
562 self.rc_command.fz.active = true;
563
564 let is_fixed_wing = match params.get_by_id(ParamId::PARAM_FIXED_WING) {
566 ParamValue::Int(val) => val != 0,
567 _ => {
568 false
571 }
572 };
573 if is_fixed_wing {
574 self.rc_command.qx.control_type = ControlType::Passthrough;
575 self.rc_command.qy.control_type = ControlType::Passthrough;
576 self.rc_command.qz.control_type = ControlType::Passthrough;
577 self.rc_command.fx.control_type = ControlType::Passthrough;
578 self.rc_command.fy.control_type = ControlType::Passthrough;
579 self.rc_command.fz.control_type = ControlType::Passthrough;
580 } else {
581 let roll_pitch_type = if let Some(forced_roll_pitch_type) = forced_roll_pitch_type {
583 forced_roll_pitch_type
584 } else if rc.switch_mapped(Switch::AttType) {
585 if rc.switch_on(Switch::AttType) {
587 ControlType::Angle
588 } else {
589 ControlType::Rate
590 }
591 } else {
592 let att_mode = match params.get_by_id(ParamId::PARAM_RC_ATTITUDE_MODE) {
594 ParamValue::Int(val) => val,
595 _ => {
596 200 }
598 };
599 match att_mode {
600 ATTITUDE_RATE_MODE => ControlType::Rate,
601 _ => {
602 ControlType::Angle
604 }
605 }
606 };
607
608 self.rc_command.qx.control_type = roll_pitch_type;
609 self.rc_command.qy.control_type = roll_pitch_type;
610
611 match roll_pitch_type {
612 ControlType::Rate => {
613 let max_rollrate = match params.get_by_id(ParamId::PARAM_RC_MAX_ROLLRATE) {
614 ParamValue::Float(val) => val,
615 _ => 1.0,
616 };
617 let max_pitchrate = match params.get_by_id(ParamId::PARAM_RC_MAX_PITCHRATE) {
618 ParamValue::Float(val) => val,
619 _ => 1.0,
620 };
621 self.rc_command.qx.value *= max_rollrate;
622 self.rc_command.qy.value *= max_pitchrate;
623 }
624 ControlType::Angle => {
625 let max_roll = match params.get_by_id(ParamId::PARAM_RC_MAX_ROLL) {
626 ParamValue::Float(val) => val,
627 _ => 1.0,
628 };
629 let max_pitch = match params.get_by_id(ParamId::PARAM_RC_MAX_PITCH) {
630 ParamValue::Float(val) => val,
631 _ => 1.0,
632 };
633 self.rc_command.qx.value *= max_roll;
634 self.rc_command.qy.value *= max_pitch;
635 }
636 _ => {}
637 }
638
639 self.rc_command.qz.control_type = ControlType::Rate;
640 let max_yawrate = match params.get_by_id(ParamId::PARAM_RC_MAX_YAWRATE) {
641 ParamValue::Float(val) => val,
642 _ => 1.0,
643 };
644 self.rc_command.qz.value *= max_yawrate;
645
646 self.rc_command.fx.control_type = ControlType::Throttle;
647 self.rc_command.fy.control_type = ControlType::Throttle;
648 self.rc_command.fz.control_type = ControlType::Throttle;
649 }
650 }
651
652 fn do_muxing(&mut self, params: &Params, rc: &Rc, now_ms: u32, force_rc_attitude: bool) {
654 let attitude_override = self.do_attitude_muxing(params, rc, now_ms, force_rc_attitude);
656 let throttle_override = self.do_throttle_muxing(params, rc);
657 self.rc_override = attitude_override | throttle_override;
658 self.rc_attitude_override = attitude_override != OVERRIDE_NO_OVERRIDE;
659 self.rc_throttle_override = throttle_override != OVERRIDE_NO_OVERRIDE;
660 self.combined_command.passthrough = self.offboard_command.passthrough;
661 }
662
663 fn attitude_stick_deviated(
664 &mut self,
665 rc: &Rc,
666 stick: Stick,
667 deviation_param: f32,
668 lag_time_ms: u32,
669 now_ms: u32,
670 ) -> bool {
671 if now_ms < self.last_stick_override_time[stick as usize].saturating_add(lag_time_ms) {
672 return true;
673 }
674
675 if (rc.stick(stick)).abs() <= deviation_param {
676 return false;
677 }
678
679 self.last_stick_override_time[stick as usize] = now_ms;
680 true
681 }
682
683 fn do_attitude_muxing(
684 &mut self,
685 params: &Params,
686 rc: &Rc,
687 now_ms: u32,
688 force_rc_attitude: bool,
689 ) -> u16 {
690 let deviation_param = match params.get_by_id(ParamId::PARAM_RC_OVERRIDE_DEVIATION) {
691 ParamValue::Float(val) => val,
692 _ => {
693 0.1 }
695 };
696
697 let lag_time_ms = match params.get_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME) {
699 ParamValue::Int(val) => val as u32,
700 _ => {
701 200 }
703 };
704
705 let switch_override =
706 rc.switch_mapped(Switch::AttOverride) && rc.switch_on(Switch::AttOverride);
707
708 let mut override_mask = if switch_override || force_rc_attitude {
709 OVERRIDE_ATT_SWITCH
710 } else {
711 OVERRIDE_NO_OVERRIDE
712 };
713
714 let x_stick_deviated =
715 self.attitude_stick_deviated(rc, Stick::X, deviation_param, lag_time_ms, now_ms);
716 if x_stick_deviated {
717 override_mask |= OVERRIDE_X;
718 }
719 if !self.offboard_command.qx.active {
720 override_mask |= OVERRIDE_OFFBOARD_X_INACTIVE;
721 }
722 self.combined_command.qx = if force_rc_attitude
723 || switch_override
724 || x_stick_deviated
725 || !self.offboard_command.qx.active
726 {
727 self.rc_command.qx
728 } else {
729 self.offboard_command.qx
730 };
731
732 let y_stick_deviated =
733 self.attitude_stick_deviated(rc, Stick::Y, deviation_param, lag_time_ms, now_ms);
734 if y_stick_deviated {
735 override_mask |= OVERRIDE_Y;
736 }
737 if !self.offboard_command.qy.active {
738 override_mask |= OVERRIDE_OFFBOARD_Y_INACTIVE;
739 }
740 self.combined_command.qy = if force_rc_attitude
741 || switch_override
742 || y_stick_deviated
743 || !self.offboard_command.qy.active
744 {
745 self.rc_command.qy
746 } else {
747 self.offboard_command.qy
748 };
749
750 let z_stick_deviated =
751 self.attitude_stick_deviated(rc, Stick::Z, deviation_param, lag_time_ms, now_ms);
752 if z_stick_deviated {
753 override_mask |= OVERRIDE_Z;
754 }
755 if !self.offboard_command.qz.active {
756 override_mask |= OVERRIDE_OFFBOARD_Z_INACTIVE;
757 }
758 self.combined_command.qz =
759 if switch_override || z_stick_deviated || !self.offboard_command.qz.active {
760 self.rc_command.qz
761 } else {
762 self.offboard_command.qz
763 };
764
765 override_mask
766 }
767
768 fn estimator_mode_safety(
769 &mut self,
770 params: &Params,
771 rc: &Rc,
772 state_manager: &StateManager,
773 ) -> EstimatorModeSafety {
774 let estimator_unhealthy = state_manager
775 .get_errors()
776 .contains(ErrorFlag::UNHEALTHY_ESTIMATOR);
777 let fixed_wing = matches!(
778 params.get_by_id(ParamId::PARAM_FIXED_WING),
779 ParamValue::Int(value) if value != 0
780 );
781 if fixed_wing || !estimator_angle_lockout_enabled(params) {
782 self.rc_angle_mode_lockout.reset();
783 return EstimatorModeSafety::default();
784 }
785
786 let lockout = self.rc_angle_mode_lockout.step(RcAngleModeLockoutInput {
787 fixed_wing,
788 estimator_unhealthy,
789 offboard_angle_requested: self.offboard_angle_requested(),
790 rc_angle_requested: rc_angle_mode_selected(params, rc),
791 rc_attitude_source_requested: self.rc_attitude_source_requested(rc),
792 att_type_switch_mapped: rc.switch_mapped(Switch::AttType),
793 att_type_switch_rate: !rc.switch_on(Switch::AttType),
794 param_mode_rate: param_rc_attitude_mode_is_rate(params),
795 });
796
797 EstimatorModeSafety {
798 force_rc_attitude: lockout.force_rc_attitude,
799 force_rc_rate: lockout.force_rc_rate,
800 force_param_rate: lockout.force_param_rate,
801 }
802 }
803
804 fn offboard_angle_requested(&self) -> bool {
805 (self.offboard_command.qx.active
806 && self.offboard_command.qx.control_type == ControlType::Angle)
807 || (self.offboard_command.qy.active
808 && self.offboard_command.qy.control_type == ControlType::Angle)
809 }
810
811 fn rc_attitude_source_requested(&self, rc: &Rc) -> bool {
812 let switch_override =
813 rc.switch_mapped(Switch::AttOverride) && rc.switch_on(Switch::AttOverride);
814 switch_override || !self.offboard_command.qx.active || !self.offboard_command.qy.active
815 }
816
817 fn do_throttle_muxing(&mut self, params: &Params, rc: &Rc) -> u16 {
818 let throttle_axis_idx = match params.get_by_id(ParamId::PARAM_RC_F_AXIS) {
819 ParamValue::Int(val) => val as u32,
820 _ => {
821 200 }
823 };
824
825 let (rc_throttle_value, offboard_throttle_channel) = match throttle_axis_idx {
826 0 => (self.rc_command.fx.value, &self.offboard_command.fx),
827 1 => (self.rc_command.fy.value, &self.offboard_command.fy),
828 _ => (self.rc_command.fz.value, &self.offboard_command.fz),
829 };
830
831 let mut override_mask = OVERRIDE_NO_OVERRIDE;
832
833 if rc.switch_mapped(Switch::ThrottleOverride) && rc.switch_on(Switch::ThrottleOverride) {
834 override_mask |= OVERRIDE_THR_SWITCH;
835 }
836
837 if offboard_throttle_channel.active {
838 let take_min = match params.get_by_id(ParamId::PARAM_RC_OVERRIDE_TAKE_MIN_THROTTLE) {
839 ParamValue::Int(val) => val != 0,
840 _ => true,
841 };
842
843 if take_min && rc_throttle_value < offboard_throttle_channel.value {
844 override_mask |= OVERRIDE_T;
845 }
846 } else {
847 override_mask |= OVERRIDE_OFFBOARD_T_INACTIVE;
848 }
849
850 if override_mask != OVERRIDE_NO_OVERRIDE {
851 self.combined_command.fx = self.rc_command.fx;
852 self.combined_command.fy = self.rc_command.fy;
853 self.combined_command.fz = self.rc_command.fz;
854 } else {
855 self.combined_command.fx = self.offboard_command.fx;
856 self.combined_command.fy = self.offboard_command.fy;
857 self.combined_command.fz = self.offboard_command.fz;
858 }
859
860 override_mask
861 }
862
863 pub fn combined_control(&self) -> &CombinedControl {
864 &self.combined_command
865 }
866
867 pub fn rc_control(&self) -> &CombinedControl {
868 &self.rc_command
869 }
870
871 pub fn get_control_mode(&self) -> ControlType {
872 self.combined_command.qx.control_type
875 }
876
877 pub fn rc_override_active(&self) -> bool {
878 self.rc_override != OVERRIDE_NO_OVERRIDE
879 }
880
881 pub fn get_rc_override(&self) -> u16 {
882 self.rc_override
883 }
884
885 pub fn is_offboard_active(&self) -> bool {
887 self.offboard_command.qx.active
889 || self.offboard_command.qy.active
890 || self.offboard_command.qz.active
891 || self.offboard_command.fx.active
892 || self.offboard_command.fy.active
893 || self.offboard_command.fz.active
894 }
895}
896
897fn rc_angle_mode_selected(params: &Params, rc: &Rc) -> bool {
898 if rc.switch_mapped(Switch::AttType) {
899 rc.switch_on(Switch::AttType)
900 } else {
901 !param_rc_attitude_mode_is_rate(params)
902 }
903}
904
905fn param_rc_attitude_mode_is_rate(params: &Params) -> bool {
906 matches!(
907 params.get_by_id(ParamId::PARAM_RC_ATTITUDE_MODE),
908 ParamValue::Int(ATTITUDE_RATE_MODE)
909 )
910}
911
912fn estimator_angle_lockout_enabled(params: &Params) -> bool {
913 matches!(
914 params.get_by_id(ParamId::PARAM_EST_ANGLE_LOCKOUT),
915 ParamValue::Int(value) if value != 0
916 )
917}
918
919impl From<ControlType> for OffboardControlMode {
920 fn from(val: ControlType) -> Self {
921 match val {
922 ControlType::Rate => OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
923 ControlType::Passthrough => OffboardControlMode::ModePassThrough,
924 ControlType::Angle => OffboardControlMode::ModeRollPitchYawrateThrottle,
925 ControlType::Throttle => OffboardControlMode::ModeRollPitchYawrateThrottle,
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::log::Logger;
934 use crate::packets::{RC_PACKET_CHANNELS, RcPacket, RosflightPacketHeader};
935
936 fn initialized_rc(params: &Params) -> Rc {
937 let mut rc = Rc::new();
938 rc.init(params);
939 rc
940 }
941
942 fn receive_rc(
943 rc: &mut Rc,
944 params: &Params,
945 state: &mut StateManager,
946 channels: [f32; RC_PACKET_CHANNELS],
947 ) {
948 rc.receive(&RcPacket {
949 header: RosflightPacketHeader {
950 timestamp: 100_000,
951 status: 0,
952 },
953 n_chan: 8,
954 chan: channels,
955 lol: false,
956 });
957 rc.run(100, params, state);
958 }
959
960 fn clear_logs() {
961 while Logger::pop().is_some() {}
962 }
963
964 fn drain_log_count() -> usize {
965 let mut count = 0;
966 while Logger::pop().is_some() {
967 count += 1;
968 }
969 count
970 }
971
972 fn set_unhealthy_estimator(state: &mut StateManager, params: &Params, unhealthy: bool) {
973 state.set_error_flag(ErrorFlag::UNHEALTHY_ESTIMATOR, unhealthy, params);
974 }
975
976 fn enable_estimator_angle_lockout(params: &mut Params) {
977 params.set_by_id(ParamId::PARAM_EST_ANGLE_LOCKOUT, ParamValue::Int(1));
978 }
979
980 fn offboard_msg(mode: OffboardControlMode) -> OffboardControlMsg {
981 OffboardControlMsg {
982 mode,
983 ignore: OffboardControlIgnore::empty(),
984 qx: 0.25,
985 qy: -0.5,
986 qz: 0.75,
987 fx: 0.0,
988 fy: 0.0,
989 fz: 0.4,
990 passthrough: [0.0; 4],
991 }
992 }
993
994 #[test]
995 fn rc_override_status_reports_upstream_stick_and_throttle_bits() {
996 let params = Params::new();
997 let mut state = StateManager::new();
998 let mut command = CommandManager::new();
999 let mut rc = initialized_rc(¶ms);
1000 let mut channels = [0.5; RC_PACKET_CHANNELS];
1001 channels[0] = 0.7;
1002 channels[2] = 0.2;
1003
1004 command.set_new_offboard_command(
1005 1_000_000,
1006 &OffboardControlMsg {
1007 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1008 ignore: OffboardControlIgnore::empty(),
1009 qx: 0.0,
1010 qy: 0.0,
1011 qz: 0.0,
1012 fx: 0.8,
1013 fy: 0.8,
1014 fz: 0.8,
1015 passthrough: [0.0; 4],
1016 },
1017 ¶ms,
1018 );
1019 receive_rc(&mut rc, ¶ms, &mut state, channels);
1020
1021 command.run(1000, ¶ms, &mut rc, &mut state);
1022
1023 assert_eq!(command.get_rc_override(), OVERRIDE_X | OVERRIDE_T);
1024 assert!(command.rc_override_active());
1025 }
1026
1027 #[test]
1028 fn take_min_throttle_uses_rc_when_rc_throttle_is_lower() {
1029 let mut params = Params::new();
1030 params.set_by_id(
1031 ParamId::PARAM_RC_OVERRIDE_TAKE_MIN_THROTTLE,
1032 ParamValue::Int(1),
1033 );
1034 let mut state = StateManager::new();
1035 let mut command = CommandManager::new();
1036 let mut rc = initialized_rc(¶ms);
1037 let mut channels = [0.5; RC_PACKET_CHANNELS];
1038 channels[2] = 0.2;
1039
1040 command.set_new_offboard_command(
1041 1_000_000,
1042 &OffboardControlMsg {
1043 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1044 ignore: OffboardControlIgnore::empty(),
1045 qx: 0.0,
1046 qy: 0.0,
1047 qz: 0.0,
1048 fx: 0.0,
1049 fy: 0.0,
1050 fz: 0.8,
1051 passthrough: [0.0; 4],
1052 },
1053 ¶ms,
1054 );
1055 receive_rc(&mut rc, ¶ms, &mut state, channels);
1056
1057 command.run(1000, ¶ms, &mut rc, &mut state);
1058
1059 let combined = command.combined_control();
1060 assert_eq!(command.get_rc_override(), OVERRIDE_T);
1061 assert_eq!(combined.fz.control_type, ControlType::Throttle);
1062 assert!((combined.fz.value - 0.2).abs() < 1e-6);
1063 }
1064
1065 #[test]
1066 fn take_min_throttle_keeps_offboard_when_offboard_throttle_is_lower() {
1067 let mut params = Params::new();
1068 params.set_by_id(
1069 ParamId::PARAM_RC_OVERRIDE_TAKE_MIN_THROTTLE,
1070 ParamValue::Int(1),
1071 );
1072 let mut state = StateManager::new();
1073 let mut command = CommandManager::new();
1074 let mut rc = initialized_rc(¶ms);
1075 let mut channels = [0.5; RC_PACKET_CHANNELS];
1076 channels[2] = 0.8;
1077
1078 command.set_new_offboard_command(
1079 1_000_000,
1080 &OffboardControlMsg {
1081 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1082 ignore: OffboardControlIgnore::empty(),
1083 qx: 0.0,
1084 qy: 0.0,
1085 qz: 0.0,
1086 fx: 0.0,
1087 fy: 0.0,
1088 fz: 0.2,
1089 passthrough: [0.0; 4],
1090 },
1091 ¶ms,
1092 );
1093 receive_rc(&mut rc, ¶ms, &mut state, channels);
1094
1095 command.run(1000, ¶ms, &mut rc, &mut state);
1096
1097 let combined = command.combined_control();
1098 assert_eq!(command.get_rc_override(), OVERRIDE_NO_OVERRIDE);
1099 assert_eq!(combined.fz.control_type, ControlType::Throttle);
1100 assert!((combined.fz.value - 0.2).abs() < 1e-6);
1101 }
1102
1103 #[test]
1104 fn take_min_throttle_uses_rc_when_throttle_switch_on_and_rc_throttle_is_lower() {
1105 let mut params = Params::new();
1106 params.set_by_id(
1107 ParamId::PARAM_RC_OVERRIDE_TAKE_MIN_THROTTLE,
1108 ParamValue::Int(1),
1109 );
1110 params.set_by_id(
1111 ParamId::PARAM_RC_THROTTLE_OVERRIDE_CHANNEL,
1112 ParamValue::Int(5),
1113 );
1114 let mut state = StateManager::new();
1115 let mut command = CommandManager::new();
1116 let mut rc = initialized_rc(¶ms);
1117 let mut channels = [0.5; RC_PACKET_CHANNELS];
1118 channels[2] = 0.2;
1119 channels[5] = 1.0;
1120
1121 command.set_new_offboard_command(
1122 1_000_000,
1123 &OffboardControlMsg {
1124 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1125 ignore: OffboardControlIgnore::empty(),
1126 qx: 0.0,
1127 qy: 0.0,
1128 qz: 0.0,
1129 fx: 0.0,
1130 fy: 0.0,
1131 fz: 0.8,
1132 passthrough: [0.0; 4],
1133 },
1134 ¶ms,
1135 );
1136 receive_rc(&mut rc, ¶ms, &mut state, channels);
1137
1138 command.run(1000, ¶ms, &mut rc, &mut state);
1139
1140 let combined = command.combined_control();
1141 assert_eq!(command.get_rc_override(), OVERRIDE_THR_SWITCH | OVERRIDE_T);
1142 assert_eq!(combined.fz.control_type, ControlType::Throttle);
1143 assert!((combined.fz.value - 0.2).abs() < 1e-6);
1144 }
1145
1146 #[test]
1147 fn take_min_throttle_with_passthrough_ned_thrust_documents_current_muxing() {
1148 let mut params = Params::new();
1149 params.set_by_id(
1150 ParamId::PARAM_RC_OVERRIDE_TAKE_MIN_THROTTLE,
1151 ParamValue::Int(1),
1152 );
1153 let mut state = StateManager::new();
1154 let mut command = CommandManager::new();
1155 let mut rc = initialized_rc(¶ms);
1156 let mut channels = [0.5; RC_PACKET_CHANNELS];
1157 channels[2] = 0.2;
1158
1159 command.set_new_offboard_command(
1160 1_000_000,
1161 &OffboardControlMsg {
1162 mode: OffboardControlMode::ModePassThrough,
1163 ignore: OffboardControlIgnore::empty(),
1164 qx: 0.0,
1165 qy: 0.0,
1166 qz: 0.0,
1167 fx: 0.0,
1168 fy: 0.0,
1169 fz: -25.0,
1170 passthrough: [0.0; 4],
1171 },
1172 ¶ms,
1173 );
1174 receive_rc(&mut rc, ¶ms, &mut state, channels);
1175
1176 command.run(1000, ¶ms, &mut rc, &mut state);
1177
1178 let combined = command.combined_control();
1179 assert_eq!(command.get_rc_override(), OVERRIDE_NO_OVERRIDE);
1180 assert_eq!(combined.fz.control_type, ControlType::Passthrough);
1181 assert_eq!(combined.fz.value, -25.0);
1182 }
1183
1184 #[test]
1185 fn rc_override_status_reports_inactive_offboard_channel_bits() {
1186 let params = Params::new();
1187 let mut state = StateManager::new();
1188 let mut command = CommandManager::new();
1189 let mut rc = initialized_rc(¶ms);
1190
1191 command.set_new_offboard_command(
1192 1_000_000,
1193 &OffboardControlMsg {
1194 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1195 ignore: OffboardControlIgnore::IGNORE_QY | OffboardControlIgnore::IGNORE_FZ,
1196 qx: 0.0,
1197 qy: 0.0,
1198 qz: 0.0,
1199 fx: 0.0,
1200 fy: 0.0,
1201 fz: 0.8,
1202 passthrough: [0.0; 4],
1203 },
1204 ¶ms,
1205 );
1206 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1207
1208 command.run(1000, ¶ms, &mut rc, &mut state);
1209
1210 assert_eq!(
1211 command.get_rc_override(),
1212 OVERRIDE_OFFBOARD_Y_INACTIVE | OVERRIDE_OFFBOARD_T_INACTIVE
1213 );
1214 assert!(command.rc_override_active());
1215 }
1216
1217 #[test]
1218 fn offboard_update_resolves_combined_command_without_new_rc_packet() {
1219 let mut params = Params::new();
1220 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1221 let mut state = StateManager::new();
1222 let mut command = CommandManager::new();
1223 let mut rc = initialized_rc(¶ms);
1224
1225 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1226 command.run(100, ¶ms, &mut rc, &mut state);
1227
1228 command.set_new_offboard_command(
1229 110_000,
1230 &OffboardControlMsg {
1231 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1232 ignore: OffboardControlIgnore::empty(),
1233 qx: 0.25,
1234 qy: -0.5,
1235 qz: 0.75,
1236 fx: 0.0,
1237 fy: 0.0,
1238 fz: 0.4,
1239 passthrough: [0.0; 4],
1240 },
1241 ¶ms,
1242 );
1243
1244 command.run(110, ¶ms, &mut rc, &mut state);
1245
1246 let combined = command.combined_control();
1247 assert_eq!(combined.qx.control_type, ControlType::Rate);
1248 assert_eq!(combined.qx.value, 0.25);
1249 assert_eq!(combined.qy.value, -0.5);
1250 assert_eq!(combined.qz.value, 0.75);
1251 assert_eq!(combined.fz.value, 0.4);
1252 assert_eq!(command.get_rc_override(), OVERRIDE_NO_OVERRIDE);
1253 }
1254
1255 #[test]
1256 fn healthy_estimator_allows_offboard_angle_mode() {
1257 let mut params = Params::new();
1258 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1259 let mut state = StateManager::new();
1260 let mut command = CommandManager::new();
1261 let mut rc = initialized_rc(¶ms);
1262
1263 command.set_new_offboard_command(
1264 1_000_000,
1265 &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1266 ¶ms,
1267 );
1268 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1269
1270 let result = command.run(1000, ¶ms, &mut rc, &mut state);
1271
1272 let combined = command.combined_control();
1273 assert_eq!(combined.qx.control_type, ControlType::Angle);
1274 assert_eq!(combined.qx.value, 0.25);
1275 assert_eq!(combined.qy.control_type, ControlType::Angle);
1276 assert_eq!(combined.qy.value, -0.5);
1277 assert_eq!(command.get_rc_override(), OVERRIDE_NO_OVERRIDE);
1278 assert!(!result.force_rc_attitude_mode_rate);
1279 }
1280
1281 #[test]
1282 fn unhealthy_estimator_allows_offboard_rate_and_passthrough_modes() {
1283 let mut params = Params::new();
1284 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1285 let mut state = StateManager::new();
1286 let mut command = CommandManager::new();
1287 let mut rc = initialized_rc(¶ms);
1288 set_unhealthy_estimator(&mut state, ¶ms, true);
1289
1290 command.set_new_offboard_command(
1291 1_000_000,
1292 &offboard_msg(OffboardControlMode::ModeRollratePitchrateYawrateThrottle),
1293 ¶ms,
1294 );
1295 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1296 let result = command.run(1000, ¶ms, &mut rc, &mut state);
1297 assert_eq!(
1298 command.combined_control().qx.control_type,
1299 ControlType::Rate
1300 );
1301 assert_eq!(command.combined_control().qx.value, 0.25);
1302 assert!(!result.force_rc_attitude_mode_rate);
1303
1304 command.set_new_offboard_command(
1305 1_001_000,
1306 &offboard_msg(OffboardControlMode::ModePassThrough),
1307 ¶ms,
1308 );
1309 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1310 let result = command.run(1001, ¶ms, &mut rc, &mut state);
1311 assert_eq!(
1312 command.combined_control().qx.control_type,
1313 ControlType::Passthrough
1314 );
1315 assert_eq!(command.combined_control().qx.value, 0.25);
1316 assert!(!result.force_rc_attitude_mode_rate);
1317 }
1318
1319 #[test]
1320 fn unhealthy_estimator_allows_angle_modes_when_lockout_disabled() {
1321 let mut params = Params::new();
1322 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1323 params.set_by_id(
1324 ParamId::PARAM_RC_ATT_CONTROL_TYPE_CHANNEL,
1325 ParamValue::Int(5),
1326 );
1327 let mut state = StateManager::new();
1328 let mut command = CommandManager::new();
1329 let mut rc = initialized_rc(¶ms);
1330 let mut channels = [0.5; RC_PACKET_CHANNELS];
1331 channels[5] = 1.0;
1332 set_unhealthy_estimator(&mut state, ¶ms, true);
1333
1334 command.set_new_offboard_command(
1335 1_000_000,
1336 &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1337 ¶ms,
1338 );
1339 receive_rc(&mut rc, ¶ms, &mut state, channels);
1340
1341 let result = command.run(1000, ¶ms, &mut rc, &mut state);
1342
1343 let combined = command.combined_control();
1344 assert_eq!(combined.qx.control_type, ControlType::Angle);
1345 assert_eq!(combined.qx.value, 0.25);
1346 assert_eq!(combined.qy.control_type, ControlType::Angle);
1347 assert_eq!(combined.qy.value, -0.5);
1348 assert!(!result.force_rc_attitude_mode_rate);
1349 }
1350
1351 #[test]
1352 fn unhealthy_estimator_denies_offboard_angle_and_uses_rc_rate() {
1353 let mut params = Params::new();
1354 enable_estimator_angle_lockout(&mut params);
1355 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1356 params.set_by_id(ParamId::PARAM_RC_MAX_ROLLRATE, ParamValue::Float(2.0));
1357 params.set_by_id(ParamId::PARAM_RC_MAX_PITCHRATE, ParamValue::Float(3.0));
1358 let mut state = StateManager::new();
1359 let mut command = CommandManager::new();
1360 let mut rc = initialized_rc(¶ms);
1361 let mut channels = [0.5; RC_PACKET_CHANNELS];
1362 channels[0] = 0.75;
1363 channels[1] = 0.25;
1364 set_unhealthy_estimator(&mut state, ¶ms, true);
1365
1366 command.set_new_offboard_command(
1367 1_000_000,
1368 &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1369 ¶ms,
1370 );
1371 receive_rc(&mut rc, ¶ms, &mut state, channels);
1372
1373 let result = command.run(1000, ¶ms, &mut rc, &mut state);
1374
1375 let combined = command.combined_control();
1376 assert_eq!(combined.qx.control_type, ControlType::Rate);
1377 assert!((combined.qx.value - 1.0).abs() < 1e-6);
1378 assert_eq!(combined.qy.control_type, ControlType::Rate);
1379 assert!((combined.qy.value + 1.5).abs() < 1e-6);
1380 assert_eq!(
1381 command.get_rc_override(),
1382 OVERRIDE_ATT_SWITCH | OVERRIDE_X | OVERRIDE_Y
1383 );
1384 assert!(result.force_rc_attitude_mode_rate);
1385 }
1386
1387 #[test]
1388 fn unhealthy_estimator_denies_rc_angle_switch_and_logs_without_spam() {
1389 clear_logs();
1390 let mut params = Params::new();
1391 enable_estimator_angle_lockout(&mut params);
1392 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1393 params.set_by_id(
1394 ParamId::PARAM_RC_ATT_CONTROL_TYPE_CHANNEL,
1395 ParamValue::Int(5),
1396 );
1397 let mut state = StateManager::new();
1398 let mut command = CommandManager::new();
1399 let mut rc = initialized_rc(¶ms);
1400 let mut channels = [0.5; RC_PACKET_CHANNELS];
1401 channels[5] = 1.0;
1402 set_unhealthy_estimator(&mut state, ¶ms, true);
1403
1404 receive_rc(&mut rc, ¶ms, &mut state, channels);
1405 let first = command.run(1000, ¶ms, &mut rc, &mut state);
1406 receive_rc(&mut rc, ¶ms, &mut state, channels);
1407 let second = command.run(1001, ¶ms, &mut rc, &mut state);
1408
1409 assert_eq!(
1410 command.combined_control().qx.control_type,
1411 ControlType::Rate
1412 );
1413 assert_eq!(
1414 command.get_rc_override(),
1415 OVERRIDE_OFFBOARD_X_INACTIVE
1416 | OVERRIDE_OFFBOARD_Y_INACTIVE
1417 | OVERRIDE_OFFBOARD_Z_INACTIVE
1418 | OVERRIDE_OFFBOARD_T_INACTIVE
1419 );
1420 assert!(!first.force_rc_attitude_mode_rate);
1421 assert!(!second.force_rc_attitude_mode_rate);
1422 assert_eq!(drain_log_count(), 2);
1423 }
1424
1425 #[test]
1426 fn switch_reset_before_estimator_recovery_waits_then_unlocks_on_recovery() {
1427 clear_logs();
1428 let mut params = Params::new();
1429 enable_estimator_angle_lockout(&mut params);
1430 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1431 params.set_by_id(
1432 ParamId::PARAM_RC_ATT_CONTROL_TYPE_CHANNEL,
1433 ParamValue::Int(5),
1434 );
1435 let mut state = StateManager::new();
1436 let mut command = CommandManager::new();
1437 let mut rc = initialized_rc(¶ms);
1438 let mut channels = [0.5; RC_PACKET_CHANNELS];
1439 channels[5] = 1.0;
1440 set_unhealthy_estimator(&mut state, ¶ms, true);
1441
1442 receive_rc(&mut rc, ¶ms, &mut state, channels);
1443 command.run(1000, ¶ms, &mut rc, &mut state);
1444 assert_eq!(drain_log_count(), 2);
1445
1446 channels[5] = 0.0;
1447 receive_rc(&mut rc, ¶ms, &mut state, channels);
1448 command.run(1001, ¶ms, &mut rc, &mut state);
1449 assert_eq!(drain_log_count(), 1);
1450 assert_eq!(
1451 command.combined_control().qx.control_type,
1452 ControlType::Rate
1453 );
1454
1455 receive_rc(&mut rc, ¶ms, &mut state, channels);
1456 command.run(1002, ¶ms, &mut rc, &mut state);
1457 assert_eq!(drain_log_count(), 0);
1458
1459 set_unhealthy_estimator(&mut state, ¶ms, false);
1460 receive_rc(&mut rc, ¶ms, &mut state, channels);
1461 command.run(1003, ¶ms, &mut rc, &mut state);
1462 assert_eq!(drain_log_count(), 1);
1463
1464 channels[5] = 1.0;
1465 receive_rc(&mut rc, ¶ms, &mut state, channels);
1466 command.run(1004, ¶ms, &mut rc, &mut state);
1467 assert_eq!(
1468 command.combined_control().qx.control_type,
1469 ControlType::Angle
1470 );
1471 }
1472
1473 #[test]
1474 fn estimator_recovery_before_switch_reset_unlocks_when_switch_moves_to_rate() {
1475 clear_logs();
1476 let mut params = Params::new();
1477 enable_estimator_angle_lockout(&mut params);
1478 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1479 params.set_by_id(
1480 ParamId::PARAM_RC_ATT_CONTROL_TYPE_CHANNEL,
1481 ParamValue::Int(5),
1482 );
1483 let mut state = StateManager::new();
1484 let mut command = CommandManager::new();
1485 let mut rc = initialized_rc(¶ms);
1486 let mut channels = [0.5; RC_PACKET_CHANNELS];
1487 channels[5] = 1.0;
1488 set_unhealthy_estimator(&mut state, ¶ms, true);
1489
1490 receive_rc(&mut rc, ¶ms, &mut state, channels);
1491 command.run(1000, ¶ms, &mut rc, &mut state);
1492 assert_eq!(drain_log_count(), 2);
1493
1494 set_unhealthy_estimator(&mut state, ¶ms, false);
1495 receive_rc(&mut rc, ¶ms, &mut state, channels);
1496 command.run(1001, ¶ms, &mut rc, &mut state);
1497 assert_eq!(
1498 command.combined_control().qx.control_type,
1499 ControlType::Rate
1500 );
1501 assert_eq!(drain_log_count(), 1);
1502
1503 channels[5] = 0.0;
1504 receive_rc(&mut rc, ¶ms, &mut state, channels);
1505 command.run(1002, ¶ms, &mut rc, &mut state);
1506 assert_eq!(
1507 command.combined_control().qx.control_type,
1508 ControlType::Rate
1509 );
1510 assert_eq!(drain_log_count(), 0);
1511
1512 channels[5] = 1.0;
1513 receive_rc(&mut rc, ¶ms, &mut state, channels);
1514 command.run(1003, ¶ms, &mut rc, &mut state);
1515 assert_eq!(
1516 command.combined_control().qx.control_type,
1517 ControlType::Angle
1518 );
1519 }
1520
1521 #[test]
1522 fn switch_must_be_rate_at_unlock_even_if_rate_was_observed_before_recovery() {
1523 clear_logs();
1524 let mut params = Params::new();
1525 enable_estimator_angle_lockout(&mut params);
1526 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1527 params.set_by_id(
1528 ParamId::PARAM_RC_ATT_CONTROL_TYPE_CHANNEL,
1529 ParamValue::Int(5),
1530 );
1531 let mut state = StateManager::new();
1532 let mut command = CommandManager::new();
1533 let mut rc = initialized_rc(¶ms);
1534 let mut channels = [0.5; RC_PACKET_CHANNELS];
1535 channels[5] = 1.0;
1536 set_unhealthy_estimator(&mut state, ¶ms, true);
1537
1538 receive_rc(&mut rc, ¶ms, &mut state, channels);
1539 command.run(1000, ¶ms, &mut rc, &mut state);
1540 assert_eq!(drain_log_count(), 2);
1541
1542 channels[5] = 0.0;
1543 receive_rc(&mut rc, ¶ms, &mut state, channels);
1544 command.run(1001, ¶ms, &mut rc, &mut state);
1545 assert_eq!(drain_log_count(), 1);
1546
1547 channels[5] = 1.0;
1548 receive_rc(&mut rc, ¶ms, &mut state, channels);
1549 command.run(1002, ¶ms, &mut rc, &mut state);
1550 assert_eq!(drain_log_count(), 2);
1551
1552 set_unhealthy_estimator(&mut state, ¶ms, false);
1553 receive_rc(&mut rc, ¶ms, &mut state, channels);
1554 command.run(1003, ¶ms, &mut rc, &mut state);
1555 assert_eq!(
1556 command.combined_control().qx.control_type,
1557 ControlType::Rate
1558 );
1559 assert_eq!(drain_log_count(), 1);
1560
1561 channels[5] = 0.0;
1562 receive_rc(&mut rc, ¶ms, &mut state, channels);
1563 command.run(1004, ¶ms, &mut rc, &mut state);
1564 assert_eq!(
1565 command.combined_control().qx.control_type,
1566 ControlType::Rate
1567 );
1568
1569 channels[5] = 1.0;
1570 receive_rc(&mut rc, ¶ms, &mut state, channels);
1571 command.run(1005, ¶ms, &mut rc, &mut state);
1572 assert_eq!(
1573 command.combined_control().qx.control_type,
1574 ControlType::Angle
1575 );
1576 }
1577
1578 #[test]
1579 fn no_switch_angle_mode_requests_param_rate_until_explicit_angle_after_recovery() {
1580 clear_logs();
1581 let mut params = Params::new();
1582 enable_estimator_angle_lockout(&mut params);
1583 params.set_by_id(
1584 ParamId::PARAM_RC_ATTITUDE_MODE,
1585 ParamValue::Int(ATTITUDE_ANGLE_MODE),
1586 );
1587 let mut state = StateManager::new();
1588 let mut command = CommandManager::new();
1589 let mut rc = initialized_rc(¶ms);
1590 set_unhealthy_estimator(&mut state, ¶ms, true);
1591
1592 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1593 let result = command.run(1000, ¶ms, &mut rc, &mut state);
1594 assert_eq!(
1595 command.combined_control().qx.control_type,
1596 ControlType::Rate
1597 );
1598 assert!(result.force_rc_attitude_mode_rate);
1599 assert_eq!(drain_log_count(), 2);
1600
1601 params.set_by_id(
1602 ParamId::PARAM_RC_ATTITUDE_MODE,
1603 ParamValue::Int(ATTITUDE_RATE_MODE),
1604 );
1605 set_unhealthy_estimator(&mut state, ¶ms, false);
1606 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1607 command.run(1001, ¶ms, &mut rc, &mut state);
1608 assert_eq!(
1609 command.combined_control().qx.control_type,
1610 ControlType::Rate
1611 );
1612 assert_eq!(drain_log_count(), 1);
1613
1614 params.set_by_id(
1615 ParamId::PARAM_RC_ATTITUDE_MODE,
1616 ParamValue::Int(ATTITUDE_ANGLE_MODE),
1617 );
1618 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1619 command.run(1002, ¶ms, &mut rc, &mut state);
1620 assert_eq!(
1621 command.combined_control().qx.control_type,
1622 ControlType::Angle
1623 );
1624 }
1625
1626 #[test]
1627 fn offboard_timeout_resolves_back_to_rc_without_new_rc_packet() {
1628 let mut params = Params::new();
1629 params.set_by_id(ParamId::PARAM_OFFBOARD_TIMEOUT, ParamValue::Int(100));
1630 params.set_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME, ParamValue::Int(0));
1631 let mut state = StateManager::new();
1632 let mut command = CommandManager::new();
1633 let mut rc = initialized_rc(¶ms);
1634
1635 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1636 command.run(100, ¶ms, &mut rc, &mut state);
1637 let rc_qx = command.rc_control().qx.value;
1638 let rc_fz = command.rc_control().fz.value;
1639
1640 command.set_new_offboard_command(
1641 110_000,
1642 &OffboardControlMsg {
1643 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1644 ignore: OffboardControlIgnore::empty(),
1645 qx: -0.25,
1646 qy: 0.5,
1647 qz: -0.75,
1648 fx: 0.0,
1649 fy: 0.0,
1650 fz: 0.4,
1651 passthrough: [0.0; 4],
1652 },
1653 ¶ms,
1654 );
1655 command.run(110, ¶ms, &mut rc, &mut state);
1656 assert_eq!(command.combined_control().qx.value, -0.25);
1657
1658 command.run(211, ¶ms, &mut rc, &mut state);
1659
1660 let combined = command.combined_control();
1661 assert_eq!(combined.qx.value, rc_qx);
1662 assert_eq!(combined.fz.value, rc_fz);
1663 assert_eq!(
1664 command.get_rc_override(),
1665 OVERRIDE_OFFBOARD_X_INACTIVE
1666 | OVERRIDE_OFFBOARD_Y_INACTIVE
1667 | OVERRIDE_OFFBOARD_Z_INACTIVE
1668 | OVERRIDE_OFFBOARD_T_INACTIVE
1669 );
1670 }
1671
1672 #[test]
1673 fn throttle_switch_override_still_reports_inactive_offboard_throttle() {
1674 let mut params = Params::new();
1675 params.set_by_id(
1676 ParamId::PARAM_RC_THROTTLE_OVERRIDE_CHANNEL,
1677 ParamValue::Int(5),
1678 );
1679 let mut state = StateManager::new();
1680 let mut command = CommandManager::new();
1681 let mut rc = initialized_rc(¶ms);
1682 let mut channels = [0.5; RC_PACKET_CHANNELS];
1683 channels[5] = 1.0;
1684
1685 command.set_new_offboard_command(
1686 1_000_000,
1687 &OffboardControlMsg {
1688 mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
1689 ignore: OffboardControlIgnore::IGNORE_FZ,
1690 qx: 0.0,
1691 qy: 0.0,
1692 qz: 0.0,
1693 fx: 0.0,
1694 fy: 0.0,
1695 fz: 0.8,
1696 passthrough: [0.0; 4],
1697 },
1698 ¶ms,
1699 );
1700 receive_rc(&mut rc, ¶ms, &mut state, channels);
1701
1702 command.run(1000, ¶ms, &mut rc, &mut state);
1703
1704 assert_eq!(
1705 command.get_rc_override(),
1706 OVERRIDE_THR_SWITCH | OVERRIDE_OFFBOARD_T_INACTIVE
1707 );
1708 assert!(command.rc_override_active());
1709 }
1710
1711 #[test]
1712 fn rc_roll_and_pitch_scale_on_their_own_axes() {
1713 let params = Params::new();
1714 let mut state = StateManager::new();
1715 let mut command = CommandManager::new();
1716 let mut rc = initialized_rc(¶ms);
1717 let mut channels = [0.5; RC_PACKET_CHANNELS];
1718 channels[0] = 0.75;
1719 channels[1] = 0.25;
1720
1721 receive_rc(&mut rc, ¶ms, &mut state, channels);
1722 command.interpret_rc(&rc, ¶ms, None);
1723
1724 assert_eq!(command.rc_control().qx.control_type, ControlType::Angle);
1725 assert_eq!(command.rc_control().qy.control_type, ControlType::Angle);
1726 assert!((command.rc_control().qx.value - 0.393).abs() < 1e-6);
1727 assert!((command.rc_control().qy.value + 0.393).abs() < 1e-6);
1728 }
1729
1730 #[test]
1731 fn offboard_passthrough_channels_survive_muxing() {
1732 let params = Params::new();
1733 let mut state = StateManager::new();
1734 let mut command = CommandManager::new();
1735 let mut rc = initialized_rc(¶ms);
1736
1737 command.set_new_offboard_command(
1738 1_000_000,
1739 &OffboardControlMsg {
1740 mode: OffboardControlMode::ModePassThrough,
1741 ignore: OffboardControlIgnore::empty(),
1742 qx: 0.0,
1743 qy: 0.0,
1744 qz: 0.0,
1745 fx: 0.0,
1746 fy: 0.0,
1747 fz: 0.5,
1748 passthrough: [0.6, 0.7, 0.8, 0.9],
1749 },
1750 ¶ms,
1751 );
1752 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1753
1754 command.run(1000, ¶ms, &mut rc, &mut state);
1755
1756 let passthrough = command.combined_control().passthrough;
1757 for (channel, expected) in passthrough.iter().zip([0.6, 0.7, 0.8, 0.9]) {
1758 assert!((channel.value - expected).abs() < 1e-6);
1759 }
1760 assert!(passthrough.iter().all(|channel| channel.active));
1761 assert!(
1762 passthrough
1763 .iter()
1764 .all(|channel| channel.control_type == ControlType::Passthrough)
1765 );
1766 }
1767
1768 #[test]
1769 fn offboard_passthrough_keeps_ned_thrust_sign_for_mixer_commands() {
1770 let params = Params::new();
1771 let mut state = StateManager::new();
1772 let mut command = CommandManager::new();
1773 let mut rc = initialized_rc(¶ms);
1774
1775 command.set_new_offboard_command(
1776 1_000_000,
1777 &OffboardControlMsg {
1778 mode: OffboardControlMode::ModePassThrough,
1779 ignore: OffboardControlIgnore::empty(),
1780 qx: 0.01,
1781 qy: -0.02,
1782 qz: 0.03,
1783 fx: 0.0,
1784 fy: 0.0,
1785 fz: -25.0,
1786 passthrough: [0.0; 4],
1787 },
1788 ¶ms,
1789 );
1790 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1791
1792 command.run(1000, ¶ms, &mut rc, &mut state);
1793
1794 let combined = command.combined_control();
1795 assert_eq!(combined.fz.control_type, ControlType::Passthrough);
1796 assert_eq!(combined.fz.value, -25.0);
1797 assert_eq!(combined.qx.control_type, ControlType::Passthrough);
1798 assert_eq!(combined.qx.value, 0.01);
1799 }
1800
1801 #[test]
1802 fn failsafe_commands_match_rosflight_channel_types() {
1803 let command = CommandManager::new();
1804
1805 assert_eq!(
1806 command.multirotor_failsafe_command.fx.control_type,
1807 ControlType::Throttle
1808 );
1809 assert_eq!(
1810 command.multirotor_failsafe_command.fy.control_type,
1811 ControlType::Throttle
1812 );
1813 assert_eq!(
1814 command.multirotor_failsafe_command.fz.control_type,
1815 ControlType::Throttle
1816 );
1817 assert_eq!(
1818 command.multirotor_failsafe_command.qx.control_type,
1819 ControlType::Angle
1820 );
1821 assert_eq!(
1822 command.multirotor_failsafe_command.qy.control_type,
1823 ControlType::Angle
1824 );
1825 assert_eq!(
1826 command.multirotor_failsafe_command.qz.control_type,
1827 ControlType::Rate
1828 );
1829 assert!(command.multirotor_failsafe_command.fx.active);
1830 assert!(command.multirotor_failsafe_command.fy.active);
1831 assert!(command.multirotor_failsafe_command.fz.active);
1832
1833 assert!(command.fixedwing_failsafe_command.fx.active);
1834 assert!(command.fixedwing_failsafe_command.fy.active);
1835 assert!(command.fixedwing_failsafe_command.fz.active);
1836 assert_eq!(
1837 command.fixedwing_failsafe_command.fz.control_type,
1838 ControlType::Passthrough
1839 );
1840 }
1841
1842 #[test]
1843 fn fixedwing_rc_command_uses_passthrough_channel_types() {
1844 let mut params = Params::new();
1845 params.set_by_id(ParamId::PARAM_FIXED_WING, ParamValue::Int(1));
1846 let mut state = StateManager::new();
1847 let mut command = CommandManager::new();
1848 let mut rc = initialized_rc(¶ms);
1849
1850 receive_rc(&mut rc, ¶ms, &mut state, [0.5; RC_PACKET_CHANNELS]);
1851 command.interpret_rc(&rc, ¶ms, None);
1852
1853 let rc_control = command.rc_control();
1854 assert_eq!(rc_control.qx.control_type, ControlType::Passthrough);
1855 assert_eq!(rc_control.qy.control_type, ControlType::Passthrough);
1856 assert_eq!(rc_control.qz.control_type, ControlType::Passthrough);
1857 assert_eq!(rc_control.fx.control_type, ControlType::Passthrough);
1858 assert_eq!(rc_control.fy.control_type, ControlType::Passthrough);
1859 assert_eq!(rc_control.fz.control_type, ControlType::Passthrough);
1860 }
1861
1862 #[test]
1863 fn fixedwing_failsafe_accepts_passthrough_throttle_outside_multirotor_range() {
1864 let mut params = Params::new();
1865 params.set_by_id(ParamId::PARAM_FIXED_WING, ParamValue::Int(1));
1866 params.set_by_id(ParamId::PARAM_FAILSAFE_THROTTLE, ParamValue::Float(1.5));
1867 let mut state = StateManager::new();
1868 let mut command = CommandManager::new();
1869
1870 command.update_failsafe_config(¶ms, &mut state);
1871
1872 assert!(!state.get_errors().contains(ErrorFlag::INVALID_FAILSAFE));
1873 assert_eq!(
1874 command.fixedwing_failsafe_command.fz.control_type,
1875 ControlType::Passthrough
1876 );
1877 assert_eq!(command.fixedwing_failsafe_command.fz.value, 0.0);
1878 }
1879}