Skip to main content

pamoja_radios/sx126x/
config.rs

1//! The settings an SX126x is configured with, and the values its registers take.
2//!
3//! Every value here comes from the SX1261/2 datasheet (Rev 2.2): the frequency word of
4//! section 13.4.1, the timeout steps of 13.1.4, the image calibration codes of 9.2.1, the
5//! LoRa modulation and packet parameters of 13.4.5 and 13.4.6, the power amplifier
6//! settings of 13.1.14 and 13.4.4, the sync words of Table 12-1, and the register values
7//! the chapter 15 workarounds write. The LoRa settings are built from a
8//! [`LinkSettings`], so a radio sends exactly the frames `pamoja-lora` computes the
9//! airtime of.
10
11use pamoja_lora::budget::{Decibels, LinkBudget};
12use pamoja_lora::LinkSettings;
13
14/// The crystal frequency the synthesizer divides, in hertz.
15pub const XTAL_HZ: u32 = 32_000_000;
16
17/// Returns the word SetRfFrequency takes for a frequency.
18///
19/// The datasheet defines the frequency as the word times the crystal frequency over
20/// 2^25, so one step of the word is 0.95 Hz. The word is rounded to the nearest step.
21///
22/// # Arguments
23///
24/// * `frequency_hz` - the carrier frequency in hertz.
25///
26/// # Returns
27///
28/// The 32-bit frequency word.
29///
30/// # Examples
31///
32/// ```
33/// use pamoja_radios::sx126x::config::frequency_word;
34///
35/// assert_eq!(frequency_word(868_100_000), 0x3641_999A);
36/// assert_eq!(frequency_word(915_000_000), 0x3930_0000);
37/// ```
38pub const fn frequency_word(frequency_hz: u32) -> u32 {
39    (((frequency_hz as u64) << 25) + (XTAL_HZ as u64 / 2)).div_euclid(XTAL_HZ as u64) as u32
40}
41
42/// Returns the frequency a SetRfFrequency word selects.
43///
44/// # Arguments
45///
46/// * `word` - the 32-bit frequency word.
47///
48/// # Returns
49///
50/// The carrier frequency in hertz, rounded to the nearest hertz.
51pub const fn frequency_from_word(word: u32) -> u32 {
52    ((word as u64 * XTAL_HZ as u64 + (1 << 24)) >> 25) as u32
53}
54
55/// The timeout word that disables a transmit or receive timeout.
56pub const NO_TIMEOUT: u32 = 0;
57
58/// The receive timeout word that keeps the radio listening until told otherwise.
59pub const RX_CONTINUOUS: u32 = 0xFF_FFFF;
60
61/// Returns the 24-bit timeout word for a duration.
62///
63/// SetTx, SetRx, and SetDIO3AsTCXOCtrl count time in steps of 15.625 us. A nonzero
64/// duration never becomes a zero word, which would disable the timeout, and a duration
65/// past the longest the chip counts, about 262 seconds, stops one step short of
66/// [`RX_CONTINUOUS`].
67///
68/// # Arguments
69///
70/// * `micros` - the duration in microseconds.
71///
72/// # Returns
73///
74/// The number of steps, from 0 to 0xFFFFFE.
75///
76/// # Examples
77///
78/// ```
79/// use pamoja_radios::sx126x::config::timeout_steps;
80///
81/// assert_eq!(timeout_steps(1_000_000), 64_000);
82/// assert_eq!(timeout_steps(0), 0);
83/// ```
84pub const fn timeout_steps(micros: u64) -> u32 {
85    if micros == 0 {
86        return NO_TIMEOUT;
87    }
88    let steps = micros.saturating_mul(64) / 1000;
89    if steps == 0 {
90        1
91    } else if steps >= RX_CONTINUOUS as u64 {
92        RX_CONTINUOUS - 1
93    } else {
94        steps as u32
95    }
96}
97
98/// Returns the two CalibrateImage codes that cover a band.
99///
100/// Section 9.2.1 calibrates image rejection between two codes in steps of 4 MHz, taking
101/// the floor of the lower edge and the ceiling of the upper one, so the calibrated range
102/// always covers the band.
103///
104/// Table 9-2 of the datasheet lists codes one step wider at the top for two bands, 0xD7 0xDB
105/// for 863 to 870 MHz and 0xE1 0xE9 for 902 to 928 MHz. Both cover the band; this is the rule
106/// Semtech's own `sx126x_driver` computes in `sx126x_cal_img_in_mhz`.
107///
108/// # Arguments
109///
110/// * `low_hz` - the lower edge of the band in hertz.
111/// * `high_hz` - the upper edge of the band in hertz.
112///
113/// # Returns
114///
115/// `freq1` and `freq2`, the parameters of CalibrateImage.
116///
117/// # Examples
118///
119/// ```
120/// use pamoja_radios::sx126x::config::image_calibration;
121///
122/// // Each code counts 4 MHz, so EU868's 863 to 870 MHz calibrates from 860 to 872 MHz,
123/// // and US915's 902 to 928 MHz from 900 to 928 MHz.
124/// let megahertz = |[low, high]: [u8; 2]| (u32::from(low) * 4, u32::from(high) * 4);
125/// assert_eq!(megahertz(image_calibration(863_000_000, 870_000_000)), (860, 872));
126/// assert_eq!(megahertz(image_calibration(902_000_000, 928_000_000)), (900, 928));
127/// ```
128pub const fn image_calibration(low_hz: u32, high_hz: u32) -> [u8; 2] {
129    const STEP_HZ: u32 = 4_000_000;
130    let low = low_hz / STEP_HZ;
131    let high = high_hz.div_ceil(STEP_HZ);
132    [clamp_byte(low), clamp_byte(high)]
133}
134
135const fn clamp_byte(value: u32) -> u8 {
136    if value > 0xFF {
137        0xFF
138    } else {
139        value as u8
140    }
141}
142
143/// The modem a SetPacketType command selects, from Table 13-38.
144#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
145pub enum PacketType {
146    /// GFSK (0x00).
147    Gfsk,
148    /// LoRa (0x01).
149    Lora,
150    /// Long Range FHSS (0x03).
151    LrFhss,
152}
153
154impl PacketType {
155    /// Returns the parameter byte.
156    ///
157    /// # Returns
158    ///
159    /// The PacketType value.
160    pub const fn code(self) -> u8 {
161        match self {
162            PacketType::Gfsk => 0x00,
163            PacketType::Lora => 0x01,
164            PacketType::LrFhss => 0x03,
165        }
166    }
167}
168
169/// The standby mode a SetStandby command selects, from Table 13-4.
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
171pub enum StandbyMode {
172    /// Running on the 13 MHz RC oscillator (0).
173    Rc,
174    /// Running on the 32 MHz crystal (1).
175    Xosc,
176}
177
178impl StandbyMode {
179    /// Returns the parameter byte.
180    ///
181    /// # Returns
182    ///
183    /// The StdbyConfig value.
184    pub const fn code(self) -> u8 {
185        match self {
186            StandbyMode::Rc => 0,
187            StandbyMode::Xosc => 1,
188        }
189    }
190}
191
192/// How the chip regulates its supply, from Table 13-16.
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
194pub enum RegulatorMode {
195    /// Only the LDO, for every mode (0).
196    Ldo,
197    /// The DC-DC converter and the LDO, for STBY_XOSC, FS, RX, and TX (1).
198    DcDc,
199}
200
201impl RegulatorMode {
202    /// Returns the parameter byte.
203    ///
204    /// # Returns
205    ///
206    /// The regModeParam value.
207    pub const fn code(self) -> u8 {
208        match self {
209            RegulatorMode::Ldo => 0,
210            RegulatorMode::DcDc => 1,
211        }
212    }
213}
214
215/// The mode the chip returns to after a transmission or reception, from Table 13-23.
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217pub enum FallbackMode {
218    /// Frequency synthesis (0x40).
219    Fs,
220    /// Standby on the crystal (0x30).
221    StandbyXosc,
222    /// Standby on the RC oscillator, the default (0x20).
223    StandbyRc,
224}
225
226impl FallbackMode {
227    /// Returns the parameter byte.
228    ///
229    /// # Returns
230    ///
231    /// The fallbackMode value.
232    pub const fn code(self) -> u8 {
233        match self {
234            FallbackMode::Fs => 0x40,
235            FallbackMode::StandbyXosc => 0x30,
236            FallbackMode::StandbyRc => 0x20,
237        }
238    }
239}
240
241/// The voltage DIO3 supplies a TCXO with, from Table 13-35.
242#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
243pub enum TcxoVoltage {
244    /// 1.6 V (0x00).
245    V1_6,
246    /// 1.7 V (0x01).
247    V1_7,
248    /// 1.8 V (0x02).
249    V1_8,
250    /// 2.2 V (0x03).
251    V2_2,
252    /// 2.4 V (0x04).
253    V2_4,
254    /// 2.7 V (0x05).
255    V2_7,
256    /// 3.0 V (0x06).
257    V3_0,
258    /// 3.3 V (0x07).
259    V3_3,
260}
261
262impl TcxoVoltage {
263    /// Names the TCXO voltage DIO3 supplies at a number of millivolts.
264    ///
265    /// # Arguments
266    ///
267    /// * `millivolts` - the supply voltage, such as `1700` for 1.7 V.
268    ///
269    /// # Returns
270    ///
271    /// The voltage, or `None` for one DIO3 cannot supply.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use pamoja_radios::sx126x::config::TcxoVoltage;
277    ///
278    /// assert_eq!(TcxoVoltage::from_millivolts(1700), Some(TcxoVoltage::V1_7));
279    /// assert_eq!(TcxoVoltage::from_millivolts(1900), None);
280    /// ```
281    pub const fn from_millivolts(millivolts: u16) -> Option<TcxoVoltage> {
282        match millivolts {
283            1600 => Some(TcxoVoltage::V1_6),
284            1700 => Some(TcxoVoltage::V1_7),
285            1800 => Some(TcxoVoltage::V1_8),
286            2200 => Some(TcxoVoltage::V2_2),
287            2400 => Some(TcxoVoltage::V2_4),
288            2700 => Some(TcxoVoltage::V2_7),
289            3000 => Some(TcxoVoltage::V3_0),
290            3300 => Some(TcxoVoltage::V3_3),
291            _ => None,
292        }
293    }
294
295    /// Returns the parameter byte.
296    ///
297    /// # Returns
298    ///
299    /// The tcxoVoltage value.
300    pub const fn code(self) -> u8 {
301        match self {
302            TcxoVoltage::V1_6 => 0x00,
303            TcxoVoltage::V1_7 => 0x01,
304            TcxoVoltage::V1_8 => 0x02,
305            TcxoVoltage::V2_2 => 0x03,
306            TcxoVoltage::V2_4 => 0x04,
307            TcxoVoltage::V2_7 => 0x05,
308            TcxoVoltage::V3_0 => 0x06,
309            TcxoVoltage::V3_3 => 0x07,
310        }
311    }
312}
313
314/// How long the power amplifier takes to ramp up, from Table 13-41.
315#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
316pub enum RampTime {
317    /// 10 us (0x00).
318    Us10,
319    /// 20 us (0x01).
320    Us20,
321    /// 40 us (0x02).
322    Us40,
323    /// 80 us (0x03).
324    Us80,
325    /// 200 us (0x04).
326    Us200,
327    /// 800 us (0x05).
328    Us800,
329    /// 1700 us (0x06).
330    Us1700,
331    /// 3400 us (0x07).
332    Us3400,
333}
334
335impl RampTime {
336    /// Returns the parameter byte.
337    ///
338    /// # Returns
339    ///
340    /// The RampTime value.
341    pub const fn code(self) -> u8 {
342        match self {
343            RampTime::Us10 => 0x00,
344            RampTime::Us20 => 0x01,
345            RampTime::Us40 => 0x02,
346            RampTime::Us80 => 0x03,
347            RampTime::Us200 => 0x04,
348            RampTime::Us800 => 0x05,
349            RampTime::Us1700 => 0x06,
350            RampTime::Us3400 => 0x07,
351        }
352    }
353
354    /// Returns the ramp time in microseconds.
355    ///
356    /// # Returns
357    ///
358    /// The duration Table 13-41 gives.
359    pub const fn micros(self) -> u32 {
360        match self {
361            RampTime::Us10 => 10,
362            RampTime::Us20 => 20,
363            RampTime::Us40 => 40,
364            RampTime::Us80 => 80,
365            RampTime::Us200 => 200,
366            RampTime::Us800 => 800,
367            RampTime::Us1700 => 1_700,
368            RampTime::Us3400 => 3_400,
369        }
370    }
371
372    /// Returns the shortest ramp time that lasts at least a duration.
373    ///
374    /// # Arguments
375    ///
376    /// * `micros` - the least ramp time wanted, in microseconds; past 3400 us, the longest
377    ///   ramp the chip offers.
378    ///
379    /// # Returns
380    ///
381    /// The ramp time.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// use pamoja_radios::sx126x::config::RampTime;
387    ///
388    /// assert_eq!(RampTime::at_least(40), RampTime::Us40);
389    /// assert_eq!(RampTime::at_least(100), RampTime::Us200);
390    /// assert_eq!(RampTime::at_least(5_000).micros(), 3_400);
391    /// ```
392    pub const fn at_least(micros: u32) -> RampTime {
393        if micros <= 10 {
394            RampTime::Us10
395        } else if micros <= 20 {
396            RampTime::Us20
397        } else if micros <= 40 {
398            RampTime::Us40
399        } else if micros <= 80 {
400            RampTime::Us80
401        } else if micros <= 200 {
402            RampTime::Us200
403        } else if micros <= 800 {
404            RampTime::Us800
405        } else if micros <= 1_700 {
406            RampTime::Us1700
407        } else {
408            RampTime::Us3400
409        }
410    }
411}
412
413/// A LoRa signal bandwidth, from Table 13-48.
414#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
415pub enum LoraBandwidth {
416    /// 7.81 kHz (0x00).
417    Khz7_8,
418    /// 10.42 kHz (0x08).
419    Khz10_4,
420    /// 15.63 kHz (0x01).
421    Khz15_6,
422    /// 20.83 kHz (0x09).
423    Khz20_8,
424    /// 31.25 kHz (0x02).
425    Khz31_25,
426    /// 41.67 kHz (0x0A).
427    Khz41_7,
428    /// 62.5 kHz (0x03).
429    Khz62_5,
430    /// 125 kHz (0x04).
431    Khz125,
432    /// 250 kHz (0x05).
433    Khz250,
434    /// 500 kHz (0x06).
435    Khz500,
436}
437
438impl LoraBandwidth {
439    const ALL: [LoraBandwidth; 10] = [
440        LoraBandwidth::Khz7_8,
441        LoraBandwidth::Khz10_4,
442        LoraBandwidth::Khz15_6,
443        LoraBandwidth::Khz20_8,
444        LoraBandwidth::Khz31_25,
445        LoraBandwidth::Khz41_7,
446        LoraBandwidth::Khz62_5,
447        LoraBandwidth::Khz125,
448        LoraBandwidth::Khz250,
449        LoraBandwidth::Khz500,
450    ];
451
452    /// Returns the parameter byte.
453    ///
454    /// # Returns
455    ///
456    /// The BW value of SetModulationParams.
457    pub const fn code(self) -> u8 {
458        match self {
459            LoraBandwidth::Khz7_8 => 0x00,
460            LoraBandwidth::Khz10_4 => 0x08,
461            LoraBandwidth::Khz15_6 => 0x01,
462            LoraBandwidth::Khz20_8 => 0x09,
463            LoraBandwidth::Khz31_25 => 0x02,
464            LoraBandwidth::Khz41_7 => 0x0A,
465            LoraBandwidth::Khz62_5 => 0x03,
466            LoraBandwidth::Khz125 => 0x04,
467            LoraBandwidth::Khz250 => 0x05,
468            LoraBandwidth::Khz500 => 0x06,
469        }
470    }
471
472    /// Returns the bandwidth in hertz, rounded to the nearest hertz.
473    ///
474    /// # Returns
475    ///
476    /// The bandwidth: 500 kHz halved, or 125 kHz divided by 3 and halved, as many times
477    /// as the setting takes.
478    pub const fn hz(self) -> u32 {
479        match self {
480            LoraBandwidth::Khz7_8 => 7_813,
481            LoraBandwidth::Khz10_4 => 10_417,
482            LoraBandwidth::Khz15_6 => 15_625,
483            LoraBandwidth::Khz20_8 => 20_833,
484            LoraBandwidth::Khz31_25 => 31_250,
485            LoraBandwidth::Khz41_7 => 41_667,
486            LoraBandwidth::Khz62_5 => 62_500,
487            LoraBandwidth::Khz125 => 125_000,
488            LoraBandwidth::Khz250 => 250_000,
489            LoraBandwidth::Khz500 => 500_000,
490        }
491    }
492
493    /// Finds the setting for a bandwidth in hertz.
494    ///
495    /// # Arguments
496    ///
497    /// * `hz` - the bandwidth in hertz, within 1% of one the chip supports, so both
498    ///   `10_417` and the datasheet's rounded `10_420` select 10.42 kHz.
499    ///
500    /// # Returns
501    ///
502    /// The setting, or `None` for a bandwidth the SX126x does not offer.
503    pub fn from_hz(hz: u32) -> Option<LoraBandwidth> {
504        LoraBandwidth::ALL.into_iter().find(|bandwidth| {
505            let nominal = u64::from(bandwidth.hz());
506            u64::from(hz).abs_diff(nominal) * 100 <= nominal
507        })
508    }
509
510    /// Finds the setting a BW value of SetModulationParams names.
511    ///
512    /// # Arguments
513    ///
514    /// * `code` - the parameter byte.
515    ///
516    /// # Returns
517    ///
518    /// The setting, or `None` for a byte Table 13-48 does not list.
519    ///
520    /// # Examples
521    ///
522    /// ```
523    /// use pamoja_radios::sx126x::config::LoraBandwidth;
524    ///
525    /// assert_eq!(LoraBandwidth::from_code(LoraBandwidth::Khz125.code()), Some(LoraBandwidth::Khz125));
526    /// assert_eq!(LoraBandwidth::from_code(0x07), None);
527    /// ```
528    pub fn from_code(code: u8) -> Option<LoraBandwidth> {
529        LoraBandwidth::ALL
530            .into_iter()
531            .find(|bandwidth| bandwidth.code() == code)
532    }
533}
534
535/// A LoRa coding rate, from Table 13-49.
536#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
537pub enum CodingRate {
538    /// 4/5 (0x01).
539    Cr4_5,
540    /// 4/6 (0x02).
541    Cr4_6,
542    /// 4/7 (0x03).
543    Cr4_7,
544    /// 4/8 (0x04).
545    Cr4_8,
546    /// 4/5 with the long interleaver (0x05).
547    Cr4_5LongInterleaver,
548    /// 4/6 with the long interleaver (0x06).
549    Cr4_6LongInterleaver,
550    /// 4/8 with the long interleaver (0x07).
551    Cr4_8LongInterleaver,
552}
553
554impl CodingRate {
555    /// Returns the parameter byte.
556    ///
557    /// # Returns
558    ///
559    /// The CR value of SetModulationParams.
560    pub const fn code(self) -> u8 {
561        match self {
562            CodingRate::Cr4_5 => 0x01,
563            CodingRate::Cr4_6 => 0x02,
564            CodingRate::Cr4_7 => 0x03,
565            CodingRate::Cr4_8 => 0x04,
566            CodingRate::Cr4_5LongInterleaver => 0x05,
567            CodingRate::Cr4_6LongInterleaver => 0x06,
568            CodingRate::Cr4_8LongInterleaver => 0x07,
569        }
570    }
571
572    /// Returns the standard-interleaver coding rate for a denominator.
573    ///
574    /// The airtime `pamoja-lora` computes assumes the standard interleaver, so a link's
575    /// coding rate always maps to one of the first four settings.
576    ///
577    /// # Arguments
578    ///
579    /// * `denominator` - the coding-rate denominator, clamped to 5 to 8.
580    ///
581    /// # Returns
582    ///
583    /// The coding rate.
584    pub const fn from_denominator(denominator: u8) -> CodingRate {
585        match denominator {
586            0..=5 => CodingRate::Cr4_5,
587            6 => CodingRate::Cr4_6,
588            7 => CodingRate::Cr4_7,
589            _ => CodingRate::Cr4_8,
590        }
591    }
592}
593
594/// The LoRa parameters of a SetModulationParams command, from Tables 13-47 to 13-50.
595///
596/// # Examples
597///
598/// ```
599/// use pamoja_lora::LinkSettings;
600/// use pamoja_radios::sx126x::config::{CodingRate, LoraBandwidth, LoraModulation};
601///
602/// // SF12 at 125 kHz spends over 16 ms on a symbol, so it needs low data rate
603/// // optimization.
604/// let modulation = LoraModulation::from_link(&LinkSettings::new(12, 125_000)).unwrap();
605/// assert_eq!(modulation.spreading_factor, 12);
606/// assert_eq!(modulation.bandwidth, LoraBandwidth::Khz125);
607/// assert_eq!(modulation.coding_rate, CodingRate::from_denominator(5));
608/// assert!(modulation.low_data_rate_optimization);
609/// ```
610#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
611pub struct LoraModulation {
612    /// The spreading factor, 5 to 12.
613    pub spreading_factor: u8,
614    /// The signal bandwidth.
615    pub bandwidth: LoraBandwidth,
616    /// The coding rate.
617    pub coding_rate: CodingRate,
618    /// Whether low data rate optimization is on.
619    pub low_data_rate_optimization: bool,
620}
621
622impl LoraModulation {
623    /// Builds the modulation parameters of a link.
624    ///
625    /// # Arguments
626    ///
627    /// * `link` - the link settings, whose spreading factor, bandwidth, coding rate, and
628    ///   low data rate optimization the radio must match.
629    ///
630    /// # Returns
631    ///
632    /// The parameters, or `None` when the link's bandwidth is not one the SX126x
633    /// offers.
634    pub fn from_link(link: &LinkSettings) -> Option<LoraModulation> {
635        Some(LoraModulation {
636            spreading_factor: link.spreading_factor(),
637            bandwidth: LoraBandwidth::from_hz(link.bandwidth_hz())?,
638            coding_rate: CodingRate::from_denominator(link.coding_rate_denominator()),
639            low_data_rate_optimization: link.low_data_rate_optimization(),
640        })
641    }
642
643    /// Returns the four parameter bytes: SF, BW, CR, and LowDataRateOptimize.
644    ///
645    /// # Returns
646    ///
647    /// ModParam1 to ModParam4.
648    pub const fn to_params(&self) -> [u8; 4] {
649        [
650            self.spreading_factor,
651            self.bandwidth.code(),
652            self.coding_rate.code(),
653            self.low_data_rate_optimization as u8,
654        ]
655    }
656}
657
658/// Reports whether an LLCC68 can use a spreading factor at a bandwidth.
659///
660/// The LLCC68 takes the SX1262's commands but not all of its settings. Its datasheet
661/// (DS.LLCC68.W.APP Rev 1.1) lists SF5 to SF11 in Table 13-47 and only the 125, 250, and
662/// 500 kHz bandwidths in Table 13-48, and notes under Table 6-1 that not every spreading
663/// factor is available at every bandwidth. Semtech's LLCC68 driver refuses SF10 and SF11 at
664/// 125 kHz and SF11 at 250 kHz, which leaves the LoRaWAN data rates the LLCC68 product page
665/// lists: up to SF9 at 125 kHz, SF10 at 250 kHz, and SF11 at 500 kHz.
666///
667/// # Arguments
668///
669/// * `spreading_factor` - the spreading factor.
670/// * `bandwidth` - the signal bandwidth.
671///
672/// # Returns
673///
674/// `true` when the LLCC68 supports the pair.
675///
676/// # Examples
677///
678/// ```
679/// use pamoja_radios::sx126x::config::{llcc68_supports, LoraBandwidth};
680///
681/// assert!(llcc68_supports(9, LoraBandwidth::Khz125));
682/// assert!(!llcc68_supports(10, LoraBandwidth::Khz125));
683/// assert!(llcc68_supports(10, LoraBandwidth::Khz250));
684/// assert!(!llcc68_supports(11, LoraBandwidth::Khz250));
685/// assert!(llcc68_supports(11, LoraBandwidth::Khz500));
686/// assert!(!llcc68_supports(12, LoraBandwidth::Khz500));
687/// assert!(!llcc68_supports(7, LoraBandwidth::Khz62_5));
688/// ```
689pub const fn llcc68_supports(spreading_factor: u8, bandwidth: LoraBandwidth) -> bool {
690    if spreading_factor < 5 || spreading_factor > 11 {
691        return false;
692    }
693    match bandwidth {
694        LoraBandwidth::Khz125 => spreading_factor <= 9,
695        LoraBandwidth::Khz250 => spreading_factor <= 10,
696        LoraBandwidth::Khz500 => true,
697        _ => false,
698    }
699}
700
701/// The LoRa parameters of a SetPacketParams command, from Tables 13-66 to 13-70.
702///
703/// # Examples
704///
705/// ```
706/// use pamoja_lora::LinkSettings;
707/// use pamoja_radios::sx126x::config::LoraPacket;
708///
709/// // A 20-byte uplink with the link's defaults: an eight-symbol preamble, an explicit
710/// // header, the CRC on, and standard IQ.
711/// let packet = LoraPacket::from_link(&LinkSettings::new(7, 125_000), 20, false);
712/// assert_eq!(packet.preamble_symbols, 8);
713/// assert!(packet.explicit_header && packet.crc && !packet.invert_iq);
714/// assert_eq!(packet.payload_len, 20);
715/// ```
716#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
717pub struct LoraPacket {
718    /// The preamble length in symbols.
719    pub preamble_symbols: u16,
720    /// Whether the frame carries an explicit header; implicit when `false`.
721    pub explicit_header: bool,
722    /// The payload length to send, or the most a receiver accepts.
723    pub payload_len: u8,
724    /// Whether the frame carries a CRC.
725    pub crc: bool,
726    /// Whether the IQ polarity is inverted, as LoRaWAN downlinks use.
727    pub invert_iq: bool,
728}
729
730impl LoraPacket {
731    /// Builds the packet parameters of a link for a payload.
732    ///
733    /// # Arguments
734    ///
735    /// * `link` - the link settings, whose preamble, header, and CRC the frame uses.
736    /// * `payload_len` - the payload length to send, or the most to accept.
737    /// * `invert_iq` - `true` for inverted IQ polarity.
738    ///
739    /// # Returns
740    ///
741    /// The parameters.
742    pub fn from_link(link: &LinkSettings, payload_len: u8, invert_iq: bool) -> LoraPacket {
743        LoraPacket {
744            preamble_symbols: link.preamble_symbols(),
745            explicit_header: link.explicit_header(),
746            payload_len,
747            crc: link.crc(),
748            invert_iq,
749        }
750    }
751
752    /// Returns the six parameter bytes: the preamble length, most significant byte first,
753    /// then the header type, the payload length, the CRC type, and the IQ setup.
754    ///
755    /// # Returns
756    ///
757    /// PacketParam1 to PacketParam6.
758    pub const fn to_params(&self) -> [u8; 6] {
759        let preamble = self.preamble_symbols.to_be_bytes();
760        [
761            preamble[0],
762            preamble[1],
763            !self.explicit_header as u8,
764            self.payload_len,
765            self.crc as u8,
766            self.invert_iq as u8,
767        ]
768    }
769}
770
771/// Which power amplifier a chip has.
772#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
773pub enum PowerAmplifier {
774    /// The low power amplifier of the SX1261, up to +15 dBm.
775    LowPower,
776    /// The high power amplifier of the SX1262 and the LLCC68, up to +22 dBm.
777    HighPower,
778}
779
780impl PowerAmplifier {
781    /// Returns the output power range SetTxParams accepts, from section 13.4.4.
782    ///
783    /// # Returns
784    ///
785    /// The lowest and highest settings in dBm: -17 to +14 for the low power amplifier
786    /// and -9 to +22 for the high power one.
787    pub const fn setting_range_dbm(self) -> (i8, i8) {
788        match self {
789            PowerAmplifier::LowPower => (-17, 14),
790            PowerAmplifier::HighPower => (-9, 22),
791        }
792    }
793}
794
795/// The parameters of a SetPaConfig command, from Table 13-20.
796#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
797pub struct PaConfig {
798    /// The duty cycle, or conduction angle, of the amplifier.
799    pub duty_cycle: u8,
800    /// The size of the SX1262's amplifier, 0x00 to 0x07; no effect on the SX1261.
801    pub hp_max: u8,
802    /// The device: 0 for the SX1262, 1 for the SX1261.
803    pub device: u8,
804    /// Reserved, always 0x01.
805    pub lut: u8,
806}
807
808impl PaConfig {
809    /// The SX1262 at +22 dBm, from Table 13-21.
810    pub const SX1262_22_DBM: PaConfig = PaConfig::new(0x04, 0x07, 0x00);
811    /// The SX1262 at +20 dBm, from Table 13-21.
812    pub const SX1262_20_DBM: PaConfig = PaConfig::new(0x03, 0x05, 0x00);
813    /// The SX1262 at +17 dBm, from Table 13-21.
814    pub const SX1262_17_DBM: PaConfig = PaConfig::new(0x02, 0x03, 0x00);
815    /// The SX1262 at +14 dBm, from Table 13-21.
816    pub const SX1262_14_DBM: PaConfig = PaConfig::new(0x02, 0x02, 0x00);
817    /// The SX1261 at +15 dBm, from Table 13-21.
818    pub const SX1261_15_DBM: PaConfig = PaConfig::new(0x06, 0x00, 0x01);
819    /// The SX1261 at +14 dBm, from Table 13-21.
820    pub const SX1261_14_DBM: PaConfig = PaConfig::new(0x04, 0x00, 0x01);
821    /// The SX1261 at +10 dBm, from Table 13-21.
822    pub const SX1261_10_DBM: PaConfig = PaConfig::new(0x01, 0x00, 0x01);
823
824    const fn new(duty_cycle: u8, hp_max: u8, device: u8) -> PaConfig {
825        PaConfig {
826            duty_cycle,
827            hp_max,
828            device,
829            lut: 0x01,
830        }
831    }
832
833    /// Returns the four parameter bytes.
834    ///
835    /// # Returns
836    ///
837    /// paDutyCycle, hpMax, deviceSel, and paLut.
838    pub const fn to_params(&self) -> [u8; 4] {
839        [self.duty_cycle, self.hp_max, self.device, self.lut]
840    }
841}
842
843/// The amplifier configuration and power setting that produce an output power.
844///
845/// # Examples
846///
847/// ```
848/// use pamoja_lora::budget::{Decibels, LinkBudget};
849/// use pamoja_radios::sx126x::config::{PaConfig, PowerAmplifier, TxPower};
850///
851/// // A 2.15 dBi whip on half a decibel of pigtail under a 16 dBm EIRP ceiling.
852/// let whip = LinkBudget {
853///     transmit_antenna_gain_dbi: Decibels::from_hundredths(215),
854///     transmit_cable_loss_db: Decibels::from_tenths(5),
855///     ..LinkBudget::default()
856/// };
857/// let power = TxPower::under_ceiling(PowerAmplifier::HighPower, &whip, Decibels::from_db(16));
858/// assert_eq!(power.setting_dbm, 14);
859/// assert_eq!(power.pa, PaConfig::SX1262_22_DBM);
860/// ```
861#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
862pub struct TxPower {
863    /// The SetPaConfig parameters.
864    pub pa: PaConfig,
865    /// The power byte of SetTxParams, in dBm.
866    pub setting_dbm: i8,
867}
868
869impl TxPower {
870    /// Chooses the settings for an output power.
871    ///
872    /// The high power amplifier keeps its +22 dBm configuration and takes the power in
873    /// SetTxParams, clamped to -9 to +22 dBm. The low power amplifier uses its +14 dBm
874    /// configuration with the power clamped to -17 to +14 dBm, and its +15 dBm
875    /// configuration, which Table 13-21 drives with a setting of +14 dBm, for anything
876    /// above.
877    ///
878    /// # Arguments
879    ///
880    /// * `amplifier` - the chip's power amplifier.
881    /// * `output_dbm` - the output power wanted at the antenna port.
882    ///
883    /// # Returns
884    ///
885    /// The configuration and the setting.
886    pub const fn for_output(amplifier: PowerAmplifier, output_dbm: i8) -> TxPower {
887        match amplifier {
888            PowerAmplifier::HighPower => TxPower {
889                pa: PaConfig::SX1262_22_DBM,
890                setting_dbm: clamp_power(output_dbm, -9, 22),
891            },
892            PowerAmplifier::LowPower if output_dbm >= 15 => TxPower {
893                pa: PaConfig::SX1261_15_DBM,
894                setting_dbm: 14,
895            },
896            PowerAmplifier::LowPower => TxPower {
897                pa: PaConfig::SX1261_14_DBM,
898                setting_dbm: clamp_power(output_dbm, -17, 14),
899            },
900        }
901    }
902
903    /// Chooses the settings that keep a link's EIRP at or under a ceiling.
904    ///
905    /// # Arguments
906    ///
907    /// * `amplifier` - the chip's power amplifier.
908    /// * `budget` - the link budget, whose transmitting antenna and cable apply.
909    /// * `eirp_ceiling_dbm` - the EIRP limit, such as a channel plan's ceiling for the
910    ///   frequency in use.
911    ///
912    /// # Returns
913    ///
914    /// The configuration and the setting, rounded down to whole decibels so the EIRP
915    /// stays under the ceiling.
916    pub fn under_ceiling(
917        amplifier: PowerAmplifier,
918        budget: &LinkBudget,
919        eirp_ceiling_dbm: Decibels,
920    ) -> TxPower {
921        let most = budget.max_transmit_power_dbm(eirp_ceiling_dbm).floor_db();
922        let output = most.clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8;
923        TxPower::for_output(amplifier, output)
924    }
925}
926
927const fn clamp_power(dbm: i8, low: i8, high: i8) -> i8 {
928    if dbm < low {
929        low
930    } else if dbm > high {
931        high
932    } else {
933        dbm
934    }
935}
936
937/// A LoRa sync word, written to the two sync word registers at 0x0740, from Table 12-1.
938#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
939pub enum SyncWord {
940    /// 0x3444, for a public network such as LoRaWAN.
941    Public,
942    /// 0x1424, for a private network, and the chip's reset value.
943    Private,
944    /// Another two-byte value.
945    Custom(u16),
946}
947
948impl SyncWord {
949    /// Returns the two register bytes, most significant first.
950    ///
951    /// # Returns
952    ///
953    /// The values of the sync word registers at 0x0740 and 0x0741.
954    pub const fn to_bytes(self) -> [u8; 2] {
955        match self {
956            SyncWord::Public => [0x34, 0x44],
957            SyncWord::Private => [0x14, 0x24],
958            SyncWord::Custom(word) => word.to_be_bytes(),
959        }
960    }
961}
962
963/// The register addresses the driver reads and writes, from Table 12-1.
964pub mod register {
965    /// The most significant byte of the LoRa sync word; the least follows at 0x0741.
966    pub const LORA_SYNC_WORD: u16 = 0x0740;
967    /// The IQ polarity setup, bit 2 of which the inverted IQ workaround sets.
968    pub const IQ_POLARITY: u16 = 0x0736;
969    /// The TX modulation register, bit 2 of which the 500 kHz workaround sets.
970    pub const TX_MODULATION: u16 = 0x0889;
971    /// The receive gain.
972    pub const RX_GAIN: u16 = 0x08AC;
973    /// The first of four bytes of random number, filled while the receiver listens.
974    pub const RANDOM_NUMBER: u16 = 0x0819;
975    /// An analog LNA control whose lowest bit Semtech's drivers clear to draw a random number.
976    pub const ANA_LNA: u16 = 0x08E2;
977    /// An analog mixer control whose top bit Semtech's drivers clear for the same reason.
978    pub const ANA_MIXER: u16 = 0x08E5;
979    /// The PA clamping configuration the antenna mismatch workaround raises.
980    pub const TX_CLAMP_CONFIG: u16 = 0x08D8;
981    /// The over current protection level.
982    pub const OCP_CONFIGURATION: u16 = 0x08E7;
983    /// The RTC control register the implicit header timeout workaround stops.
984    pub const RTC_CONTROL: u16 = 0x0902;
985    /// The trimming capacitor on the XTA pin.
986    pub const XTA_TRIM: u16 = 0x0911;
987    /// The trimming capacitor on the XTB pin.
988    pub const XTB_TRIM: u16 = 0x0912;
989    /// The event mask the implicit header timeout workaround clears.
990    pub const EVENT_MASK: u16 = 0x0944;
991}
992
993/// The receive gain register value for power saving, the default, from Table 9-3.
994pub const RX_GAIN_POWER_SAVING: u8 = 0x94;
995
996/// The receive gain register value for boosted sensitivity, from Table 9-3.
997pub const RX_GAIN_BOOSTED: u8 = 0x96;
998
999/// The RTC control value that stops the timer, from section 15.3.
1000pub const RTC_STOP: u8 = 0x00;
1001
1002/// The SetCadParams exit mode that returns to standby once the detection is done, from
1003/// Table 13-71.
1004pub const CAD_ONLY: u8 = 0x00;
1005
1006/// The SetCadParams exit mode that starts receiving when activity is detected.
1007pub const CAD_RX: u8 = 0x01;
1008
1009/// The cadDetMin every Semtech driver uses.
1010pub const CAD_DETECT_MIN: u8 = 10;
1011
1012/// The cadSymbolNum code for a number of symbols, from Table 13-71.
1013///
1014/// # Arguments
1015///
1016/// * `symbols` - how many symbols to listen over; 1, 2, 4, 8 or 16, rounded down to one of
1017///   them.
1018///
1019/// # Returns
1020///
1021/// The code, 0x00 to 0x04.
1022#[must_use]
1023pub const fn cad_symbols(symbols: u8) -> u8 {
1024    match symbols {
1025        0..=1 => 0x00,
1026        2..=3 => 0x01,
1027        4..=7 => 0x02,
1028        8..=15 => 0x03,
1029        _ => 0x04,
1030    }
1031}
1032
1033/// The cadDetPeak for a spreading factor and bandwidth, as Semtech's own radio layer sets
1034/// it.
1035///
1036/// The values are those of `ral_sx126x_get_lora_cad_det_peak` in LoRa Basics Modem, which
1037/// is what Semtech's relay listens with. Listening over more symbols lowers the threshold,
1038/// since there is more to go on.
1039///
1040/// # Arguments
1041///
1042/// * `spreading_factor` - 5 to 12.
1043/// * `bandwidth_hz` - the channel bandwidth.
1044/// * `symbols` - the cadSymbolNum code from [`cad_symbols`].
1045///
1046/// # Returns
1047///
1048/// The threshold.
1049#[must_use]
1050pub const fn cad_detect_peak(spreading_factor: u8, bandwidth_hz: u32, symbols: u8) -> u8 {
1051    let above_five = spreading_factor.saturating_sub(5);
1052    let index = if above_five > 7 { 7 } else { above_five } as usize;
1053    let wide = [22u8, 23, 25, 26, 30, 31, 33, 35];
1054    let narrow = [20u8, 21, 22, 24, 24, 25, 27, 27];
1055    let peak = if bandwidth_hz >= 500_000 {
1056        wide[index]
1057    } else {
1058        narrow[index]
1059    };
1060    match symbols {
1061        0x02 => peak - 1,
1062        0x03 | 0x04 => peak - 2,
1063        _ => peak,
1064    }
1065}
1066
1067/// Returns the TX modulation register value for a transmission, from section 15.1.
1068///
1069/// Bit 2 is cleared for a 500 kHz LoRa bandwidth and set for every other bandwidth.
1070///
1071/// # Arguments
1072///
1073/// * `current` - the register value read back from the chip.
1074/// * `bandwidth` - the LoRa bandwidth about to be used.
1075///
1076/// # Returns
1077///
1078/// The value to write.
1079pub const fn tx_modulation(current: u8, bandwidth: LoraBandwidth) -> u8 {
1080    match bandwidth {
1081        LoraBandwidth::Khz500 => current & !0x04,
1082        _ => current | 0x04,
1083    }
1084}
1085
1086/// Returns the TX clamp register value that resists antenna mismatch, from section 15.2.
1087///
1088/// Bits 4 to 1 are set, which the datasheet asks of the SX1262 after a power on reset or
1089/// a cold wake.
1090///
1091/// # Arguments
1092///
1093/// * `current` - the register value read back from the chip.
1094///
1095/// # Returns
1096///
1097/// The value to write.
1098pub const fn tx_clamp(current: u8) -> u8 {
1099    current | 0x1E
1100}
1101
1102/// Returns the IQ polarity register value for a polarity, from section 15.4.
1103///
1104/// Bit 2 is cleared for inverted IQ and set for standard IQ.
1105///
1106/// # Arguments
1107///
1108/// * `current` - the register value read back from the chip.
1109/// * `invert_iq` - `true` for inverted IQ polarity.
1110///
1111/// # Returns
1112///
1113/// The value to write.
1114pub const fn iq_polarity(current: u8, invert_iq: bool) -> u8 {
1115    if invert_iq {
1116        current & !0x04
1117    } else {
1118        current | 0x04
1119    }
1120}
1121
1122/// Returns the event mask value that clears a pending RTC timeout, from section 15.3.
1123///
1124/// The datasheet names the register; the bit, bit 1, is the one Semtech's reference
1125/// `sx126x_driver` sets in `sx126x_stop_rtc`.
1126///
1127/// # Arguments
1128///
1129/// * `current` - the register value read back from the chip.
1130///
1131/// # Returns
1132///
1133/// The value to write.
1134pub const fn event_clear(current: u8) -> u8 {
1135    current | 0x02
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141
1142    #[test]
1143    fn the_frequency_word_matches_the_reference_driver() {
1144        // Semtech's sx126x_convert_freq_in_hz_to_pll_step, evaluated for these frequencies.
1145        let words = [
1146            (433_175_000, 0x1B12_CCCD),
1147            (868_100_000, 0x3641_999A),
1148            (868_300_000, 0x3644_CCCD),
1149            (868_500_000, 0x3648_0000),
1150            (869_525_000, 0x3658_6666),
1151            (902_300_000, 0x3864_CCCD),
1152            (915_000_000, 0x3930_0000),
1153            (923_300_000, 0x39B4_CCCD),
1154            (150_000_000, 0x0960_0000),
1155            (960_000_000, 0x3C00_0000),
1156        ];
1157        for (hz, word) in words {
1158            assert_eq!(frequency_word(hz), word, "{hz} Hz");
1159            assert!(frequency_from_word(word).abs_diff(hz) <= 1, "{hz} Hz back");
1160        }
1161    }
1162
1163    #[test]
1164    fn the_timeout_counts_in_steps_of_15625_nanoseconds() {
1165        assert_eq!(timeout_steps(15_625), 1_000);
1166        assert_eq!(timeout_steps(3_000_000), 192_000);
1167        assert_eq!(timeout_steps(1), 1);
1168        assert_eq!(timeout_steps(u64::MAX), RX_CONTINUOUS - 1);
1169    }
1170
1171    #[test]
1172    fn the_image_calibration_takes_the_floor_and_the_ceiling() {
1173        assert_eq!(image_calibration(430_000_000, 440_000_000), [0x6B, 0x6E]);
1174        assert_eq!(image_calibration(470_000_000, 510_000_000), [0x75, 0x80]);
1175        assert_eq!(image_calibration(779_000_000, 787_000_000), [0xC2, 0xC5]);
1176        assert_eq!(image_calibration(863_000_000, 870_000_000), [0xD7, 0xDA]);
1177        assert_eq!(image_calibration(902_000_000, 928_000_000), [0xE1, 0xE8]);
1178    }
1179
1180    #[test]
1181    fn every_bandwidth_round_trips_through_its_frequency() {
1182        for bandwidth in LoraBandwidth::ALL {
1183            assert_eq!(LoraBandwidth::from_hz(bandwidth.hz()), Some(bandwidth));
1184        }
1185        assert_eq!(LoraBandwidth::from_hz(10_420), Some(LoraBandwidth::Khz10_4));
1186        assert_eq!(LoraBandwidth::from_hz(7_810), Some(LoraBandwidth::Khz7_8));
1187        assert_eq!(LoraBandwidth::from_hz(200_000), None);
1188    }
1189
1190    #[test]
1191    fn the_codes_follow_the_datasheet_tables() {
1192        let bandwidths = [
1193            (LoraBandwidth::Khz7_8, 0x00),
1194            (LoraBandwidth::Khz10_4, 0x08),
1195            (LoraBandwidth::Khz15_6, 0x01),
1196            (LoraBandwidth::Khz20_8, 0x09),
1197            (LoraBandwidth::Khz31_25, 0x02),
1198            (LoraBandwidth::Khz41_7, 0x0A),
1199            (LoraBandwidth::Khz62_5, 0x03),
1200            (LoraBandwidth::Khz125, 0x04),
1201            (LoraBandwidth::Khz250, 0x05),
1202            (LoraBandwidth::Khz500, 0x06),
1203        ];
1204        for (bandwidth, code) in bandwidths {
1205            assert_eq!(bandwidth.code(), code);
1206        }
1207        assert_eq!(CodingRate::from_denominator(5).code(), 0x01);
1208        assert_eq!(CodingRate::from_denominator(8).code(), 0x04);
1209        assert_eq!(CodingRate::Cr4_8LongInterleaver.code(), 0x07);
1210        assert_eq!(RampTime::Us3400.code(), 0x07);
1211        assert_eq!(TcxoVoltage::V1_8.code(), 0x02);
1212        assert_eq!(FallbackMode::Fs.code(), 0x40);
1213        assert_eq!(PacketType::LrFhss.code(), 0x03);
1214    }
1215
1216    #[test]
1217    fn a_link_becomes_its_modulation_and_packet_parameters() {
1218        let slow = LinkSettings::new(11, 125_000).with_coding_rate(6);
1219        assert_eq!(
1220            LoraModulation::from_link(&slow).map(|m| m.to_params()),
1221            Some([0x0B, 0x04, 0x02, 0x01])
1222        );
1223        let fast = LinkSettings::new(7, 500_000);
1224        assert_eq!(
1225            LoraModulation::from_link(&fast).map(|m| m.to_params()),
1226            Some([0x07, 0x06, 0x01, 0x00])
1227        );
1228        let slowest = LinkSettings::new(12, 125_000);
1229        assert_eq!(
1230            LoraModulation::from_link(&slowest).map(|m| m.to_params()),
1231            Some([0x0C, 0x04, 0x01, 0x01])
1232        );
1233        assert_eq!(
1234            LoraPacket::from_link(&LinkSettings::new(7, 125_000), 20, false).to_params(),
1235            [0x00, 0x08, 0x00, 0x14, 0x01, 0x00]
1236        );
1237        assert_eq!(
1238            LoraModulation::from_link(&LinkSettings::new(7, 200_000)),
1239            None
1240        );
1241        let bare = LinkSettings::new(9, 125_000)
1242            .with_preamble(300)
1243            .implicit_header()
1244            .without_crc();
1245        assert_eq!(
1246            LoraPacket::from_link(&bare, 51, true).to_params(),
1247            [0x01, 0x2C, 0x01, 0x33, 0x00, 0x01]
1248        );
1249    }
1250
1251    #[test]
1252    fn the_amplifier_settings_follow_table_13_21_and_section_13_4_4() {
1253        assert_eq!(
1254            PaConfig::SX1262_22_DBM.to_params(),
1255            [0x04, 0x07, 0x00, 0x01]
1256        );
1257        assert_eq!(
1258            PaConfig::SX1262_20_DBM.to_params(),
1259            [0x03, 0x05, 0x00, 0x01]
1260        );
1261        assert_eq!(
1262            PaConfig::SX1262_17_DBM.to_params(),
1263            [0x02, 0x03, 0x00, 0x01]
1264        );
1265        assert_eq!(
1266            PaConfig::SX1262_14_DBM.to_params(),
1267            [0x02, 0x02, 0x00, 0x01]
1268        );
1269        assert_eq!(
1270            PaConfig::SX1261_15_DBM.to_params(),
1271            [0x06, 0x00, 0x01, 0x01]
1272        );
1273        assert_eq!(
1274            PaConfig::SX1261_14_DBM.to_params(),
1275            [0x04, 0x00, 0x01, 0x01]
1276        );
1277        assert_eq!(
1278            PaConfig::SX1261_10_DBM.to_params(),
1279            [0x01, 0x00, 0x01, 0x01]
1280        );
1281
1282        let high = |dbm| TxPower::for_output(PowerAmplifier::HighPower, dbm);
1283        assert_eq!(high(22).setting_dbm, 22);
1284        assert_eq!(high(30).setting_dbm, 22);
1285        assert_eq!(high(-20).setting_dbm, -9);
1286        let low = |dbm| TxPower::for_output(PowerAmplifier::LowPower, dbm);
1287        assert_eq!(
1288            low(15),
1289            TxPower {
1290                pa: PaConfig::SX1261_15_DBM,
1291                setting_dbm: 14
1292            }
1293        );
1294        assert_eq!(
1295            low(14),
1296            TxPower {
1297                pa: PaConfig::SX1261_14_DBM,
1298                setting_dbm: 14
1299            }
1300        );
1301        assert_eq!(low(-30).setting_dbm, -17);
1302        assert_eq!(PowerAmplifier::LowPower.setting_range_dbm(), (-17, 14));
1303    }
1304
1305    #[test]
1306    fn the_power_under_a_ceiling_rounds_down_for_the_antenna() {
1307        let collinear = LinkBudget {
1308            transmit_antenna_gain_dbi: Decibels::from_db(6),
1309            ..LinkBudget::default()
1310        };
1311        let power =
1312            TxPower::under_ceiling(PowerAmplifier::HighPower, &collinear, Decibels::from_db(16));
1313        assert_eq!(power.setting_dbm, 10);
1314        let yagi = LinkBudget {
1315            transmit_antenna_gain_dbi: Decibels::from_hundredths(915),
1316            ..LinkBudget::default()
1317        };
1318        let power = TxPower::under_ceiling(PowerAmplifier::HighPower, &yagi, Decibels::from_db(30));
1319        assert_eq!(power.setting_dbm, 20);
1320    }
1321
1322    #[test]
1323    fn the_sync_words_and_workaround_values_follow_the_datasheet() {
1324        assert_eq!(SyncWord::Public.to_bytes(), [0x34, 0x44]);
1325        assert_eq!(SyncWord::Private.to_bytes(), [0x14, 0x24]);
1326        assert_eq!(SyncWord::Custom(0x1234).to_bytes(), [0x12, 0x34]);
1327        assert_eq!(tx_modulation(0x01, LoraBandwidth::Khz125), 0x05);
1328        assert_eq!(tx_modulation(0x05, LoraBandwidth::Khz500), 0x01);
1329        assert_eq!(tx_clamp(0xC8), 0xDE);
1330        assert_eq!(iq_polarity(0x0D, true), 0x09);
1331        assert_eq!(iq_polarity(0x09, false), 0x0D);
1332        assert_eq!(event_clear(0x00), 0x02);
1333    }
1334
1335    #[test]
1336    fn the_detection_tuning_is_semtechs_own() {
1337        assert_eq!(cad_symbols(1), 0x00);
1338        assert_eq!(cad_symbols(2), 0x01);
1339        assert_eq!(cad_symbols(5), 0x02);
1340        assert_eq!(cad_symbols(255), 0x04);
1341
1342        // LoRa Basics Modem's table, over two symbols.
1343        for (sf, narrow, wide) in [
1344            (5u8, 20u8, 22u8),
1345            (6, 21, 23),
1346            (7, 22, 25),
1347            (8, 24, 26),
1348            (9, 24, 30),
1349            (10, 25, 31),
1350            (11, 27, 33),
1351            (12, 27, 35),
1352        ] {
1353            assert_eq!(cad_detect_peak(sf, 125_000, 0x01), narrow, "SF{sf}");
1354            assert_eq!(cad_detect_peak(sf, 250_000, 0x01), narrow, "SF{sf} wider");
1355            assert_eq!(cad_detect_peak(sf, 500_000, 0x01), wide, "SF{sf} widest");
1356        }
1357        assert_eq!(
1358            cad_detect_peak(9, 125_000, 0x02),
1359            23,
1360            "four symbols, one lower"
1361        );
1362        assert_eq!(
1363            cad_detect_peak(9, 125_000, 0x03),
1364            22,
1365            "eight symbols, two lower"
1366        );
1367    }
1368}