Skip to main content

veloxity_core/
comm.rs

1pub mod interface;
2pub mod messages;
3
4use crate::board::{self, BoardIo};
5use crate::comm::messages::{Messages, Store, enums::*, messages::*};
6use crate::command::CommandManager;
7use crate::estimator::AttitudeEstimate;
8use crate::events::{
9    AuxCommandReceived, BoardCommandRequested, CalibrationRequested, CommEventQueues, CommResponse,
10    CommandEventQueues, CompanionEventQueues, CompanionHeartbeatReceived, ConfigInfoRequested,
11    ExternalAttitudeReceived, OffboardControlRequested, ParamDefaultsRequested, ParamEventQueues,
12    ParamListRequested, ParamReadRequested, ParamSetRequested, RcTrimCalibrationRequested,
13    ResetOriginRequested, VersionRequested,
14};
15use crate::math::FlightFloat;
16use crate::packets::{RC_PACKET_CHANNELS, RangeType};
17use crate::params::{ParamId, ParamValue, Params};
18use crate::sensors::ProcessedSensors;
19use crate::state_machine::StateManager;
20use core::marker::PhantomData;
21
22#[cfg(feature = "runtime-diagnostics")]
23use core::sync::atomic::{AtomicU32, Ordering};
24
25#[cfg(feature = "runtime-diagnostics")]
26macro_rules! telemetry_sent_counter {
27    ($name:ident) => {
28        #[unsafe(no_mangle)]
29        pub static $name: AtomicU32 = AtomicU32::new(0);
30    };
31}
32
33#[cfg(feature = "runtime-diagnostics")]
34telemetry_sent_counter!(VELOXITY_DIAG_TELEM_IMU_SENT);
35#[cfg(feature = "runtime-diagnostics")]
36telemetry_sent_counter!(VELOXITY_DIAG_TELEM_MAG_SENT);
37#[cfg(feature = "runtime-diagnostics")]
38telemetry_sent_counter!(VELOXITY_DIAG_TELEM_BARO_SENT);
39#[cfg(feature = "runtime-diagnostics")]
40telemetry_sent_counter!(VELOXITY_DIAG_TELEM_PITOT_SENT);
41#[cfg(feature = "runtime-diagnostics")]
42telemetry_sent_counter!(VELOXITY_DIAG_TELEM_RANGE_SENT);
43#[cfg(feature = "runtime-diagnostics")]
44telemetry_sent_counter!(VELOXITY_DIAG_TELEM_GNSS_SENT);
45#[cfg(feature = "runtime-diagnostics")]
46telemetry_sent_counter!(VELOXITY_DIAG_TELEM_BATTERY_SENT);
47#[cfg(feature = "runtime-diagnostics")]
48telemetry_sent_counter!(VELOXITY_DIAG_TELEM_RC_SENT);
49#[cfg(feature = "runtime-diagnostics")]
50telemetry_sent_counter!(VELOXITY_DIAG_TIMESYNC_RESPONSE_SENT);
51
52const MAV_TYPE_FIXED_WING: u8 = 1;
53const MAV_TYPE_QUADROTOR: u8 = 2;
54const OUTPUT_RAW_IMU_DIVISOR: u64 = 8;
55const TELEMETRY_RATE_DISABLED: u16 = u16::MAX;
56pub const MAX_TELEMETRY_RATE_HZ: i32 = 2_000;
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum NamedTelemetryStream {
60    Heartbeat,
61    Status,
62    Imu,
63    Rc,
64    Attitude,
65    OutputRaw,
66    Gnss,
67    DiffPressure,
68    Baro,
69    Mag,
70    Range,
71    Battery,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum RealtimeTelemetryPriorityGate {
76    /// Use the stream's normal telemetry rate and freshness gates.
77    DueDeadline,
78    /// Send a fresh sample immediately, without waiting for the stream's rate gate.
79    FreshSample,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct RealtimeTelemetryPriority {
84    pub stream: NamedTelemetryStream,
85    pub gate: RealtimeTelemetryPriorityGate,
86}
87
88pub struct TelemetryCtx<'a, B, S, A, R>
89where
90    B: BoardIo,
91    R: FlightFloat,
92{
93    pub board: &'a mut B,
94    pub now_us: u64,
95    pub state: &'a StateManager,
96    pub command: &'a CommandManager,
97    pub params: &'a Params,
98    pub estimator_state: &'a S,
99    pub sensors: &'a ProcessedSensors<R>,
100    pub actuator_commands: &'a A,
101    pub sensor_error_count: u16,
102    pub loop_time_us: u16,
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct TelemetryRates {
107    pub heartbeat_hz: u16,
108    pub status_hz: u16,
109    pub imu_hz: u16,
110    pub attitude_hz: u16,
111    pub output_raw_hz: u16,
112    pub diff_pressure_hz: u16,
113    pub baro_hz: u16,
114    pub mag_hz: u16,
115    pub range_hz: u16,
116    pub battery_hz: u16,
117    pub gnss_hz: u16,
118    pub rc_hz: u16,
119    pub output_raw_imu_divisor: u64,
120}
121
122impl TelemetryRates {
123    pub fn from_params(params: &Params) -> Self {
124        Self {
125            heartbeat_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_HEARTBEAT_HZ),
126            status_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_STATUS_HZ),
127            imu_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_IMU_HZ),
128            attitude_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_ATTITUDE_HZ),
129            output_raw_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_OUTPUT_RAW_HZ),
130            diff_pressure_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_DIFF_PRESSURE_HZ),
131            baro_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_BARO_HZ),
132            mag_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_MAG_HZ),
133            range_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_RANGE_HZ),
134            battery_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_BATTERY_HZ),
135            gnss_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_GNSS_HZ),
136            rc_hz: telemetry_rate_param(params, ParamId::PARAM_TELEM_RC_HZ),
137            output_raw_imu_divisor: 0,
138        }
139    }
140
141    pub const fn upstream() -> Self {
142        Self {
143            heartbeat_hz: 1,
144            status_hz: 10,
145            imu_hz: 0,
146            attitude_hz: 0,
147            output_raw_hz: 0,
148            diff_pressure_hz: 0,
149            baro_hz: 0,
150            mag_hz: 0,
151            range_hz: 0,
152            battery_hz: 0,
153            gnss_hz: 0,
154            rc_hz: 0,
155            output_raw_imu_divisor: OUTPUT_RAW_IMU_DIVISOR,
156        }
157    }
158
159    pub const fn bounded_high_rate_transport() -> Self {
160        Self {
161            heartbeat_hz: 1,
162            status_hz: 10,
163            imu_hz: 400,
164            attitude_hz: 50,
165            output_raw_hz: 50,
166            diff_pressure_hz: 50,
167            baro_hz: 25,
168            mag_hz: 25,
169            range_hz: 50,
170            battery_hz: 25,
171            gnss_hz: 10,
172            rc_hz: 100,
173            output_raw_imu_divisor: 0,
174        }
175    }
176}
177
178fn telemetry_rate_param(params: &Params, id: ParamId) -> u16 {
179    match params.get_by_id(id) {
180        ParamValue::Int(-1) => TELEMETRY_RATE_DISABLED,
181        ParamValue::Int(value) if value < -1 => TELEMETRY_RATE_DISABLED,
182        ParamValue::Int(value) => value.min(MAX_TELEMETRY_RATE_HZ) as u16,
183        _ => TELEMETRY_RATE_DISABLED,
184    }
185}
186
187pub fn telemetry_stream_for_param(id: ParamId) -> Option<NamedTelemetryStream> {
188    match id {
189        ParamId::PARAM_TELEM_HEARTBEAT_HZ => Some(NamedTelemetryStream::Heartbeat),
190        ParamId::PARAM_TELEM_STATUS_HZ => Some(NamedTelemetryStream::Status),
191        ParamId::PARAM_TELEM_IMU_HZ => Some(NamedTelemetryStream::Imu),
192        ParamId::PARAM_TELEM_ATTITUDE_HZ => Some(NamedTelemetryStream::Attitude),
193        ParamId::PARAM_TELEM_OUTPUT_RAW_HZ => Some(NamedTelemetryStream::OutputRaw),
194        ParamId::PARAM_TELEM_DIFF_PRESSURE_HZ => Some(NamedTelemetryStream::DiffPressure),
195        ParamId::PARAM_TELEM_BARO_HZ => Some(NamedTelemetryStream::Baro),
196        ParamId::PARAM_TELEM_MAG_HZ => Some(NamedTelemetryStream::Mag),
197        ParamId::PARAM_TELEM_RANGE_HZ => Some(NamedTelemetryStream::Range),
198        ParamId::PARAM_TELEM_BATTERY_HZ => Some(NamedTelemetryStream::Battery),
199        ParamId::PARAM_TELEM_GNSS_HZ => Some(NamedTelemetryStream::Gnss),
200        ParamId::PARAM_TELEM_RC_HZ => Some(NamedTelemetryStream::Rc),
201        _ => None,
202    }
203}
204
205#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
206struct TelemetryRateState {
207    imu_us: u64,
208    attitude_us: u64,
209    output_raw_us: u64,
210    diff_pressure_us: u64,
211    baro_us: u64,
212    mag_us: u64,
213    range_us: u64,
214    battery_us: u64,
215    gnss_us: u64,
216    rc_us: u64,
217}
218
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220struct TelemetryFreshnessState {
221    imu: Option<u64>,
222    attitude: Option<u64>,
223    output_raw: Option<u64>,
224    diff_pressure: Option<u64>,
225    baro: Option<u64>,
226    mag: Option<u64>,
227    range: Option<u64>,
228    battery: Option<u64>,
229    gnss: Option<u64>,
230    rc: Option<u64>,
231}
232
233impl TelemetryFreshnessState {
234    fn last_sent(&self, stream: NamedTelemetryStream) -> Option<u64> {
235        match stream {
236            NamedTelemetryStream::Imu => self.imu,
237            NamedTelemetryStream::Attitude => self.attitude,
238            NamedTelemetryStream::OutputRaw => self.output_raw,
239            NamedTelemetryStream::DiffPressure => self.diff_pressure,
240            NamedTelemetryStream::Baro => self.baro,
241            NamedTelemetryStream::Mag => self.mag,
242            NamedTelemetryStream::Range => self.range,
243            NamedTelemetryStream::Battery => self.battery,
244            NamedTelemetryStream::Gnss => self.gnss,
245            NamedTelemetryStream::Rc => self.rc,
246            NamedTelemetryStream::Heartbeat | NamedTelemetryStream::Status => None,
247        }
248    }
249
250    fn mark_sent(&mut self, stream: NamedTelemetryStream, timestamp: u64) {
251        let slot = match stream {
252            NamedTelemetryStream::Imu => &mut self.imu,
253            NamedTelemetryStream::Attitude => &mut self.attitude,
254            NamedTelemetryStream::OutputRaw => &mut self.output_raw,
255            NamedTelemetryStream::DiffPressure => &mut self.diff_pressure,
256            NamedTelemetryStream::Baro => &mut self.baro,
257            NamedTelemetryStream::Mag => &mut self.mag,
258            NamedTelemetryStream::Range => &mut self.range,
259            NamedTelemetryStream::Battery => &mut self.battery,
260            NamedTelemetryStream::Gnss => &mut self.gnss,
261            NamedTelemetryStream::Rc => &mut self.rc,
262            NamedTelemetryStream::Heartbeat | NamedTelemetryStream::Status => return,
263        };
264        *slot = Some(timestamp);
265    }
266}
267
268fn telemetry_stream_sample_timestamp<R: FlightFloat>(
269    stream: NamedTelemetryStream,
270    sensors: &ProcessedSensors<R>,
271) -> Option<u64> {
272    match stream {
273        NamedTelemetryStream::Imu
274        | NamedTelemetryStream::Attitude
275        | NamedTelemetryStream::OutputRaw => sensors.imu.map(|packet| packet.header.timestamp),
276        NamedTelemetryStream::Rc => sensors.rc.map(|packet| packet.header.timestamp),
277        NamedTelemetryStream::Gnss => sensors.gnss.map(|packet| packet.header.timestamp),
278        NamedTelemetryStream::DiffPressure => sensors.pitot.map(|packet| packet.header.timestamp),
279        NamedTelemetryStream::Baro => sensors.baro.map(|packet| packet.header.timestamp),
280        NamedTelemetryStream::Mag => sensors.mag.map(|packet| packet.header.timestamp),
281        NamedTelemetryStream::Range => sensors.range.map(|packet| packet.header.timestamp),
282        NamedTelemetryStream::Battery => sensors.battery.map(|packet| packet.header.timestamp),
283        NamedTelemetryStream::Heartbeat | NamedTelemetryStream::Status => None,
284    }
285}
286
287fn stream_due(now_us: u64, last_us: &mut u64, rate_hz: u16) -> bool {
288    if rate_hz == TELEMETRY_RATE_DISABLED {
289        return false;
290    }
291    if rate_hz == 0 {
292        if *last_us == 0 {
293            *last_us = now_us;
294        }
295        return true;
296    }
297
298    let interval_us = 1_000_000_u64 / rate_hz as u64;
299    if *last_us == 0 {
300        *last_us = now_us;
301        true
302    } else if now_us.saturating_sub(*last_us) >= interval_us {
303        let elapsed_intervals = now_us.saturating_sub(*last_us) / interval_us;
304        *last_us = last_us.saturating_add(elapsed_intervals.saturating_mul(interval_us));
305        true
306    } else {
307        false
308    }
309}
310
311fn stream_due_deadline_us(now_us: u64, last_us: u64, rate_hz: u16) -> Option<u64> {
312    if rate_hz == TELEMETRY_RATE_DISABLED {
313        return None;
314    }
315    if rate_hz == 0 {
316        return Some(if last_us == 0 { 0 } else { now_us });
317    }
318    if last_us == 0 {
319        return Some(0);
320    }
321
322    let interval_us = 1_000_000_u64 / rate_hz as u64;
323    let deadline_us = last_us.saturating_add(interval_us);
324    let elapsed_us = now_us.saturating_sub(last_us);
325    if elapsed_us >= interval_us {
326        Some(deadline_us)
327    } else {
328        None
329    }
330}
331
332fn fixed_rate_due(now_us: u64, last_us: u64, rate_hz: u16) -> bool {
333    if rate_hz == TELEMETRY_RATE_DISABLED {
334        return false;
335    }
336    if rate_hz == 0 {
337        return true;
338    }
339
340    let interval_us = 1_000_000_u64 / rate_hz as u64;
341    now_us.saturating_sub(last_us) >= interval_us
342}
343
344fn fixed_rate_due_deadline_us(now_us: u64, last_us: u64, rate_hz: u16) -> Option<u64> {
345    if rate_hz == TELEMETRY_RATE_DISABLED {
346        return None;
347    }
348    if rate_hz == 0 {
349        return Some(now_us);
350    }
351
352    let interval_us = 1_000_000_u64 / rate_hz as u64;
353    let deadline_us = last_us.saturating_add(interval_us);
354    let elapsed_us = now_us.saturating_sub(last_us);
355    if elapsed_us >= interval_us {
356        Some(deadline_us)
357    } else {
358        None
359    }
360}
361
362pub const fn str_to_fixed_bytes(input: &str) -> [u8; 16] {
363    let mut buffer = [0u8; 16];
364    let input_bytes = input.as_bytes();
365
366    // Determine how many bytes to copy (at most 16)
367    let len_to_copy = if input_bytes.len() > 16 {
368        16
369    } else {
370        input_bytes.len()
371    };
372
373    // Copy the bytes from the input string
374    let mut i = 0;
375    while i < len_to_copy {
376        buffer[i] = input_bytes[i];
377        i += 1;
378    }
379
380    // If the input was shorter than 16, the spot after the last character
381    // is already a 0 from the initial buffer creation, so it is null-terminated.
382    // If the input was 16 or longer, the buffer is full and not null-terminated.
383
384    buffer
385}
386
387fn param_int(params: &Params, id: ParamId) -> i32 {
388    match params.get_by_id(id) {
389        ParamValue::Int(value) => value,
390        _ => 0,
391    }
392}
393
394pub struct CommManager<B, T>
395where
396    B: board::BoardIo,
397    T: interface::CommInterface<B>,
398{
399    last_heartbeat_us: u64,
400    last_status_send_us: u64,
401    output_raw_imu_count: u64,
402    telemetry_rates: TelemetryRates,
403    telemetry_rate_state: TelemetryRateState,
404    telemetry_freshness: TelemetryFreshnessState,
405
406    pub sysid: u8,
407    comm_link: T,
408    pub msgs: Messages,
409    _board_marker: PhantomData<B>,
410}
411
412impl<B, T> CommManager<B, T>
413where
414    B: board::BoardIo,
415    T: interface::CommInterface<B>,
416{
417    pub fn new(comm_link: T, now_us: u64) -> Self {
418        CommManager {
419            last_heartbeat_us: now_us,
420            last_status_send_us: now_us,
421            output_raw_imu_count: 0,
422            telemetry_rates: TelemetryRates::upstream(),
423            telemetry_rate_state: TelemetryRateState::default(),
424            telemetry_freshness: TelemetryFreshnessState::default(),
425
426            sysid: 1,
427            comm_link,
428            msgs: Messages::default(),
429            _board_marker: PhantomData,
430        }
431    }
432
433    #[cfg(test)]
434    pub(crate) fn comm_link(&self) -> &T {
435        &self.comm_link
436    }
437
438    pub fn process_incoming_messages(&mut self, board: &mut B) {
439        self.comm_link
440            .handle_incoming_messages(board, &mut self.msgs);
441    }
442
443    pub fn has_pending_messages(&self) -> bool {
444        self.msgs.has_pending()
445    }
446
447    pub fn set_telemetry_rates(&mut self, telemetry_rates: TelemetryRates) {
448        // Preserve freshness: rate changes must not make a retained packet fresh again.
449        self.telemetry_rates = telemetry_rates;
450        self.telemetry_rate_state = TelemetryRateState::default();
451        self.output_raw_imu_count = 0;
452    }
453
454    pub fn configure_telemetry_from_params(&mut self, params: &Params) {
455        self.set_telemetry_rates(TelemetryRates::from_params(params));
456    }
457
458    /// Applies one live telemetry parameter without disturbing the deadlines
459    /// of unrelated streams. The changed stream begins a new period at
460    /// `now_us`, avoiding a catch-up burst.
461    pub fn update_telemetry_param(&mut self, params: &Params, id: ParamId, now_us: u64) -> bool {
462        let Some(stream) = telemetry_stream_for_param(id) else {
463            return false;
464        };
465        let rate_hz = telemetry_rate_param(params, id);
466        match stream {
467            NamedTelemetryStream::Heartbeat => {
468                self.telemetry_rates.heartbeat_hz = rate_hz;
469                self.last_heartbeat_us = now_us;
470            }
471            NamedTelemetryStream::Status => {
472                self.telemetry_rates.status_hz = rate_hz;
473                self.last_status_send_us = now_us;
474            }
475            NamedTelemetryStream::Imu => {
476                self.telemetry_rates.imu_hz = rate_hz;
477                self.telemetry_rate_state.imu_us = now_us;
478            }
479            NamedTelemetryStream::Rc => {
480                self.telemetry_rates.rc_hz = rate_hz;
481                self.telemetry_rate_state.rc_us = now_us;
482            }
483            NamedTelemetryStream::Attitude => {
484                self.telemetry_rates.attitude_hz = rate_hz;
485                self.telemetry_rate_state.attitude_us = now_us;
486            }
487            NamedTelemetryStream::OutputRaw => {
488                self.telemetry_rates.output_raw_hz = rate_hz;
489                self.telemetry_rates.output_raw_imu_divisor = 0;
490                self.telemetry_rate_state.output_raw_us = now_us;
491                self.output_raw_imu_count = 0;
492            }
493            NamedTelemetryStream::Gnss => {
494                self.telemetry_rates.gnss_hz = rate_hz;
495                self.telemetry_rate_state.gnss_us = now_us;
496            }
497            NamedTelemetryStream::DiffPressure => {
498                self.telemetry_rates.diff_pressure_hz = rate_hz;
499                self.telemetry_rate_state.diff_pressure_us = now_us;
500            }
501            NamedTelemetryStream::Baro => {
502                self.telemetry_rates.baro_hz = rate_hz;
503                self.telemetry_rate_state.baro_us = now_us;
504            }
505            NamedTelemetryStream::Mag => {
506                self.telemetry_rates.mag_hz = rate_hz;
507                self.telemetry_rate_state.mag_us = now_us;
508            }
509            NamedTelemetryStream::Range => {
510                self.telemetry_rates.range_hz = rate_hz;
511                self.telemetry_rate_state.range_us = now_us;
512            }
513            NamedTelemetryStream::Battery => {
514                self.telemetry_rates.battery_hz = rate_hz;
515                self.telemetry_rate_state.battery_us = now_us;
516            }
517        }
518        true
519    }
520
521    #[cfg(test)]
522    pub(crate) fn telemetry_rates(&self) -> TelemetryRates {
523        self.telemetry_rates
524    }
525
526    pub fn named_telemetry_due<R>(
527        &self,
528        now_us: u64,
529        processed_sensors: &ProcessedSensors<R>,
530    ) -> bool
531    where
532        R: FlightFloat,
533    {
534        self.select_due_named_telemetry_stream(now_us, processed_sensors)
535            .is_some()
536    }
537
538    #[cfg(feature = "runtime-diagnostics")]
539    pub(crate) fn telemetry_sample_was_sent(
540        &self,
541        stream: NamedTelemetryStream,
542        timestamp: u64,
543    ) -> bool {
544        self.telemetry_freshness.last_sent(stream) == Some(timestamp)
545    }
546
547    fn select_due_named_telemetry_stream<R>(
548        &self,
549        now_us: u64,
550        processed_sensors: &ProcessedSensors<R>,
551    ) -> Option<NamedTelemetryStream>
552    where
553        R: FlightFloat,
554    {
555        let mut selected = None;
556        let mut selected_deadline = u64::MAX;
557
558        let consider = |selected: &mut Option<NamedTelemetryStream>,
559                        selected_deadline: &mut u64,
560                        stream: NamedTelemetryStream| {
561            let Some(deadline) =
562                self.named_telemetry_stream_deadline(stream, now_us, processed_sensors)
563            else {
564                return;
565            };
566            if selected.is_none() || deadline < *selected_deadline {
567                *selected = Some(stream);
568                *selected_deadline = deadline;
569            }
570        };
571
572        consider(
573            &mut selected,
574            &mut selected_deadline,
575            NamedTelemetryStream::Heartbeat,
576        );
577        consider(
578            &mut selected,
579            &mut selected_deadline,
580            NamedTelemetryStream::Status,
581        );
582
583        if processed_sensors.imu.is_some() {
584            consider(
585                &mut selected,
586                &mut selected_deadline,
587                NamedTelemetryStream::Imu,
588            );
589            consider(
590                &mut selected,
591                &mut selected_deadline,
592                NamedTelemetryStream::Attitude,
593            );
594            consider(
595                &mut selected,
596                &mut selected_deadline,
597                NamedTelemetryStream::OutputRaw,
598            );
599        }
600
601        if processed_sensors.rc.is_some() {
602            consider(
603                &mut selected,
604                &mut selected_deadline,
605                NamedTelemetryStream::Rc,
606            );
607        }
608        if processed_sensors.gnss.is_some() {
609            consider(
610                &mut selected,
611                &mut selected_deadline,
612                NamedTelemetryStream::Gnss,
613            );
614        }
615        if processed_sensors.pitot.is_some() {
616            consider(
617                &mut selected,
618                &mut selected_deadline,
619                NamedTelemetryStream::DiffPressure,
620            );
621        }
622        if processed_sensors.baro.is_some() {
623            consider(
624                &mut selected,
625                &mut selected_deadline,
626                NamedTelemetryStream::Baro,
627            );
628        }
629        if processed_sensors.mag.is_some() {
630            consider(
631                &mut selected,
632                &mut selected_deadline,
633                NamedTelemetryStream::Mag,
634            );
635        }
636        if processed_sensors.range.is_some() {
637            consider(
638                &mut selected,
639                &mut selected_deadline,
640                NamedTelemetryStream::Range,
641            );
642        }
643        if processed_sensors.battery.is_some() {
644            consider(
645                &mut selected,
646                &mut selected_deadline,
647                NamedTelemetryStream::Battery,
648            );
649        }
650
651        selected
652    }
653
654    fn named_telemetry_stream_deadline<R>(
655        &self,
656        stream: NamedTelemetryStream,
657        now_us: u64,
658        processed_sensors: &ProcessedSensors<R>,
659    ) -> Option<u64>
660    where
661        R: FlightFloat,
662    {
663        if !matches!(
664            stream,
665            NamedTelemetryStream::Heartbeat | NamedTelemetryStream::Status
666        ) {
667            let timestamp = telemetry_stream_sample_timestamp(stream, processed_sensors)?;
668            if self.telemetry_freshness.last_sent(stream) == Some(timestamp) {
669                return None;
670            }
671        }
672
673        match stream {
674            NamedTelemetryStream::Heartbeat => fixed_rate_due_deadline_us(
675                now_us,
676                self.last_heartbeat_us,
677                self.telemetry_rates.heartbeat_hz,
678            ),
679            NamedTelemetryStream::Status => fixed_rate_due_deadline_us(
680                now_us,
681                self.last_status_send_us,
682                self.telemetry_rates.status_hz,
683            ),
684            NamedTelemetryStream::Imu => stream_due_deadline_us(
685                now_us,
686                self.telemetry_rate_state.imu_us,
687                self.telemetry_rates.imu_hz,
688            ),
689            NamedTelemetryStream::Rc => processed_sensors.rc.as_ref().and_then(|_| {
690                stream_due_deadline_us(
691                    now_us,
692                    self.telemetry_rate_state.rc_us,
693                    self.telemetry_rates.rc_hz,
694                )
695            }),
696            NamedTelemetryStream::Attitude => processed_sensors.imu.as_ref().and_then(|_| {
697                stream_due_deadline_us(
698                    now_us,
699                    self.telemetry_rate_state.attitude_us,
700                    self.telemetry_rates.attitude_hz,
701                )
702            }),
703            NamedTelemetryStream::OutputRaw => processed_sensors.imu.as_ref().and_then(|_| {
704                if self.telemetry_rates.output_raw_hz == 0 {
705                    (self.telemetry_rates.output_raw_imu_divisor != 0
706                        && self.output_raw_imu_count % self.telemetry_rates.output_raw_imu_divisor
707                            == 0)
708                        .then_some(now_us)
709                } else {
710                    stream_due_deadline_us(
711                        now_us,
712                        self.telemetry_rate_state.output_raw_us,
713                        self.telemetry_rates.output_raw_hz,
714                    )
715                }
716            }),
717            NamedTelemetryStream::Gnss => processed_sensors.gnss.as_ref().and_then(|_| {
718                stream_due_deadline_us(
719                    now_us,
720                    self.telemetry_rate_state.gnss_us,
721                    self.telemetry_rates.gnss_hz,
722                )
723            }),
724            NamedTelemetryStream::DiffPressure => processed_sensors.pitot.as_ref().and_then(|_| {
725                stream_due_deadline_us(
726                    now_us,
727                    self.telemetry_rate_state.diff_pressure_us,
728                    self.telemetry_rates.diff_pressure_hz,
729                )
730            }),
731            NamedTelemetryStream::Baro => processed_sensors.baro.as_ref().and_then(|_| {
732                stream_due_deadline_us(
733                    now_us,
734                    self.telemetry_rate_state.baro_us,
735                    self.telemetry_rates.baro_hz,
736                )
737            }),
738            NamedTelemetryStream::Mag => processed_sensors.mag.as_ref().and_then(|_| {
739                stream_due_deadline_us(
740                    now_us,
741                    self.telemetry_rate_state.mag_us,
742                    self.telemetry_rates.mag_hz,
743                )
744            }),
745            NamedTelemetryStream::Range => processed_sensors.range.as_ref().and_then(|_| {
746                stream_due_deadline_us(
747                    now_us,
748                    self.telemetry_rate_state.range_us,
749                    self.telemetry_rates.range_hz,
750                )
751            }),
752            NamedTelemetryStream::Battery => processed_sensors.battery.as_ref().and_then(|_| {
753                stream_due_deadline_us(
754                    now_us,
755                    self.telemetry_rate_state.battery_us,
756                    self.telemetry_rates.battery_hz,
757                )
758            }),
759        }
760    }
761
762    fn targets_this_system(&self, target_system: u8) -> bool {
763        target_system == self.sysid
764    }
765
766    pub fn send_named_telemetry_streams<S, A, R>(&mut self, ctx: TelemetryCtx<'_, B, S, A, R>)
767    where
768        S: AttitudeEstimate,
769        A: AsRef<[R]>,
770        R: FlightFloat,
771    {
772        let TelemetryCtx {
773            board,
774            now_us,
775            state: state_manager,
776            command: command_manager,
777            params,
778            estimator_state,
779            sensors: processed_sensors,
780            actuator_commands,
781            sensor_error_count,
782            loop_time_us,
783        } = ctx;
784
785        if fixed_rate_due(
786            now_us,
787            self.last_heartbeat_us,
788            self.telemetry_rates.heartbeat_hz,
789        ) {
790            self.send_rosflight_heartbeat(
791                board,
792                HeartbeatMsg {
793                    autopilot: 0,
794                    base_mode: 0,
795                    custom_mode: 0,
796                    mavlink_version: 0,
797                    system_status: 0,
798                    type_: if param_int(params, ParamId::PARAM_FIXED_WING) != 0 {
799                        MAV_TYPE_FIXED_WING
800                    } else {
801                        MAV_TYPE_QUADROTOR
802                    },
803                },
804            );
805            self.last_heartbeat_us = now_us;
806        }
807
808        if fixed_rate_due(
809            now_us,
810            self.last_status_send_us,
811            self.telemetry_rates.status_hz,
812        ) {
813            self.send_rosflight_status(
814                board,
815                RosflightStatusMsg {
816                    armed: state_manager.is_armed() as u8,
817                    failsafe: state_manager.is_in_failsafe() as u8,
818                    rc_override: command_manager.get_rc_override(),
819                    offboard: command_manager.is_offboard_active() as u8,
820                    error_code: state_manager.get_errors(),
821                    control_mode: command_manager.get_control_mode().into(),
822                    num_errors: sensor_error_count as i16,
823                    loop_time_us: loop_time_us as i16,
824                },
825            );
826            self.last_status_send_us = now_us;
827        }
828
829        if let Some(imu_packet) = processed_sensors.imu {
830            let imu_timestamp = imu_packet.header.timestamp;
831            if self
832                .telemetry_freshness
833                .last_sent(NamedTelemetryStream::Imu)
834                != Some(imu_timestamp)
835                && stream_due(
836                    now_us,
837                    &mut self.telemetry_rate_state.imu_us,
838                    self.telemetry_rates.imu_hz,
839                )
840            {
841                self.send_rosflight_small_imu(
842                    board,
843                    SmallImuMsg {
844                        temperature: imu_packet.temperature,
845                        time_boot_us: imu_packet.header.timestamp,
846                        xacc: imu_packet.accel[0].to_f32_lossy(),
847                        yacc: imu_packet.accel[1].to_f32_lossy(),
848                        zacc: imu_packet.accel[2].to_f32_lossy(),
849                        xgyro: imu_packet.gyro[0].to_f32_lossy(),
850                        ygyro: imu_packet.gyro[1].to_f32_lossy(),
851                        zgyro: imu_packet.gyro[2].to_f32_lossy(),
852                    },
853                );
854                self.telemetry_freshness
855                    .mark_sent(NamedTelemetryStream::Imu, imu_timestamp);
856            }
857
858            let q = estimator_state.q();
859            let qd = estimator_state.q_dot();
860            let rollspeed = 2.0 * (q[0] * qd[1] - q[1] * qd[0] - q[2] * qd[3] + q[3] * qd[2]);
861            let pitchspeed = 2.0 * (q[0] * qd[2] - q[1] * qd[3] - q[2] * qd[0] + q[3] * qd[1]);
862            let yawspeed = 2.0 * (q[0] * qd[3] - q[1] * qd[2] - q[2] * qd[1] + q[3] * qd[0]);
863
864            if self
865                .telemetry_freshness
866                .last_sent(NamedTelemetryStream::Attitude)
867                != Some(imu_timestamp)
868                && stream_due(
869                    now_us,
870                    &mut self.telemetry_rate_state.attitude_us,
871                    self.telemetry_rates.attitude_hz,
872                )
873            {
874                self.send_rosflight_attitude_quaternion(
875                    board,
876                    AttitudeQuaternionMsg {
877                        time_boot_ms: (imu_packet.header.timestamp / 1000) as u32,
878                        q1: q[0],
879                        q2: q[1],
880                        q3: q[2],
881                        q4: q[3],
882                        rollspeed,
883                        pitchspeed,
884                        yawspeed,
885                    },
886                );
887                self.telemetry_freshness
888                    .mark_sent(NamedTelemetryStream::Attitude, imu_timestamp);
889            }
890
891            if self
892                .telemetry_freshness
893                .last_sent(NamedTelemetryStream::OutputRaw)
894                != Some(imu_timestamp)
895            {
896                if self.telemetry_rates.output_raw_hz == 0 {
897                    let output_raw_due = self.telemetry_rates.output_raw_imu_divisor != 0
898                        && self.output_raw_imu_count % self.telemetry_rates.output_raw_imu_divisor
899                            == 0;
900                    if output_raw_due {
901                        self.send_output_raw(board, actuator_commands);
902                    }
903                    // Divisor-based output intentionally consumes every fresh IMU sample,
904                    // including samples for which no OUTPUT_RAW message is emitted.
905                    self.telemetry_freshness
906                        .mark_sent(NamedTelemetryStream::OutputRaw, imu_timestamp);
907                    self.output_raw_imu_count = self.output_raw_imu_count.wrapping_add(1);
908                } else {
909                    let output_raw_due = stream_due(
910                        now_us,
911                        &mut self.telemetry_rate_state.output_raw_us,
912                        self.telemetry_rates.output_raw_hz,
913                    );
914                    if output_raw_due {
915                        self.send_output_raw(board, actuator_commands);
916                        self.telemetry_freshness
917                            .mark_sent(NamedTelemetryStream::OutputRaw, imu_timestamp);
918                    }
919                }
920            }
921        }
922
923        if let Some(packet) = processed_sensors.pitot {
924            if self
925                .telemetry_freshness
926                .last_sent(NamedTelemetryStream::DiffPressure)
927                != Some(packet.header.timestamp)
928                && stream_due(
929                    now_us,
930                    &mut self.telemetry_rate_state.diff_pressure_us,
931                    self.telemetry_rates.diff_pressure_hz,
932                )
933            {
934                self.send_rosflight_diff_pressure(
935                    board,
936                    DiffPressureMsg {
937                        velocity: packet.indicated_airspeed,
938                        diff_pressure: packet.differential_pressure,
939                        temperature: packet.temperature,
940                    },
941                );
942                self.telemetry_freshness
943                    .mark_sent(NamedTelemetryStream::DiffPressure, packet.header.timestamp);
944            }
945        }
946
947        if let Some(packet) = processed_sensors.baro {
948            if self
949                .telemetry_freshness
950                .last_sent(NamedTelemetryStream::Baro)
951                != Some(packet.header.timestamp)
952                && stream_due(
953                    now_us,
954                    &mut self.telemetry_rate_state.baro_us,
955                    self.telemetry_rates.baro_hz,
956                )
957            {
958                self.send_rosflight_small_baro(
959                    board,
960                    SmallBaroMsg {
961                        altitude: packet.altitude,
962                        pressure: packet.pressure,
963                        temperature: packet.temperature,
964                    },
965                );
966                self.telemetry_freshness
967                    .mark_sent(NamedTelemetryStream::Baro, packet.header.timestamp);
968            }
969        }
970
971        if let Some(packet) = processed_sensors.mag {
972            if self
973                .telemetry_freshness
974                .last_sent(NamedTelemetryStream::Mag)
975                != Some(packet.header.timestamp)
976                && stream_due(
977                    now_us,
978                    &mut self.telemetry_rate_state.mag_us,
979                    self.telemetry_rates.mag_hz,
980                )
981            {
982                self.send_rosflight_small_mag(
983                    board,
984                    SmallMagMsg {
985                        xmag: packet.flux[0],
986                        ymag: packet.flux[1],
987                        zmag: packet.flux[2],
988                    },
989                );
990                self.telemetry_freshness
991                    .mark_sent(NamedTelemetryStream::Mag, packet.header.timestamp);
992            }
993        }
994
995        if let Some(packet) = processed_sensors.range {
996            if self
997                .telemetry_freshness
998                .last_sent(NamedTelemetryStream::Range)
999                != Some(packet.header.timestamp)
1000                && stream_due(
1001                    now_us,
1002                    &mut self.telemetry_rate_state.range_us,
1003                    self.telemetry_rates.range_hz,
1004                )
1005            {
1006                self.send_rosflight_small_range(
1007                    board,
1008                    SmallRangeMsg {
1009                        type_: match packet.range_type {
1010                            RangeType::Sonar => RosflightRangeType::RosflightRangeSonar,
1011                            RangeType::Lidar => RosflightRangeType::RosflightRangeLidar,
1012                        },
1013                        range: packet.range,
1014                        max_range: packet.max_range,
1015                        min_range: packet.min_range,
1016                    },
1017                );
1018                self.telemetry_freshness
1019                    .mark_sent(NamedTelemetryStream::Range, packet.header.timestamp);
1020            }
1021        }
1022
1023        if let Some(packet) = processed_sensors.battery {
1024            if self
1025                .telemetry_freshness
1026                .last_sent(NamedTelemetryStream::Battery)
1027                != Some(packet.header.timestamp)
1028                && stream_due(
1029                    now_us,
1030                    &mut self.telemetry_rate_state.battery_us,
1031                    self.telemetry_rates.battery_hz,
1032                )
1033            {
1034                self.send_rosflight_battery_status(
1035                    board,
1036                    BatteryStatusMsg {
1037                        battery_voltage: packet.voltage,
1038                        battery_current: packet.current,
1039                    },
1040                );
1041                self.telemetry_freshness
1042                    .mark_sent(NamedTelemetryStream::Battery, packet.header.timestamp);
1043            }
1044        }
1045
1046        if let Some(packet) = processed_sensors.gnss {
1047            if self
1048                .telemetry_freshness
1049                .last_sent(NamedTelemetryStream::Gnss)
1050                != Some(packet.header.timestamp)
1051                && stream_due(
1052                    now_us,
1053                    &mut self.telemetry_rate_state.gnss_us,
1054                    self.telemetry_rates.gnss_hz,
1055                )
1056            {
1057                self.send_rosflight_gnss(
1058                    board,
1059                    RosflightGnssMsg {
1060                        rosflight_timestamp: packet.header.timestamp,
1061                        seconds: packet.unix_seconds,
1062                        nanos: packet.unix_nanos,
1063                        fix_type: packet.fix_type,
1064                        num_sat: packet.num_sats,
1065                        lat: packet.lat,
1066                        lon: packet.lon,
1067                        height_msl: packet.height_msl,
1068                        vel_n: packet.vel_n,
1069                        vel_e: packet.vel_e,
1070                        vel_d: packet.vel_d,
1071                        s_acc: packet.s_acc,
1072                        h_acc: packet.h_acc,
1073                        v_acc: packet.v_acc,
1074                    },
1075                );
1076                self.telemetry_freshness
1077                    .mark_sent(NamedTelemetryStream::Gnss, packet.header.timestamp);
1078            }
1079        }
1080
1081        if let Some(packet) = processed_sensors.rc {
1082            if self.telemetry_freshness.last_sent(NamedTelemetryStream::Rc)
1083                != Some(packet.header.timestamp)
1084                && stream_due(
1085                    now_us,
1086                    &mut self.telemetry_rate_state.rc_us,
1087                    self.telemetry_rates.rc_hz,
1088                )
1089            {
1090                let mut channels = [0u16; RC_PACKET_CHANNELS];
1091                let count = (packet.n_chan as usize).min(8).min(RC_PACKET_CHANNELS);
1092                for (dst, src) in channels.iter_mut().zip(packet.chan.iter()).take(count) {
1093                    *dst = (*src * 1000.0 + 1000.0) as u16;
1094                }
1095
1096                self.send_rosflight_rc_raw(
1097                    board,
1098                    RcChannelsMsg {
1099                        time_boot_ms: board.clock_millis(),
1100                        chancount: count as u8,
1101                        channels,
1102                        rssi: 0,
1103                    },
1104                );
1105                self.telemetry_freshness
1106                    .mark_sent(NamedTelemetryStream::Rc, packet.header.timestamp);
1107            }
1108        }
1109    }
1110
1111    pub fn send_one_named_telemetry_stream<S, A, R>(
1112        &mut self,
1113        mut ctx: TelemetryCtx<'_, B, S, A, R>,
1114    ) -> bool
1115    where
1116        S: AttitudeEstimate,
1117        A: AsRef<[R]>,
1118        R: FlightFloat,
1119    {
1120        let Some(stream) = self.select_due_named_telemetry_stream(ctx.now_us, ctx.sensors) else {
1121            return false;
1122        };
1123
1124        self.send_selected_named_telemetry_stream(stream, &mut ctx)
1125    }
1126
1127    pub fn send_named_telemetry_stream_if_due<S, A, R>(
1128        &mut self,
1129        stream: NamedTelemetryStream,
1130        mut ctx: TelemetryCtx<'_, B, S, A, R>,
1131    ) -> bool
1132    where
1133        S: AttitudeEstimate,
1134        A: AsRef<[R]>,
1135        R: FlightFloat,
1136    {
1137        if self
1138            .named_telemetry_stream_deadline(stream, ctx.now_us, ctx.sensors)
1139            .is_none()
1140        {
1141            return false;
1142        }
1143
1144        self.send_selected_named_telemetry_stream(stream, &mut ctx)
1145    }
1146
1147    pub fn send_named_telemetry_stream_with_gate<S, A, R>(
1148        &mut self,
1149        priority: RealtimeTelemetryPriority,
1150        mut ctx: TelemetryCtx<'_, B, S, A, R>,
1151    ) -> bool
1152    where
1153        S: AttitudeEstimate,
1154        A: AsRef<[R]>,
1155        R: FlightFloat,
1156    {
1157        match priority.gate {
1158            RealtimeTelemetryPriorityGate::DueDeadline => {
1159                self.send_named_telemetry_stream_if_due(priority.stream, ctx)
1160            }
1161            RealtimeTelemetryPriorityGate::FreshSample => {
1162                if priority.stream != NamedTelemetryStream::Imu {
1163                    return self.send_named_telemetry_stream_if_due(priority.stream, ctx);
1164                }
1165                self.send_selected_named_telemetry_stream_with_gate(
1166                    priority.stream,
1167                    priority.gate,
1168                    &mut ctx,
1169                )
1170            }
1171        }
1172    }
1173
1174    fn send_selected_named_telemetry_stream<S, A, R>(
1175        &mut self,
1176        stream: NamedTelemetryStream,
1177        ctx: &mut TelemetryCtx<'_, B, S, A, R>,
1178    ) -> bool
1179    where
1180        S: AttitudeEstimate,
1181        A: AsRef<[R]>,
1182        R: FlightFloat,
1183    {
1184        self.send_selected_named_telemetry_stream_with_gate(
1185            stream,
1186            RealtimeTelemetryPriorityGate::DueDeadline,
1187            ctx,
1188        )
1189    }
1190
1191    fn send_selected_named_telemetry_stream_with_gate<S, A, R>(
1192        &mut self,
1193        stream: NamedTelemetryStream,
1194        gate: RealtimeTelemetryPriorityGate,
1195        ctx: &mut TelemetryCtx<'_, B, S, A, R>,
1196    ) -> bool
1197    where
1198        S: AttitudeEstimate,
1199        A: AsRef<[R]>,
1200        R: FlightFloat,
1201    {
1202        let sent = match stream {
1203            NamedTelemetryStream::Heartbeat => {
1204                self.send_rosflight_heartbeat(
1205                    ctx.board,
1206                    HeartbeatMsg {
1207                        autopilot: 0,
1208                        base_mode: 0,
1209                        custom_mode: 0,
1210                        mavlink_version: 0,
1211                        system_status: 0,
1212                        type_: if param_int(ctx.params, ParamId::PARAM_FIXED_WING) != 0 {
1213                            MAV_TYPE_FIXED_WING
1214                        } else {
1215                            MAV_TYPE_QUADROTOR
1216                        },
1217                    },
1218                );
1219                self.last_heartbeat_us = ctx.now_us;
1220                true
1221            }
1222            NamedTelemetryStream::Status => {
1223                self.send_rosflight_status(
1224                    ctx.board,
1225                    RosflightStatusMsg {
1226                        armed: ctx.state.is_armed() as u8,
1227                        failsafe: ctx.state.is_in_failsafe() as u8,
1228                        rc_override: ctx.command.get_rc_override(),
1229                        offboard: ctx.command.is_offboard_active() as u8,
1230                        error_code: ctx.state.get_errors(),
1231                        control_mode: ctx.command.get_control_mode().into(),
1232                        num_errors: ctx.sensor_error_count as i16,
1233                        loop_time_us: ctx.loop_time_us as i16,
1234                    },
1235                );
1236                self.last_status_send_us = ctx.now_us;
1237                true
1238            }
1239            NamedTelemetryStream::Imu => match gate {
1240                RealtimeTelemetryPriorityGate::DueDeadline => {
1241                    self.send_imu_if_due(ctx.board, ctx.now_us, ctx.sensors)
1242                }
1243                RealtimeTelemetryPriorityGate::FreshSample => {
1244                    self.send_imu_if_fresh(ctx.board, ctx.now_us, ctx.sensors)
1245                }
1246            },
1247            NamedTelemetryStream::Rc => self.send_rc_if_due(ctx.board, ctx.now_us, ctx.sensors),
1248            NamedTelemetryStream::Attitude => match ctx.sensors.imu {
1249                Some(imu_packet) => {
1250                    if !stream_due(
1251                        ctx.now_us,
1252                        &mut self.telemetry_rate_state.attitude_us,
1253                        self.telemetry_rates.attitude_hz,
1254                    ) {
1255                        false
1256                    } else {
1257                        let q = ctx.estimator_state.q();
1258                        let qd = ctx.estimator_state.q_dot();
1259                        let rollspeed =
1260                            2.0 * (q[0] * qd[1] - q[1] * qd[0] - q[2] * qd[3] + q[3] * qd[2]);
1261                        let pitchspeed =
1262                            2.0 * (q[0] * qd[2] - q[1] * qd[3] - q[2] * qd[0] + q[3] * qd[1]);
1263                        let yawspeed =
1264                            2.0 * (q[0] * qd[3] - q[1] * qd[2] - q[2] * qd[1] + q[3] * qd[0]);
1265                        self.send_rosflight_attitude_quaternion(
1266                            ctx.board,
1267                            AttitudeQuaternionMsg {
1268                                time_boot_ms: (imu_packet.header.timestamp / 1000) as u32,
1269                                q1: q[0],
1270                                q2: q[1],
1271                                q3: q[2],
1272                                q4: q[3],
1273                                rollspeed,
1274                                pitchspeed,
1275                                yawspeed,
1276                            },
1277                        );
1278                        true
1279                    }
1280                }
1281                None => false,
1282            },
1283            NamedTelemetryStream::OutputRaw => {
1284                if ctx.sensors.imu.is_none() {
1285                    false
1286                } else {
1287                    let output_raw_due = if self.telemetry_rates.output_raw_hz == 0 {
1288                        self.telemetry_rates.output_raw_imu_divisor != 0
1289                            && self.output_raw_imu_count
1290                                % self.telemetry_rates.output_raw_imu_divisor
1291                                == 0
1292                    } else {
1293                        stream_due(
1294                            ctx.now_us,
1295                            &mut self.telemetry_rate_state.output_raw_us,
1296                            self.telemetry_rates.output_raw_hz,
1297                        )
1298                    };
1299                    self.output_raw_imu_count = self.output_raw_imu_count.wrapping_add(1);
1300                    if !output_raw_due {
1301                        false
1302                    } else {
1303                        self.send_output_raw(ctx.board, ctx.actuator_commands);
1304                        true
1305                    }
1306                }
1307            }
1308            NamedTelemetryStream::Gnss => match ctx.sensors.gnss {
1309                Some(packet) => {
1310                    if !stream_due(
1311                        ctx.now_us,
1312                        &mut self.telemetry_rate_state.gnss_us,
1313                        self.telemetry_rates.gnss_hz,
1314                    ) {
1315                        false
1316                    } else {
1317                        self.send_rosflight_gnss(
1318                            ctx.board,
1319                            RosflightGnssMsg {
1320                                rosflight_timestamp: packet.header.timestamp,
1321                                seconds: packet.unix_seconds,
1322                                nanos: packet.unix_nanos,
1323                                fix_type: packet.fix_type,
1324                                num_sat: packet.num_sats,
1325                                lat: packet.lat,
1326                                lon: packet.lon,
1327                                height_msl: packet.height_msl,
1328                                vel_n: packet.vel_n,
1329                                vel_e: packet.vel_e,
1330                                vel_d: packet.vel_d,
1331                                s_acc: packet.s_acc,
1332                                h_acc: packet.h_acc,
1333                                v_acc: packet.v_acc,
1334                            },
1335                        );
1336                        true
1337                    }
1338                }
1339                None => false,
1340            },
1341            NamedTelemetryStream::DiffPressure => match ctx.sensors.pitot {
1342                Some(packet) => {
1343                    if !stream_due(
1344                        ctx.now_us,
1345                        &mut self.telemetry_rate_state.diff_pressure_us,
1346                        self.telemetry_rates.diff_pressure_hz,
1347                    ) {
1348                        false
1349                    } else {
1350                        self.send_rosflight_diff_pressure(
1351                            ctx.board,
1352                            DiffPressureMsg {
1353                                velocity: packet.indicated_airspeed,
1354                                diff_pressure: packet.differential_pressure,
1355                                temperature: packet.temperature,
1356                            },
1357                        );
1358                        true
1359                    }
1360                }
1361                None => false,
1362            },
1363            NamedTelemetryStream::Baro => match ctx.sensors.baro {
1364                Some(packet) => {
1365                    if !stream_due(
1366                        ctx.now_us,
1367                        &mut self.telemetry_rate_state.baro_us,
1368                        self.telemetry_rates.baro_hz,
1369                    ) {
1370                        false
1371                    } else {
1372                        self.send_rosflight_small_baro(
1373                            ctx.board,
1374                            SmallBaroMsg {
1375                                altitude: packet.altitude,
1376                                pressure: packet.pressure,
1377                                temperature: packet.temperature,
1378                            },
1379                        );
1380                        true
1381                    }
1382                }
1383                None => false,
1384            },
1385            NamedTelemetryStream::Mag => match ctx.sensors.mag {
1386                Some(packet) => {
1387                    if !stream_due(
1388                        ctx.now_us,
1389                        &mut self.telemetry_rate_state.mag_us,
1390                        self.telemetry_rates.mag_hz,
1391                    ) {
1392                        false
1393                    } else {
1394                        self.send_rosflight_small_mag(
1395                            ctx.board,
1396                            SmallMagMsg {
1397                                xmag: packet.flux[0],
1398                                ymag: packet.flux[1],
1399                                zmag: packet.flux[2],
1400                            },
1401                        );
1402                        true
1403                    }
1404                }
1405                None => false,
1406            },
1407            NamedTelemetryStream::Range => match ctx.sensors.range {
1408                Some(packet) => {
1409                    if !stream_due(
1410                        ctx.now_us,
1411                        &mut self.telemetry_rate_state.range_us,
1412                        self.telemetry_rates.range_hz,
1413                    ) {
1414                        false
1415                    } else {
1416                        self.send_rosflight_small_range(
1417                            ctx.board,
1418                            SmallRangeMsg {
1419                                type_: match packet.range_type {
1420                                    RangeType::Sonar => RosflightRangeType::RosflightRangeSonar,
1421                                    RangeType::Lidar => RosflightRangeType::RosflightRangeLidar,
1422                                },
1423                                range: packet.range,
1424                                max_range: packet.max_range,
1425                                min_range: packet.min_range,
1426                            },
1427                        );
1428                        true
1429                    }
1430                }
1431                None => false,
1432            },
1433            NamedTelemetryStream::Battery => match ctx.sensors.battery {
1434                Some(packet) => {
1435                    if !stream_due(
1436                        ctx.now_us,
1437                        &mut self.telemetry_rate_state.battery_us,
1438                        self.telemetry_rates.battery_hz,
1439                    ) {
1440                        false
1441                    } else {
1442                        self.send_rosflight_battery_status(
1443                            ctx.board,
1444                            BatteryStatusMsg {
1445                                battery_voltage: packet.voltage,
1446                                battery_current: packet.current,
1447                            },
1448                        );
1449                        true
1450                    }
1451                }
1452                None => false,
1453            },
1454        };
1455        if sent {
1456            #[cfg(feature = "runtime-diagnostics")]
1457            match stream {
1458                NamedTelemetryStream::Imu => {
1459                    VELOXITY_DIAG_TELEM_IMU_SENT.fetch_add(1, Ordering::Relaxed)
1460                }
1461                NamedTelemetryStream::Mag => {
1462                    VELOXITY_DIAG_TELEM_MAG_SENT.fetch_add(1, Ordering::Relaxed)
1463                }
1464                NamedTelemetryStream::Baro => {
1465                    VELOXITY_DIAG_TELEM_BARO_SENT.fetch_add(1, Ordering::Relaxed)
1466                }
1467                NamedTelemetryStream::DiffPressure => {
1468                    VELOXITY_DIAG_TELEM_PITOT_SENT.fetch_add(1, Ordering::Relaxed)
1469                }
1470                NamedTelemetryStream::Range => {
1471                    VELOXITY_DIAG_TELEM_RANGE_SENT.fetch_add(1, Ordering::Relaxed)
1472                }
1473                NamedTelemetryStream::Gnss => {
1474                    VELOXITY_DIAG_TELEM_GNSS_SENT.fetch_add(1, Ordering::Relaxed)
1475                }
1476                NamedTelemetryStream::Battery => {
1477                    VELOXITY_DIAG_TELEM_BATTERY_SENT.fetch_add(1, Ordering::Relaxed)
1478                }
1479                NamedTelemetryStream::Rc => {
1480                    VELOXITY_DIAG_TELEM_RC_SENT.fetch_add(1, Ordering::Relaxed)
1481                }
1482                NamedTelemetryStream::Heartbeat
1483                | NamedTelemetryStream::Status
1484                | NamedTelemetryStream::Attitude
1485                | NamedTelemetryStream::OutputRaw => 0,
1486            };
1487            if let Some(timestamp) = telemetry_stream_sample_timestamp(stream, ctx.sensors) {
1488                self.telemetry_freshness.mark_sent(stream, timestamp);
1489            }
1490        }
1491        sent
1492    }
1493
1494    fn send_imu_if_due<R>(
1495        &mut self,
1496        board: &mut B,
1497        now_us: u64,
1498        processed_sensors: &ProcessedSensors<R>,
1499    ) -> bool
1500    where
1501        R: FlightFloat,
1502    {
1503        let Some(imu_packet) = processed_sensors.imu else {
1504            return false;
1505        };
1506        if self
1507            .telemetry_freshness
1508            .last_sent(NamedTelemetryStream::Imu)
1509            == Some(imu_packet.header.timestamp)
1510        {
1511            return false;
1512        }
1513        if !stream_due(
1514            now_us,
1515            &mut self.telemetry_rate_state.imu_us,
1516            self.telemetry_rates.imu_hz,
1517        ) {
1518            return false;
1519        }
1520        self.send_rosflight_small_imu(
1521            board,
1522            SmallImuMsg {
1523                temperature: imu_packet.temperature,
1524                time_boot_us: imu_packet.header.timestamp,
1525                xacc: imu_packet.accel[0].to_f32_lossy(),
1526                yacc: imu_packet.accel[1].to_f32_lossy(),
1527                zacc: imu_packet.accel[2].to_f32_lossy(),
1528                xgyro: imu_packet.gyro[0].to_f32_lossy(),
1529                ygyro: imu_packet.gyro[1].to_f32_lossy(),
1530                zgyro: imu_packet.gyro[2].to_f32_lossy(),
1531            },
1532        );
1533        true
1534    }
1535
1536    fn send_imu_if_fresh<R>(
1537        &mut self,
1538        board: &mut B,
1539        now_us: u64,
1540        processed_sensors: &ProcessedSensors<R>,
1541    ) -> bool
1542    where
1543        R: FlightFloat,
1544    {
1545        let Some(imu_packet) = processed_sensors.imu else {
1546            return false;
1547        };
1548        if self
1549            .telemetry_freshness
1550            .last_sent(NamedTelemetryStream::Imu)
1551            == Some(imu_packet.header.timestamp)
1552        {
1553            return false;
1554        }
1555        self.send_rosflight_small_imu(
1556            board,
1557            SmallImuMsg {
1558                temperature: imu_packet.temperature,
1559                time_boot_us: imu_packet.header.timestamp,
1560                xacc: imu_packet.accel[0].to_f32_lossy(),
1561                yacc: imu_packet.accel[1].to_f32_lossy(),
1562                zacc: imu_packet.accel[2].to_f32_lossy(),
1563                xgyro: imu_packet.gyro[0].to_f32_lossy(),
1564                ygyro: imu_packet.gyro[1].to_f32_lossy(),
1565                zgyro: imu_packet.gyro[2].to_f32_lossy(),
1566            },
1567        );
1568        self.telemetry_rate_state.imu_us = now_us;
1569        true
1570    }
1571
1572    fn send_rc_if_due<R>(
1573        &mut self,
1574        board: &mut B,
1575        now_us: u64,
1576        processed_sensors: &ProcessedSensors<R>,
1577    ) -> bool
1578    where
1579        R: FlightFloat,
1580    {
1581        let Some(packet) = processed_sensors.rc else {
1582            return false;
1583        };
1584        if !stream_due(
1585            now_us,
1586            &mut self.telemetry_rate_state.rc_us,
1587            self.telemetry_rates.rc_hz,
1588        ) {
1589            return false;
1590        }
1591        let mut channels = [0u16; RC_PACKET_CHANNELS];
1592        let count = (packet.n_chan as usize).min(8).min(RC_PACKET_CHANNELS);
1593        for (dst, src) in channels.iter_mut().zip(packet.chan.iter()).take(count) {
1594            *dst = (*src * 1000.0 + 1000.0) as u16;
1595        }
1596        self.send_rosflight_rc_raw(
1597            board,
1598            RcChannelsMsg {
1599                time_boot_ms: board.clock_millis(),
1600                chancount: count as u8,
1601                channels,
1602                rssi: 0,
1603            },
1604        );
1605        true
1606    }
1607
1608    fn send_output_raw<A, R>(&mut self, board: &mut B, actuator_commands: &A)
1609    where
1610        A: AsRef<[R]>,
1611        R: FlightFloat,
1612    {
1613        let mut values = [0.0f32; 14];
1614        for (dst, src) in values.iter_mut().zip(actuator_commands.as_ref().iter()) {
1615            *dst = src.to_f32_lossy();
1616        }
1617        self.send_rosflight_output_raw(
1618            board,
1619            RosflightOutputRawMsg {
1620                stamp: board.clock_millis() as u64,
1621                values,
1622            },
1623        );
1624    }
1625
1626    pub fn act_on_messages(
1627        &mut self,
1628        param_events: &mut ParamEventQueues,
1629        comm_events: &mut CommEventQueues,
1630        command_events: &mut CommandEventQueues,
1631        companion_events: &mut CompanionEventQueues,
1632        board: &mut B,
1633    ) {
1634        if let Some(msg) = self.msgs.heartbeat.take() {
1635            companion_events
1636                .heartbeats
1637                .push_or_log(CompanionHeartbeatReceived { msg }, "companion heartbeat");
1638        }
1639
1640        while let Some(msg) = Store::<ParamRequestReadMsg>::take(&mut self.msgs) {
1641            if self.targets_this_system(msg.target_system) {
1642                param_events.read_requests.push_or_log(
1643                    ParamReadRequested {
1644                        identifier: msg.param_identifier,
1645                    },
1646                    "param read request",
1647                );
1648            }
1649        }
1650
1651        if let Some(msg) = self.msgs.param_request_list.take() {
1652            if self.targets_this_system(msg.target_system) {
1653                param_events
1654                    .list_requests
1655                    .push_or_log(ParamListRequested, "param list request");
1656            }
1657        }
1658
1659        // next check for timesync messages
1660        let msg_opt: Option<TimesyncMsg> = self.msgs.timesync.take();
1661        if let Some(mut msg) = msg_opt {
1662            if msg.tc1 == 0 {
1663                msg.tc1 = (board.clock_micros() * 1000) as i64;
1664                self.send_timesync(board, msg);
1665                #[cfg(feature = "runtime-diagnostics")]
1666                VELOXITY_DIAG_TIMESYNC_RESPONSE_SENT.fetch_add(1, Ordering::Relaxed);
1667            }
1668        }
1669
1670        if let Some(msg) = self.msgs.offboard_control.take() {
1671            let now_us = board.clock_micros();
1672            command_events
1673                .offboard_control_requests
1674                .push_or_log(OffboardControlRequested { now_us, msg }, "offboard control");
1675        }
1676
1677        if let Some(msg) = self.msgs.aux_cmd.take() {
1678            companion_events
1679                .aux_commands
1680                .push_or_log(AuxCommandReceived { msg }, "aux command");
1681        }
1682
1683        if let Some(msg) = self.msgs.external_attitude.take() {
1684            companion_events
1685                .external_attitudes
1686                .push_or_log(ExternalAttitudeReceived { msg }, "external attitude");
1687        }
1688
1689        while !param_events.set_requests.is_full() {
1690            let Some(msg) = Store::<ParamSetMsg>::take(&mut self.msgs) else {
1691                break;
1692            };
1693
1694            if self.targets_this_system(msg.target_system) {
1695                let pushed = param_events.set_requests.push_or_log(
1696                    ParamSetRequested {
1697                        value: msg.param_value,
1698                        param_id_bytes: msg.param_id,
1699                    },
1700                    "param set request",
1701                );
1702                debug_assert!(pushed);
1703            }
1704        }
1705
1706        // now act on ROSflight Commands
1707
1708        let cmd_msg_opt = self.msgs.cmd.take();
1709        if let Some(msg) = cmd_msg_opt {
1710            // Assume failure unless explicitly set to success
1711            let success = RosflightCmdResponse::RosflightCmdFailed;
1712            let mut send_ack_now = true;
1713
1714            match msg.command {
1715                RosflightCmd::RcCalibration => {
1716                    if command_events.rc_trim_calibration_requests.push_or_log(
1717                        RcTrimCalibrationRequested {
1718                            command: msg.command,
1719                        },
1720                        "rc trim calibration",
1721                    ) {
1722                        send_ack_now = false;
1723                    }
1724                }
1725                RosflightCmd::AccelCalibration => {
1726                    if command_events.calibration_requests.push_or_log(
1727                        CalibrationRequested {
1728                            command: msg.command,
1729                        },
1730                        "accel calibration",
1731                    ) {
1732                        send_ack_now = false;
1733                    }
1734                }
1735                RosflightCmd::GyroCalibration => {
1736                    if command_events.calibration_requests.push_or_log(
1737                        CalibrationRequested {
1738                            command: msg.command,
1739                        },
1740                        "gyro calibration",
1741                    ) {
1742                        send_ack_now = false;
1743                    }
1744                }
1745                RosflightCmd::BaroCalibration => {
1746                    if command_events.calibration_requests.push_or_log(
1747                        CalibrationRequested {
1748                            command: msg.command,
1749                        },
1750                        "baro calibration",
1751                    ) {
1752                        send_ack_now = false;
1753                    }
1754                }
1755                RosflightCmd::AirspeedCalibration => {
1756                    if command_events.calibration_requests.push_or_log(
1757                        CalibrationRequested {
1758                            command: msg.command,
1759                        },
1760                        "airspeed calibration",
1761                    ) {
1762                        send_ack_now = false;
1763                    }
1764                }
1765                RosflightCmd::ReadParams => {
1766                    if command_events.board_command_requests.push_or_log(
1767                        BoardCommandRequested {
1768                            command: msg.command,
1769                        },
1770                        "read params command",
1771                    ) {
1772                        send_ack_now = false;
1773                    }
1774                }
1775                RosflightCmd::WriteParams => {
1776                    if command_events.board_command_requests.push_or_log(
1777                        BoardCommandRequested {
1778                            command: msg.command,
1779                        },
1780                        "write params command",
1781                    ) {
1782                        send_ack_now = false;
1783                    }
1784                }
1785                RosflightCmd::SetParamDefaults => {
1786                    if command_events.param_defaults_requests.push_or_log(
1787                        ParamDefaultsRequested {
1788                            command: msg.command,
1789                        },
1790                        "param defaults command",
1791                    ) {
1792                        send_ack_now = false;
1793                    }
1794                }
1795                RosflightCmd::Reboot => {
1796                    if command_events.board_command_requests.push_or_log(
1797                        BoardCommandRequested {
1798                            command: msg.command,
1799                        },
1800                        "reboot command",
1801                    ) {
1802                        send_ack_now = false;
1803                    }
1804                }
1805                RosflightCmd::RebootToBootloader => {
1806                    if command_events.board_command_requests.push_or_log(
1807                        BoardCommandRequested {
1808                            command: msg.command,
1809                        },
1810                        "bootloader command",
1811                    ) {
1812                        send_ack_now = false;
1813                    }
1814                }
1815                RosflightCmd::SendVersion => {
1816                    if command_events.version_requests.push_or_log(
1817                        VersionRequested {
1818                            command: msg.command,
1819                        },
1820                        "version command",
1821                    ) {
1822                        send_ack_now = false;
1823                    }
1824                }
1825                RosflightCmd::ResetOrigin => {
1826                    if command_events.reset_origin_requests.push_or_log(
1827                        ResetOriginRequested {
1828                            command: msg.command,
1829                        },
1830                        "reset origin command",
1831                    ) {
1832                        send_ack_now = false;
1833                    }
1834                }
1835                RosflightCmd::SendAllConfigInfos => {
1836                    if command_events.config_info_requests.push_or_log(
1837                        ConfigInfoRequested {
1838                            command: msg.command,
1839                        },
1840                        "config info command",
1841                    ) {
1842                        send_ack_now = false;
1843                    }
1844                }
1845            } // end match
1846
1847            if send_ack_now {
1848                let ack_msg = RosflightCmdAckMsg {
1849                    command: msg.command,
1850                    success,
1851                };
1852                comm_events
1853                    .responses
1854                    .push_or_log(CommResponse::CmdAck(ack_msg), "command ack response");
1855            }
1856        } // end if let Some(msg)
1857    }
1858
1859    pub fn send_comm_responses(&mut self, board: &mut B, comm_events: &mut CommEventQueues) {
1860        self.send_comm_responses_limited(board, comm_events, usize::MAX);
1861    }
1862
1863    pub fn send_comm_responses_limited(
1864        &mut self,
1865        board: &mut B,
1866        comm_events: &mut CommEventQueues,
1867        max_responses: usize,
1868    ) -> usize {
1869        let mut sent = 0;
1870        while sent < max_responses
1871            && let Some(response) = comm_events.responses.pop()
1872        {
1873            match response {
1874                CommResponse::ParamValue(msg) => {
1875                    if msg.param_index == ParamId::PARAM_SYSTEM_ID as u16 {
1876                        if let ParamValue::Int(new_sysid) = msg.param_value {
1877                            self.sysid = new_sysid as u8;
1878                        }
1879                    }
1880                    self.comm_link.send_named_value(board, self.sysid, msg);
1881                }
1882                CommResponse::CmdAck(msg) => {
1883                    self.comm_link.send_cmd_ack(board, self.sysid, msg);
1884                }
1885                CommResponse::Version(msg) => {
1886                    self.comm_link.send_version(board, self.sysid, msg);
1887                }
1888                CommResponse::Statustext(msg) => {
1889                    self.comm_link.send_statustext(board, self.sysid, msg);
1890                }
1891                CommResponse::HardError(msg) => {
1892                    self.comm_link.send_hard_error(board, self.sysid, msg);
1893                }
1894            }
1895            sent += 1;
1896        }
1897        sent
1898    }
1899
1900    pub fn send_timesync(&mut self, board: &mut B, msg: TimesyncMsg) {
1901        self.comm_link.send_timesync(board, self.sysid, msg);
1902    }
1903
1904    pub fn send_rosflight_heartbeat(&mut self, board: &mut B, msg: HeartbeatMsg) {
1905        self.comm_link.send_heartbeat(board, self.sysid, msg);
1906    }
1907
1908    pub fn send_rosflight_status(&mut self, board: &mut B, msg: RosflightStatusMsg) {
1909        self.comm_link.send_status(board, self.sysid, msg);
1910    }
1911
1912    pub fn send_rosflight_attitude_quaternion(
1913        &mut self,
1914        board: &mut B,
1915        msg: AttitudeQuaternionMsg,
1916    ) {
1917        self.comm_link.send_attitude(board, self.sysid, msg);
1918    }
1919
1920    pub fn send_rosflight_small_imu(&mut self, board: &mut B, msg: SmallImuMsg) {
1921        self.comm_link.send_imu(board, self.sysid, msg);
1922    }
1923
1924    pub fn send_rosflight_small_baro(&mut self, board: &mut B, msg: SmallBaroMsg) {
1925        self.comm_link.send_baro(board, self.sysid, msg);
1926    }
1927
1928    pub fn send_rosflight_diff_pressure(&mut self, board: &mut B, msg: DiffPressureMsg) {
1929        self.comm_link.send_diff_pressure(board, self.sysid, msg);
1930    }
1931
1932    pub fn send_rosflight_small_mag(&mut self, board: &mut B, msg: SmallMagMsg) {
1933        self.comm_link.send_mag(board, self.sysid, msg);
1934    }
1935
1936    pub fn send_rosflight_small_range(&mut self, board: &mut B, msg: SmallRangeMsg) {
1937        self.comm_link.send_range(board, self.sysid, msg);
1938    }
1939
1940    pub fn send_rosflight_battery_status(&mut self, board: &mut B, msg: BatteryStatusMsg) {
1941        self.comm_link.send_battery_status(board, self.sysid, msg);
1942    }
1943
1944    pub fn send_rosflight_gnss(&mut self, board: &mut B, msg: RosflightGnssMsg) {
1945        self.comm_link.send_gnss(board, self.sysid, msg);
1946    }
1947
1948    pub fn send_rosflight_rc_raw(&mut self, board: &mut B, msg: RcChannelsMsg) {
1949        self.comm_link.send_rc_raw(board, self.sysid, msg);
1950    }
1951
1952    pub fn send_rosflight_output_raw(&mut self, board: &mut B, msg: RosflightOutputRawMsg) {
1953        self.comm_link.send_output_raw(board, self.sysid, msg);
1954    }
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959    use super::*;
1960    use crate::{
1961        board::BoardIo,
1962        comm::messages::{
1963            enums::{
1964                OffboardControlIgnore, OffboardControlMode, ParamIdentifier, RosflightAuxCmdType,
1965                RosflightCmd, RosflightCmdResponse,
1966            },
1967            messages::{
1968                ExternalAttitudeMsg, HeartbeatMsg, OffboardControlMsg, ParamRequestListMsg,
1969                ParamRequestReadMsg, ParamSetMsg, RosflightAuxCmdMsg, RosflightCmdMsg, TimesyncMsg,
1970            },
1971        },
1972        command::CommandManager,
1973        command::service::{self as command_service, CommandRequestCtx},
1974        controller::quad::QuadController,
1975        events::{
1976            CommEventQueues, CommResponse, CommandEventQueues, CompanionEventQueues,
1977            ParamEventQueues,
1978        },
1979        params::service::{self as param_service, ParamListState, ParamServiceCtx},
1980        params::{ParamId, ParamValue, Params},
1981        sensors::ProcessedSensors,
1982        sensors::processors::CalibrationFlags,
1983        state_machine::{Event, StateManager},
1984        test_support::{RecordingCommLink, TestBoard},
1985    };
1986
1987    fn initialized_state() -> StateManager {
1988        let params = Params::new();
1989        let mut state = StateManager::new();
1990        state.update(Event::INITIALIZED, &params);
1991        state
1992    }
1993
1994    #[test]
1995    fn live_telemetry_update_changes_only_one_stream_and_restarts_its_period() {
1996        let mut params = Params::new();
1997        let mut manager =
1998            CommManager::<TestBoard, RecordingCommLink>::new(RecordingCommLink::new(), 10);
1999        manager.configure_telemetry_from_params(&params);
2000        let original = manager.telemetry_rates();
2001
2002        params.set_by_id(ParamId::PARAM_TELEM_BARO_HZ, ParamValue::Int(20));
2003        assert!(manager.update_telemetry_param(&params, ParamId::PARAM_TELEM_BARO_HZ, 1_000_000,));
2004
2005        let updated = manager.telemetry_rates();
2006        assert_eq!(updated.baro_hz, 20);
2007        assert_eq!(updated.imu_hz, original.imu_hz);
2008        assert_eq!(updated.rc_hz, original.rc_hz);
2009        assert_eq!(manager.telemetry_rate_state.baro_us, 1_000_000);
2010        assert!(stream_due_deadline_us(1_049_999, 1_000_000, updated.baro_hz).is_none());
2011        assert!(stream_due_deadline_us(1_050_000, 1_000_000, updated.baro_hz).is_some());
2012    }
2013
2014    #[test]
2015    fn telemetry_rate_minus_one_disables_and_zero_is_always_eligible() {
2016        let mut params = Params::new();
2017        params.set_by_id(ParamId::PARAM_TELEM_BARO_HZ, ParamValue::Int(-1));
2018        let disabled = TelemetryRates::from_params(&params).baro_hz;
2019        assert_eq!(disabled, TELEMETRY_RATE_DISABLED);
2020        assert!(!stream_due(1_000, &mut 0, disabled));
2021        assert_eq!(stream_due_deadline_us(1_000, 0, disabled), None);
2022
2023        params.set_by_id(ParamId::PARAM_TELEM_BARO_HZ, ParamValue::Int(0));
2024        let whenever = TelemetryRates::from_params(&params).baro_hz;
2025        assert!(stream_due(1_000, &mut 0, whenever));
2026        assert!(stream_due(1_001, &mut 1_000, whenever));
2027    }
2028
2029    fn apply_test_command_requests(
2030        command_events: &mut CommandEventQueues,
2031        comm_events: &mut CommEventQueues,
2032        board: &mut TestBoard,
2033        params: &mut Params,
2034        flags: &mut CalibrationFlags,
2035    ) {
2036        let state = initialized_state();
2037        let mut command = CommandManager::new();
2038        let mut controller = QuadController::<f64>::default();
2039        let mut param_events = ParamEventQueues::default();
2040        command_service::apply_command_requests(&mut CommandRequestCtx {
2041            requests: command_events,
2042            param_events: &mut param_events,
2043            comm_events,
2044            state: &state,
2045            command: &mut command,
2046            controller: &mut controller,
2047            board,
2048            flags,
2049            params,
2050        });
2051    }
2052
2053    fn apply_test_param_service(
2054        params: &mut Params,
2055        param_list_state: &mut ParamListState,
2056        param_events: &mut ParamEventQueues,
2057        comm_events: &mut CommEventQueues,
2058    ) {
2059        param_service::service_param_events(&mut ParamServiceCtx {
2060            params,
2061            state: param_list_state,
2062            events: param_events,
2063            comm_events,
2064        });
2065    }
2066
2067    fn telemetry_ctx<'a, S, A, R>(
2068        board: &'a mut TestBoard,
2069        now_us: u64,
2070        state: &'a StateManager,
2071        command: &'a CommandManager,
2072        params: &'a Params,
2073        estimator_state: &'a S,
2074        sensors: &'a ProcessedSensors<R>,
2075        actuator_commands: &'a A,
2076    ) -> TelemetryCtx<'a, TestBoard, S, A, R>
2077    where
2078        R: FlightFloat,
2079    {
2080        TelemetryCtx {
2081            board,
2082            now_us,
2083            state,
2084            command,
2085            params,
2086            estimator_state,
2087            sensors,
2088            actuator_commands,
2089            sensor_error_count: 0,
2090            loop_time_us: 0,
2091        }
2092    }
2093
2094    fn companion_events() -> CompanionEventQueues {
2095        CompanionEventQueues::default()
2096    }
2097
2098    #[test]
2099    fn stream_due_preserves_deadline_cadence_after_late_send() {
2100        let mut last_us = 0;
2101
2102        assert!(stream_due(1_000, &mut last_us, 400));
2103        assert_eq!(last_us, 1_000);
2104
2105        assert!(!stream_due(3_400, &mut last_us, 400));
2106        assert_eq!(last_us, 1_000);
2107
2108        assert!(stream_due(3_700, &mut last_us, 400));
2109        assert_eq!(last_us, 3_500);
2110
2111        assert!(!stream_due(5_900, &mut last_us, 400));
2112        assert_eq!(last_us, 3_500);
2113
2114        assert!(stream_due(6_100, &mut last_us, 400));
2115        assert_eq!(last_us, 6_000);
2116
2117        assert!(stream_due(13_900, &mut last_us, 400));
2118        assert_eq!(last_us, 13_500);
2119    }
2120
2121    #[test]
2122    fn param_set_emits_request_without_mutating_or_acknowledging() {
2123        let mut board = TestBoard::default();
2124        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2125        let params = Params::new();
2126        let mut param_events = ParamEventQueues::default();
2127        let mut comm_events = CommEventQueues::default();
2128        let mut command_events = CommandEventQueues::default();
2129
2130        manager.msgs.store(ParamSetMsg {
2131            target_system: 1,
2132            target_component: 1,
2133            param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2134            param_value: ParamValue::Int(42),
2135        });
2136
2137        manager.act_on_messages(
2138            &mut param_events,
2139            &mut comm_events,
2140            &mut command_events,
2141            &mut companion_events(),
2142            &mut board,
2143        );
2144
2145        assert_eq!(
2146            params.get_by_id(ParamId::PARAM_SYSTEM_ID),
2147            ParamValue::Int(1)
2148        );
2149        assert_eq!(manager.comm_link.sent_param_value_count, 0);
2150
2151        let request = param_events.set_requests.pop().unwrap();
2152        assert_eq!(request.value, ParamValue::Int(42));
2153        assert_eq!(request.param_id_bytes, *b"SYS_ID\0\0\0\0\0\0\0\0\0\0");
2154    }
2155
2156    #[test]
2157    fn param_set_ingress_waits_when_ecs_queue_is_full() {
2158        let mut board = TestBoard::default();
2159        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2160        let mut param_events = ParamEventQueues::default();
2161        let mut comm_events = CommEventQueues::default();
2162        let mut command_events = CommandEventQueues::default();
2163
2164        for value in 10..15 {
2165            manager.msgs.store(ParamSetMsg {
2166                target_system: 1,
2167                target_component: 1,
2168                param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2169                param_value: ParamValue::Int(value),
2170            });
2171        }
2172
2173        manager.act_on_messages(
2174            &mut param_events,
2175            &mut comm_events,
2176            &mut command_events,
2177            &mut companion_events(),
2178            &mut board,
2179        );
2180
2181        assert!(!param_events.set_requests.is_full());
2182        assert_eq!(param_events.set_requests.len(), 5);
2183        assert_eq!(manager.msgs.param_set.len(), 0);
2184
2185        let _ = param_events.set_requests.pop();
2186        manager.act_on_messages(
2187            &mut param_events,
2188            &mut comm_events,
2189            &mut command_events,
2190            &mut companion_events(),
2191            &mut board,
2192        );
2193
2194        assert!(!param_events.set_requests.is_full());
2195        assert_eq!(manager.msgs.param_set.len(), 0);
2196        let values: heapless::Vec<_, 8> = param_events
2197            .set_requests
2198            .iter()
2199            .map(|req| req.value)
2200            .collect();
2201        assert_eq!(
2202            values.as_slice(),
2203            &[
2204                ParamValue::Int(11),
2205                ParamValue::Int(12),
2206                ParamValue::Int(13),
2207                ParamValue::Int(14),
2208            ]
2209        );
2210    }
2211
2212    #[test]
2213    fn param_set_ingress_accepts_full_companion_param_burst() {
2214        let mut board = TestBoard::default();
2215        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2216        let mut param_events = ParamEventQueues::default();
2217        let mut comm_events = CommEventQueues::default();
2218        let mut command_events = CommandEventQueues::default();
2219
2220        for value in 0..360 {
2221            manager.msgs.store(ParamSetMsg {
2222                target_system: 1,
2223                target_component: 1,
2224                param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2225                param_value: ParamValue::Int(value),
2226            });
2227        }
2228
2229        assert_eq!(manager.msgs.param_set.len(), 360);
2230
2231        manager.act_on_messages(
2232            &mut param_events,
2233            &mut comm_events,
2234            &mut command_events,
2235            &mut companion_events(),
2236            &mut board,
2237        );
2238
2239        assert_eq!(manager.msgs.param_set.len(), 0);
2240        assert_eq!(param_events.set_requests.len(), 360);
2241        assert_eq!(
2242            param_events.set_requests.iter().next().unwrap().value,
2243            ParamValue::Int(0)
2244        );
2245        assert_eq!(
2246            param_events.set_requests.iter().last().unwrap().value,
2247            ParamValue::Int(359)
2248        );
2249    }
2250
2251    #[test]
2252    fn param_messages_for_other_system_are_ignored() {
2253        let mut board = TestBoard::default();
2254        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2255        let mut param_events = ParamEventQueues::default();
2256        let mut comm_events = CommEventQueues::default();
2257        let mut command_events = CommandEventQueues::default();
2258
2259        manager.msgs.store(ParamSetMsg {
2260            target_system: 42,
2261            target_component: 1,
2262            param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2263            param_value: ParamValue::Int(42),
2264        });
2265        manager.msgs.param_request_list = Some(ParamRequestListMsg {
2266            target_system: 42,
2267            target_component: 1,
2268        });
2269        manager.msgs.store(ParamRequestReadMsg {
2270            target_system: 42,
2271            target_component: 1,
2272            param_identifier: ParamIdentifier::ID(*b"SYS_ID\0\0\0\0\0\0\0\0\0\0"),
2273        });
2274
2275        manager.act_on_messages(
2276            &mut param_events,
2277            &mut comm_events,
2278            &mut command_events,
2279            &mut companion_events(),
2280            &mut board,
2281        );
2282
2283        assert!(param_events.set_requests.is_empty());
2284        assert!(param_events.list_requests.is_empty());
2285        assert!(param_events.read_requests.is_empty());
2286    }
2287
2288    #[test]
2289    fn param_request_list_emits_request_without_streaming_from_comms() {
2290        let mut board = TestBoard::default();
2291        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2292        let mut params = Params::new();
2293        let mut param_list_state = ParamListState::default();
2294        let mut param_events = ParamEventQueues::default();
2295        let mut comm_events = CommEventQueues::default();
2296        let mut command_events = CommandEventQueues::default();
2297
2298        manager.msgs.param_request_list = Some(ParamRequestListMsg {
2299            target_system: 1,
2300            target_component: 1,
2301        });
2302
2303        manager.act_on_messages(
2304            &mut param_events,
2305            &mut comm_events,
2306            &mut command_events,
2307            &mut companion_events(),
2308            &mut board,
2309        );
2310
2311        assert_eq!(manager.comm_link.sent_param_value_count, 0);
2312        assert_eq!(param_events.list_requests.len(), 1);
2313
2314        apply_test_param_service(
2315            &mut params,
2316            &mut param_list_state,
2317            &mut param_events,
2318            &mut comm_events,
2319        );
2320
2321        manager.send_comm_responses(&mut board, &mut comm_events);
2322
2323        assert_eq!(manager.comm_link.sent_param_value_count, 1);
2324        let sent = manager.comm_link.sent_param_values[0].unwrap();
2325        assert_eq!(sent.param_index, ParamId::PARAM_BAUD_RATE as u16);
2326        assert_eq!(sent.param_value, ParamValue::Int(921600));
2327    }
2328
2329    #[test]
2330    fn param_request_read_emits_request_without_reading_from_comms() {
2331        let mut board = TestBoard::default();
2332        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2333        let mut params = Params::new();
2334        params.set_by_id(ParamId::PARAM_SYSTEM_ID, ParamValue::Int(42));
2335        let mut param_events = ParamEventQueues::default();
2336        let mut comm_events = CommEventQueues::default();
2337        let mut command_events = CommandEventQueues::default();
2338
2339        manager.msgs.store(ParamRequestReadMsg {
2340            target_system: 1,
2341            target_component: 1,
2342            param_identifier: ParamIdentifier::ID(*b"SYS_ID\0\0\0\0\0\0\0\0\0\0"),
2343        });
2344
2345        manager.act_on_messages(
2346            &mut param_events,
2347            &mut comm_events,
2348            &mut command_events,
2349            &mut companion_events(),
2350            &mut board,
2351        );
2352
2353        assert_eq!(manager.comm_link.sent_param_value_count, 0);
2354        assert_eq!(param_events.read_requests.len(), 1);
2355
2356        let mut param_list_state = ParamListState::default();
2357        apply_test_param_service(
2358            &mut params,
2359            &mut param_list_state,
2360            &mut param_events,
2361            &mut comm_events,
2362        );
2363
2364        manager.send_comm_responses(&mut board, &mut comm_events);
2365
2366        assert_eq!(manager.comm_link.sent_param_value_count, 1);
2367        let sent = manager.comm_link.sent_param_values[0].unwrap();
2368        assert_eq!(sent.param_index, ParamId::PARAM_SYSTEM_ID as u16);
2369        assert_eq!(sent.param_value, ParamValue::Int(42));
2370    }
2371
2372    #[test]
2373    fn param_request_read_burst_preserves_all_missing_index_requests() {
2374        let mut board = TestBoard::default();
2375        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2376        let mut param_events = ParamEventQueues::default();
2377        let mut comm_events = CommEventQueues::default();
2378        let mut command_events = CommandEventQueues::default();
2379
2380        for index in [330, 331, 332, 333, 334, 335, 336] {
2381            manager.msgs.store(ParamRequestReadMsg {
2382                target_system: 1,
2383                target_component: 1,
2384                param_identifier: ParamIdentifier::INDEX(index),
2385            });
2386        }
2387
2388        manager.act_on_messages(
2389            &mut param_events,
2390            &mut comm_events,
2391            &mut command_events,
2392            &mut companion_events(),
2393            &mut board,
2394        );
2395
2396        let received: heapless::Vec<_, 8> = param_events
2397            .read_requests
2398            .iter()
2399            .map(|req| req.identifier)
2400            .collect();
2401
2402        assert_eq!(received.len(), 7);
2403        assert_eq!(received[0], ParamIdentifier::INDEX(330));
2404        assert_eq!(received[6], ParamIdentifier::INDEX(336));
2405    }
2406
2407    #[test]
2408    fn send_comm_responses_sends_param_value_and_updates_sysid() {
2409        let mut board = TestBoard::default();
2410        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2411        let mut comm_events = CommEventQueues::default();
2412
2413        let _ = comm_events
2414            .responses
2415            .push(CommResponse::ParamValue(ParamValueMsg {
2416                param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2417                param_value: ParamValue::Int(42),
2418                param_count: 1,
2419                param_index: ParamId::PARAM_SYSTEM_ID as u16,
2420            }));
2421
2422        manager.send_comm_responses(&mut board, &mut comm_events);
2423
2424        assert_eq!(manager.sysid, 42);
2425        assert_eq!(manager.comm_link.sent_param_value_count, 1);
2426
2427        let sent = manager.comm_link.sent_param_values[0].unwrap();
2428        assert_eq!(sent.param_id, *b"SYS_ID\0\0\0\0\0\0\0\0\0\0");
2429        assert_eq!(sent.param_value, ParamValue::Int(42));
2430        assert!(comm_events.responses.is_empty());
2431    }
2432
2433    #[test]
2434    fn send_comm_responses_sends_command_ack_and_version() {
2435        let mut board = TestBoard::default();
2436        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2437        let mut comm_events = CommEventQueues::default();
2438
2439        let _ = comm_events
2440            .responses
2441            .push(CommResponse::Version(RosflightVersionMsg {
2442                version: [7; 50],
2443            }));
2444        let _ = comm_events
2445            .responses
2446            .push(CommResponse::CmdAck(RosflightCmdAckMsg {
2447                command: RosflightCmd::SendVersion,
2448                success: RosflightCmdResponse::RosflightCmdSuccess,
2449            }));
2450        let _ = comm_events
2451            .responses
2452            .push(CommResponse::Statustext(StatustextMsg {
2453                severity: Severity::Info,
2454                text: [9; 50],
2455            }));
2456
2457        manager.send_comm_responses(&mut board, &mut comm_events);
2458
2459        assert_eq!(manager.comm_link().version_count, 1);
2460        assert_eq!(manager.comm_link().last_version.unwrap().version, [7; 50]);
2461        assert_eq!(manager.comm_link().cmd_ack_count, 1);
2462        assert_eq!(manager.comm_link().statustext_count, 1);
2463        assert_eq!(manager.comm_link().last_statustext.unwrap().text, [9; 50]);
2464        assert!(matches!(
2465            manager.comm_link().last_cmd_ack.unwrap().command,
2466            RosflightCmd::SendVersion
2467        ));
2468        assert!(comm_events.responses.is_empty());
2469    }
2470
2471    #[test]
2472    fn send_version_command_enqueues_version_and_ack_responses() {
2473        let mut board = TestBoard::default();
2474        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2475        let mut param_events = ParamEventQueues::default();
2476        let mut comm_events = CommEventQueues::default();
2477        let mut command_events = CommandEventQueues::default();
2478
2479        manager.msgs.cmd = Some(RosflightCmdMsg {
2480            command: RosflightCmd::SendVersion,
2481        });
2482
2483        manager.act_on_messages(
2484            &mut param_events,
2485            &mut comm_events,
2486            &mut command_events,
2487            &mut companion_events(),
2488            &mut board,
2489        );
2490
2491        assert_eq!(manager.comm_link().version_count, 0);
2492        assert_eq!(manager.comm_link().cmd_ack_count, 0);
2493        assert!(comm_events.responses.is_empty());
2494        assert_eq!(command_events.version_requests.len(), 1);
2495
2496        let mut params = Params::new();
2497        let mut cal_flags = CalibrationFlags::empty();
2498        apply_test_command_requests(
2499            &mut command_events,
2500            &mut comm_events,
2501            &mut board,
2502            &mut params,
2503            &mut cal_flags,
2504        );
2505        manager.send_comm_responses(&mut board, &mut comm_events);
2506
2507        assert_eq!(manager.comm_link().version_count, 1);
2508        assert_eq!(manager.comm_link().cmd_ack_count, 1);
2509        assert!(matches!(
2510            manager.comm_link().last_cmd_ack.unwrap().success,
2511            RosflightCmdResponse::RosflightCmdSuccess
2512        ));
2513    }
2514
2515    #[test]
2516    fn param_set_pipeline_defers_ack_until_after_apply_stage() {
2517        let mut board = TestBoard::default();
2518        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2519        let mut params = Params::new();
2520        let mut param_events = ParamEventQueues::default();
2521        let mut comm_events = CommEventQueues::default();
2522        let mut command_events = CommandEventQueues::default();
2523
2524        manager.msgs.store(ParamSetMsg {
2525            target_system: 1,
2526            target_component: 1,
2527            param_id: *b"SYS_ID\0\0\0\0\0\0\0\0\0\0",
2528            param_value: ParamValue::Int(42),
2529        });
2530
2531        manager.act_on_messages(
2532            &mut param_events,
2533            &mut comm_events,
2534            &mut command_events,
2535            &mut companion_events(),
2536            &mut board,
2537        );
2538
2539        assert_eq!(
2540            params.get_by_id(ParamId::PARAM_SYSTEM_ID),
2541            ParamValue::Int(1)
2542        );
2543        assert_eq!(manager.comm_link.sent_param_value_count, 0);
2544
2545        let mut param_list_state = ParamListState::default();
2546        apply_test_param_service(
2547            &mut params,
2548            &mut param_list_state,
2549            &mut param_events,
2550            &mut comm_events,
2551        );
2552
2553        assert_eq!(
2554            params.get_by_id(ParamId::PARAM_SYSTEM_ID),
2555            ParamValue::Int(42)
2556        );
2557        assert_eq!(manager.comm_link.sent_param_value_count, 0);
2558
2559        let change = param_events.changes.iter().next().unwrap();
2560        assert_eq!(change.id, ParamId::PARAM_SYSTEM_ID);
2561        assert_eq!(change.old, ParamValue::Int(1));
2562        assert_eq!(change.new, ParamValue::Int(42));
2563
2564        manager.send_comm_responses(&mut board, &mut comm_events);
2565
2566        assert_eq!(manager.sysid, 42);
2567        assert_eq!(manager.comm_link.sent_param_value_count, 1);
2568        assert!(comm_events.responses.is_empty());
2569    }
2570
2571    #[test]
2572    fn named_telemetry_sends_sensor_state_and_output_messages() {
2573        let mut board = TestBoard {
2574            current_time_us: 1_100_000,
2575            tx_write_count: 0,
2576            ..Default::default()
2577        };
2578        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
2579        let state_manager = StateManager::new();
2580        let command_manager = CommandManager::new();
2581        let params = Params::new();
2582        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
2583        let actuator_commands = [0.1, 0.2, 0.3, 0.4];
2584        let mut processed_sensors = ProcessedSensors::<f64>::default();
2585        processed_sensors.imu = Some(crate::packets::ImuPacket {
2586            header: crate::packets::RosflightPacketHeader {
2587                timestamp: 9_000,
2588                status: 0,
2589            },
2590            accel: [1.0, 2.0, 3.0],
2591            gyro: [4.0, 5.0, 6.0],
2592            temperature: 25.0,
2593            seq: 1,
2594        });
2595        processed_sensors.pitot = Some(crate::packets::PitotPacket {
2596            differential_pressure: 12.5,
2597            indicated_airspeed: 8.25,
2598            temperature: 24.0,
2599            ..Default::default()
2600        });
2601        processed_sensors.baro = Some(crate::packets::BaroPacket {
2602            altitude: 123.0,
2603            pressure: 95_000.0,
2604            temperature: 22.0,
2605            ..Default::default()
2606        });
2607        processed_sensors.range = Some(crate::packets::RangePacket {
2608            range: 3.5,
2609            min_range: 0.25,
2610            max_range: 8.0,
2611            range_type: crate::packets::RangeType::Lidar,
2612            ..Default::default()
2613        });
2614        processed_sensors.gnss = Some(crate::packets::GNSSPacket {
2615            header: crate::packets::RosflightPacketHeader {
2616                timestamp: 77_000,
2617                status: 0,
2618            },
2619            unix_seconds: 1_700_000_001,
2620            unix_nanos: 123_456_789,
2621            height_msl: 1_402.25,
2622            num_sats: 9,
2623            ..Default::default()
2624        });
2625
2626        let now_us = board.clock_micros();
2627
2628        manager.send_named_telemetry_streams(telemetry_ctx(
2629            &mut board,
2630            now_us,
2631            &state_manager,
2632            &command_manager,
2633            &params,
2634            &estimator_state,
2635            &processed_sensors,
2636            &actuator_commands,
2637        ));
2638
2639        assert_eq!(manager.comm_link().heartbeat_count, 1);
2640        assert_eq!(manager.comm_link().status_count, 1);
2641        assert_eq!(manager.comm_link().imu_count, 1);
2642        assert_eq!(manager.comm_link().attitude_count, 1);
2643        assert_eq!(manager.comm_link().diff_pressure_count, 1);
2644        assert_eq!(manager.comm_link().baro_count, 1);
2645        assert_eq!(manager.comm_link().range_count, 1);
2646        assert_eq!(manager.comm_link().gnss_count, 1);
2647        assert_eq!(manager.comm_link().output_raw_count, 1);
2648        assert_eq!(manager.comm_link().last_imu.unwrap().temperature, 25.0);
2649        assert_eq!(
2650            manager.comm_link().last_diff_pressure.unwrap().velocity,
2651            8.25
2652        );
2653        assert_eq!(manager.comm_link().last_baro.unwrap().altitude, 123.0);
2654        let range = manager.comm_link().last_range.unwrap();
2655        assert!(matches!(
2656            range.type_,
2657            RosflightRangeType::RosflightRangeLidar
2658        ));
2659        assert_eq!(range.min_range, 0.25);
2660        assert_eq!(range.max_range, 8.0);
2661        let gnss = manager.comm_link().last_gnss.unwrap();
2662        assert_eq!(gnss.seconds, 1_700_000_001);
2663        assert_eq!(gnss.nanos, 123_456_789);
2664        assert_eq!(gnss.height_msl, 1_402.25);
2665
2666        let output = manager.comm_link().last_output_raw.unwrap();
2667        assert_eq!(output.stamp, 1100);
2668        assert_eq!(output.values[0], 0.1);
2669        assert_eq!(output.values[1], 0.2);
2670        assert_eq!(output.values[2], 0.3);
2671        assert_eq!(output.values[3], 0.4);
2672    }
2673
2674    #[test]
2675    fn realtime_named_telemetry_sends_deadline_ordered_streams() {
2676        let mut board = TestBoard {
2677            current_time_us: 1_100_000,
2678            tx_write_count: 0,
2679            ..Default::default()
2680        };
2681        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
2682        manager.set_telemetry_rates(TelemetryRates::bounded_high_rate_transport());
2683        let state_manager = StateManager::new();
2684        let command_manager = CommandManager::new();
2685        let params = Params::new();
2686        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
2687        let actuator_commands = [0.1, 0.2, 0.3, 0.4];
2688        let mut processed_sensors = ProcessedSensors::<f64>::default();
2689        processed_sensors.imu = Some(crate::packets::ImuPacket {
2690            header: crate::packets::RosflightPacketHeader {
2691                timestamp: 9_000,
2692                status: 0,
2693            },
2694            accel: [1.0, 2.0, 3.0],
2695            gyro: [4.0, 5.0, 6.0],
2696            temperature: 25.0,
2697            seq: 1,
2698        });
2699        processed_sensors.baro = Some(crate::packets::BaroPacket {
2700            altitude: 123.0,
2701            pressure: 95_000.0,
2702            temperature: 22.0,
2703            ..Default::default()
2704        });
2705
2706        let now_us = board.clock_micros();
2707        assert!(manager.send_one_named_telemetry_stream(telemetry_ctx(
2708            &mut board,
2709            now_us,
2710            &state_manager,
2711            &command_manager,
2712            &params,
2713            &estimator_state,
2714            &processed_sensors,
2715            &actuator_commands,
2716        )));
2717        assert_eq!(manager.comm_link().imu_count, 1);
2718        assert_eq!(manager.comm_link().attitude_count, 0);
2719        assert_eq!(manager.comm_link().output_raw_count, 0);
2720        assert_eq!(manager.comm_link().baro_count, 0);
2721
2722        assert!(manager.send_one_named_telemetry_stream(telemetry_ctx(
2723            &mut board,
2724            now_us,
2725            &state_manager,
2726            &command_manager,
2727            &params,
2728            &estimator_state,
2729            &processed_sensors,
2730            &actuator_commands,
2731        )));
2732        assert_eq!(manager.comm_link().imu_count, 1);
2733        assert_eq!(manager.comm_link().attitude_count, 1);
2734        assert_eq!(manager.comm_link().output_raw_count, 0);
2735        assert_eq!(manager.comm_link().baro_count, 0);
2736
2737        assert!(manager.send_one_named_telemetry_stream(telemetry_ctx(
2738            &mut board,
2739            now_us,
2740            &state_manager,
2741            &command_manager,
2742            &params,
2743            &estimator_state,
2744            &processed_sensors,
2745            &actuator_commands,
2746        )));
2747        assert_eq!(manager.comm_link().output_raw_count, 1);
2748        assert_eq!(manager.comm_link().baro_count, 0);
2749
2750        assert!(manager.send_one_named_telemetry_stream(telemetry_ctx(
2751            &mut board,
2752            now_us,
2753            &state_manager,
2754            &command_manager,
2755            &params,
2756            &estimator_state,
2757            &processed_sensors,
2758            &actuator_commands,
2759        )));
2760        assert_eq!(manager.comm_link().output_raw_count, 1);
2761        assert_eq!(manager.comm_link().baro_count, 1);
2762    }
2763
2764    #[test]
2765    fn realtime_priority_telemetry_uses_stream_due_and_freshness_gates() {
2766        let mut board = TestBoard {
2767            current_time_us: 1_100_000,
2768            tx_write_count: 0,
2769            ..Default::default()
2770        };
2771        let now_us = board.clock_micros();
2772        let mut manager = CommManager::new(RecordingCommLink::new(), now_us);
2773        manager.set_telemetry_rates(TelemetryRates::bounded_high_rate_transport());
2774        manager.last_heartbeat_us = now_us;
2775        manager.last_status_send_us = now_us;
2776        manager.telemetry_rate_state.rc_us = 1_000_000;
2777        manager.telemetry_rate_state.imu_us = 1_097_500;
2778
2779        let state_manager = StateManager::new();
2780        let command_manager = CommandManager::new();
2781        let params = Params::new();
2782        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
2783        let actuator_commands = [0.1, 0.2, 0.3, 0.4];
2784        let mut processed_sensors = ProcessedSensors::<f64>::default();
2785        processed_sensors.imu = Some(crate::packets::ImuPacket {
2786            header: crate::packets::RosflightPacketHeader {
2787                timestamp: 9_000,
2788                status: 0,
2789            },
2790            accel: [1.0, 2.0, 3.0],
2791            gyro: [4.0, 5.0, 6.0],
2792            temperature: 25.0,
2793            seq: 1,
2794        });
2795        processed_sensors.rc = Some(crate::packets::RcPacket {
2796            header: crate::packets::RosflightPacketHeader {
2797                timestamp: 8_000,
2798                status: 0,
2799            },
2800            n_chan: 8,
2801            chan: [0.5; RC_PACKET_CHANNELS],
2802            lol: false,
2803        });
2804
2805        assert!(manager.send_named_telemetry_stream_if_due(
2806            NamedTelemetryStream::Imu,
2807            telemetry_ctx(
2808                &mut board,
2809                now_us,
2810                &state_manager,
2811                &command_manager,
2812                &params,
2813                &estimator_state,
2814                &processed_sensors,
2815                &actuator_commands,
2816            ),
2817        ));
2818        assert_eq!(manager.comm_link().imu_count, 1);
2819        assert_eq!(manager.comm_link().rc_channels_count, 0);
2820
2821        assert!(!manager.send_named_telemetry_stream_if_due(
2822            NamedTelemetryStream::Imu,
2823            telemetry_ctx(
2824                &mut board,
2825                now_us,
2826                &state_manager,
2827                &command_manager,
2828                &params,
2829                &estimator_state,
2830                &processed_sensors,
2831                &actuator_commands,
2832            ),
2833        ));
2834        assert_eq!(manager.comm_link().imu_count, 1);
2835
2836        assert!(manager.send_one_named_telemetry_stream(telemetry_ctx(
2837            &mut board,
2838            now_us,
2839            &state_manager,
2840            &command_manager,
2841            &params,
2842            &estimator_state,
2843            &processed_sensors,
2844            &actuator_commands,
2845        )));
2846        assert_eq!(manager.comm_link().imu_count, 1);
2847    }
2848
2849    #[test]
2850    fn realtime_priority_telemetry_fresh_sample_gate_bypasses_imu_due_phase() {
2851        let mut board = TestBoard {
2852            current_time_us: 1_100_000,
2853            tx_write_count: 0,
2854            ..Default::default()
2855        };
2856        let now_us = board.clock_micros();
2857        let mut manager = CommManager::new(RecordingCommLink::new(), now_us);
2858        manager.set_telemetry_rates(TelemetryRates::bounded_high_rate_transport());
2859        manager.last_heartbeat_us = now_us;
2860        manager.last_status_send_us = now_us;
2861        manager.telemetry_rate_state.imu_us = now_us - 1_000;
2862
2863        let state_manager = StateManager::new();
2864        let command_manager = CommandManager::new();
2865        let params = Params::new();
2866        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
2867        let actuator_commands = [0.1, 0.2, 0.3, 0.4];
2868        let mut processed_sensors = ProcessedSensors::<f64>::default();
2869        processed_sensors.imu = Some(crate::packets::ImuPacket {
2870            header: crate::packets::RosflightPacketHeader {
2871                timestamp: 9_000,
2872                status: 0,
2873            },
2874            accel: [1.0, 2.0, 3.0],
2875            gyro: [4.0, 5.0, 6.0],
2876            temperature: 25.0,
2877            seq: 1,
2878        });
2879
2880        assert!(!manager.send_named_telemetry_stream_if_due(
2881            NamedTelemetryStream::Imu,
2882            telemetry_ctx(
2883                &mut board,
2884                now_us,
2885                &state_manager,
2886                &command_manager,
2887                &params,
2888                &estimator_state,
2889                &processed_sensors,
2890                &actuator_commands,
2891            ),
2892        ));
2893        assert_eq!(manager.comm_link().imu_count, 0);
2894
2895        assert!(manager.send_named_telemetry_stream_with_gate(
2896            RealtimeTelemetryPriority {
2897                stream: NamedTelemetryStream::Imu,
2898                gate: RealtimeTelemetryPriorityGate::FreshSample,
2899            },
2900            telemetry_ctx(
2901                &mut board,
2902                now_us,
2903                &state_manager,
2904                &command_manager,
2905                &params,
2906                &estimator_state,
2907                &processed_sensors,
2908                &actuator_commands,
2909            ),
2910        ));
2911        assert_eq!(manager.comm_link().imu_count, 1);
2912
2913        assert!(!manager.send_named_telemetry_stream_with_gate(
2914            RealtimeTelemetryPriority {
2915                stream: NamedTelemetryStream::Imu,
2916                gate: RealtimeTelemetryPriorityGate::FreshSample,
2917            },
2918            telemetry_ctx(
2919                &mut board,
2920                now_us,
2921                &state_manager,
2922                &command_manager,
2923                &params,
2924                &estimator_state,
2925                &processed_sensors,
2926                &actuator_commands,
2927            ),
2928        ));
2929        assert_eq!(manager.comm_link().imu_count, 1);
2930    }
2931
2932    #[test]
2933    fn bulk_telemetry_sends_each_sensor_timestamp_once_and_keeps_periodic_streams_running() {
2934        let mut board = TestBoard {
2935            current_time_us: 1_100_000,
2936            ..Default::default()
2937        };
2938        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
2939        let state_manager = StateManager::new();
2940        let command_manager = CommandManager::new();
2941        let params = Params::new();
2942        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
2943        let actuator_commands = [0.0; 4];
2944        let mut sensors = ProcessedSensors::<f64>::default();
2945        sensors.imu = Some(crate::packets::ImuPacket::default());
2946        sensors.pitot = Some(crate::packets::PitotPacket::default());
2947        sensors.baro = Some(crate::packets::BaroPacket::default());
2948        sensors.mag = Some(crate::packets::MagPacket::default());
2949        sensors.range = Some(crate::packets::RangePacket::default());
2950        sensors.battery = Some(crate::packets::BatteryPacket::default());
2951        sensors.gnss = Some(crate::packets::GNSSPacket::default());
2952        sensors.rc = Some(crate::packets::RcPacket::default());
2953
2954        let send = |manager: &mut CommManager<TestBoard, RecordingCommLink>,
2955                    board: &mut TestBoard| {
2956            let now_us = board.clock_micros();
2957            manager.send_named_telemetry_streams(telemetry_ctx(
2958                board,
2959                now_us,
2960                &state_manager,
2961                &command_manager,
2962                &params,
2963                &estimator_state,
2964                &sensors,
2965                &actuator_commands,
2966            ));
2967        };
2968
2969        // Timestamp zero is a valid first sample and must be sent once.
2970        send(&mut manager, &mut board);
2971        send(&mut manager, &mut board);
2972
2973        assert_eq!(manager.comm_link().imu_count, 1);
2974        assert_eq!(manager.comm_link().attitude_count, 1);
2975        assert_eq!(manager.comm_link().output_raw_count, 1);
2976        assert_eq!(manager.comm_link().diff_pressure_count, 1);
2977        assert_eq!(manager.comm_link().baro_count, 1);
2978        assert_eq!(manager.comm_link().mag_count, 1);
2979        assert_eq!(manager.comm_link().range_count, 1);
2980        assert_eq!(manager.comm_link().battery_count, 1);
2981        assert_eq!(manager.comm_link().gnss_count, 1);
2982        assert_eq!(manager.comm_link().rc_channels_count, 1);
2983        assert_eq!(manager.comm_link().heartbeat_count, 1);
2984        assert_eq!(manager.comm_link().status_count, 1);
2985
2986        // Heartbeat and status are timer-driven and continue without fresh sensors.
2987        board.current_time_us += 1_000_000;
2988        send(&mut manager, &mut board);
2989        assert_eq!(manager.comm_link().heartbeat_count, 2);
2990        assert_eq!(manager.comm_link().status_count, 2);
2991        assert_eq!(manager.comm_link().baro_count, 1);
2992        assert_eq!(manager.comm_link().mag_count, 1);
2993    }
2994
2995    #[test]
2996    fn budgeted_telemetry_filters_stale_streams_without_starving_fresh_ones() {
2997        let mut board = TestBoard {
2998            current_time_us: 1_000,
2999            ..Default::default()
3000        };
3001        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3002        let mut rates = TelemetryRates::upstream();
3003        rates.heartbeat_hz = TELEMETRY_RATE_DISABLED;
3004        rates.status_hz = TELEMETRY_RATE_DISABLED;
3005        manager.set_telemetry_rates(rates);
3006        let state_manager = StateManager::new();
3007        let command_manager = CommandManager::new();
3008        let params = Params::new();
3009        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3010        let actuator_commands = [0.0; 4];
3011        let mut sensors = ProcessedSensors::<f64>::default();
3012        sensors.baro = Some(crate::packets::BaroPacket {
3013            header: crate::packets::RosflightPacketHeader {
3014                timestamp: 10,
3015                status: 0,
3016            },
3017            ..Default::default()
3018        });
3019        sensors.mag = Some(crate::packets::MagPacket {
3020            header: crate::packets::RosflightPacketHeader {
3021                timestamp: 20,
3022                status: 0,
3023            },
3024            ..Default::default()
3025        });
3026
3027        let send_one = |manager: &mut CommManager<TestBoard, RecordingCommLink>,
3028                        board: &mut TestBoard,
3029                        sensors: &ProcessedSensors<f64>| {
3030            let now_us = board.clock_micros();
3031            manager.send_one_named_telemetry_stream(telemetry_ctx(
3032                board,
3033                now_us,
3034                &state_manager,
3035                &command_manager,
3036                &params,
3037                &estimator_state,
3038                sensors,
3039                &actuator_commands,
3040            ))
3041        };
3042
3043        assert!(send_one(&mut manager, &mut board, &sensors));
3044        assert!(send_one(&mut manager, &mut board, &sensors));
3045        assert!(!send_one(&mut manager, &mut board, &sensors));
3046        assert_eq!(manager.comm_link().baro_count, 1);
3047        assert_eq!(manager.comm_link().mag_count, 1);
3048
3049        sensors.mag.as_mut().unwrap().header.timestamp = 21;
3050        assert!(send_one(&mut manager, &mut board, &sensors));
3051        assert!(!send_one(&mut manager, &mut board, &sensors));
3052        assert_eq!(manager.comm_link().baro_count, 1);
3053        assert_eq!(manager.comm_link().mag_count, 2);
3054    }
3055
3056    #[test]
3057    fn rate_limited_sensor_retains_latest_unsent_sample_until_due() {
3058        let mut board = TestBoard {
3059            current_time_us: 1_000,
3060            ..Default::default()
3061        };
3062        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3063        let mut rates = TelemetryRates::upstream();
3064        rates.heartbeat_hz = TELEMETRY_RATE_DISABLED;
3065        rates.status_hz = TELEMETRY_RATE_DISABLED;
3066        rates.baro_hz = 10;
3067        manager.set_telemetry_rates(rates);
3068        let state_manager = StateManager::new();
3069        let command_manager = CommandManager::new();
3070        let params = Params::new();
3071        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3072        let actuator_commands = [0.0; 4];
3073        let mut sensors = ProcessedSensors::<f64>::default();
3074        sensors.baro = Some(crate::packets::BaroPacket {
3075            header: crate::packets::RosflightPacketHeader {
3076                timestamp: 1,
3077                status: 0,
3078            },
3079            altitude: 1.0,
3080            ..Default::default()
3081        });
3082
3083        let send_baro = |manager: &mut CommManager<TestBoard, RecordingCommLink>,
3084                         board: &mut TestBoard,
3085                         sensors: &ProcessedSensors<f64>| {
3086            let now_us = board.clock_micros();
3087            manager.send_named_telemetry_stream_if_due(
3088                NamedTelemetryStream::Baro,
3089                telemetry_ctx(
3090                    board,
3091                    now_us,
3092                    &state_manager,
3093                    &command_manager,
3094                    &params,
3095                    &estimator_state,
3096                    sensors,
3097                    &actuator_commands,
3098                ),
3099            )
3100        };
3101
3102        assert!(send_baro(&mut manager, &mut board, &sensors));
3103        sensors.baro.as_mut().unwrap().header.timestamp = 2;
3104        sensors.baro.as_mut().unwrap().altitude = 2.0;
3105        board.current_time_us = 50_000;
3106        assert!(!send_baro(&mut manager, &mut board, &sensors));
3107        sensors.baro.as_mut().unwrap().header.timestamp = 3;
3108        sensors.baro.as_mut().unwrap().altitude = 3.0;
3109        board.current_time_us = 101_000;
3110        assert!(send_baro(&mut manager, &mut board, &sensors));
3111        assert_eq!(manager.comm_link().baro_count, 2);
3112        assert_eq!(manager.comm_link().last_baro.unwrap().altitude, 3.0);
3113
3114        manager.set_telemetry_rates(rates);
3115        board.current_time_us = 201_000;
3116        assert!(!send_baro(&mut manager, &mut board, &sensors));
3117        assert_eq!(manager.comm_link().baro_count, 2);
3118    }
3119
3120    #[test]
3121    fn imu_attitude_and_output_track_the_same_source_timestamp_independently() {
3122        let mut board = TestBoard {
3123            current_time_us: 1_000,
3124            ..Default::default()
3125        };
3126        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3127        let mut rates = TelemetryRates::upstream();
3128        rates.heartbeat_hz = TELEMETRY_RATE_DISABLED;
3129        rates.status_hz = TELEMETRY_RATE_DISABLED;
3130        rates.output_raw_hz = 1_000;
3131        rates.output_raw_imu_divisor = 0;
3132        manager.set_telemetry_rates(rates);
3133        let state_manager = StateManager::new();
3134        let command_manager = CommandManager::new();
3135        let params = Params::new();
3136        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3137        let actuator_commands = [0.0; 4];
3138        let mut sensors = ProcessedSensors::<f64>::default();
3139        sensors.imu = Some(crate::packets::ImuPacket::default());
3140
3141        for stream in [
3142            NamedTelemetryStream::Imu,
3143            NamedTelemetryStream::Attitude,
3144            NamedTelemetryStream::OutputRaw,
3145        ] {
3146            let now_us = board.clock_micros();
3147            assert!(manager.send_named_telemetry_stream_if_due(
3148                stream,
3149                telemetry_ctx(
3150                    &mut board,
3151                    now_us,
3152                    &state_manager,
3153                    &command_manager,
3154                    &params,
3155                    &estimator_state,
3156                    &sensors,
3157                    &actuator_commands,
3158                ),
3159            ));
3160        }
3161        assert_eq!(manager.comm_link().imu_count, 1);
3162        assert_eq!(manager.comm_link().attitude_count, 1);
3163        assert_eq!(manager.comm_link().output_raw_count, 1);
3164
3165        for stream in [
3166            NamedTelemetryStream::Imu,
3167            NamedTelemetryStream::Attitude,
3168            NamedTelemetryStream::OutputRaw,
3169        ] {
3170            let now_us = board.clock_micros();
3171            assert!(!manager.send_named_telemetry_stream_if_due(
3172                stream,
3173                telemetry_ctx(
3174                    &mut board,
3175                    now_us,
3176                    &state_manager,
3177                    &command_manager,
3178                    &params,
3179                    &estimator_state,
3180                    &sensors,
3181                    &actuator_commands,
3182                ),
3183            ));
3184        }
3185    }
3186
3187    #[test]
3188    fn named_telemetry_matches_upstream_output_raw_every_eighth_imu_sample() {
3189        let mut board = TestBoard {
3190            current_time_us: 1_100_000,
3191            tx_write_count: 0,
3192            ..Default::default()
3193        };
3194        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3195        let state_manager = StateManager::new();
3196        let command_manager = CommandManager::new();
3197        let params = Params::new();
3198        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3199        let mut processed_sensors = ProcessedSensors::<f64>::default();
3200        processed_sensors.imu = Some(crate::packets::ImuPacket::default());
3201        let actuator_commands = [0.0; 4];
3202
3203        for i in 0..8 {
3204            processed_sensors.imu.as_mut().unwrap().header.timestamp = i;
3205            board.current_time_us = 1_100_000 + i;
3206            let now_us = board.clock_micros();
3207            manager.send_named_telemetry_streams(telemetry_ctx(
3208                &mut board,
3209                now_us,
3210                &state_manager,
3211                &command_manager,
3212                &params,
3213                &estimator_state,
3214                &processed_sensors,
3215                &actuator_commands,
3216            ));
3217        }
3218
3219        assert_eq!(manager.comm_link().imu_count, 8);
3220        assert_eq!(manager.comm_link().attitude_count, 8);
3221        assert_eq!(manager.comm_link().output_raw_count, 1);
3222
3223        board.current_time_us += 1;
3224        processed_sensors.imu.as_mut().unwrap().header.timestamp = 8;
3225        let now_us = board.clock_micros();
3226        manager.send_named_telemetry_streams(telemetry_ctx(
3227            &mut board,
3228            now_us,
3229            &state_manager,
3230            &command_manager,
3231            &params,
3232            &estimator_state,
3233            &processed_sensors,
3234            &actuator_commands,
3235        ));
3236
3237        assert_eq!(manager.comm_link().output_raw_count, 2);
3238    }
3239
3240    #[test]
3241    fn explicit_telemetry_rates_bound_high_rate_streams() {
3242        let mut board = TestBoard {
3243            current_time_us: 1_000,
3244            tx_write_count: 0,
3245            ..Default::default()
3246        };
3247        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3248        manager.set_telemetry_rates(TelemetryRates::bounded_high_rate_transport());
3249        let state_manager = StateManager::new();
3250        let command_manager = CommandManager::new();
3251        let params = Params::new();
3252        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3253        let mut processed_sensors = ProcessedSensors::<f64>::default();
3254        processed_sensors.imu = Some(crate::packets::ImuPacket::default());
3255        processed_sensors.baro = Some(crate::packets::BaroPacket::default());
3256        let actuator_commands = [0.0; 4];
3257
3258        let send_at = |manager: &mut CommManager<TestBoard, RecordingCommLink>,
3259                       board: &mut TestBoard,
3260                       processed_sensors: &mut ProcessedSensors<f64>,
3261                       now_us: u64| {
3262            board.current_time_us = now_us;
3263            processed_sensors.imu.as_mut().unwrap().header.timestamp = now_us;
3264            processed_sensors.baro.as_mut().unwrap().header.timestamp = now_us;
3265            manager.send_named_telemetry_streams(telemetry_ctx(
3266                board,
3267                now_us,
3268                &state_manager,
3269                &command_manager,
3270                &params,
3271                &estimator_state,
3272                &processed_sensors,
3273                &actuator_commands,
3274            ));
3275        };
3276
3277        send_at(&mut manager, &mut board, &mut processed_sensors, 1_000);
3278        assert_eq!(manager.comm_link().imu_count, 1);
3279        assert_eq!(manager.comm_link().attitude_count, 1);
3280        assert_eq!(manager.comm_link().baro_count, 1);
3281        assert_eq!(manager.comm_link().output_raw_count, 1);
3282
3283        send_at(&mut manager, &mut board, &mut processed_sensors, 2_000);
3284        assert_eq!(manager.comm_link().imu_count, 1);
3285        assert_eq!(manager.comm_link().attitude_count, 1);
3286        assert_eq!(manager.comm_link().baro_count, 1);
3287        assert_eq!(manager.comm_link().output_raw_count, 1);
3288
3289        send_at(&mut manager, &mut board, &mut processed_sensors, 3_500);
3290        assert_eq!(manager.comm_link().imu_count, 2);
3291        assert_eq!(manager.comm_link().attitude_count, 1);
3292        assert_eq!(manager.comm_link().baro_count, 1);
3293        assert_eq!(manager.comm_link().output_raw_count, 1);
3294
3295        send_at(&mut manager, &mut board, &mut processed_sensors, 11_000);
3296        assert_eq!(manager.comm_link().imu_count, 3);
3297        assert_eq!(manager.comm_link().attitude_count, 1);
3298        assert_eq!(manager.comm_link().baro_count, 1);
3299        assert_eq!(manager.comm_link().output_raw_count, 1);
3300
3301        send_at(&mut manager, &mut board, &mut processed_sensors, 21_000);
3302        assert_eq!(manager.comm_link().imu_count, 4);
3303        assert_eq!(manager.comm_link().attitude_count, 2);
3304        assert_eq!(manager.comm_link().baro_count, 1);
3305        assert_eq!(manager.comm_link().output_raw_count, 2);
3306
3307        send_at(&mut manager, &mut board, &mut processed_sensors, 41_000);
3308        assert_eq!(manager.comm_link().imu_count, 5);
3309        assert_eq!(manager.comm_link().attitude_count, 3);
3310        assert_eq!(manager.comm_link().baro_count, 2);
3311        assert_eq!(manager.comm_link().output_raw_count, 3);
3312    }
3313
3314    #[test]
3315    fn named_rc_telemetry_matches_upstream_raw_channel_packing() {
3316        let mut board = TestBoard {
3317            current_time_us: 1_234_000,
3318            tx_write_count: 0,
3319            ..Default::default()
3320        };
3321        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3322        let state_manager = StateManager::new();
3323        let command_manager = CommandManager::new();
3324        let params = Params::new();
3325        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3326        let mut processed_sensors = ProcessedSensors::<f64>::default();
3327        let mut rc_packet = crate::packets::RcPacket::default();
3328        rc_packet.n_chan = RC_PACKET_CHANNELS as u32;
3329        let test_channels = [
3330            -1.0, -0.5, 0.0, 0.5, 1.0, 0.25, -0.25, 0.75, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 0.99,
3331            -0.99, 1.0, -1.0,
3332        ];
3333        rc_packet.chan[..test_channels.len()].copy_from_slice(&test_channels);
3334        processed_sensors.rc = Some(rc_packet);
3335        let now_us = board.clock_micros();
3336        let actuator_commands = [0.0; 4];
3337
3338        manager.send_named_telemetry_streams(telemetry_ctx(
3339            &mut board,
3340            now_us,
3341            &state_manager,
3342            &command_manager,
3343            &params,
3344            &estimator_state,
3345            &processed_sensors,
3346            &actuator_commands,
3347        ));
3348
3349        let msg = manager.comm_link().last_rc_channels.unwrap();
3350        assert_eq!(manager.comm_link().rc_channels_count, 1);
3351        assert_eq!(msg.time_boot_ms, 1234);
3352        assert_eq!(msg.chancount, 8);
3353        assert_eq!(msg.rssi, 0);
3354        assert_eq!(
3355            &msg.channels[..8],
3356            &[0, 500, 1000, 1500, 2000, 1250, 750, 1750]
3357        );
3358        assert!(msg.channels[8..].iter().all(|channel| *channel == 0));
3359    }
3360
3361    #[test]
3362    fn named_status_telemetry_reports_command_manager_override_state() {
3363        let mut board = TestBoard {
3364            current_time_us: 1_100_000,
3365            tx_write_count: 0,
3366            ..Default::default()
3367        };
3368        let mut manager = CommManager::new(RecordingCommLink::new(), 0);
3369        let state_manager = StateManager::new();
3370        let mut command_manager = CommandManager::new();
3371        let params = Params::new();
3372        let estimator_state = crate::estimator::quad::AttitudeState::<f64>::default();
3373        let processed_sensors = ProcessedSensors::<f64>::default();
3374        let actuator_commands = [0.0, 0.0, 0.0, 0.0];
3375
3376        command_manager.set_new_offboard_command(
3377            board.clock_micros(),
3378            &OffboardControlMsg {
3379                mode: OffboardControlMode::ModeRollratePitchrateYawrateThrottle,
3380                ignore: OffboardControlIgnore::empty(),
3381                qx: 0.0,
3382                qy: 0.0,
3383                qz: 0.0,
3384                fx: 0.0,
3385                fy: 0.0,
3386                fz: 0.0,
3387                passthrough: [0.0; 4],
3388            },
3389            &params,
3390        );
3391
3392        manager.send_named_telemetry_streams(telemetry_ctx(
3393            &mut board,
3394            1_100_000,
3395            &state_manager,
3396            &command_manager,
3397            &params,
3398            &estimator_state,
3399            &processed_sensors,
3400            &actuator_commands,
3401        ));
3402
3403        let status = manager.comm_link().last_status.unwrap();
3404        assert_eq!(status.offboard, 1);
3405        assert_eq!(status.rc_override, 0);
3406    }
3407
3408    #[test]
3409    fn calibration_command_ack_is_sent_when_calibration_starts() {
3410        let mut board = TestBoard::default();
3411        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3412        let mut param_events = ParamEventQueues::default();
3413        let mut comm_events = CommEventQueues::default();
3414        let mut command_events = CommandEventQueues::default();
3415        let mut cal_flags = CalibrationFlags::empty();
3416        let mut params = Params::new();
3417
3418        manager.msgs.cmd = Some(RosflightCmdMsg {
3419            command: RosflightCmd::GyroCalibration,
3420        });
3421
3422        manager.act_on_messages(
3423            &mut param_events,
3424            &mut comm_events,
3425            &mut command_events,
3426            &mut companion_events(),
3427            &mut board,
3428        );
3429
3430        assert!(cal_flags.is_empty());
3431        apply_test_command_requests(
3432            &mut command_events,
3433            &mut comm_events,
3434            &mut board,
3435            &mut params,
3436            &mut cal_flags,
3437        );
3438
3439        assert!(cal_flags.contains(CalibrationFlags::GYRO));
3440        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3441        manager.send_comm_responses(&mut board, &mut comm_events);
3442        assert_eq!(manager.comm_link().cmd_ack_count, 1);
3443
3444        let ack = manager.comm_link().last_cmd_ack.unwrap();
3445        assert!(matches!(ack.command, RosflightCmd::GyroCalibration));
3446        assert!(matches!(
3447            ack.success,
3448            RosflightCmdResponse::RosflightCmdSuccess
3449        ));
3450    }
3451
3452    #[test]
3453    fn offboard_control_message_emits_command_event() {
3454        let mut board = TestBoard {
3455            current_time_us: 55_000,
3456            tx_write_count: 0,
3457            ..Default::default()
3458        };
3459        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3460        let mut param_events = ParamEventQueues::default();
3461        let mut comm_events = CommEventQueues::default();
3462        let mut command_events = CommandEventQueues::default();
3463
3464        manager.msgs.offboard_control = Some(OffboardControlMsg {
3465            mode: OffboardControlMode::ModeRollPitchYawrateThrottle,
3466            ignore: OffboardControlIgnore::IGNORE_FY,
3467            qx: 0.1,
3468            qy: 0.2,
3469            qz: 0.3,
3470            fx: 0.4,
3471            fy: 0.5,
3472            fz: 0.6,
3473            passthrough: [0.0; 4],
3474        });
3475
3476        manager.act_on_messages(
3477            &mut param_events,
3478            &mut comm_events,
3479            &mut command_events,
3480            &mut companion_events(),
3481            &mut board,
3482        );
3483
3484        let request = command_events.offboard_control_requests.pop().unwrap();
3485        assert_eq!(request.now_us, 55_000);
3486        assert_eq!(
3487            request.msg.mode,
3488            OffboardControlMode::ModeRollPitchYawrateThrottle
3489        );
3490        assert!(
3491            request
3492                .msg
3493                .ignore
3494                .contains(OffboardControlIgnore::IGNORE_FY)
3495        );
3496        assert_eq!(request.msg.qx, 0.1);
3497    }
3498
3499    #[test]
3500    fn companion_inputs_emit_companion_events() {
3501        let mut board = TestBoard::default();
3502        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3503        let mut param_events = ParamEventQueues::default();
3504        let mut comm_events = CommEventQueues::default();
3505        let mut command_events = CommandEventQueues::default();
3506        let mut companion_events = CompanionEventQueues::default();
3507
3508        manager.msgs.heartbeat = Some(HeartbeatMsg {
3509            type_: 1,
3510            autopilot: 2,
3511            base_mode: 3,
3512            custom_mode: 4,
3513            system_status: 5,
3514            mavlink_version: 6,
3515        });
3516        let mut aux = RosflightAuxCmdMsg {
3517            type_array: [RosflightAuxCmdType::Disabled; 14],
3518            aux_cmd_array: [0.0; 14],
3519        };
3520        aux.type_array[1] = RosflightAuxCmdType::Motor;
3521        aux.aux_cmd_array[1] = 0.4;
3522        manager.msgs.aux_cmd = Some(aux);
3523        manager.msgs.external_attitude = Some(ExternalAttitudeMsg {
3524            qw: 1.0,
3525            qx: 0.1,
3526            qy: 0.2,
3527            qz: 0.3,
3528        });
3529
3530        manager.act_on_messages(
3531            &mut param_events,
3532            &mut comm_events,
3533            &mut command_events,
3534            &mut companion_events,
3535            &mut board,
3536        );
3537
3538        assert_eq!(companion_events.heartbeats.len(), 1);
3539        assert_eq!(companion_events.aux_commands.len(), 1);
3540        assert_eq!(companion_events.external_attitudes.len(), 1);
3541        assert_eq!(
3542            companion_events.heartbeats.pop().unwrap().msg.system_status,
3543            5
3544        );
3545        let aux_event = companion_events.aux_commands.pop().unwrap();
3546        assert!(matches!(
3547            aux_event.msg.type_array[1],
3548            RosflightAuxCmdType::Motor
3549        ));
3550        assert_eq!(aux_event.msg.aux_cmd_array[1], 0.4);
3551        assert_eq!(
3552            companion_events.external_attitudes.pop().unwrap().msg.qz,
3553            0.3
3554        );
3555    }
3556
3557    #[test]
3558    fn timesync_responds_only_to_requests_and_uses_local_time() {
3559        let mut board = TestBoard {
3560            current_time_us: 123,
3561            ..Default::default()
3562        };
3563        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3564        let mut param_events = ParamEventQueues::default();
3565        let mut comm_events = CommEventQueues::default();
3566        let mut command_events = CommandEventQueues::default();
3567
3568        manager.msgs.timesync = Some(TimesyncMsg { tc1: 99, ts1: 55 });
3569        manager.act_on_messages(
3570            &mut param_events,
3571            &mut comm_events,
3572            &mut command_events,
3573            &mut companion_events(),
3574            &mut board,
3575        );
3576        assert_eq!(manager.comm_link().timesync_count, 0);
3577
3578        manager.msgs.timesync = Some(TimesyncMsg { tc1: 0, ts1: 55 });
3579        manager.act_on_messages(
3580            &mut param_events,
3581            &mut comm_events,
3582            &mut command_events,
3583            &mut companion_events(),
3584            &mut board,
3585        );
3586
3587        assert_eq!(manager.comm_link().timesync_count, 1);
3588        let response = manager.comm_link().last_timesync.unwrap();
3589        assert_eq!(response.tc1, 123_000);
3590        assert_eq!(response.ts1, 55);
3591    }
3592
3593    #[test]
3594    fn set_param_defaults_emits_request_and_defers_ack() {
3595        let mut board = TestBoard::default();
3596        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3597        let mut params = Params::new();
3598        params.set_by_id(ParamId::PARAM_SYSTEM_ID, ParamValue::Int(42));
3599        let mut param_events = ParamEventQueues::default();
3600        let mut comm_events = CommEventQueues::default();
3601        let mut command_events = CommandEventQueues::default();
3602
3603        manager.msgs.cmd = Some(RosflightCmdMsg {
3604            command: RosflightCmd::SetParamDefaults,
3605        });
3606
3607        manager.act_on_messages(
3608            &mut param_events,
3609            &mut comm_events,
3610            &mut command_events,
3611            &mut companion_events(),
3612            &mut board,
3613        );
3614
3615        assert_eq!(
3616            params.get_by_id(ParamId::PARAM_SYSTEM_ID),
3617            ParamValue::Int(42)
3618        );
3619        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3620
3621        let mut cal_flags = CalibrationFlags::empty();
3622        apply_test_command_requests(
3623            &mut command_events,
3624            &mut comm_events,
3625            &mut board,
3626            &mut params,
3627            &mut cal_flags,
3628        );
3629
3630        assert_eq!(
3631            params.get_by_id(ParamId::PARAM_SYSTEM_ID),
3632            ParamValue::Int(1)
3633        );
3634        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3635        manager.send_comm_responses(&mut board, &mut comm_events);
3636        assert_eq!(manager.comm_link().cmd_ack_count, 1);
3637
3638        let ack = manager.comm_link().last_cmd_ack.unwrap();
3639        assert!(matches!(ack.command, RosflightCmd::SetParamDefaults));
3640        assert!(matches!(
3641            ack.success,
3642            RosflightCmdResponse::RosflightCmdSuccess
3643        ));
3644    }
3645
3646    #[test]
3647    fn board_command_emits_request_and_defers_ack() {
3648        let mut board = TestBoard::default();
3649        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3650        let mut param_events = ParamEventQueues::default();
3651        let mut comm_events = CommEventQueues::default();
3652        let mut command_events = CommandEventQueues::default();
3653
3654        manager.msgs.cmd = Some(RosflightCmdMsg {
3655            command: RosflightCmd::WriteParams,
3656        });
3657
3658        manager.act_on_messages(
3659            &mut param_events,
3660            &mut comm_events,
3661            &mut command_events,
3662            &mut companion_events(),
3663            &mut board,
3664        );
3665
3666        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3667        assert!(comm_events.responses.is_empty());
3668
3669        let request = command_events.board_command_requests.pop().unwrap();
3670        assert!(matches!(request.command, RosflightCmd::WriteParams));
3671    }
3672
3673    #[test]
3674    fn rc_trim_calibration_emits_request_and_defers_ack() {
3675        let mut board = TestBoard::default();
3676        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3677        let mut param_events = ParamEventQueues::default();
3678        let mut comm_events = CommEventQueues::default();
3679        let mut command_events = CommandEventQueues::default();
3680
3681        manager.msgs.cmd = Some(RosflightCmdMsg {
3682            command: RosflightCmd::RcCalibration,
3683        });
3684
3685        manager.act_on_messages(
3686            &mut param_events,
3687            &mut comm_events,
3688            &mut command_events,
3689            &mut companion_events(),
3690            &mut board,
3691        );
3692
3693        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3694        assert!(comm_events.responses.is_empty());
3695
3696        let request = command_events.rc_trim_calibration_requests.pop().unwrap();
3697        assert!(matches!(request.command, RosflightCmd::RcCalibration));
3698    }
3699
3700    #[test]
3701    fn reset_origin_emits_request_and_defers_ack() {
3702        let mut board = TestBoard::default();
3703        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3704        let mut param_events = ParamEventQueues::default();
3705        let mut comm_events = CommEventQueues::default();
3706        let mut command_events = CommandEventQueues::default();
3707
3708        manager.msgs.cmd = Some(RosflightCmdMsg {
3709            command: RosflightCmd::ResetOrigin,
3710        });
3711
3712        manager.act_on_messages(
3713            &mut param_events,
3714            &mut comm_events,
3715            &mut command_events,
3716            &mut companion_events(),
3717            &mut board,
3718        );
3719
3720        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3721        assert!(comm_events.responses.is_empty());
3722
3723        let request = command_events.reset_origin_requests.pop().unwrap();
3724        assert!(matches!(request.command, RosflightCmd::ResetOrigin));
3725    }
3726
3727    #[test]
3728    fn send_all_config_infos_emits_request_and_defers_ack() {
3729        let mut board = TestBoard::default();
3730        let mut manager = CommManager::new(RecordingCommLink::new(), board.clock_micros());
3731        let mut param_events = ParamEventQueues::default();
3732        let mut comm_events = CommEventQueues::default();
3733        let mut command_events = CommandEventQueues::default();
3734
3735        manager.msgs.cmd = Some(RosflightCmdMsg {
3736            command: RosflightCmd::SendAllConfigInfos,
3737        });
3738
3739        manager.act_on_messages(
3740            &mut param_events,
3741            &mut comm_events,
3742            &mut command_events,
3743            &mut companion_events(),
3744            &mut board,
3745        );
3746
3747        assert_eq!(manager.comm_link().cmd_ack_count, 0);
3748        assert!(comm_events.responses.is_empty());
3749
3750        let request = command_events.config_info_requests.pop().unwrap();
3751        assert!(matches!(request.command, RosflightCmd::SendAllConfigInfos));
3752    }
3753}