Skip to main content

pamoja_radios/sx1302/
sx1261.rs

1//! Bringing up the SX1261 that listens beside the concentrator.
2//!
3//! [`lbt`](super::lbt) builds the frames that ask this radio whether a channel is busy, but
4//! two of the opcodes it sends are not in the SX1261's own command set. They exist only once
5//! the radio's patch memory holds Semtech's patch, so a gateway that sends them to a radio
6//! straight out of reset is talking to a chip that has never heard of them, and gets silence
7//! rather than an answer. This module is the half that comes first: park the radio, load the
8//! patch and prove it took, calibrate the image for the band, and set up the receiver the
9//! check listens with.
10//!
11//! The radio takes commands rather than register writes. Every one of them is a single opcode
12//! byte followed by its payload, which is what a [`Frame`] carries, and the few registers that
13//! are written go inside the payload of a write command rather than on a bus of their own.
14//!
15//! Three things here are not what they look like:
16//!
17//! - The patch is 386 words rather than bytes, written four bytes at a time from
18//!   [`PRAM_BASE`], and it does not take effect until [`commit`] is sent.
19//! - Whether it took is read as text. [`PRAM_VERSION`] is compared against the last four
20//!   characters of the version string, not the whole of it.
21//! - The last level a spectral scan reports repeats the level before it, because it counts
22//!   what was heard below the lowest threshold rather than at a new one.
23//!
24//! Nothing in this module opens a bus.
25
26use super::sx1250;
27
28/// Writes one of the radio's registers, with the address and value inside the payload.
29pub const OP_WRITE_REGISTER: u8 = 0x0d;
30
31/// Reads the radio's registers from an address given in the payload.
32pub const OP_READ_REGISTER: u8 = 0x1d;
33
34/// Reports what the radio failed at, which is how a refused calibration is noticed.
35pub const OP_GET_DEVICE_ERRORS: u8 = 0x17;
36
37/// Parks the radio, on either of the two clocks.
38pub const OP_SET_STANDBY: u8 = 0x80;
39
40/// Puts the radio into continuous receive.
41pub const OP_SET_RX: u8 = 0x82;
42
43/// Tunes the radio.
44pub const OP_SET_RF_FREQUENCY: u8 = 0x86;
45
46/// Chooses the modulation the receiver expects.
47pub const OP_SET_PACKET_TYPE: u8 = 0x8a;
48
49/// Sets the bitrate, the shaping, the bandwidth, and the deviation.
50pub const OP_SET_MODULATION_PARAMS: u8 = 0x8b;
51
52/// Sets the preamble, the sync word length, and how a packet's length is carried.
53pub const OP_SET_PACKET_PARAMS: u8 = 0x8c;
54
55/// Points the transmit and receive buffers at somewhere in the radio's memory.
56pub const OP_SET_BUFFER_BASE_ADDRESS: u8 = 0x8f;
57
58/// Calibrates the image rejection for one band.
59pub const OP_CALIBRATE_IMAGE: u8 = 0x98;
60
61/// Asks the radio what mode it is in and how the last command went.
62pub const OP_GET_STATUS: u8 = 0xc0;
63
64/// Puts the radio on frequency without receiving, which is where it waits between checks.
65pub const OP_SET_FS: u8 = 0xc1;
66
67/// Takes the patch just written into use.
68///
69/// This opcode is not in the radio's published command set. It appears only in the reference
70/// implementation, where the patch does nothing until it is sent.
71pub const OP_UPDATE_PRAM: u8 = 0xd9;
72
73/// The register holding how long the radio averages a signal level over.
74///
75/// It is written twice for opposite reasons: with [`RSSI_WINDOW_ARMED`] to set the averaging
76/// up before a check, and with zero to release the radio afterwards, which is what
77/// [`lbt::stop`](super::lbt::stop) and an abandoned spectral scan both do.
78pub const REG_RSSI_WINDOW: u16 = 0x089b;
79
80/// The averaging window a carrier check listens with.
81pub const RSSI_WINDOW_ARMED: u8 = 0x05 << 2;
82
83/// The register that trades a little current for a lower noise floor.
84pub const REG_SENSITIVITY: u16 = 0x08ac;
85
86/// What [`REG_SENSITIVITY`] is set to, which the reference applies to every board.
87pub const SENSITIVITY_VALUE: u8 = 0xcb;
88
89/// The register that has to be opened before the patch is written, and closed after.
90pub const REG_PATCH_UPDATE: u16 = 0x0610;
91
92/// Opens [`REG_PATCH_UPDATE`] so the patch can be written.
93pub const PATCH_UPDATE_OPEN: u8 = 0x10;
94
95/// Closes [`REG_PATCH_UPDATE`] once the patch is written.
96pub const PATCH_UPDATE_CLOSED: u8 = 0x00;
97
98/// Where the version string the radio is running sits.
99pub const REG_PRAM_VERSION: u16 = 0x0320;
100
101/// Where a spectral scan reports whether it is finished.
102pub const REG_SCAN_STATUS: u16 = 0x07cd;
103
104/// Where a spectral scan leaves its counts.
105pub const REG_SCAN_RESULTS: u16 = 0x0401;
106
107/// Where the patch is written, one word every four addresses.
108pub const PRAM_BASE: u16 = 0x8000;
109
110/// How many words the patch holds.
111pub const PRAM_WORDS: usize = 386;
112
113/// The last four characters of the version string a loaded patch answers with.
114pub const PRAM_VERSION: &str = "2D06";
115
116/// How many characters the version string holds.
117pub const VERSION_LEN: usize = 15;
118
119/// How many bytes a version read moves: the address, one the radio answers over, then the
120/// string itself.
121pub const VERSION_TRANSFER: usize = 3 + VERSION_LEN;
122
123/// The whole byte the radio answers with once it is parked and ready.
124///
125/// The reference checks for this value rather than for its fields, and so does
126/// [`Status::parked`], because it is what a working radio replies. Read as fields it is a
127/// mode of [`Mode::StandbyRc`] and a command status of 1, which the datasheet reserves. The
128/// reference composes it from a mode constant that is shifted into its field and a ready
129/// constant that is not, so the value is right and the arithmetic behind it is not.
130pub const STANDBY_READY: u8 = 0x22;
131
132/// The bits of a status byte that carry anything. The top and bottom bits are not answered.
133pub const STATUS_MASK: u8 = 0x7e;
134
135/// How long an image calibration takes before the radio can be asked whether it worked.
136pub const CALIBRATION_WAIT_MS: u32 = 10;
137
138/// How many levels a spectral scan reports.
139pub const SCAN_LEVELS: usize = 33;
140
141/// How many bytes a spectral scan's counts take: the address, one the radio answers over,
142/// then two bytes for each level.
143pub const SCAN_TRANSFER: usize = 3 + SCAN_LEVELS * 2;
144
145/// The longest payload any command here carries, which is the packet settings.
146pub const MAX_PAYLOAD: usize = 9;
147
148/// One command: the opcode, and the payload that follows it.
149///
150/// A frame is the whole of what goes out for one command, so a caller writes the opcode and
151/// then the payload and does not have to know which commands take one.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct Frame {
154    opcode: u8,
155    payload: [u8; MAX_PAYLOAD],
156    len: u8,
157}
158
159impl Frame {
160    const fn build(opcode: u8, payload: [u8; MAX_PAYLOAD], len: u8) -> Frame {
161        Frame {
162            opcode,
163            payload,
164            len,
165        }
166    }
167
168    /// The opcode byte this command starts with.
169    ///
170    /// # Returns
171    ///
172    /// The byte that goes out first.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use pamoja_radios::sx1302::sx1261::{standby, Standby, OP_SET_STANDBY};
178    ///
179    /// assert_eq!(standby(Standby::Rc).opcode(), OP_SET_STANDBY);
180    /// ```
181    #[must_use]
182    pub const fn opcode(&self) -> u8 {
183        self.opcode
184    }
185
186    /// What follows the opcode.
187    ///
188    /// # Returns
189    ///
190    /// The payload, which is empty for the commands that take none.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use pamoja_radios::sx1302::sx1261::{standby, Standby, free_running};
196    ///
197    /// assert_eq!(standby(Standby::Rc).payload(), &[0x00]);
198    /// assert!(free_running().payload().is_empty(), "this one takes no payload");
199    /// ```
200    #[must_use]
201    pub fn payload(&self) -> &[u8] {
202        &self.payload[..self.len as usize]
203    }
204}
205
206/// Which clock the radio is parked on.
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum Standby {
209    /// The radio's own oscillator, which is where it wakes up and where the patch is loaded.
210    Rc,
211    /// The crystal, which costs more current and holds frequency.
212    Xosc,
213}
214
215impl Standby {
216    /// The byte that selects this clock.
217    ///
218    /// # Returns
219    ///
220    /// The value the command takes.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use pamoja_radios::sx1302::sx1261::Standby;
226    ///
227    /// assert_eq!(Standby::Rc.byte(), 0x00);
228    /// assert_eq!(Standby::Xosc.byte(), 0x01);
229    /// ```
230    #[must_use]
231    pub const fn byte(self) -> u8 {
232        match self {
233            Standby::Rc => 0x00,
234            Standby::Xosc => 0x01,
235        }
236    }
237}
238
239/// What the radio is doing, from the middle bits of a status byte.
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum Mode {
242    /// Parked on its own oscillator.
243    StandbyRc,
244    /// Parked on the crystal.
245    StandbyXosc,
246    /// On frequency, neither receiving nor transmitting.
247    FrequencySynthesis,
248    /// Receiving.
249    Receiving,
250    /// Transmitting.
251    Transmitting,
252}
253
254impl Mode {
255    /// The bits this mode occupies in a status byte.
256    ///
257    /// # Returns
258    ///
259    /// The value as it sits in the byte, already in its field.
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use pamoja_radios::sx1302::sx1261::Mode;
265    ///
266    /// assert_eq!(Mode::StandbyRc.bits(), 0x20);
267    /// assert_eq!(Mode::Receiving.bits(), 0x50);
268    /// ```
269    #[must_use]
270    pub const fn bits(self) -> u8 {
271        match self {
272            Mode::StandbyRc => 0x20,
273            Mode::StandbyXosc => 0x30,
274            Mode::FrequencySynthesis => 0x40,
275            Mode::Receiving => 0x50,
276            Mode::Transmitting => 0x60,
277        }
278    }
279
280    /// Reads the mode out of a status byte.
281    ///
282    /// # Arguments
283    ///
284    /// * `status` - the byte the radio answered with.
285    ///
286    /// # Returns
287    ///
288    /// The mode, or `None` for a value the datasheet does not give one for.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// use pamoja_radios::sx1302::sx1261::{Mode, STANDBY_READY};
294    ///
295    /// assert_eq!(Mode::of(STANDBY_READY), Some(Mode::StandbyRc));
296    /// assert_eq!(Mode::of(0x00), None);
297    /// ```
298    #[must_use]
299    pub const fn of(status: u8) -> Option<Mode> {
300        match status & 0x70 {
301            0x20 => Some(Mode::StandbyRc),
302            0x30 => Some(Mode::StandbyXosc),
303            0x40 => Some(Mode::FrequencySynthesis),
304            0x50 => Some(Mode::Receiving),
305            0x60 => Some(Mode::Transmitting),
306            _ => None,
307        }
308    }
309}
310
311/// How the last command went, from the low bits of a status byte.
312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313pub enum CommandStatus {
314    /// There is something for the host to read.
315    DataAvailable,
316    /// The command ran out of time.
317    Timeout,
318    /// The radio could not process the command.
319    ProcessingError,
320    /// The radio understood the command and could not carry it out.
321    ExecutionFailed,
322    /// A transmission finished.
323    TransmitDone,
324}
325
326impl CommandStatus {
327    /// Reads the command status out of a status byte.
328    ///
329    /// # Arguments
330    ///
331    /// * `status` - the byte the radio answered with.
332    ///
333    /// # Returns
334    ///
335    /// The status, or `None` for a value the datasheet reserves.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use pamoja_radios::sx1302::sx1261::CommandStatus;
341    ///
342    /// assert_eq!(CommandStatus::of(0x24), Some(CommandStatus::DataAvailable));
343    /// assert_eq!(CommandStatus::of(0x2c), Some(CommandStatus::TransmitDone));
344    /// ```
345    #[must_use]
346    pub const fn of(status: u8) -> Option<CommandStatus> {
347        match (status >> 1) & 0x07 {
348            0x02 => Some(CommandStatus::DataAvailable),
349            0x03 => Some(CommandStatus::Timeout),
350            0x04 => Some(CommandStatus::ProcessingError),
351            0x05 => Some(CommandStatus::ExecutionFailed),
352            0x06 => Some(CommandStatus::TransmitDone),
353            _ => None,
354        }
355    }
356}
357
358/// What the radio answered when asked how it was.
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub struct Status {
361    /// The byte as it arrived, with the bits the radio does not answer cleared.
362    pub byte: u8,
363    /// What the radio is doing.
364    pub mode: Option<Mode>,
365    /// How the last command went.
366    pub command: Option<CommandStatus>,
367}
368
369impl Status {
370    /// Reads a status byte.
371    ///
372    /// # Arguments
373    ///
374    /// * `byte` - what the radio answered.
375    ///
376    /// # Returns
377    ///
378    /// The byte and the fields inside it.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use pamoja_radios::sx1302::sx1261::{Mode, Status, STANDBY_READY};
384    ///
385    /// let status = Status::read(STANDBY_READY);
386    /// assert_eq!(status.mode, Some(Mode::StandbyRc));
387    /// assert!(status.parked(), "this is the answer a parked radio gives");
388    /// ```
389    #[must_use]
390    pub const fn read(byte: u8) -> Status {
391        let byte = byte & STATUS_MASK;
392        Status {
393            byte,
394            mode: Mode::of(byte),
395            command: CommandStatus::of(byte),
396        }
397    }
398
399    /// Whether the radio is parked and ready for the patch.
400    ///
401    /// # Returns
402    ///
403    /// Whether the answer is [`STANDBY_READY`], which is the whole byte a parked radio gives
404    /// rather than a test of either field.
405    ///
406    /// # Examples
407    ///
408    /// ```
409    /// use pamoja_radios::sx1302::sx1261::{Status, STANDBY_READY};
410    ///
411    /// assert!(Status::read(STANDBY_READY).parked());
412    /// assert!(!Status::read(0x50).parked(), "receiving is not parked");
413    /// ```
414    #[must_use]
415    pub const fn parked(&self) -> bool {
416        self.byte == STANDBY_READY
417    }
418}
419
420/// Turns a frequency into the number the radio is tuned with.
421///
422/// # Arguments
423///
424/// * `hertz` - the carrier.
425///
426/// # Returns
427///
428/// The value the tune command takes.
429///
430/// # Examples
431///
432/// ```
433/// use pamoja_radios::sx1302::sx1261::frequency_value;
434///
435/// assert_eq!(frequency_value(868_100_000), 910_268_825);
436/// ```
437#[must_use]
438pub const fn frequency_value(hertz: u32) -> u32 {
439    ((hertz as u64 * (1 << 25)) / 32_000_000) as u32
440}
441
442/// Parks the radio.
443///
444/// # Arguments
445///
446/// * `clock` - which clock to park on.
447///
448/// # Returns
449///
450/// The command.
451///
452/// # Examples
453///
454/// ```
455/// use pamoja_radios::sx1302::sx1261::{standby, Standby, OP_SET_STANDBY};
456///
457/// let frame = standby(Standby::Rc);
458/// assert_eq!(frame.opcode(), OP_SET_STANDBY);
459/// assert_eq!(frame.payload(), &[0x00]);
460/// ```
461#[must_use]
462pub const fn standby(clock: Standby) -> Frame {
463    Frame::build(OP_SET_STANDBY, [clock.byte(), 0, 0, 0, 0, 0, 0, 0, 0], 1)
464}
465
466/// Puts the radio on frequency without receiving, which is where it waits between checks.
467///
468/// # Returns
469///
470/// The command, which takes no payload.
471///
472/// # Examples
473///
474/// ```
475/// use pamoja_radios::sx1302::sx1261::{free_running, OP_SET_FS};
476///
477/// assert_eq!(free_running().opcode(), OP_SET_FS);
478/// assert!(free_running().payload().is_empty());
479/// ```
480#[must_use]
481pub const fn free_running() -> Frame {
482    Frame::build(OP_SET_FS, [0; MAX_PAYLOAD], 0)
483}
484
485/// Asks the radio how it is.
486///
487/// # Returns
488///
489/// The command. The radio answers over the byte the payload leaves for it, which
490/// [`Status::read`] then reads.
491///
492/// # Examples
493///
494/// ```
495/// use pamoja_radios::sx1302::sx1261::{status, OP_GET_STATUS};
496///
497/// assert_eq!(status().opcode(), OP_GET_STATUS);
498/// assert_eq!(status().payload(), &[0x00], "the radio answers over this byte");
499/// ```
500#[must_use]
501pub const fn status() -> Frame {
502    Frame::build(OP_GET_STATUS, [0; MAX_PAYLOAD], 1)
503}
504
505/// Writes one of the radio's registers.
506///
507/// # Arguments
508///
509/// * `register` - the address.
510/// * `value` - what to write.
511///
512/// # Returns
513///
514/// The command.
515///
516/// # Examples
517///
518/// ```
519/// use pamoja_radios::sx1302::sx1261::{write_register, REG_SENSITIVITY, SENSITIVITY_VALUE};
520///
521/// let frame = write_register(REG_SENSITIVITY, SENSITIVITY_VALUE);
522/// assert_eq!(frame.payload(), &[0x08, 0xac, 0xcb]);
523/// ```
524#[must_use]
525pub const fn write_register(register: u16, value: u8) -> Frame {
526    Frame::build(
527        OP_WRITE_REGISTER,
528        [
529            (register >> 8) as u8,
530            (register & 0xff) as u8,
531            value,
532            0,
533            0,
534            0,
535            0,
536            0,
537            0,
538        ],
539        3,
540    )
541}
542
543/// The commands that bring a parked radio up for carrier checks.
544///
545/// # Returns
546///
547/// Parking the radio, pointing its buffers somewhere, and trading a little current for a
548/// lower noise floor. The radio is asked for its [`status`] between the first and the second,
549/// and [`Status::parked`] has to hold before the rest are worth sending.
550///
551/// # Examples
552///
553/// ```
554/// use pamoja_radios::sx1302::sx1261::{setup, OP_SET_BUFFER_BASE_ADDRESS, OP_SET_STANDBY};
555///
556/// let frames = setup();
557/// assert_eq!(frames[0].opcode(), OP_SET_STANDBY);
558/// assert_eq!(frames[1].opcode(), OP_SET_BUFFER_BASE_ADDRESS);
559/// assert_eq!(frames[1].payload(), &[0x80, 0x80]);
560/// assert_eq!(frames[2].payload(), &[0x08, 0xac, 0xcb], "the sensitivity register");
561/// ```
562#[must_use]
563pub const fn setup() -> [Frame; 3] {
564    [
565        standby(Standby::Rc),
566        Frame::build(
567            OP_SET_BUFFER_BASE_ADDRESS,
568            [0x80, 0x80, 0, 0, 0, 0, 0, 0, 0],
569            2,
570        ),
571        write_register(REG_SENSITIVITY, SENSITIVITY_VALUE),
572    ]
573}
574
575/// Calibrates the image rejection for the band a carrier falls in.
576///
577/// # Arguments
578///
579/// * `hertz` - the carrier the radio will listen on.
580///
581/// # Returns
582///
583/// The command, or `None` for a frequency outside every band the radio is calibrated for.
584/// The bands are the same ones the front ends use, so they are read from
585/// [`sx1250::image_band`] rather than written out twice.
586///
587/// # Examples
588///
589/// ```
590/// use pamoja_radios::sx1302::sx1261::{calibrate, OP_CALIBRATE_IMAGE};
591///
592/// let frame = calibrate(868_100_000).expect("868 MHz is a band the radio knows");
593/// assert_eq!(frame.opcode(), OP_CALIBRATE_IMAGE);
594/// assert_eq!(frame.payload(), &[0xd7, 0xdb]);
595/// assert!(calibrate(600_000_000).is_none(), "no band covers this");
596/// ```
597#[must_use]
598pub const fn calibrate(hertz: u32) -> Option<Frame> {
599    match sx1250::image_band(hertz) {
600        Some([low, high]) => Some(Frame::build(
601            OP_CALIBRATE_IMAGE,
602            [low, high, 0, 0, 0, 0, 0, 0, 0],
603            2,
604        )),
605        None => None,
606    }
607}
608
609/// Asks the radio what it has failed at.
610///
611/// # Returns
612///
613/// The command. The radio answers over the bytes the payload leaves for it, which
614/// [`calibration_failed`] then reads.
615///
616/// # Examples
617///
618/// ```
619/// use pamoja_radios::sx1302::sx1261::{device_errors, OP_GET_DEVICE_ERRORS};
620///
621/// assert_eq!(device_errors().opcode(), OP_GET_DEVICE_ERRORS);
622/// assert_eq!(device_errors().payload().len(), 3);
623/// ```
624#[must_use]
625pub const fn device_errors() -> Frame {
626    Frame::build(OP_GET_DEVICE_ERRORS, [0; MAX_PAYLOAD], 3)
627}
628
629/// Whether the radio reports that an image calibration did not work.
630///
631/// # Arguments
632///
633/// * `answer` - the three bytes [`device_errors`] came back with.
634///
635/// # Returns
636///
637/// Whether the image calibration bit is set.
638///
639/// # Examples
640///
641/// ```
642/// use pamoja_radios::sx1302::sx1261::calibration_failed;
643///
644/// assert!(!calibration_failed([0x00, 0x00, 0x00]));
645/// assert!(calibration_failed([0x00, 0x00, 0x10]));
646/// assert!(!calibration_failed([0x00, 0x00, 0x08]), "a different fault, not this one");
647/// ```
648#[must_use]
649pub const fn calibration_failed(answer: [u8; 3]) -> bool {
650    (answer[2] >> 4) & 0x01 != 0
651}
652
653/// Opens the radio so the patch can be written.
654///
655/// # Returns
656///
657/// The command that goes out before the first word.
658///
659/// # Examples
660///
661/// ```
662/// use pamoja_radios::sx1302::sx1261::open_patch;
663///
664/// assert_eq!(open_patch().payload(), &[0x06, 0x10, 0x10]);
665/// ```
666#[must_use]
667pub const fn open_patch() -> Frame {
668    write_register(REG_PATCH_UPDATE, PATCH_UPDATE_OPEN)
669}
670
671/// Closes the radio once the patch is written.
672///
673/// # Returns
674///
675/// The command that goes out after the last word, before [`commit`].
676///
677/// # Examples
678///
679/// ```
680/// use pamoja_radios::sx1302::sx1261::close_patch;
681///
682/// assert_eq!(close_patch().payload(), &[0x06, 0x10, 0x00]);
683/// ```
684#[must_use]
685pub const fn close_patch() -> Frame {
686    write_register(REG_PATCH_UPDATE, PATCH_UPDATE_CLOSED)
687}
688
689/// Writes one word of the patch.
690///
691/// # Arguments
692///
693/// * `index` - which word, counting from zero.
694/// * `word` - the word itself.
695///
696/// # Returns
697///
698/// The command. The address is four apart from its neighbors, because each word takes four
699/// of them.
700///
701/// # Examples
702///
703/// ```
704/// use pamoja_radios::sx1302::sx1261::patch_word;
705///
706/// assert_eq!(patch_word(0, 0x00337fe1).payload(), &[0x80, 0x00, 0x00, 0x33, 0x7f, 0xe1]);
707/// assert_eq!(patch_word(1, 0).payload()[..2], [0x80, 0x04], "four addresses on");
708/// ```
709#[must_use]
710pub const fn patch_word(index: usize, word: u32) -> Frame {
711    let address = PRAM_BASE as usize + 4 * index;
712    Frame::build(
713        OP_WRITE_REGISTER,
714        [
715            (address >> 8) as u8,
716            (address & 0xff) as u8,
717            (word >> 24) as u8,
718            (word >> 16) as u8,
719            (word >> 8) as u8,
720            word as u8,
721            0,
722            0,
723            0,
724        ],
725        6,
726    )
727}
728
729/// Takes the patch into use.
730///
731/// # Returns
732///
733/// The command, which takes no payload. Until it is sent the patch is written but not
734/// running, and the version string still reads as whatever it did before.
735///
736/// # Examples
737///
738/// ```
739/// use pamoja_radios::sx1302::sx1261::{commit, OP_UPDATE_PRAM};
740///
741/// assert_eq!(commit().opcode(), OP_UPDATE_PRAM);
742/// assert!(commit().payload().is_empty());
743/// ```
744#[must_use]
745pub const fn commit() -> Frame {
746    Frame::build(OP_UPDATE_PRAM, [0; MAX_PAYLOAD], 0)
747}
748
749/// Asks the radio which patch it is running.
750///
751/// # Returns
752///
753/// The address to read from, and how many bytes the read moves. The string arrives after the
754/// address and the byte the radio answers over, which is what [`version`] accounts for.
755///
756/// # Examples
757///
758/// ```
759/// use pamoja_radios::sx1302::sx1261::{version_read, VERSION_TRANSFER};
760///
761/// let (address, length) = version_read();
762/// assert_eq!(address, 0x0320);
763/// assert_eq!(length, VERSION_TRANSFER);
764/// ```
765#[must_use]
766pub const fn version_read() -> (u16, usize) {
767    (REG_PRAM_VERSION, VERSION_TRANSFER)
768}
769
770/// Reads the version string out of what the radio answered.
771///
772/// # Arguments
773///
774/// * `answer` - the bytes the read moved, starting with the address that went out.
775///
776/// # Returns
777///
778/// The string, or `None` when the answer is too short or is not text.
779///
780/// # Examples
781///
782/// ```
783/// use pamoja_radios::sx1302::sx1261::version;
784///
785/// let mut answer = [0u8; 18];
786/// answer[3..].copy_from_slice(b"sx1261_v1.12D06");
787/// assert_eq!(version(&answer), Some("sx1261_v1.12D06"));
788/// ```
789#[must_use]
790pub fn version(answer: &[u8]) -> Option<&str> {
791    let text = answer.get(3..VERSION_TRANSFER)?;
792    core::str::from_utf8(text).ok()
793}
794
795/// Whether the radio is running the patch this crate writes.
796///
797/// # Arguments
798///
799/// * `answer` - the bytes a version read moved.
800///
801/// # Returns
802///
803/// Whether the string ends in [`PRAM_VERSION`]. Only the last four characters are compared,
804/// because the rest of the string names a build rather than the patch.
805///
806/// # Examples
807///
808/// ```
809/// use pamoja_radios::sx1302::sx1261::patched;
810///
811/// let mut answer = [0u8; 18];
812/// answer[3..].copy_from_slice(b"sx1261_v1.12D06");
813/// assert!(patched(&answer));
814///
815/// answer[3..].copy_from_slice(b"sx1261_v1.10000");
816/// assert!(!patched(&answer), "a radio still running what it came with");
817/// ```
818#[must_use]
819pub fn patched(answer: &[u8]) -> bool {
820    let Some(text) = version(answer) else {
821        return false;
822    };
823    let Some(tail) = text.get(text.len().saturating_sub(PRAM_VERSION.len())..) else {
824        return false;
825    };
826    tail == PRAM_VERSION
827}
828
829/// What went wrong reading a patch.
830#[derive(Clone, Copy, Debug, PartialEq, Eq)]
831pub enum PatchError {
832    /// The text does not hold as many words as the patch has.
833    WrongCount {
834        /// How many words were found.
835        found: usize,
836    },
837}
838
839impl core::fmt::Display for PatchError {
840    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
841        match self {
842            PatchError::WrongCount { found } => write!(
843                f,
844                "the patch holds {found} words, and the radio takes {PRAM_WORDS}"
845            ),
846        }
847    }
848}
849
850/// Reads a patch from the form Semtech ships it in.
851///
852/// The patch arrives as a C array of 32-bit words, written without leading zeros, so a word
853/// is between one and eight characters long. That is read here rather than converted first,
854/// so the file a board's vendor ships is the file that is loaded.
855///
856/// # Arguments
857///
858/// * `text` - the file's contents.
859/// * `into` - where the words are put.
860///
861/// # Returns
862///
863/// Nothing, with `into` filled.
864///
865/// # Errors
866///
867/// [`PatchError::WrongCount`] when the text does not hold exactly [`PRAM_WORDS`] words. A
868/// short count means the file is truncated or is not a patch, and a long one means it is
869/// something else, so neither is loaded.
870///
871/// # Examples
872///
873/// ```
874/// use pamoja_radios::sx1302::sx1261::{read_patch, PRAM_WORDS};
875///
876/// let mut text = String::from("const uint32_t pram[] = {\n");
877/// for _ in 0..PRAM_WORDS {
878///     text.push_str("0x337fe1,\n");
879/// }
880/// text.push_str("};\n");
881///
882/// let mut patch = [0u32; PRAM_WORDS];
883/// read_patch(&text, &mut patch).expect("that is a patch of the right length");
884/// assert_eq!(patch[0], 0x337fe1);
885/// ```
886pub fn read_patch(text: &str, into: &mut [u32; PRAM_WORDS]) -> Result<(), PatchError> {
887    let raw = text.as_bytes();
888    let mut found = 0usize;
889    let mut at = 0usize;
890
891    while at + 2 < raw.len() {
892        if raw[at] != b'0' || (raw[at + 1] | 0x20) != b'x' {
893            at += 1;
894            continue;
895        }
896
897        let mut value = 0u32;
898        let mut digits = 0usize;
899        while let Some(digit) = raw.get(at + 2 + digits).copied().and_then(hex_digit) {
900            value = (value << 4) | digit as u32;
901            digits += 1;
902            if digits == 8 {
903                break;
904            }
905        }
906
907        if digits == 0 {
908            at += 1;
909            continue;
910        }
911
912        if let Some(slot) = into.get_mut(found) {
913            *slot = value;
914        }
915        found += 1;
916        at += 2 + digits;
917    }
918
919    if found == PRAM_WORDS {
920        Ok(())
921    } else {
922        Err(PatchError::WrongCount { found })
923    }
924}
925
926const fn hex_digit(byte: u8) -> Option<u8> {
927    match byte {
928        b'0'..=b'9' => Some(byte - b'0'),
929        b'a'..=b'f' => Some(byte - b'a' + 10),
930        b'A'..=b'F' => Some(byte - b'A' + 10),
931        _ => None,
932    }
933}
934
935/// How wide the receiver listens.
936#[derive(Clone, Copy, Debug, PartialEq, Eq)]
937pub enum Bandwidth {
938    /// Matching a 125 kHz channel.
939    Khz125,
940    /// Matching a 250 kHz channel.
941    Khz250,
942}
943
944impl Bandwidth {
945    /// The byte that selects this width.
946    ///
947    /// # Returns
948    ///
949    /// The value the modulation command takes. The receiver is wider than the channel it
950    /// covers, because it is measuring a level rather than decoding anything.
951    ///
952    /// # Examples
953    ///
954    /// ```
955    /// use pamoja_radios::sx1302::sx1261::Bandwidth;
956    ///
957    /// assert_eq!(Bandwidth::Khz125.byte(), 0x0a);
958    /// assert_eq!(Bandwidth::Khz250.byte(), 0x09);
959    /// ```
960    #[must_use]
961    pub const fn byte(self) -> u8 {
962        match self {
963            Bandwidth::Khz125 => 0x0a,
964            Bandwidth::Khz250 => 0x09,
965        }
966    }
967}
968
969/// The commands that point the receiver at a channel and start it listening.
970///
971/// # Arguments
972///
973/// * `hertz` - the carrier to listen on.
974/// * `bandwidth` - how wide to listen.
975///
976/// # Returns
977///
978/// Releasing whatever the radio was doing, putting it on frequency, tuning it, setting the
979/// averaging window, and then the modulation, the packet shape, and continuous receive. The
980/// radio is left listening, which is what a carrier check then reads a level from.
981///
982/// # Examples
983///
984/// ```
985/// use pamoja_radios::sx1302::sx1261::{receive, Bandwidth, OP_SET_RX};
986///
987/// let frames = receive(868_100_000, Bandwidth::Khz125);
988/// assert_eq!(frames[0].payload(), &[0x08, 0x9b, 0x00], "release it first");
989/// assert_eq!(frames[2].payload(), &[0x36, 0x41, 0x99, 0x99], "tuned to 868.1 MHz");
990/// assert_eq!(frames[7].opcode(), OP_SET_RX);
991/// assert_eq!(frames[7].payload(), &[0xff, 0xff, 0xff], "and it listens until told not to");
992/// ```
993#[must_use]
994pub const fn receive(hertz: u32, bandwidth: Bandwidth) -> [Frame; 8] {
995    let tuned = frequency_value(hertz);
996    [
997        write_register(REG_RSSI_WINDOW, 0x00),
998        free_running(),
999        Frame::build(
1000            OP_SET_RF_FREQUENCY,
1001            [
1002                (tuned >> 24) as u8,
1003                (tuned >> 16) as u8,
1004                (tuned >> 8) as u8,
1005                tuned as u8,
1006                0,
1007                0,
1008                0,
1009                0,
1010                0,
1011            ],
1012            4,
1013        ),
1014        write_register(REG_RSSI_WINDOW, RSSI_WINDOW_ARMED),
1015        Frame::build(OP_SET_PACKET_TYPE, [0x00; MAX_PAYLOAD], 1),
1016        Frame::build(
1017            OP_SET_MODULATION_PARAMS,
1018            [
1019                0x00,
1020                0x14,
1021                0x00,
1022                0x00,
1023                bandwidth.byte(),
1024                0x02,
1025                0xe9,
1026                0x0f,
1027                0x00,
1028            ],
1029            8,
1030        ),
1031        Frame::build(
1032            OP_SET_PACKET_PARAMS,
1033            [0x00, 0x20, 0x05, 0x20, 0x00, 0x01, 0xff, 0x00, 0x00],
1034            9,
1035        ),
1036        Frame::build(OP_SET_RX, [0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0], 3),
1037    ]
1038}
1039
1040/// How far along a spectral scan is.
1041#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1042pub enum ScanStatus {
1043    /// No scan has run.
1044    None,
1045    /// A scan is running.
1046    Running,
1047    /// A scan was stopped before it finished.
1048    Aborted,
1049    /// A scan finished, so its counts are worth reading.
1050    Completed,
1051}
1052
1053impl ScanStatus {
1054    /// Reads what the radio answered.
1055    ///
1056    /// # Arguments
1057    ///
1058    /// * `byte` - the value read from [`REG_SCAN_STATUS`].
1059    ///
1060    /// # Returns
1061    ///
1062    /// The state, or `None` for a value that is none of them.
1063    ///
1064    /// # Examples
1065    ///
1066    /// ```
1067    /// use pamoja_radios::sx1302::sx1261::ScanStatus;
1068    ///
1069    /// assert_eq!(ScanStatus::of(0x00), Some(ScanStatus::None));
1070    /// assert_eq!(ScanStatus::of(0x0f), Some(ScanStatus::Running));
1071    /// assert_eq!(ScanStatus::of(0xff), Some(ScanStatus::Completed));
1072    /// assert_eq!(ScanStatus::of(0x01), None);
1073    /// ```
1074    #[must_use]
1075    pub const fn of(byte: u8) -> Option<ScanStatus> {
1076        match byte {
1077            0x00 => Some(ScanStatus::None),
1078            0x0f => Some(ScanStatus::Running),
1079            0xf0 => Some(ScanStatus::Aborted),
1080            0xff => Some(ScanStatus::Completed),
1081            _ => None,
1082        }
1083    }
1084}
1085
1086/// One level a spectral scan counted at.
1087#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1088pub struct Level {
1089    /// The level itself, in dBm.
1090    pub dbm: i16,
1091    /// How many of the scan's samples were at least this strong.
1092    pub count: u16,
1093}
1094
1095/// Reads what a spectral scan counted.
1096///
1097/// # Arguments
1098///
1099/// * `answer` - the bytes the read moved, starting with the address that went out.
1100/// * `rssi_offset` - the board's correction, in dB, from its own calibration.
1101///
1102/// # Returns
1103///
1104/// The levels, strongest first, four decibels apart, or `None` when the answer is too short.
1105///
1106/// The last level repeats the one before it rather than continuing down, because it counts
1107/// everything heard below the lowest threshold rather than at a level of its own. Subtracting
1108/// one count from the other is what a caller does with it.
1109///
1110/// # Examples
1111///
1112/// ```
1113/// use pamoja_radios::sx1302::sx1261::{scan_counts, SCAN_TRANSFER};
1114///
1115/// let mut answer = [0u8; SCAN_TRANSFER];
1116/// answer[3] = 0x01;
1117/// answer[4] = 0x2c;
1118///
1119/// let levels = scan_counts(&answer, -3).expect("that is a whole answer");
1120/// assert_eq!(levels[0].dbm, -3, "the strongest level, corrected for the board");
1121/// assert_eq!(levels[0].count, 300);
1122/// assert_eq!(levels[1].dbm, -7, "four decibels down");
1123/// assert_eq!(levels[32].dbm, levels[31].dbm, "everything below the last one");
1124/// ```
1125#[must_use]
1126pub fn scan_counts(answer: &[u8], rssi_offset: i8) -> Option<[Level; SCAN_LEVELS]> {
1127    let counts = answer.get(3..SCAN_TRANSFER)?;
1128    let mut levels = [Level { dbm: 0, count: 0 }; SCAN_LEVELS];
1129    for (at, level) in levels.iter_mut().enumerate() {
1130        let step = if at == SCAN_LEVELS - 1 { at - 1 } else { at };
1131        *level = Level {
1132            dbm: -(step as i16) * 4 + rssi_offset as i16,
1133            count: u16::from_be_bytes([counts[at * 2], counts[at * 2 + 1]]),
1134        };
1135    }
1136    Some(levels)
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use super::*;
1142
1143    #[test]
1144    fn the_setup_matches_the_reference() {
1145        let frames = setup();
1146        assert_eq!(frames[0].opcode(), OP_SET_STANDBY);
1147        assert_eq!(frames[0].payload(), &[0x00]);
1148        assert_eq!(frames[1].opcode(), OP_SET_BUFFER_BASE_ADDRESS);
1149        assert_eq!(frames[1].payload(), &[0x80, 0x80]);
1150        assert_eq!(frames[2].opcode(), OP_WRITE_REGISTER);
1151        assert_eq!(frames[2].payload(), &[0x08, 0xac, 0xcb]);
1152    }
1153
1154    #[test]
1155    fn every_calibration_band_matches_the_reference() {
1156        let bands = [
1157            (435_000_000u32, [0x6b, 0x6f]),
1158            (490_000_000, [0x75, 0x81]),
1159            (783_000_000, [0xc1, 0xc5]),
1160            (868_000_000, [0xd7, 0xdb]),
1161            (915_000_000, [0xe1, 0xe9]),
1162        ];
1163        for (hertz, expected) in bands {
1164            let frame = calibrate(hertz).expect("the band is covered");
1165            assert_eq!(frame.payload(), expected, "at {hertz} Hz");
1166        }
1167        assert!(calibrate(430_000_000).is_none(), "the edge is outside");
1168        assert!(calibrate(928_000_000).is_none(), "so is the other one");
1169    }
1170
1171    #[test]
1172    fn a_patch_word_lands_four_addresses_on_from_the_last() {
1173        assert_eq!(
1174            patch_word(0, 0x0033_7fe1).payload(),
1175            &[0x80, 0x00, 0x00, 0x33, 0x7f, 0xe1]
1176        );
1177        assert_eq!(
1178            patch_word(1, 0x0033_7fdb).payload(),
1179            &[0x80, 0x04, 0x00, 0x33, 0x7f, 0xdb]
1180        );
1181        assert_eq!(patch_word(385, 0).payload()[..2], [0x86, 0x04]);
1182    }
1183
1184    #[test]
1185    fn a_patch_is_read_from_the_form_it_ships_in() {
1186        let mut text = String::from("const uint32_t pram[] = {\n");
1187        for at in 0..PRAM_WORDS {
1188            text.push_str(&format!("0x{:x},\n", at));
1189        }
1190        text.push_str("};\n\n#define PRAM_COUNT 386\n");
1191
1192        let mut patch = [0u32; PRAM_WORDS];
1193        read_patch(&text, &mut patch).expect("the count is right");
1194        assert_eq!(patch[0], 0);
1195        assert_eq!(patch[385], 385);
1196    }
1197
1198    #[test]
1199    fn a_patch_of_the_wrong_length_is_refused_rather_than_loaded() {
1200        let mut patch = [0u32; PRAM_WORDS];
1201        assert_eq!(
1202            read_patch("0x1, 0x2,", &mut patch),
1203            Err(PatchError::WrongCount { found: 2 })
1204        );
1205
1206        let mut text = String::new();
1207        for _ in 0..PRAM_WORDS + 1 {
1208            text.push_str("0x1,\n");
1209        }
1210        assert_eq!(
1211            read_patch(&text, &mut patch),
1212            Err(PatchError::WrongCount {
1213                found: PRAM_WORDS + 1
1214            })
1215        );
1216    }
1217
1218    #[test]
1219    fn words_are_read_whole_rather_than_a_byte_at_a_time() {
1220        let mut text = String::new();
1221        for _ in 0..PRAM_WORDS {
1222            text.push_str("0x3f3fff,\n");
1223        }
1224        let mut patch = [0u32; PRAM_WORDS];
1225        read_patch(&text, &mut patch).expect("the count is right");
1226        assert_eq!(
1227            patch[0], 0x003f_3fff,
1228            "six characters are one word, not three"
1229        );
1230    }
1231
1232    #[test]
1233    fn only_the_last_four_characters_of_the_version_are_compared() {
1234        let mut answer = [0u8; VERSION_TRANSFER];
1235        answer[3..].copy_from_slice(b"sx1261_v1.1_2D0");
1236        assert!(!patched(&answer), "this one is cut short of the version");
1237
1238        let mut answer = [0u8; VERSION_TRANSFER];
1239        answer[3..].copy_from_slice(b"anything___2D06");
1240        assert!(patched(&answer), "the build before it does not matter");
1241    }
1242
1243    #[test]
1244    fn a_short_version_answer_is_not_read_as_a_patched_radio() {
1245        assert!(!patched(&[]));
1246        assert!(!patched(&[0; 4]));
1247        assert_eq!(version(&[0; 4]), None);
1248    }
1249
1250    #[test]
1251    fn the_receive_settings_match_the_reference() {
1252        let frames = receive(868_100_000, Bandwidth::Khz125);
1253        assert_eq!(frames[0].payload(), &[0x08, 0x9b, 0x00]);
1254        assert_eq!(frames[1].opcode(), OP_SET_FS);
1255        assert_eq!(frames[2].opcode(), OP_SET_RF_FREQUENCY);
1256        assert_eq!(frames[3].payload(), &[0x08, 0x9b, 0x14]);
1257        assert_eq!(frames[4].opcode(), OP_SET_PACKET_TYPE);
1258        assert_eq!(frames[4].payload(), &[0x00], "listening for FSK, not LoRa");
1259        assert_eq!(
1260            frames[5].payload(),
1261            &[0x00, 0x14, 0x00, 0x00, 0x0a, 0x02, 0xe9, 0x0f]
1262        );
1263        assert_eq!(
1264            frames[6].payload(),
1265            &[0x00, 0x20, 0x05, 0x20, 0x00, 0x01, 0xff, 0x00, 0x00]
1266        );
1267        assert_eq!(frames[7].payload(), &[0xff, 0xff, 0xff]);
1268    }
1269
1270    #[test]
1271    fn a_wider_channel_listens_wider() {
1272        let narrow = receive(868_100_000, Bandwidth::Khz125);
1273        let wide = receive(868_100_000, Bandwidth::Khz250);
1274        assert_eq!(narrow[5].payload()[4], 0x0a);
1275        assert_eq!(wide[5].payload()[4], 0x09);
1276    }
1277
1278    #[test]
1279    fn the_tuning_matches_the_front_ends_arithmetic() {
1280        // A whole megahertz lands on a round value, which is what makes it worth anchoring to.
1281        assert_eq!(frequency_value(868_000_000), 0x3640_0000);
1282        assert_eq!(frequency_value(915_000_000), 959447040);
1283        assert_eq!(frequency_value(868_100_000), 910_268_825);
1284
1285        // The widening matters: a carrier this high overflows 32 bits before the divide.
1286        assert_eq!(frequency_value(928_000_000), 973078528);
1287    }
1288
1289    #[test]
1290    fn a_status_byte_reads_as_its_fields() {
1291        assert_eq!(Status::read(STANDBY_READY).mode, Some(Mode::StandbyRc));
1292        assert_eq!(Status::read(0x54).mode, Some(Mode::Receiving));
1293        assert_eq!(
1294            Status::read(0x54).command,
1295            Some(CommandStatus::DataAvailable)
1296        );
1297        assert_eq!(Status::read(0xa2).byte, 0x22, "the top bit is not answered");
1298        assert!(Status::read(0xa3).parked(), "nor is the bottom one");
1299    }
1300
1301    #[test]
1302    fn a_refused_calibration_is_told_apart_from_every_other_fault() {
1303        assert!(calibration_failed([0xff, 0xff, 0xff]));
1304        assert!(
1305            !calibration_failed([0xff, 0xff, 0xef]),
1306            "every fault but this"
1307        );
1308    }
1309
1310    #[test]
1311    fn the_last_scan_level_counts_what_was_below_the_one_before_it() {
1312        let mut answer = [0u8; SCAN_TRANSFER];
1313        for at in 0..SCAN_LEVELS {
1314            answer[3 + at * 2] = 0x00;
1315            answer[3 + at * 2 + 1] = at as u8;
1316        }
1317
1318        let levels = scan_counts(&answer, 0).expect("that is a whole answer");
1319        assert_eq!(levels[0].dbm, 0);
1320        assert_eq!(levels[31].dbm, -124);
1321        assert_eq!(levels[32].dbm, -124, "the same level, counted below it");
1322        assert_eq!(levels[32].count, 32);
1323    }
1324
1325    #[test]
1326    fn a_scan_answer_that_is_cut_short_is_refused() {
1327        assert!(scan_counts(&[0u8; SCAN_TRANSFER - 1], 0).is_none());
1328        assert!(scan_counts(&[], 0).is_none());
1329    }
1330
1331    #[test]
1332    fn the_board_correction_moves_every_level_together() {
1333        let answer = [0u8; SCAN_TRANSFER];
1334        let plain = scan_counts(&answer, 0).expect("a whole answer");
1335        let corrected = scan_counts(&answer, -3).expect("a whole answer");
1336        for at in 0..SCAN_LEVELS {
1337            assert_eq!(corrected[at].dbm, plain[at].dbm - 3);
1338        }
1339    }
1340}