Skip to main content

veloxity_core/
command.rs

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,        // Channel is is in rate mode (rad/s)
14    Angle,       // Channel command is in angle mode (rad)
15    Throttle,    // Channel is controlling throttle setting
16    Passthrough, // Channel directly passes PWM input to the mixer
17}
18
19// simpler than the enum representation in c++ command_manager.h
20pub(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    // Command structs
225    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], // for x, y, and z sticks
232
233    // State flags
234    rc_throttle_override: bool,
235    rc_attitude_override: bool,
236    rc_override: u16,
237    rc_angle_mode_lockout: RcAngleModeLockoutMachine,
238}
239
240impl CommandManager {
241    /// Creates a new CommandManager with default initial values.
242    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            // The "happy path": it's an Int, as expected
342            ParamValue::Int(axis) => {
343                match axis {
344                    // Note: `axis` is an i32, not `&i32`, so no deref `*` needed
345                    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            // Error case: it's the wrong type.
351            // We log the error and apply a safe default (Fz).
352            _ => {
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, // <-- Must be mutable
368    ) -> 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        // --- 1. Failsafe Action (C++ lines 240-243) ---
379        // This is the highest priority. If in failsafe, override all commands.
380        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; // Exit early
391        }
392
393        // --- 3. Offboard Timeout "Fail-over" (C++ lines 246-252) ---
394        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        // --- 2. RC Update (C++ line 244) ---
400        // Fresh RC packets update the RC input command. Combined command resolution runs below
401        // even without fresh RC so offboard updates and timeouts are not gated by RC cadence.
402        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        // --- 4. Muxing (C++ lines 253-256) ---
412        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    /// Receives a new offboard control command.
439    /// This should be called from CommManager::act_on_messages
440    pub fn set_new_offboard_command(
441        &mut self,
442        now_us: u64,
443        msg: &OffboardControlMsg,
444        _params: &Params,
445    ) {
446        // We got a new command, so update the timestamp
447        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    /// Port of C++ `interpret_rc` (command_manager.cpp:101)
510    fn interpret_rc(
511        &mut self,
512        rc: &Rc,
513        params: &Params,
514        forced_roll_pitch_type: Option<ControlType>,
515    ) {
516        // Read all stick values from the RC unit
517        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        // C++: line 109, logic for F-axis (type-safe)
523        match params.get_by_id(ParamId::PARAM_RC_F_AXIS) {
524            // "Happy path": it's an Int as expected
525            ParamValue::Int(axis) => {
526                match axis {
527                    0 => {
528                        // X_AXIS
529                        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                        // Y_AXIS
535                        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                        // Z_AXIS (default)
541                        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            // Error case: param is wrong type!
548            _ => {
549                // Default to Z_AXIS for safety
550                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        // All RC channels are always active... active for rc doesn't really mean anything. Active on offboard command means override rc...
557        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        // C++: lines 130-153
565        let is_fixed_wing = match params.get_by_id(ParamId::PARAM_FIXED_WING) {
566            ParamValue::Int(val) => val != 0,
567            _ => {
568                // This is a param definition error. Default to the safer
569                // (non-fixed-wing) case.
570                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            // check if we've mapped the AttType channel...
582            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 we have, probe it to know if we should use rate or angle for qx and qy
586                if rc.switch_on(Switch::AttType) {
587                    ControlType::Angle
588                } else {
589                    ControlType::Rate
590                }
591            } else {
592                // if not, fall back to the parameter Attitude mode
593                let att_mode = match params.get_by_id(ParamId::PARAM_RC_ATTITUDE_MODE) {
594                    ParamValue::Int(val) => val,
595                    _ => {
596                        200 // Default value from C++ params
597                    }
598                };
599                match att_mode {
600                    ATTITUDE_RATE_MODE => ControlType::Rate,
601                    _ => {
602                        // if we're not in rate mode, we're in pitch mode...
603                        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    /// This is the C++ `run` function's muxing logic
653    fn do_muxing(&mut self, params: &Params, rc: &Rc, now_ms: u32, force_rc_attitude: bool) {
654        // C++: command_manager.cpp (lines 253-256)
655        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 // Default value from C++ params
694            }
695        };
696
697        // --- REFACTORED: PARAM_OVERRIDE_LAG_TIME ---
698        let lag_time_ms = match params.get_by_id(ParamId::PARAM_OVERRIDE_LAG_TIME) {
699            ParamValue::Int(val) => val as u32,
700            _ => {
701                200 // Default value from C++ params
702            }
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 // Default value from C++ params
822            }
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        // We assume the qx (roll) channel represents the
873        // primary attitude control mode.
874        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    /// Port of C++ `offboard_control_active` (line 220)
886    pub fn is_offboard_active(&self) -> bool {
887        // This is true if *any* offboard channel is active
888        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(&params);
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            &params,
1018        );
1019        receive_rc(&mut rc, &params, &mut state, channels);
1020
1021        command.run(1000, &params, &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(&params);
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            &params,
1054        );
1055        receive_rc(&mut rc, &params, &mut state, channels);
1056
1057        command.run(1000, &params, &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(&params);
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            &params,
1092        );
1093        receive_rc(&mut rc, &params, &mut state, channels);
1094
1095        command.run(1000, &params, &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(&params);
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            &params,
1135        );
1136        receive_rc(&mut rc, &params, &mut state, channels);
1137
1138        command.run(1000, &params, &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(&params);
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            &params,
1173        );
1174        receive_rc(&mut rc, &params, &mut state, channels);
1175
1176        command.run(1000, &params, &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(&params);
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            &params,
1205        );
1206        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1207
1208        command.run(1000, &params, &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(&params);
1224
1225        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1226        command.run(100, &params, &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            &params,
1242        );
1243
1244        command.run(110, &params, &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(&params);
1262
1263        command.set_new_offboard_command(
1264            1_000_000,
1265            &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1266            &params,
1267        );
1268        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1269
1270        let result = command.run(1000, &params, &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(&params);
1288        set_unhealthy_estimator(&mut state, &params, true);
1289
1290        command.set_new_offboard_command(
1291            1_000_000,
1292            &offboard_msg(OffboardControlMode::ModeRollratePitchrateYawrateThrottle),
1293            &params,
1294        );
1295        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1296        let result = command.run(1000, &params, &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            &params,
1308        );
1309        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1310        let result = command.run(1001, &params, &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(&params);
1330        let mut channels = [0.5; RC_PACKET_CHANNELS];
1331        channels[5] = 1.0;
1332        set_unhealthy_estimator(&mut state, &params, true);
1333
1334        command.set_new_offboard_command(
1335            1_000_000,
1336            &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1337            &params,
1338        );
1339        receive_rc(&mut rc, &params, &mut state, channels);
1340
1341        let result = command.run(1000, &params, &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(&params);
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, &params, true);
1365
1366        command.set_new_offboard_command(
1367            1_000_000,
1368            &offboard_msg(OffboardControlMode::ModeRollPitchYawrateThrottle),
1369            &params,
1370        );
1371        receive_rc(&mut rc, &params, &mut state, channels);
1372
1373        let result = command.run(1000, &params, &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(&params);
1400        let mut channels = [0.5; RC_PACKET_CHANNELS];
1401        channels[5] = 1.0;
1402        set_unhealthy_estimator(&mut state, &params, true);
1403
1404        receive_rc(&mut rc, &params, &mut state, channels);
1405        let first = command.run(1000, &params, &mut rc, &mut state);
1406        receive_rc(&mut rc, &params, &mut state, channels);
1407        let second = command.run(1001, &params, &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(&params);
1438        let mut channels = [0.5; RC_PACKET_CHANNELS];
1439        channels[5] = 1.0;
1440        set_unhealthy_estimator(&mut state, &params, true);
1441
1442        receive_rc(&mut rc, &params, &mut state, channels);
1443        command.run(1000, &params, &mut rc, &mut state);
1444        assert_eq!(drain_log_count(), 2);
1445
1446        channels[5] = 0.0;
1447        receive_rc(&mut rc, &params, &mut state, channels);
1448        command.run(1001, &params, &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, &params, &mut state, channels);
1456        command.run(1002, &params, &mut rc, &mut state);
1457        assert_eq!(drain_log_count(), 0);
1458
1459        set_unhealthy_estimator(&mut state, &params, false);
1460        receive_rc(&mut rc, &params, &mut state, channels);
1461        command.run(1003, &params, &mut rc, &mut state);
1462        assert_eq!(drain_log_count(), 1);
1463
1464        channels[5] = 1.0;
1465        receive_rc(&mut rc, &params, &mut state, channels);
1466        command.run(1004, &params, &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(&params);
1486        let mut channels = [0.5; RC_PACKET_CHANNELS];
1487        channels[5] = 1.0;
1488        set_unhealthy_estimator(&mut state, &params, true);
1489
1490        receive_rc(&mut rc, &params, &mut state, channels);
1491        command.run(1000, &params, &mut rc, &mut state);
1492        assert_eq!(drain_log_count(), 2);
1493
1494        set_unhealthy_estimator(&mut state, &params, false);
1495        receive_rc(&mut rc, &params, &mut state, channels);
1496        command.run(1001, &params, &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, &params, &mut state, channels);
1505        command.run(1002, &params, &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, &params, &mut state, channels);
1514        command.run(1003, &params, &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(&params);
1534        let mut channels = [0.5; RC_PACKET_CHANNELS];
1535        channels[5] = 1.0;
1536        set_unhealthy_estimator(&mut state, &params, true);
1537
1538        receive_rc(&mut rc, &params, &mut state, channels);
1539        command.run(1000, &params, &mut rc, &mut state);
1540        assert_eq!(drain_log_count(), 2);
1541
1542        channels[5] = 0.0;
1543        receive_rc(&mut rc, &params, &mut state, channels);
1544        command.run(1001, &params, &mut rc, &mut state);
1545        assert_eq!(drain_log_count(), 1);
1546
1547        channels[5] = 1.0;
1548        receive_rc(&mut rc, &params, &mut state, channels);
1549        command.run(1002, &params, &mut rc, &mut state);
1550        assert_eq!(drain_log_count(), 2);
1551
1552        set_unhealthy_estimator(&mut state, &params, false);
1553        receive_rc(&mut rc, &params, &mut state, channels);
1554        command.run(1003, &params, &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, &params, &mut state, channels);
1563        command.run(1004, &params, &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, &params, &mut state, channels);
1571        command.run(1005, &params, &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(&params);
1590        set_unhealthy_estimator(&mut state, &params, true);
1591
1592        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1593        let result = command.run(1000, &params, &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, &params, false);
1606        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1607        command.run(1001, &params, &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, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1619        command.run(1002, &params, &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(&params);
1634
1635        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1636        command.run(100, &params, &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            &params,
1654        );
1655        command.run(110, &params, &mut rc, &mut state);
1656        assert_eq!(command.combined_control().qx.value, -0.25);
1657
1658        command.run(211, &params, &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(&params);
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            &params,
1699        );
1700        receive_rc(&mut rc, &params, &mut state, channels);
1701
1702        command.run(1000, &params, &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(&params);
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, &params, &mut state, channels);
1722        command.interpret_rc(&rc, &params, 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(&params);
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            &params,
1751        );
1752        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1753
1754        command.run(1000, &params, &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(&params);
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            &params,
1789        );
1790        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1791
1792        command.run(1000, &params, &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(&params);
1849
1850        receive_rc(&mut rc, &params, &mut state, [0.5; RC_PACKET_CHANNELS]);
1851        command.interpret_rc(&rc, &params, 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(&params, &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}