Skip to main content

pamoja_ffi/
radios.rs

1//! The C ABI for LoRa radio chips.
2//!
3//! These functions wrap [`pamoja_radios`] for callers that drive a radio through the flat
4//! C boundary: the bytes of each Semtech SX126x command, the chip's answers decoded, the
5//! amplifier settings a regional power ceiling allows, and the silence a duty-cycle limit
6//! forces after each transmission. A caller with its own SPI access sends each command in
7//! one transaction, once BUSY is low, and reads a query's answer in the same transaction.
8//!
9//! The SX1276 family is driven through registers rather than commands, so for it these give
10//! the register addresses, the values the settings put in them, and the decoded readings.
11//!
12//! A command is at most ten bytes, so it crosses by value as [`PamojaSx126xCommand`] and
13//! costs no allocation. The duty-cycle guard keeps state between calls, so it is a handle.
14
15use pamoja_radios::duty::DutyCycle;
16use pamoja_radios::sx126x::command::{self, Command, Query};
17use pamoja_radios::sx126x::config::{
18    self, LoraModulation, LoraPacket, PaConfig, PacketType, PowerAmplifier, RampTime, StandbyMode,
19    SyncWord, TxPower,
20};
21use pamoja_radios::sx126x::irq::Irq;
22use pamoja_radios::sx126x::status::{
23    rssi_inst_dbm, ChipMode, CommandStatus, DeviceErrors, PacketStatus, RxBufferStatus, Status,
24};
25
26use pamoja_radios::sx126x::config::llcc68_supports;
27use pamoja_radios::sx127x::config::{
28    self as sx127x_config, LoraBandwidth as Sx127xBandwidth, LoraModulation as Sx127xModulation,
29    ModulationError, PaOutput, TxPower as Sx127xPower,
30};
31use pamoja_radios::sx127x::irq::IrqFlags;
32use pamoja_radios::sx127x::register::{self as sx127x_register, Mode as Sx127xMode};
33use pamoja_radios::sx127x::status::{
34    rssi_dbm as sx127x_rssi_dbm, ModemStatus as Sx127xModemStatus,
35    PacketStatus as Sx127xPacketStatus, Port,
36};
37
38use pamoja_lora::budget::Decibels;
39
40use crate::lora::{link_budget, settings, PamojaLoraLink, PamojaLoraLinkBudget};
41use crate::{set_last_error, PamojaStatus};
42
43/// The most bytes one SX126x command takes, opcode included.
44pub const PAMOJA_SX126X_COMMAND_MAX: usize = 10;
45
46/// The receive timeout word that keeps an SX126x listening until another command stops it.
47pub const PAMOJA_SX126X_RX_CONTINUOUS: u32 = 0xFF_FFFF;
48
49/// The LoRa sync word of a public network such as LoRaWAN.
50pub const PAMOJA_SX126X_SYNC_WORD_PUBLIC: u16 = 0x3444;
51
52/// The LoRa sync word of a private network, and the chip's reset value.
53pub const PAMOJA_SX126X_SYNC_WORD_PRIVATE: u16 = 0x1424;
54
55/// The register that holds the most significant byte of the LoRa sync word.
56pub const PAMOJA_SX126X_REGISTER_LORA_SYNC_WORD: u16 = 0x0740;
57
58/// The IRQ bit raised when a packet has been sent.
59pub const PAMOJA_SX126X_IRQ_TX_DONE: u16 = 1 << 0;
60/// The IRQ bit raised when a packet has been received.
61pub const PAMOJA_SX126X_IRQ_RX_DONE: u16 = 1 << 1;
62/// The IRQ bit raised when a preamble has been detected.
63pub const PAMOJA_SX126X_IRQ_PREAMBLE_DETECTED: u16 = 1 << 2;
64/// The IRQ bit raised when a valid (G)FSK sync word has been detected.
65pub const PAMOJA_SX126X_IRQ_SYNC_WORD_VALID: u16 = 1 << 3;
66/// The IRQ bit raised when a valid LoRa header has been received.
67pub const PAMOJA_SX126X_IRQ_HEADER_VALID: u16 = 1 << 4;
68/// The IRQ bit raised when a LoRa header failed its CRC.
69pub const PAMOJA_SX126X_IRQ_HEADER_ERROR: u16 = 1 << 5;
70/// The IRQ bit raised when a packet failed its CRC.
71pub const PAMOJA_SX126X_IRQ_CRC_ERROR: u16 = 1 << 6;
72/// The IRQ bit raised when channel activity detection has finished.
73pub const PAMOJA_SX126X_IRQ_CAD_DONE: u16 = 1 << 7;
74/// The IRQ bit raised when channel activity detection heard LoRa.
75pub const PAMOJA_SX126X_IRQ_CAD_DETECTED: u16 = 1 << 8;
76/// The IRQ bit raised when a transmission or reception timed out.
77pub const PAMOJA_SX126X_IRQ_TIMEOUT: u16 = 1 << 9;
78/// The IRQ bit raised at each long-range FHSS hop.
79pub const PAMOJA_SX126X_IRQ_LR_FHSS_HOP: u16 = 1 << 14;
80/// Every IRQ bit the chip defines.
81pub const PAMOJA_SX126X_IRQ_ALL: u16 = 0x43FF;
82
83/// The device error bit for a failed RC64k calibration.
84pub const PAMOJA_SX126X_ERROR_RC64K_CALIBRATION: u16 = 1 << 0;
85/// The device error bit for a failed RC13M calibration.
86pub const PAMOJA_SX126X_ERROR_RC13M_CALIBRATION: u16 = 1 << 1;
87/// The device error bit for a failed PLL calibration.
88pub const PAMOJA_SX126X_ERROR_PLL_CALIBRATION: u16 = 1 << 2;
89/// The device error bit for a failed ADC calibration.
90pub const PAMOJA_SX126X_ERROR_ADC_CALIBRATION: u16 = 1 << 3;
91/// The device error bit for a failed image calibration.
92pub const PAMOJA_SX126X_ERROR_IMAGE_CALIBRATION: u16 = 1 << 4;
93/// The device error bit for a crystal oscillator that failed to start.
94pub const PAMOJA_SX126X_ERROR_XOSC_START: u16 = 1 << 5;
95/// The device error bit for a PLL that failed to lock.
96pub const PAMOJA_SX126X_ERROR_PLL_LOCK: u16 = 1 << 6;
97/// The device error bit for a power amplifier that failed to ramp.
98pub const PAMOJA_SX126X_ERROR_PA_RAMP: u16 = 1 << 8;
99
100// The header generator does not read the crates this one depends on, so these carry their
101// value rather than the name of the constant that defines it.
102const _: () = assert!(PAMOJA_SX126X_COMMAND_MAX == command::MAX_LEN);
103const _: () = assert!(PAMOJA_SX126X_RX_CONTINUOUS == config::RX_CONTINUOUS);
104const _: () =
105    assert!(PAMOJA_SX126X_SYNC_WORD_PUBLIC == u16::from_be_bytes(SyncWord::Public.to_bytes()));
106const _: () =
107    assert!(PAMOJA_SX126X_SYNC_WORD_PRIVATE == u16::from_be_bytes(SyncWord::Private.to_bytes()));
108const _: () = assert!(PAMOJA_SX126X_REGISTER_LORA_SYNC_WORD == config::register::LORA_SYNC_WORD);
109const _: () = assert!(PAMOJA_SX126X_IRQ_TX_DONE == Irq::TX_DONE.bits());
110const _: () = assert!(PAMOJA_SX126X_IRQ_RX_DONE == Irq::RX_DONE.bits());
111const _: () = assert!(PAMOJA_SX126X_IRQ_PREAMBLE_DETECTED == Irq::PREAMBLE_DETECTED.bits());
112const _: () = assert!(PAMOJA_SX126X_IRQ_SYNC_WORD_VALID == Irq::SYNC_WORD_VALID.bits());
113const _: () = assert!(PAMOJA_SX126X_IRQ_HEADER_VALID == Irq::HEADER_VALID.bits());
114const _: () = assert!(PAMOJA_SX126X_IRQ_HEADER_ERROR == Irq::HEADER_ERROR.bits());
115const _: () = assert!(PAMOJA_SX126X_IRQ_CRC_ERROR == Irq::CRC_ERROR.bits());
116const _: () = assert!(PAMOJA_SX126X_IRQ_CAD_DONE == Irq::CAD_DONE.bits());
117const _: () = assert!(PAMOJA_SX126X_IRQ_CAD_DETECTED == Irq::CAD_DETECTED.bits());
118const _: () = assert!(PAMOJA_SX126X_IRQ_TIMEOUT == Irq::TIMEOUT.bits());
119const _: () = assert!(PAMOJA_SX126X_IRQ_LR_FHSS_HOP == Irq::LR_FHSS_HOP.bits());
120const _: () = assert!(PAMOJA_SX126X_IRQ_ALL == Irq::ALL.bits());
121const _: () =
122    assert!(PAMOJA_SX126X_ERROR_RC64K_CALIBRATION == DeviceErrors::RC64K_CALIBRATION.bits());
123const _: () =
124    assert!(PAMOJA_SX126X_ERROR_RC13M_CALIBRATION == DeviceErrors::RC13M_CALIBRATION.bits());
125const _: () = assert!(PAMOJA_SX126X_ERROR_PLL_CALIBRATION == DeviceErrors::PLL_CALIBRATION.bits());
126const _: () = assert!(PAMOJA_SX126X_ERROR_ADC_CALIBRATION == DeviceErrors::ADC_CALIBRATION.bits());
127const _: () =
128    assert!(PAMOJA_SX126X_ERROR_IMAGE_CALIBRATION == DeviceErrors::IMAGE_CALIBRATION.bits());
129const _: () = assert!(PAMOJA_SX126X_ERROR_XOSC_START == DeviceErrors::XOSC_START.bits());
130const _: () = assert!(PAMOJA_SX126X_ERROR_PLL_LOCK == DeviceErrors::PLL_LOCK.bits());
131const _: () = assert!(PAMOJA_SX126X_ERROR_PA_RAMP == DeviceErrors::PA_RAMP.bits());
132
133/// The RegVersion value of an SX1276, SX1277, SX1278, or SX1279.
134pub const PAMOJA_SX127X_VERSION: u8 = 0x12;
135
136/// The bit of an SX127x address byte that makes an access a write.
137pub const PAMOJA_SX127X_WRITE: u8 = 0x80;
138
139/// RegDioMapping1 with DIO0 signaling RxDone.
140pub const PAMOJA_SX127X_DIO0_RX_DONE: u8 = 0x00;
141
142/// RegDioMapping1 with DIO0 signaling TxDone.
143pub const PAMOJA_SX127X_DIO0_TX_DONE: u8 = 0x40;
144
145/// RegDioMapping1 with DIO0 signaling CadDone.
146pub const PAMOJA_SX127X_DIO0_CAD_DONE: u8 = 0x80;
147
148/// RegPaDac at its reset value.
149pub const PAMOJA_SX127X_PA_DAC_DEFAULT: u8 = 0x84;
150
151/// RegPaDac with the +20 dBm setting on PA_BOOST.
152pub const PAMOJA_SX127X_PA_DAC_HIGH_POWER: u8 = 0x87;
153
154/// The RegImageCal bit that starts a calibration.
155pub const PAMOJA_SX127X_IMAGE_CAL_START: u8 = 0x40;
156
157/// The RegImageCal bit set while a calibration runs.
158pub const PAMOJA_SX127X_IMAGE_CAL_RUNNING: u8 = 0x20;
159
160/// The sync word the datasheet reserves for LoRaWAN.
161pub const PAMOJA_SX127X_SYNC_WORD_PUBLIC: u8 = 0x34;
162
163/// The private sync word, and the chip's reset value.
164pub const PAMOJA_SX127X_SYNC_WORD_PRIVATE: u8 = 0x12;
165
166/// RegLna with maximum gain and the high frequency LNA boost.
167pub const PAMOJA_SX127X_LNA_BOOSTED: u8 = 0x23;
168
169/// RegTcxo for a module clocked by a TCXO.
170pub const PAMOJA_SX127X_TCXO_INPUT_ON: u8 = 0x19;
171
172/// The SX127x register RegFifo, the LoRa data buffer read or written at RegFifoAddrPtr.
173pub const PAMOJA_SX127X_REG_FIFO: u8 = 0x00;
174
175/// The SX127x register RegOpMode: LoRa or FSK, the register page, and the operating mode.
176pub const PAMOJA_SX127X_REG_OP_MODE: u8 = 0x01;
177
178/// The SX127x register RegFrfMsb, the top byte of the carrier word.
179pub const PAMOJA_SX127X_REG_FRF_MSB: u8 = 0x06;
180
181/// The SX127x register RegFrfMid, the middle byte of the carrier word.
182pub const PAMOJA_SX127X_REG_FRF_MID: u8 = 0x07;
183
184/// The SX127x register RegFrfLsb, the low byte of the carrier word.
185pub const PAMOJA_SX127X_REG_FRF_LSB: u8 = 0x08;
186
187/// The SX127x register RegPaConfig: the amplifier output, its maximum, and the power.
188pub const PAMOJA_SX127X_REG_PA_CONFIG: u8 = 0x09;
189
190/// The SX127x register RegPaRamp, the amplifier ramp time.
191pub const PAMOJA_SX127X_REG_PA_RAMP: u8 = 0x0A;
192
193/// The SX127x register RegOcp, the amplifier current limit.
194pub const PAMOJA_SX127X_REG_OCP: u8 = 0x0B;
195
196/// The SX127x register RegLna, the LNA gain and current.
197pub const PAMOJA_SX127X_REG_LNA: u8 = 0x0C;
198
199/// The SX127x register RegFifoAddrPtr, where the next RegFifo access lands.
200pub const PAMOJA_SX127X_REG_FIFO_ADDR_PTR: u8 = 0x0D;
201
202/// The SX127x register RegFifoTxBaseAddr, where a transmitted payload starts.
203pub const PAMOJA_SX127X_REG_FIFO_TX_BASE_ADDR: u8 = 0x0E;
204
205/// The SX127x register RegFifoRxBaseAddr, where received payloads start.
206pub const PAMOJA_SX127X_REG_FIFO_RX_BASE_ADDR: u8 = 0x0F;
207
208/// The SX127x register RegFifoRxCurrentAddr, where the last packet starts.
209pub const PAMOJA_SX127X_REG_FIFO_RX_CURRENT_ADDR: u8 = 0x10;
210
211/// The SX127x register RegIrqFlagsMask, the interrupts masked off.
212pub const PAMOJA_SX127X_REG_IRQ_FLAGS_MASK: u8 = 0x11;
213
214/// The SX127x register RegIrqFlags, the interrupts raised.
215pub const PAMOJA_SX127X_REG_IRQ_FLAGS: u8 = 0x12;
216
217/// The SX127x register RegRxNbBytes, the payload length of the last packet.
218pub const PAMOJA_SX127X_REG_RX_NB_BYTES: u8 = 0x13;
219
220/// The SX127x register RegModemStat, the live state of the modem.
221pub const PAMOJA_SX127X_REG_MODEM_STAT: u8 = 0x18;
222
223/// The SX127x register RegPktSnrValue, the SNR of the last packet.
224pub const PAMOJA_SX127X_REG_PKT_SNR_VALUE: u8 = 0x19;
225
226/// The SX127x register RegPktRssiValue, the RSSI of the last packet.
227pub const PAMOJA_SX127X_REG_PKT_RSSI_VALUE: u8 = 0x1A;
228
229/// The SX127x register RegRssiValue, the RSSI heard right now.
230pub const PAMOJA_SX127X_REG_RSSI_VALUE: u8 = 0x1B;
231
232/// The SX127x register RegHopChannel, the PLL lock and the CRC the header announced.
233pub const PAMOJA_SX127X_REG_HOP_CHANNEL: u8 = 0x1C;
234
235/// The SX127x register RegModemConfig1: bandwidth, coding rate, and header mode.
236pub const PAMOJA_SX127X_REG_MODEM_CONFIG_1: u8 = 0x1D;
237
238/// The SX127x register RegModemConfig2: spreading factor, CRC, and the timeout's top bits.
239pub const PAMOJA_SX127X_REG_MODEM_CONFIG_2: u8 = 0x1E;
240
241/// The SX127x register RegSymbTimeoutLsb, the low byte of the symbol timeout.
242pub const PAMOJA_SX127X_REG_SYMB_TIMEOUT_LSB: u8 = 0x1F;
243
244/// The SX127x register RegPreambleMsb, the high byte of the preamble length.
245pub const PAMOJA_SX127X_REG_PREAMBLE_MSB: u8 = 0x20;
246
247/// The SX127x register RegPreambleLsb, the low byte of the preamble length.
248pub const PAMOJA_SX127X_REG_PREAMBLE_LSB: u8 = 0x21;
249
250/// The SX127x register RegPayloadLength, the payload length to send.
251pub const PAMOJA_SX127X_REG_PAYLOAD_LENGTH: u8 = 0x22;
252
253/// The SX127x register RegMaxPayloadLength, the longest payload accepted.
254pub const PAMOJA_SX127X_REG_MAX_PAYLOAD_LENGTH: u8 = 0x23;
255
256/// The SX127x register RegModemConfig3: low data rate optimization and the AGC.
257pub const PAMOJA_SX127X_REG_MODEM_CONFIG_3: u8 = 0x26;
258
259/// The SX127x register RegRssiWideband, a wideband RSSI sample.
260pub const PAMOJA_SX127X_REG_RSSI_WIDEBAND: u8 = 0x2C;
261
262/// The SX127x register RegIfFreq2, which the spurious reception erratum sets.
263pub const PAMOJA_SX127X_REG_IF_FREQ_2: u8 = 0x2F;
264
265/// The SX127x register RegIfFreq1, which the spurious reception erratum clears.
266pub const PAMOJA_SX127X_REG_IF_FREQ_1: u8 = 0x30;
267
268/// The SX127x register RegDetectOptimize: the automatic IF and detection optimization.
269pub const PAMOJA_SX127X_REG_DETECT_OPTIMIZE: u8 = 0x31;
270
271/// The SX127x register RegInvertIQ, the IQ polarity of each path.
272pub const PAMOJA_SX127X_REG_INVERT_IQ: u8 = 0x33;
273
274/// The SX127x register RegHighBwOptimize1, which the 500 kHz erratum sets.
275pub const PAMOJA_SX127X_REG_HIGH_BW_OPTIMIZE_1: u8 = 0x36;
276
277/// The SX127x register RegDetectionThreshold, the LoRa detection threshold.
278pub const PAMOJA_SX127X_REG_DETECTION_THRESHOLD: u8 = 0x37;
279
280/// The SX127x register RegSyncWord, the LoRa sync word.
281pub const PAMOJA_SX127X_REG_SYNC_WORD: u8 = 0x39;
282
283/// The SX127x register RegHighBwOptimize2, which the 500 kHz erratum sets.
284pub const PAMOJA_SX127X_REG_HIGH_BW_OPTIMIZE_2: u8 = 0x3A;
285
286/// The SX127x register RegInvertIQ2, which completes an IQ inversion.
287pub const PAMOJA_SX127X_REG_INVERT_IQ_2: u8 = 0x3B;
288
289/// The SX127x register RegImageCal, at the address of RegInvertIQ2 on the FSK page.
290pub const PAMOJA_SX127X_REG_IMAGE_CAL: u8 = 0x3B;
291
292/// The SX127x register RegDioMapping1, the events DIO0 to DIO3 signal.
293pub const PAMOJA_SX127X_REG_DIO_MAPPING_1: u8 = 0x40;
294
295/// The SX127x register RegDioMapping2, the events DIO4 and DIO5 signal.
296pub const PAMOJA_SX127X_REG_DIO_MAPPING_2: u8 = 0x41;
297
298/// The SX127x register RegVersion, the silicon revision.
299pub const PAMOJA_SX127X_REG_VERSION: u8 = 0x42;
300
301/// The SX127x register RegTcxo, a crystal or a TCXO on XTA.
302pub const PAMOJA_SX127X_REG_TCXO: u8 = 0x4B;
303
304/// The SX127x register RegPaDac, the +20 dBm setting of PA_BOOST.
305pub const PAMOJA_SX127X_REG_PA_DAC: u8 = 0x4D;
306
307/// The SX127x operating mode code for sleep, the only mode that may switch between LoRa and FSK.
308pub const PAMOJA_SX127X_MODE_SLEEP: u8 = 0;
309
310/// The SX127x operating mode code for standby.
311pub const PAMOJA_SX127X_MODE_STANDBY: u8 = 1;
312
313/// The SX127x operating mode code for frequency synthesis for transmit.
314pub const PAMOJA_SX127X_MODE_FS_TX: u8 = 2;
315
316/// The SX127x operating mode code for transmit one packet.
317pub const PAMOJA_SX127X_MODE_TX: u8 = 3;
318
319/// The SX127x operating mode code for frequency synthesis for receive.
320pub const PAMOJA_SX127X_MODE_FS_RX: u8 = 4;
321
322/// The SX127x operating mode code for receive packet after packet.
323pub const PAMOJA_SX127X_MODE_RX_CONTINUOUS: u8 = 5;
324
325/// The SX127x operating mode code for receive one packet or time out.
326pub const PAMOJA_SX127X_MODE_RX_SINGLE: u8 = 6;
327
328/// The SX127x operating mode code for channel activity detection.
329pub const PAMOJA_SX127X_MODE_CAD: u8 = 7;
330
331/// The SX127x interrupt flag: channel activity detection heard a LoRa signal.
332pub const PAMOJA_SX127X_IRQ_CAD_DETECTED: u8 = 0x01;
333
334/// The SX127x interrupt flag: frequency hopping moved to the next channel.
335pub const PAMOJA_SX127X_IRQ_FHSS_CHANGE_CHANNEL: u8 = 0x02;
336
337/// The SX127x interrupt flag: channel activity detection finished.
338pub const PAMOJA_SX127X_IRQ_CAD_DONE: u8 = 0x04;
339
340/// The SX127x interrupt flag: the payload has been transmitted.
341pub const PAMOJA_SX127X_IRQ_TX_DONE: u8 = 0x08;
342
343/// The SX127x interrupt flag: a valid header was received.
344pub const PAMOJA_SX127X_IRQ_VALID_HEADER: u8 = 0x10;
345
346/// The SX127x interrupt flag: the payload failed its CRC.
347pub const PAMOJA_SX127X_IRQ_PAYLOAD_CRC_ERROR: u8 = 0x20;
348
349/// The SX127x interrupt flag: a packet has been received.
350pub const PAMOJA_SX127X_IRQ_RX_DONE: u8 = 0x40;
351
352/// The SX127x interrupt flag: a single reception timed out.
353pub const PAMOJA_SX127X_IRQ_RX_TIMEOUT: u8 = 0x80;
354
355/// The SX127x interrupt flag: every interrupt, which writing back clears.
356pub const PAMOJA_SX127X_IRQ_ALL: u8 = 0xFF;
357
358// The header generator does not read the crates this one depends on, so these carry their
359// value rather than the name of the constant that defines it.
360const _: () = assert!(PAMOJA_SX127X_VERSION == sx127x_register::VERSION_SX1276);
361const _: () = assert!(PAMOJA_SX127X_WRITE == sx127x_register::WRITE);
362const _: () = assert!(PAMOJA_SX127X_DIO0_RX_DONE == sx127x_config::DIO0_RX_DONE);
363const _: () = assert!(PAMOJA_SX127X_DIO0_TX_DONE == sx127x_config::DIO0_TX_DONE);
364const _: () = assert!(PAMOJA_SX127X_DIO0_CAD_DONE == sx127x_config::DIO0_CAD_DONE);
365const _: () = assert!(PAMOJA_SX127X_PA_DAC_DEFAULT == sx127x_config::PA_DAC_DEFAULT);
366const _: () = assert!(PAMOJA_SX127X_PA_DAC_HIGH_POWER == sx127x_config::PA_DAC_HIGH_POWER);
367const _: () = assert!(PAMOJA_SX127X_IMAGE_CAL_START == sx127x_config::IMAGE_CAL_START);
368const _: () = assert!(PAMOJA_SX127X_IMAGE_CAL_RUNNING == sx127x_config::IMAGE_CAL_RUNNING);
369const _: () = assert!(PAMOJA_SX127X_SYNC_WORD_PUBLIC == sx127x_config::SyncWord::Public.to_byte());
370const _: () =
371    assert!(PAMOJA_SX127X_SYNC_WORD_PRIVATE == sx127x_config::SyncWord::Private.to_byte());
372const _: () = assert!(PAMOJA_SX127X_LNA_BOOSTED == sx127x_config::LNA_BOOSTED);
373const _: () = assert!(PAMOJA_SX127X_TCXO_INPUT_ON == sx127x_config::TCXO_INPUT_ON);
374const _: () = assert!(PAMOJA_SX127X_REG_FIFO == sx127x_register::FIFO);
375const _: () = assert!(PAMOJA_SX127X_REG_OP_MODE == sx127x_register::OP_MODE);
376const _: () = assert!(PAMOJA_SX127X_REG_FRF_MSB == sx127x_register::FRF_MSB);
377const _: () = assert!(PAMOJA_SX127X_REG_FRF_MID == sx127x_register::FRF_MID);
378const _: () = assert!(PAMOJA_SX127X_REG_FRF_LSB == sx127x_register::FRF_LSB);
379const _: () = assert!(PAMOJA_SX127X_REG_PA_CONFIG == sx127x_register::PA_CONFIG);
380const _: () = assert!(PAMOJA_SX127X_REG_PA_RAMP == sx127x_register::PA_RAMP);
381const _: () = assert!(PAMOJA_SX127X_REG_OCP == sx127x_register::OCP);
382const _: () = assert!(PAMOJA_SX127X_REG_LNA == sx127x_register::LNA);
383const _: () = assert!(PAMOJA_SX127X_REG_FIFO_ADDR_PTR == sx127x_register::FIFO_ADDR_PTR);
384const _: () = assert!(PAMOJA_SX127X_REG_FIFO_TX_BASE_ADDR == sx127x_register::FIFO_TX_BASE_ADDR);
385const _: () = assert!(PAMOJA_SX127X_REG_FIFO_RX_BASE_ADDR == sx127x_register::FIFO_RX_BASE_ADDR);
386const _: () =
387    assert!(PAMOJA_SX127X_REG_FIFO_RX_CURRENT_ADDR == sx127x_register::FIFO_RX_CURRENT_ADDR);
388const _: () = assert!(PAMOJA_SX127X_REG_IRQ_FLAGS_MASK == sx127x_register::IRQ_FLAGS_MASK);
389const _: () = assert!(PAMOJA_SX127X_REG_IRQ_FLAGS == sx127x_register::IRQ_FLAGS);
390const _: () = assert!(PAMOJA_SX127X_REG_RX_NB_BYTES == sx127x_register::RX_NB_BYTES);
391const _: () = assert!(PAMOJA_SX127X_REG_MODEM_STAT == sx127x_register::MODEM_STAT);
392const _: () = assert!(PAMOJA_SX127X_REG_PKT_SNR_VALUE == sx127x_register::PKT_SNR_VALUE);
393const _: () = assert!(PAMOJA_SX127X_REG_PKT_RSSI_VALUE == sx127x_register::PKT_RSSI_VALUE);
394const _: () = assert!(PAMOJA_SX127X_REG_RSSI_VALUE == sx127x_register::RSSI_VALUE);
395const _: () = assert!(PAMOJA_SX127X_REG_HOP_CHANNEL == sx127x_register::HOP_CHANNEL);
396const _: () = assert!(PAMOJA_SX127X_REG_MODEM_CONFIG_1 == sx127x_register::MODEM_CONFIG_1);
397const _: () = assert!(PAMOJA_SX127X_REG_MODEM_CONFIG_2 == sx127x_register::MODEM_CONFIG_2);
398const _: () = assert!(PAMOJA_SX127X_REG_SYMB_TIMEOUT_LSB == sx127x_register::SYMB_TIMEOUT_LSB);
399const _: () = assert!(PAMOJA_SX127X_REG_PREAMBLE_MSB == sx127x_register::PREAMBLE_MSB);
400const _: () = assert!(PAMOJA_SX127X_REG_PREAMBLE_LSB == sx127x_register::PREAMBLE_LSB);
401const _: () = assert!(PAMOJA_SX127X_REG_PAYLOAD_LENGTH == sx127x_register::PAYLOAD_LENGTH);
402const _: () = assert!(PAMOJA_SX127X_REG_MAX_PAYLOAD_LENGTH == sx127x_register::MAX_PAYLOAD_LENGTH);
403const _: () = assert!(PAMOJA_SX127X_REG_MODEM_CONFIG_3 == sx127x_register::MODEM_CONFIG_3);
404const _: () = assert!(PAMOJA_SX127X_REG_RSSI_WIDEBAND == sx127x_register::RSSI_WIDEBAND);
405const _: () = assert!(PAMOJA_SX127X_REG_IF_FREQ_2 == sx127x_register::IF_FREQ_2);
406const _: () = assert!(PAMOJA_SX127X_REG_IF_FREQ_1 == sx127x_register::IF_FREQ_1);
407const _: () = assert!(PAMOJA_SX127X_REG_DETECT_OPTIMIZE == sx127x_register::DETECT_OPTIMIZE);
408const _: () = assert!(PAMOJA_SX127X_REG_INVERT_IQ == sx127x_register::INVERT_IQ);
409const _: () = assert!(PAMOJA_SX127X_REG_HIGH_BW_OPTIMIZE_1 == sx127x_register::HIGH_BW_OPTIMIZE_1);
410const _: () =
411    assert!(PAMOJA_SX127X_REG_DETECTION_THRESHOLD == sx127x_register::DETECTION_THRESHOLD);
412const _: () = assert!(PAMOJA_SX127X_REG_SYNC_WORD == sx127x_register::SYNC_WORD);
413const _: () = assert!(PAMOJA_SX127X_REG_HIGH_BW_OPTIMIZE_2 == sx127x_register::HIGH_BW_OPTIMIZE_2);
414const _: () = assert!(PAMOJA_SX127X_REG_INVERT_IQ_2 == sx127x_register::INVERT_IQ_2);
415const _: () = assert!(PAMOJA_SX127X_REG_IMAGE_CAL == sx127x_register::IMAGE_CAL);
416const _: () = assert!(PAMOJA_SX127X_REG_DIO_MAPPING_1 == sx127x_register::DIO_MAPPING_1);
417const _: () = assert!(PAMOJA_SX127X_REG_DIO_MAPPING_2 == sx127x_register::DIO_MAPPING_2);
418const _: () = assert!(PAMOJA_SX127X_REG_VERSION == sx127x_register::VERSION);
419const _: () = assert!(PAMOJA_SX127X_REG_TCXO == sx127x_register::TCXO);
420const _: () = assert!(PAMOJA_SX127X_REG_PA_DAC == sx127x_register::PA_DAC);
421const _: () = assert!(PAMOJA_SX127X_MODE_SLEEP == Sx127xMode::Sleep.code());
422const _: () = assert!(PAMOJA_SX127X_MODE_STANDBY == Sx127xMode::Standby.code());
423const _: () = assert!(PAMOJA_SX127X_MODE_FS_TX == Sx127xMode::FsTx.code());
424const _: () = assert!(PAMOJA_SX127X_MODE_TX == Sx127xMode::Tx.code());
425const _: () = assert!(PAMOJA_SX127X_MODE_FS_RX == Sx127xMode::FsRx.code());
426const _: () = assert!(PAMOJA_SX127X_MODE_RX_CONTINUOUS == Sx127xMode::RxContinuous.code());
427const _: () = assert!(PAMOJA_SX127X_MODE_RX_SINGLE == Sx127xMode::RxSingle.code());
428const _: () = assert!(PAMOJA_SX127X_MODE_CAD == Sx127xMode::Cad.code());
429const _: () = assert!(PAMOJA_SX127X_IRQ_CAD_DETECTED == IrqFlags::CAD_DETECTED.bits());
430const _: () =
431    assert!(PAMOJA_SX127X_IRQ_FHSS_CHANGE_CHANNEL == IrqFlags::FHSS_CHANGE_CHANNEL.bits());
432const _: () = assert!(PAMOJA_SX127X_IRQ_CAD_DONE == IrqFlags::CAD_DONE.bits());
433const _: () = assert!(PAMOJA_SX127X_IRQ_TX_DONE == IrqFlags::TX_DONE.bits());
434const _: () = assert!(PAMOJA_SX127X_IRQ_VALID_HEADER == IrqFlags::VALID_HEADER.bits());
435const _: () = assert!(PAMOJA_SX127X_IRQ_PAYLOAD_CRC_ERROR == IrqFlags::PAYLOAD_CRC_ERROR.bits());
436const _: () = assert!(PAMOJA_SX127X_IRQ_RX_DONE == IrqFlags::RX_DONE.bits());
437const _: () = assert!(PAMOJA_SX127X_IRQ_RX_TIMEOUT == IrqFlags::RX_TIMEOUT.bits());
438const _: () = assert!(PAMOJA_SX127X_IRQ_ALL == IrqFlags::ALL.bits());
439
440/// The bytes of one SX126x command, in the order the chip receives them.
441#[repr(C)]
442#[derive(Clone, Copy, Debug, PartialEq, Eq)]
443pub struct PamojaSx126xCommand {
444    /// The opcode and its parameters; only the first `len` bytes are the command.
445    pub bytes: [u8; PAMOJA_SX126X_COMMAND_MAX],
446    /// How many bytes the command takes.
447    pub len: u8,
448}
449
450/// A command the chip answers in the same SPI transaction.
451#[repr(C)]
452#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453pub struct PamojaSx126xQuery {
454    /// The bytes to send, ending with the NOP during which the status byte comes back.
455    pub command: PamojaSx126xCommand,
456    /// How many bytes of answer to read after them.
457    pub answer_len: u32,
458}
459
460/// The amplifier configuration and power setting that produce an output power, from
461/// Table 13-21 of the SX1261/2 datasheet.
462#[repr(C)]
463#[derive(Clone, Copy, Debug, PartialEq, Eq)]
464pub struct PamojaSx126xTxPower {
465    /// paDutyCycle, the conduction angle of the amplifier.
466    pub pa_duty_cycle: u8,
467    /// hpMax, the size of the SX1262 amplifier; no effect on the SX1261.
468    pub hp_max: u8,
469    /// deviceSel: `0` for the SX1262 and the LLCC68, `1` for the SX1261.
470    pub device_sel: u8,
471    /// paLut, reserved and always `1`.
472    pub pa_lut: u8,
473    /// The power byte of SetTxParams, in dBm.
474    pub setting_dbm: i8,
475}
476
477/// The mode an SX126x reports in its status byte.
478#[repr(u8)]
479#[derive(Clone, Copy, Debug, PartialEq, Eq)]
480pub enum PamojaSx126xChipMode {
481    /// A value the datasheet leaves unused.
482    Other = 0,
483    /// Standby on the 13 MHz RC oscillator.
484    StandbyRc = 2,
485    /// Standby on the 32 MHz crystal.
486    StandbyXosc = 3,
487    /// Frequency synthesis.
488    Fs = 4,
489    /// Receiving.
490    Rx = 5,
491    /// Transmitting.
492    Tx = 6,
493}
494
495/// How the last command went, as an SX126x reports it in its status byte.
496#[repr(u8)]
497#[derive(Clone, Copy, Debug, PartialEq, Eq)]
498pub enum PamojaSx126xCommandStatus {
499    /// A value the datasheet leaves unused, which includes a command that went well.
500    Other = 0,
501    /// A packet has been received and waits in the data buffer.
502    DataAvailable = 2,
503    /// A command timed out.
504    Timeout = 3,
505    /// A command could not be processed.
506    ProcessingError = 4,
507    /// A command failed to execute.
508    ExecutionFailure = 5,
509    /// A transmission has finished.
510    TxDone = 6,
511}
512
513/// A decoded SX126x status byte.
514#[repr(C)]
515#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516pub struct PamojaSx126xStatus {
517    /// The mode the chip is in.
518    pub chip_mode: PamojaSx126xChipMode,
519    /// How the last command went.
520    pub command_status: PamojaSx126xCommandStatus,
521    /// `true` for a timeout, a processing error, or an execution failure.
522    pub error: bool,
523}
524
525/// The signal levels of the last LoRa packet received, in hundredths of a decibel.
526#[repr(C)]
527#[derive(Clone, Copy, Debug, PartialEq, Eq)]
528pub struct PamojaSx126xPacketStatus {
529    /// The RSSI averaged over the packet, in hundredths of a dBm.
530    pub rssi_centi_dbm: i32,
531    /// The estimated signal-to-noise ratio, in hundredths of a dB.
532    pub snr_centi_db: i32,
533    /// The estimated RSSI of the LoRa signal after despreading, in hundredths of a dBm.
534    pub signal_rssi_centi_dbm: i32,
535}
536
537/// Where a received payload sits in the data buffer.
538#[repr(C)]
539#[derive(Clone, Copy, Debug, PartialEq, Eq)]
540pub struct PamojaSx126xRxBufferStatus {
541    /// The length of the payload in bytes.
542    pub payload_len: u8,
543    /// The buffer offset of its first byte.
544    pub start: u8,
545}
546
547/// An opaque handle to a duty-cycle guard.
548///
549/// Record each transmission with [`pamoja_radio_duty_cycle_transmitted`], ask
550/// [`pamoja_radio_duty_cycle_ready`] before the next, and release it with
551/// [`pamoja_radio_duty_cycle_free`].
552pub struct PamojaRadioDutyCycle {
553    guard: DutyCycle,
554}
555
556/// Returns the word SetRfFrequency takes for a frequency.
557///
558/// # Arguments
559///
560/// * `frequency_hz` - the carrier frequency in hertz.
561///
562/// # Returns
563///
564/// The frequency times 2^25 over the 32 MHz crystal, rounded to the nearest step.
565#[no_mangle]
566pub extern "C" fn pamoja_sx126x_frequency_word(frequency_hz: u32) -> u32 {
567    config::frequency_word(frequency_hz)
568}
569
570/// Returns the 24-bit timeout word SetTx and SetRx take for a duration.
571///
572/// # Arguments
573///
574/// * `timeout_us` - the duration in microseconds.
575///
576/// # Returns
577///
578/// The number of 15.625 us steps; a nonzero duration never becomes the zero word that
579/// disables the timeout, and nothing reaches [`PAMOJA_SX126X_RX_CONTINUOUS`].
580#[no_mangle]
581pub extern "C" fn pamoja_sx126x_timeout_steps(timeout_us: u64) -> u32 {
582    config::timeout_steps(timeout_us)
583}
584
585/// Returns the two CalibrateImage codes that cover a band.
586///
587/// # Arguments
588///
589/// * `low_hz` - the lower edge of the band in hertz.
590/// * `high_hz` - the upper edge of the band in hertz.
591///
592/// # Returns
593///
594/// `freq1` in the high byte and `freq2` in the low byte.
595#[no_mangle]
596pub extern "C" fn pamoja_sx126x_image_calibration(low_hz: u32, high_hz: u32) -> u16 {
597    u16::from_be_bytes(config::image_calibration(low_hz, high_hz))
598}
599
600/// Returns the shortest amplifier ramp time the chip offers that lasts at least a duration.
601///
602/// # Arguments
603///
604/// * `at_least_us` - the least ramp time wanted, in microseconds.
605///
606/// # Returns
607///
608/// The ramp time in microseconds, one of the eight Table 13-41 gives.
609#[no_mangle]
610pub extern "C" fn pamoja_sx126x_ramp_time_us(at_least_us: u32) -> u32 {
611    RampTime::at_least(at_least_us).micros()
612}
613
614/// Chooses the amplifier settings for an output power.
615///
616/// # Arguments
617///
618/// * `high_power` - `true` for the high power amplifier of the SX1262 and the LLCC68,
619///   `false` for the low power amplifier of the SX1261.
620/// * `output_dbm` - the output power wanted at the antenna port.
621///
622/// # Returns
623///
624/// The configuration and the power setting, clamped to what the amplifier allows.
625#[no_mangle]
626pub extern "C" fn pamoja_sx126x_tx_power_for_output(
627    high_power: bool,
628    output_dbm: i8,
629) -> PamojaSx126xTxPower {
630    power_of(TxPower::for_output(amplifier(high_power), output_dbm))
631}
632
633/// Chooses the amplifier settings that keep a link's EIRP at or under a ceiling.
634///
635/// # Arguments
636///
637/// * `high_power` - `true` for the high power amplifier, `false` for the low power one.
638/// * `budget` - the link budget, whose transmitting antenna and cable apply.
639/// * `eirp_ceiling_centi_dbm` - the EIRP limit, in hundredths of a dBm.
640///
641/// # Returns
642///
643/// The configuration and the power setting, rounded down to whole decibels.
644#[no_mangle]
645pub extern "C" fn pamoja_sx126x_tx_power_under_ceiling(
646    high_power: bool,
647    budget: PamojaLoraLinkBudget,
648    eirp_ceiling_centi_dbm: i32,
649) -> PamojaSx126xTxPower {
650    power_of(TxPower::under_ceiling(
651        amplifier(high_power),
652        &link_budget(budget),
653        Decibels::from_hundredths(eirp_ceiling_centi_dbm),
654    ))
655}
656
657/// SetStandby into STDBY_RC.
658///
659/// # Returns
660///
661/// The command.
662#[no_mangle]
663pub extern "C" fn pamoja_sx126x_set_standby() -> PamojaSx126xCommand {
664    command_of(command::set_standby(StandbyMode::Rc))
665}
666
667/// SetPacketType for LoRa.
668///
669/// # Returns
670///
671/// The command.
672#[no_mangle]
673pub extern "C" fn pamoja_sx126x_set_packet_type_lora() -> PamojaSx126xCommand {
674    command_of(command::set_packet_type(PacketType::Lora))
675}
676
677/// SetRfFrequency for a carrier frequency.
678///
679/// # Arguments
680///
681/// * `frequency_hz` - the carrier frequency in hertz.
682///
683/// # Returns
684///
685/// The command.
686#[no_mangle]
687pub extern "C" fn pamoja_sx126x_set_rf_frequency(frequency_hz: u32) -> PamojaSx126xCommand {
688    command_of(command::set_rf_frequency(config::frequency_word(
689        frequency_hz,
690    )))
691}
692
693/// CalibrateImage over a band.
694///
695/// # Arguments
696///
697/// * `low_hz` - the lower edge of the band in hertz.
698/// * `high_hz` - the upper edge of the band in hertz.
699///
700/// # Returns
701///
702/// The command.
703#[no_mangle]
704pub extern "C" fn pamoja_sx126x_calibrate_image(low_hz: u32, high_hz: u32) -> PamojaSx126xCommand {
705    command_of(command::calibrate_image(config::image_calibration(
706        low_hz, high_hz,
707    )))
708}
709
710/// SetPaConfig for a power setting.
711///
712/// # Arguments
713///
714/// * `power` - the settings from [`pamoja_sx126x_tx_power_for_output`] or
715///   [`pamoja_sx126x_tx_power_under_ceiling`].
716///
717/// # Returns
718///
719/// The command.
720#[no_mangle]
721pub extern "C" fn pamoja_sx126x_set_pa_config(power: PamojaSx126xTxPower) -> PamojaSx126xCommand {
722    command_of(command::set_pa_config(tx_power(power).pa))
723}
724
725/// SetTxParams for a power setting and a ramp time.
726///
727/// # Arguments
728///
729/// * `power` - the power settings.
730/// * `ramp_us` - the least amplifier ramp time wanted, in microseconds; the chip takes the
731///   shortest of its eight ramp times that lasts at least this long.
732///
733/// # Returns
734///
735/// The command.
736#[no_mangle]
737pub extern "C" fn pamoja_sx126x_set_tx_params(
738    power: PamojaSx126xTxPower,
739    ramp_us: u32,
740) -> PamojaSx126xCommand {
741    command_of(command::set_tx_params(
742        power.setting_dbm,
743        RampTime::at_least(ramp_us),
744    ))
745}
746
747/// SetModulationParams for a LoRa link.
748///
749/// # Arguments
750///
751/// * `link` - the link settings.
752/// * `out_command` - receives the command.
753///
754/// # Returns
755///
756/// [`PamojaStatus::Ok`] with `*out_command` set, or [`PamojaStatus::InvalidArgument`] if
757/// the link's bandwidth is not one the SX126x offers or `out_command` is null.
758///
759/// # Safety
760///
761/// `out_command` must point to a writable [`PamojaSx126xCommand`], or be null.
762#[no_mangle]
763pub unsafe extern "C" fn pamoja_sx126x_set_lora_modulation_params(
764    link: PamojaLoraLink,
765    out_command: *mut PamojaSx126xCommand,
766) -> PamojaStatus {
767    if out_command.is_null() {
768        set_last_error("out_command must not be null".to_owned());
769        return PamojaStatus::InvalidArgument;
770    }
771    match LoraModulation::from_link(&settings(link)) {
772        Some(modulation) => {
773            *out_command = command_of(command::set_lora_modulation_params(modulation));
774            PamojaStatus::Ok
775        }
776        None => {
777            set_last_error(format!(
778                "the SX126x has no {} Hz LoRa bandwidth",
779                link.bandwidth_hz
780            ));
781            PamojaStatus::InvalidArgument
782        }
783    }
784}
785
786/// SetPacketParams for a LoRa link and a payload.
787///
788/// # Arguments
789///
790/// * `link` - the link settings, whose preamble, header, and CRC the frame uses.
791/// * `payload_len` - the payload length to send, or the most a receiver accepts.
792/// * `invert_iq` - `true` for inverted IQ, as a LoRaWAN gateway sends downlinks.
793///
794/// # Returns
795///
796/// The command.
797#[no_mangle]
798pub extern "C" fn pamoja_sx126x_set_lora_packet_params(
799    link: PamojaLoraLink,
800    payload_len: u8,
801    invert_iq: bool,
802) -> PamojaSx126xCommand {
803    command_of(command::set_lora_packet_params(LoraPacket::from_link(
804        &settings(link),
805        payload_len,
806        invert_iq,
807    )))
808}
809
810/// SetDioIrqParams: which interrupts are enabled and which DIO line each raises.
811///
812/// # Arguments
813///
814/// * `irq` - the interrupts to enable, as `PAMOJA_SX126X_IRQ_*` bits.
815/// * `dio1` - the interrupts routed to DIO1.
816/// * `dio2` - the interrupts routed to DIO2.
817/// * `dio3` - the interrupts routed to DIO3.
818///
819/// # Returns
820///
821/// The command.
822#[no_mangle]
823pub extern "C" fn pamoja_sx126x_set_dio_irq_params(
824    irq: u16,
825    dio1: u16,
826    dio2: u16,
827    dio3: u16,
828) -> PamojaSx126xCommand {
829    command_of(command::set_dio_irq_params(
830        Irq::from_bits(irq),
831        Irq::from_bits(dio1),
832        Irq::from_bits(dio2),
833        Irq::from_bits(dio3),
834    ))
835}
836
837/// ClearIrqStatus for a set of interrupts.
838///
839/// # Arguments
840///
841/// * `irq` - the interrupts to clear, as `PAMOJA_SX126X_IRQ_*` bits.
842///
843/// # Returns
844///
845/// The command.
846#[no_mangle]
847pub extern "C" fn pamoja_sx126x_clear_irq_status(irq: u16) -> PamojaSx126xCommand {
848    command_of(command::clear_irq_status(Irq::from_bits(irq)))
849}
850
851/// SetTx with a timeout.
852///
853/// # Arguments
854///
855/// * `timeout_us` - how long the chip may transmit before it raises TIMEOUT, in
856///   microseconds; `0` disables the timeout.
857///
858/// # Returns
859///
860/// The command.
861#[no_mangle]
862pub extern "C" fn pamoja_sx126x_set_tx(timeout_us: u64) -> PamojaSx126xCommand {
863    command_of(command::set_tx(config::timeout_steps(timeout_us)))
864}
865
866/// SetRx with a timeout.
867///
868/// # Arguments
869///
870/// * `timeout_us` - how long the chip listens for a packet to start, in microseconds;
871///   `0` listens for a single packet with no timeout.
872///
873/// # Returns
874///
875/// The command.
876#[no_mangle]
877pub extern "C" fn pamoja_sx126x_set_rx(timeout_us: u64) -> PamojaSx126xCommand {
878    command_of(command::set_rx(config::timeout_steps(timeout_us)))
879}
880
881/// SetRx in continuous mode, receiving packet after packet until another command.
882///
883/// # Returns
884///
885/// The command.
886#[no_mangle]
887pub extern "C" fn pamoja_sx126x_set_rx_continuous() -> PamojaSx126xCommand {
888    command_of(command::set_rx(config::RX_CONTINUOUS))
889}
890
891/// SetSleep, without an RTC wake-up.
892///
893/// # Arguments
894///
895/// * `warm_start` - `true` to keep the configuration in retention.
896///
897/// # Returns
898///
899/// The command.
900#[no_mangle]
901pub extern "C" fn pamoja_sx126x_set_sleep(warm_start: bool) -> PamojaSx126xCommand {
902    command_of(command::set_sleep(warm_start, false))
903}
904
905/// The start of a WriteRegister transaction; the register values follow it in the same
906/// transaction.
907///
908/// # Arguments
909///
910/// * `address` - the first register's address.
911///
912/// # Returns
913///
914/// The opcode and the address.
915#[no_mangle]
916pub extern "C" fn pamoja_sx126x_write_register_header(address: u16) -> PamojaSx126xCommand {
917    command_of(command::write_register(address))
918}
919
920/// The start of a WriteBuffer transaction; the payload follows it in the same transaction.
921///
922/// # Arguments
923///
924/// * `offset` - where in the data buffer the first byte goes.
925///
926/// # Returns
927///
928/// The opcode and the offset.
929#[no_mangle]
930pub extern "C" fn pamoja_sx126x_write_buffer_header(offset: u8) -> PamojaSx126xCommand {
931    command_of(command::write_buffer(offset))
932}
933
934/// GetStatus, answered by the status byte.
935///
936/// # Returns
937///
938/// The query.
939#[no_mangle]
940pub extern "C" fn pamoja_sx126x_get_status() -> PamojaSx126xQuery {
941    query_of(command::get_status())
942}
943
944/// GetIrqStatus, answered by the two IRQ bytes.
945///
946/// # Returns
947///
948/// The query.
949#[no_mangle]
950pub extern "C" fn pamoja_sx126x_get_irq_status() -> PamojaSx126xQuery {
951    query_of(command::get_irq_status())
952}
953
954/// GetRxBufferStatus, answered by the payload length and its offset.
955///
956/// # Returns
957///
958/// The query.
959#[no_mangle]
960pub extern "C" fn pamoja_sx126x_get_rx_buffer_status() -> PamojaSx126xQuery {
961    query_of(command::get_rx_buffer_status())
962}
963
964/// GetPacketStatus, answered by the three LoRa signal level bytes.
965///
966/// # Returns
967///
968/// The query.
969#[no_mangle]
970pub extern "C" fn pamoja_sx126x_get_packet_status() -> PamojaSx126xQuery {
971    query_of(command::get_packet_status())
972}
973
974/// GetRssiInst, answered by the instantaneous RSSI byte.
975///
976/// # Returns
977///
978/// The query.
979#[no_mangle]
980pub extern "C" fn pamoja_sx126x_get_rssi_inst() -> PamojaSx126xQuery {
981    query_of(command::get_rssi_inst())
982}
983
984/// GetDeviceErrors, answered by the two device error bytes.
985///
986/// # Returns
987///
988/// The query.
989#[no_mangle]
990pub extern "C" fn pamoja_sx126x_get_device_errors() -> PamojaSx126xQuery {
991    query_of(command::get_device_errors())
992}
993
994/// ReadRegister for consecutive registers.
995///
996/// # Arguments
997///
998/// * `address` - the first register's address.
999/// * `len` - how many registers to read.
1000///
1001/// # Returns
1002///
1003/// The query.
1004#[no_mangle]
1005pub extern "C" fn pamoja_sx126x_read_register(address: u16, len: u8) -> PamojaSx126xQuery {
1006    query_of(command::read_register(address, usize::from(len)))
1007}
1008
1009/// ReadBuffer for a run of the data buffer.
1010///
1011/// # Arguments
1012///
1013/// * `offset` - where in the buffer the first byte is.
1014/// * `len` - how many bytes to read.
1015///
1016/// # Returns
1017///
1018/// The query.
1019#[no_mangle]
1020pub extern "C" fn pamoja_sx126x_read_buffer(offset: u8, len: u8) -> PamojaSx126xQuery {
1021    query_of(command::read_buffer(offset, usize::from(len)))
1022}
1023
1024/// Decodes a status byte.
1025///
1026/// # Arguments
1027///
1028/// * `byte` - the status byte.
1029///
1030/// # Returns
1031///
1032/// The chip mode, how the last command went, and whether that was an error.
1033#[no_mangle]
1034pub extern "C" fn pamoja_sx126x_status_from_byte(byte: u8) -> PamojaSx126xStatus {
1035    let status = Status::from_byte(byte);
1036    PamojaSx126xStatus {
1037        chip_mode: chip_mode(status.chip_mode),
1038        command_status: command_status(status.command_status),
1039        error: status.is_error(),
1040    }
1041}
1042
1043/// Decodes a GetIrqStatus answer.
1044///
1045/// # Arguments
1046///
1047/// * `high` - the first answer byte, IrqStatus(15:8).
1048/// * `low` - the second answer byte, IrqStatus(7:0).
1049///
1050/// # Returns
1051///
1052/// The pending interrupts, as `PAMOJA_SX126X_IRQ_*` bits.
1053#[no_mangle]
1054pub extern "C" fn pamoja_sx126x_irq_from_bytes(high: u8, low: u8) -> u16 {
1055    Irq::from_bytes([high, low]).bits()
1056}
1057
1058/// Decodes a GetDeviceErrors answer.
1059///
1060/// # Arguments
1061///
1062/// * `high` - the first answer byte.
1063/// * `low` - the second answer byte.
1064///
1065/// # Returns
1066///
1067/// The flagged errors, as `PAMOJA_SX126X_ERROR_*` bits.
1068#[no_mangle]
1069pub extern "C" fn pamoja_sx126x_device_errors_from_bytes(high: u8, low: u8) -> u16 {
1070    DeviceErrors::from_bytes([high, low]).bits()
1071}
1072
1073/// Decodes a LoRa GetPacketStatus answer.
1074///
1075/// # Arguments
1076///
1077/// * `rssi_pkt` - RssiPkt.
1078/// * `snr_pkt` - SnrPkt.
1079/// * `signal_rssi_pkt` - SignalRssiPkt.
1080///
1081/// # Returns
1082///
1083/// The three signal levels, exact to a hundredth of a decibel.
1084#[no_mangle]
1085pub extern "C" fn pamoja_sx126x_packet_status_from_bytes(
1086    rssi_pkt: u8,
1087    snr_pkt: u8,
1088    signal_rssi_pkt: u8,
1089) -> PamojaSx126xPacketStatus {
1090    let status = PacketStatus::from_bytes([rssi_pkt, snr_pkt, signal_rssi_pkt]);
1091    PamojaSx126xPacketStatus {
1092        rssi_centi_dbm: status.rssi_dbm.hundredths(),
1093        snr_centi_db: status.snr_db.hundredths(),
1094        signal_rssi_centi_dbm: status.signal_rssi_dbm.hundredths(),
1095    }
1096}
1097
1098/// Decodes a GetRxBufferStatus answer.
1099///
1100/// # Arguments
1101///
1102/// * `payload_len` - PayloadLengthRx.
1103/// * `start` - RxStartBufferPointer.
1104///
1105/// # Returns
1106///
1107/// The payload length and where it starts in the data buffer.
1108#[no_mangle]
1109pub extern "C" fn pamoja_sx126x_rx_buffer_status_from_bytes(
1110    payload_len: u8,
1111    start: u8,
1112) -> PamojaSx126xRxBufferStatus {
1113    let status = RxBufferStatus::from_bytes([payload_len, start]);
1114    PamojaSx126xRxBufferStatus {
1115        payload_len: status.payload_len,
1116        start: status.start,
1117    }
1118}
1119
1120/// Decodes a GetRssiInst answer.
1121///
1122/// # Arguments
1123///
1124/// * `byte` - RssiInst.
1125///
1126/// # Returns
1127///
1128/// The instantaneous RSSI in hundredths of a dBm.
1129#[no_mangle]
1130pub extern "C" fn pamoja_sx126x_rssi_inst_centi_dbm(byte: u8) -> i32 {
1131    rssi_inst_dbm(byte).hundredths()
1132}
1133
1134/// Creates a duty-cycle guard, ready to transmit at once.
1135///
1136/// # Arguments
1137///
1138/// * `permille` - the limit in parts per thousand, so `10` is 1%; `0` forbids
1139///   transmitting and `1000` or more imposes no silence.
1140///
1141/// # Returns
1142///
1143/// A handle the caller must release with [`pamoja_radio_duty_cycle_free`].
1144#[no_mangle]
1145pub extern "C" fn pamoja_radio_duty_cycle_new(permille: u32) -> *mut PamojaRadioDutyCycle {
1146    Box::into_raw(Box::new(PamojaRadioDutyCycle {
1147        guard: DutyCycle::new(permille),
1148    }))
1149}
1150
1151/// Records a transmission and the silence it owes.
1152///
1153/// # Arguments
1154///
1155/// * `guard` - the duty-cycle guard.
1156/// * `started_us` - when the transmission started, in microseconds on the caller's clock.
1157/// * `link` - the settings the frame was sent with.
1158/// * `payload_len` - the payload length in bytes.
1159///
1160/// # Returns
1161///
1162/// The frame's time on air in microseconds, or 0 if `guard` is null.
1163///
1164/// # Safety
1165///
1166/// `guard` must be a live handle from [`pamoja_radio_duty_cycle_new`], or null.
1167#[no_mangle]
1168pub unsafe extern "C" fn pamoja_radio_duty_cycle_transmitted(
1169    guard: *mut PamojaRadioDutyCycle,
1170    started_us: u64,
1171    link: PamojaLoraLink,
1172    payload_len: usize,
1173) -> u64 {
1174    if guard.is_null() {
1175        return 0;
1176    }
1177    (*guard)
1178        .guard
1179        .transmitted(started_us, &settings(link), payload_len)
1180}
1181
1182/// Returns how long the radio must still stay silent.
1183///
1184/// # Arguments
1185///
1186/// * `guard` - the duty-cycle guard.
1187/// * `now_us` - the current time in microseconds on the caller's clock.
1188///
1189/// # Returns
1190///
1191/// The remaining silence in microseconds, zero when a transmission may start, or
1192/// `UINT64_MAX` when the limit forbids transmitting or `guard` is null.
1193///
1194/// # Safety
1195///
1196/// `guard` must be a live handle from [`pamoja_radio_duty_cycle_new`], or null.
1197#[no_mangle]
1198pub unsafe extern "C" fn pamoja_radio_duty_cycle_wait_us(
1199    guard: *const PamojaRadioDutyCycle,
1200    now_us: u64,
1201) -> u64 {
1202    if guard.is_null() {
1203        return u64::MAX;
1204    }
1205    (*guard).guard.wait_us(now_us)
1206}
1207
1208/// Reports whether a transmission may start now.
1209///
1210/// # Arguments
1211///
1212/// * `guard` - the duty-cycle guard.
1213/// * `now_us` - the current time in microseconds on the caller's clock.
1214///
1215/// # Returns
1216///
1217/// `true` once the silence the last transmission owed has passed, or `false` if `guard`
1218/// is null.
1219///
1220/// # Safety
1221///
1222/// `guard` must be a live handle from [`pamoja_radio_duty_cycle_new`], or null.
1223#[no_mangle]
1224pub unsafe extern "C" fn pamoja_radio_duty_cycle_ready(
1225    guard: *const PamojaRadioDutyCycle,
1226    now_us: u64,
1227) -> bool {
1228    !guard.is_null() && (*guard).guard.ready(now_us)
1229}
1230
1231/// Returns the earliest time the next transmission may start.
1232///
1233/// # Arguments
1234///
1235/// * `guard` - the duty-cycle guard.
1236///
1237/// # Returns
1238///
1239/// A time in microseconds on the caller's clock, or `UINT64_MAX` when the limit forbids
1240/// transmitting or `guard` is null.
1241///
1242/// # Safety
1243///
1244/// `guard` must be a live handle from [`pamoja_radio_duty_cycle_new`], or null.
1245#[no_mangle]
1246pub unsafe extern "C" fn pamoja_radio_duty_cycle_earliest_us(
1247    guard: *const PamojaRadioDutyCycle,
1248) -> u64 {
1249    if guard.is_null() {
1250        return u64::MAX;
1251    }
1252    (*guard).guard.earliest_us()
1253}
1254
1255/// Releases a duty-cycle guard handle.
1256///
1257/// Passing null is a no-op.
1258///
1259/// # Safety
1260///
1261/// `guard` must be a handle from [`pamoja_radio_duty_cycle_new`] that has not already been
1262/// freed, or null. After this call it must not be used again.
1263#[no_mangle]
1264pub unsafe extern "C" fn pamoja_radio_duty_cycle_free(guard: *mut PamojaRadioDutyCycle) {
1265    if !guard.is_null() {
1266        drop(Box::from_raw(guard));
1267    }
1268}
1269
1270/// Copies a command's bytes into the value that crosses the boundary.
1271fn command_of(command: Command) -> PamojaSx126xCommand {
1272    let bytes = command.as_bytes();
1273    let mut out = [0u8; PAMOJA_SX126X_COMMAND_MAX];
1274    out[..bytes.len()].copy_from_slice(bytes);
1275    PamojaSx126xCommand {
1276        bytes: out,
1277        len: bytes.len() as u8,
1278    }
1279}
1280
1281/// Copies a query into the value that crosses the boundary.
1282fn query_of(query: Query) -> PamojaSx126xQuery {
1283    PamojaSx126xQuery {
1284        command: command_of(query.command),
1285        answer_len: query.answer_len as u32,
1286    }
1287}
1288
1289/// Names the amplifier a flag selects.
1290fn amplifier(high_power: bool) -> PowerAmplifier {
1291    if high_power {
1292        PowerAmplifier::HighPower
1293    } else {
1294        PowerAmplifier::LowPower
1295    }
1296}
1297
1298/// Flattens power settings for the boundary.
1299fn power_of(power: TxPower) -> PamojaSx126xTxPower {
1300    PamojaSx126xTxPower {
1301        pa_duty_cycle: power.pa.duty_cycle,
1302        hp_max: power.pa.hp_max,
1303        device_sel: power.pa.device,
1304        pa_lut: power.pa.lut,
1305        setting_dbm: power.setting_dbm,
1306    }
1307}
1308
1309/// Rebuilds power settings from the fields that crossed the boundary.
1310fn tx_power(power: PamojaSx126xTxPower) -> TxPower {
1311    TxPower {
1312        pa: PaConfig {
1313            duty_cycle: power.pa_duty_cycle,
1314            hp_max: power.hp_max,
1315            device: power.device_sel,
1316            lut: power.pa_lut,
1317        },
1318        setting_dbm: power.setting_dbm,
1319    }
1320}
1321
1322/// Maps a decoded chip mode to the value that crosses the boundary.
1323fn chip_mode(mode: ChipMode) -> PamojaSx126xChipMode {
1324    if mode == ChipMode::StandbyRc {
1325        PamojaSx126xChipMode::StandbyRc
1326    } else if mode == ChipMode::StandbyXosc {
1327        PamojaSx126xChipMode::StandbyXosc
1328    } else if mode == ChipMode::Fs {
1329        PamojaSx126xChipMode::Fs
1330    } else if mode == ChipMode::Rx {
1331        PamojaSx126xChipMode::Rx
1332    } else if mode == ChipMode::Tx {
1333        PamojaSx126xChipMode::Tx
1334    } else {
1335        PamojaSx126xChipMode::Other
1336    }
1337}
1338
1339/// Maps a decoded command status to the value that crosses the boundary.
1340fn command_status(status: CommandStatus) -> PamojaSx126xCommandStatus {
1341    if status == CommandStatus::DataAvailable {
1342        PamojaSx126xCommandStatus::DataAvailable
1343    } else if status == CommandStatus::Timeout {
1344        PamojaSx126xCommandStatus::Timeout
1345    } else if status == CommandStatus::ProcessingError {
1346        PamojaSx126xCommandStatus::ProcessingError
1347    } else if status == CommandStatus::ExecutionFailure {
1348        PamojaSx126xCommandStatus::ExecutionFailure
1349    } else if status == CommandStatus::TxDone {
1350        PamojaSx126xCommandStatus::TxDone
1351    } else {
1352        PamojaSx126xCommandStatus::Other
1353    }
1354}
1355
1356/// The amplifier settings of an SX127x: RegPaConfig, RegPaDac, and RegOcp.
1357#[repr(C)]
1358#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1359pub struct PamojaSx127xTxPower {
1360    /// RegPaConfig: PaSelect, MaxPower, and OutputPower.
1361    pub pa_config: u8,
1362    /// RegPaDac: the +20 dBm setting above +17 dBm on PA_BOOST, else its reset value.
1363    pub pa_dac: u8,
1364    /// RegOcp: the current limit.
1365    pub ocp: u8,
1366    /// The output power the settings produce, in dBm.
1367    pub output_dbm: i8,
1368}
1369
1370/// The LoRa modem registers of an SX127x for a link.
1371#[repr(C)]
1372#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1373pub struct PamojaSx127xModem {
1374    /// RegModemConfig1: bandwidth, coding rate, and header mode.
1375    pub modem_config_1: u8,
1376    /// RegModemConfig2: spreading factor, CRC, and the top bits of the symbol timeout.
1377    pub modem_config_2: u8,
1378    /// RegModemConfig3: low data rate optimization and the AGC.
1379    pub modem_config_3: u8,
1380    /// The DetectionOptimize bits of RegDetectOptimize, 0x05 for SF6 and 0x03 otherwise, to
1381    /// put in its low three bits.
1382    pub detection_optimize: u8,
1383    /// RegDetectionThreshold.
1384    pub detection_threshold: u8,
1385}
1386
1387/// The signal levels of a packet an SX127x received, in hundredths of a decibel.
1388#[repr(C)]
1389#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1390pub struct PamojaSx127xPacketStatus {
1391    /// The RSSI averaged over the packet, in hundredths of a dBm.
1392    pub rssi_centi_dbm: i32,
1393    /// The estimated signal-to-noise ratio, in hundredths of a dB.
1394    pub snr_centi_db: i32,
1395    /// The strength of the packet itself, in hundredths of a dBm.
1396    pub signal_rssi_centi_dbm: i32,
1397}
1398
1399/// The live state of an SX127x LoRa modem, from RegModemStat.
1400#[repr(C)]
1401#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1402pub struct PamojaSx127xModemStatus {
1403    /// The coding rate denominator the last header announced, 5 to 8, or 0 for a reserved
1404    /// value.
1405    pub coding_rate_denominator: u8,
1406    /// The modem is clear.
1407    pub clear: bool,
1408    /// The header of the packet under way is valid.
1409    pub header_valid: bool,
1410    /// A reception is under way.
1411    pub rx_ongoing: bool,
1412    /// The modem has synchronized on the end of the preamble.
1413    pub signal_synchronized: bool,
1414    /// A LoRa preamble has been detected.
1415    pub signal_detected: bool,
1416}
1417
1418/// The writes of the SX127x 500 kHz sensitivity erratum.
1419#[repr(C)]
1420#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1421pub struct PamojaSx127xHighBwOptimize {
1422    /// The RegHighBwOptimize1 value.
1423    pub optimize_1: u8,
1424    /// The RegHighBwOptimize2 value, when `has_optimize_2` is set.
1425    pub optimize_2: u8,
1426    /// Whether RegHighBwOptimize2 is written.
1427    pub has_optimize_2: bool,
1428}
1429
1430/// The receive settings of the SX127x spurious reception erratum.
1431#[repr(C)]
1432#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1433pub struct PamojaSx127xSpuriousReception {
1434    /// Whether AutomaticIFOn, bit 7 of RegDetectOptimize, stays on.
1435    pub automatic_if: bool,
1436    /// Whether RegIfFreq2 is set by hand, with RegIfFreq1 cleared.
1437    pub has_if_freq_2: bool,
1438    /// The RegIfFreq2 value, when `has_if_freq_2` is set.
1439    pub if_freq_2: u8,
1440    /// How far above the carrier to receive, in hertz.
1441    pub offset_hz: u32,
1442}
1443
1444/// Reports whether an LLCC68 supports a link's spreading factor at its bandwidth.
1445///
1446/// # Arguments
1447///
1448/// * `link` - the link settings.
1449///
1450/// # Returns
1451///
1452/// `true` when the SX126x offers the bandwidth and the LLCC68 supports the pair: up to SF9
1453/// at 125 kHz, SF10 at 250 kHz, and SF11 at 500 kHz.
1454#[no_mangle]
1455pub extern "C" fn pamoja_sx126x_llcc68_supports(link: PamojaLoraLink) -> bool {
1456    LoraModulation::from_link(&settings(link)).is_some_and(|modulation| {
1457        llcc68_supports(modulation.spreading_factor, modulation.bandwidth)
1458    })
1459}
1460
1461/// Returns the 24-bit RegFrf word an SX127x takes for a frequency.
1462///
1463/// # Arguments
1464///
1465/// * `frequency_hz` - the carrier frequency in hertz.
1466///
1467/// # Returns
1468///
1469/// The word, in steps of 32 MHz over 2^19, rounded to the nearest step.
1470#[no_mangle]
1471pub extern "C" fn pamoja_sx127x_frequency_word(frequency_hz: u32) -> u32 {
1472    sx127x_config::frequency_word(frequency_hz)
1473}
1474
1475/// Returns the frequency an SX127x RegFrf word selects.
1476///
1477/// # Arguments
1478///
1479/// * `word` - the 24-bit frequency word.
1480///
1481/// # Returns
1482///
1483/// The carrier frequency in hertz, rounded to the nearest hertz.
1484#[no_mangle]
1485pub extern "C" fn pamoja_sx127x_frequency_from_word(word: u32) -> u32 {
1486    sx127x_config::frequency_from_word(word)
1487}
1488
1489/// Returns the SX127x address byte that reads a register.
1490///
1491/// # Arguments
1492///
1493/// * `address` - the register address.
1494///
1495/// # Returns
1496///
1497/// The address with the write bit clear.
1498#[no_mangle]
1499pub extern "C" fn pamoja_sx127x_read_address(address: u8) -> u8 {
1500    sx127x_register::read_address(address)
1501}
1502
1503/// Returns the SX127x address byte that writes a register.
1504///
1505/// # Arguments
1506///
1507/// * `address` - the register address.
1508///
1509/// # Returns
1510///
1511/// The address with the write bit set.
1512#[no_mangle]
1513pub extern "C" fn pamoja_sx127x_write_address(address: u8) -> u8 {
1514    sx127x_register::write_address(address)
1515}
1516
1517/// Returns the RegOpMode value for a LoRa operating mode.
1518///
1519/// # Arguments
1520///
1521/// * `mode` - a `PAMOJA_SX127X_MODE_` code; only its low three bits are read.
1522///
1523/// # Returns
1524///
1525/// The RegOpMode value, with the LoRa register page selected.
1526#[no_mangle]
1527pub extern "C" fn pamoja_sx127x_lora_op_mode(mode: u8) -> u8 {
1528    sx127x_register::lora_op_mode(Sx127xMode::from_op_mode(mode))
1529}
1530
1531/// Returns the RegOpMode value for an FSK operating mode, which image calibration needs.
1532///
1533/// # Arguments
1534///
1535/// * `mode` - a `PAMOJA_SX127X_MODE_` code; only its low three bits are read.
1536///
1537/// # Returns
1538///
1539/// The RegOpMode value.
1540#[no_mangle]
1541pub extern "C" fn pamoja_sx127x_fsk_op_mode(mode: u8) -> u8 {
1542    sx127x_register::fsk_op_mode(Sx127xMode::from_op_mode(mode))
1543}
1544
1545/// Returns the operating mode a RegOpMode value holds.
1546///
1547/// # Arguments
1548///
1549/// * `op_mode` - the RegOpMode value.
1550///
1551/// # Returns
1552///
1553/// The `PAMOJA_SX127X_MODE_` code.
1554#[no_mangle]
1555pub extern "C" fn pamoja_sx127x_mode_from_op_mode(op_mode: u8) -> u8 {
1556    Sx127xMode::from_op_mode(op_mode).code()
1557}
1558
1559/// Returns the LoRa modem registers for a link at a carrier.
1560///
1561/// # Arguments
1562///
1563/// * `link` - the link settings.
1564/// * `frequency_hz` - the carrier frequency in hertz, which rules out 250 and 500 kHz in the
1565///   lowest band.
1566/// * `symbol_timeout` - the single reception timeout in symbols, whose top bits go in
1567///   RegModemConfig2.
1568/// * `out_modem` - receives the registers.
1569///
1570/// # Returns
1571///
1572/// [`PamojaStatus::Ok`] with `*out_modem` set, or [`PamojaStatus::InvalidArgument`] if the
1573/// SX127x cannot use the link at the carrier or `out_modem` is null.
1574///
1575/// # Safety
1576///
1577/// `out_modem` must point to a writable [`PamojaSx127xModem`], or be null.
1578#[no_mangle]
1579pub unsafe extern "C" fn pamoja_sx127x_modem(
1580    link: PamojaLoraLink,
1581    frequency_hz: u32,
1582    symbol_timeout: u16,
1583    out_modem: *mut PamojaSx127xModem,
1584) -> PamojaStatus {
1585    if out_modem.is_null() {
1586        set_last_error("out_modem must not be null".to_owned());
1587        return PamojaStatus::InvalidArgument;
1588    }
1589    match sx127x_modulation(link, frequency_hz) {
1590        Ok(modulation) => {
1591            *out_modem = PamojaSx127xModem {
1592                modem_config_1: modulation.modem_config_1(),
1593                modem_config_2: modulation.modem_config_2(symbol_timeout),
1594                modem_config_3: modulation.modem_config_3(),
1595                detection_optimize: modulation.detect_optimize(0),
1596                detection_threshold: modulation.detection_threshold(),
1597            };
1598            PamojaStatus::Ok
1599        }
1600        Err(error) => {
1601            set_last_error(modulation_message(error));
1602            PamojaStatus::InvalidArgument
1603        }
1604    }
1605}
1606
1607/// Returns the SX127x single reception timeout for a duration, in the link's symbols.
1608///
1609/// # Arguments
1610///
1611/// * `link` - the link settings.
1612/// * `timeout_us` - how long to listen for a preamble, in microseconds.
1613///
1614/// # Returns
1615///
1616/// The timeout rounded up to whole symbols, from 4 to 1023.
1617#[no_mangle]
1618pub extern "C" fn pamoja_sx127x_symbol_timeout(link: PamojaLoraLink, timeout_us: u64) -> u16 {
1619    sx127x_config::symbol_timeout(&settings(link), timeout_us)
1620}
1621
1622/// Returns the SX127x amplifier settings for an output power.
1623///
1624/// # Arguments
1625///
1626/// * `pa_boost` - `true` for the PA_BOOST output, `false` for RFO.
1627/// * `output_dbm` - the output power wanted, clamped to what the output delivers.
1628///
1629/// # Returns
1630///
1631/// The settings.
1632#[no_mangle]
1633pub extern "C" fn pamoja_sx127x_tx_power_for_output(
1634    pa_boost: bool,
1635    output_dbm: i8,
1636) -> PamojaSx127xTxPower {
1637    sx127x_power_of(Sx127xPower::for_output(pa_output(pa_boost), output_dbm))
1638}
1639
1640/// Returns the SX127x amplifier settings that keep a link's EIRP at or under a ceiling.
1641///
1642/// # Arguments
1643///
1644/// * `pa_boost` - `true` for the PA_BOOST output, `false` for RFO.
1645/// * `budget` - the link budget, whose transmitting antenna and cable apply.
1646/// * `eirp_ceiling_centi_dbm` - the EIRP limit in hundredths of a dBm.
1647///
1648/// # Returns
1649///
1650/// The settings, rounded down to whole decibels.
1651#[no_mangle]
1652pub extern "C" fn pamoja_sx127x_tx_power_under_ceiling(
1653    pa_boost: bool,
1654    budget: PamojaLoraLinkBudget,
1655    eirp_ceiling_centi_dbm: i32,
1656) -> PamojaSx127xTxPower {
1657    sx127x_power_of(Sx127xPower::under_ceiling(
1658        pa_output(pa_boost),
1659        &link_budget(budget),
1660        Decibels::from_hundredths(eirp_ceiling_centi_dbm),
1661    ))
1662}
1663
1664/// Returns RegOcp for a current limit.
1665///
1666/// # Arguments
1667///
1668/// * `milliamps` - the most current the amplifier may draw.
1669///
1670/// # Returns
1671///
1672/// The register value with the protection on.
1673#[no_mangle]
1674pub extern "C" fn pamoja_sx127x_ocp_register(milliamps: u16) -> u8 {
1675    sx127x_config::ocp_register(milliamps)
1676}
1677
1678/// Returns RegInvertIQ for the IQ polarity of each path.
1679///
1680/// # Arguments
1681///
1682/// * `receive` - whether to invert the receive path.
1683/// * `transmit` - whether to invert the transmit path.
1684///
1685/// # Returns
1686///
1687/// The register value, with the transmit bit set for normal IQ as the reference drivers
1688/// have it.
1689#[no_mangle]
1690pub extern "C" fn pamoja_sx127x_invert_iq(receive: bool, transmit: bool) -> u8 {
1691    sx127x_config::invert_iq(receive, transmit)
1692}
1693
1694/// Returns RegInvertIQ2 for the path in use.
1695///
1696/// # Arguments
1697///
1698/// * `inverted` - whether that path is inverted.
1699///
1700/// # Returns
1701///
1702/// 0x19 when inverted, else 0x1D.
1703#[no_mangle]
1704pub extern "C" fn pamoja_sx127x_invert_iq_2(inverted: bool) -> u8 {
1705    sx127x_config::invert_iq_2(inverted)
1706}
1707
1708/// Returns the writes of the 500 kHz sensitivity erratum for a link at a carrier.
1709///
1710/// # Arguments
1711///
1712/// * `link` - the link settings, whose bandwidth decides.
1713/// * `frequency_hz` - the carrier frequency in hertz.
1714/// * `out_optimize` - receives the writes.
1715///
1716/// # Returns
1717///
1718/// [`PamojaStatus::Ok`] with `*out_optimize` set, or [`PamojaStatus::InvalidArgument`] if the
1719/// SX127x has no such bandwidth or `out_optimize` is null.
1720///
1721/// # Safety
1722///
1723/// `out_optimize` must point to a writable [`PamojaSx127xHighBwOptimize`], or be null.
1724#[no_mangle]
1725pub unsafe extern "C" fn pamoja_sx127x_high_bw_optimize(
1726    link: PamojaLoraLink,
1727    frequency_hz: u32,
1728    out_optimize: *mut PamojaSx127xHighBwOptimize,
1729) -> PamojaStatus {
1730    if out_optimize.is_null() {
1731        set_last_error("out_optimize must not be null".to_owned());
1732        return PamojaStatus::InvalidArgument;
1733    }
1734    let Some(bandwidth) = sx127x_bandwidth(link) else {
1735        return PamojaStatus::InvalidArgument;
1736    };
1737    let (optimize_1, optimize_2) = sx127x_config::high_bw_optimize(bandwidth, frequency_hz);
1738    *out_optimize = PamojaSx127xHighBwOptimize {
1739        optimize_1,
1740        optimize_2: optimize_2.unwrap_or(0),
1741        has_optimize_2: optimize_2.is_some(),
1742    };
1743    PamojaStatus::Ok
1744}
1745
1746/// Returns the receive settings of the spurious reception erratum for a link.
1747///
1748/// # Arguments
1749///
1750/// * `link` - the link settings, whose bandwidth decides.
1751/// * `out_erratum` - receives the settings.
1752///
1753/// # Returns
1754///
1755/// [`PamojaStatus::Ok`] with `*out_erratum` set, or [`PamojaStatus::InvalidArgument`] if the
1756/// SX127x has no such bandwidth or `out_erratum` is null.
1757///
1758/// # Safety
1759///
1760/// `out_erratum` must point to a writable [`PamojaSx127xSpuriousReception`], or be null.
1761#[no_mangle]
1762pub unsafe extern "C" fn pamoja_sx127x_spurious_reception(
1763    link: PamojaLoraLink,
1764    out_erratum: *mut PamojaSx127xSpuriousReception,
1765) -> PamojaStatus {
1766    if out_erratum.is_null() {
1767        set_last_error("out_erratum must not be null".to_owned());
1768        return PamojaStatus::InvalidArgument;
1769    }
1770    let Some(bandwidth) = sx127x_bandwidth(link) else {
1771        return PamojaStatus::InvalidArgument;
1772    };
1773    let erratum = sx127x_config::spurious_reception(bandwidth);
1774    *out_erratum = PamojaSx127xSpuriousReception {
1775        automatic_if: erratum.automatic_if,
1776        has_if_freq_2: erratum.if_freq_2.is_some(),
1777        if_freq_2: erratum.if_freq_2.unwrap_or(0),
1778        offset_hz: erratum.offset_hz,
1779    };
1780    PamojaStatus::Ok
1781}
1782
1783/// Returns RegImageCal to start a calibration.
1784///
1785/// # Arguments
1786///
1787/// * `current` - the register's current value.
1788///
1789/// # Returns
1790///
1791/// The value with ImageCalStart set and AutoImageCalOn clear.
1792#[no_mangle]
1793pub extern "C" fn pamoja_sx127x_image_cal_start(current: u8) -> u8 {
1794    sx127x_config::image_cal_start(current)
1795}
1796
1797/// Returns RegDetectOptimize with AutomaticIFOn set or clear.
1798///
1799/// # Arguments
1800///
1801/// * `current` - the register's current value.
1802/// * `automatic_if` - whether the automatic IF stays on.
1803///
1804/// # Returns
1805///
1806/// The register value.
1807#[no_mangle]
1808pub extern "C" fn pamoja_sx127x_automatic_if(current: u8, automatic_if: bool) -> u8 {
1809    sx127x_config::automatic_if(current, automatic_if)
1810}
1811
1812/// Decodes RegPktSnrValue and RegPktRssiValue.
1813///
1814/// # Arguments
1815///
1816/// * `pkt_snr` - the RegPktSnrValue byte.
1817/// * `pkt_rssi` - the RegPktRssiValue byte.
1818/// * `frequency_hz` - the carrier the packet was received on, which picks the RF port's
1819///   offset.
1820///
1821/// # Returns
1822///
1823/// The three levels in hundredths of a decibel.
1824#[no_mangle]
1825pub extern "C" fn pamoja_sx127x_packet_status_from_bytes(
1826    pkt_snr: u8,
1827    pkt_rssi: u8,
1828    frequency_hz: u32,
1829) -> PamojaSx127xPacketStatus {
1830    let status =
1831        Sx127xPacketStatus::from_bytes([pkt_snr, pkt_rssi], Port::for_frequency(frequency_hz));
1832    PamojaSx127xPacketStatus {
1833        rssi_centi_dbm: status.rssi_dbm.hundredths(),
1834        snr_centi_db: status.snr_db.hundredths(),
1835        signal_rssi_centi_dbm: status.signal_rssi_dbm.hundredths(),
1836    }
1837}
1838
1839/// Decodes RegRssiValue.
1840///
1841/// # Arguments
1842///
1843/// * `byte` - the RegRssiValue byte.
1844/// * `frequency_hz` - the carrier the receiver is tuned to, which picks the RF port's offset.
1845///
1846/// # Returns
1847///
1848/// The RSSI in hundredths of a dBm.
1849#[no_mangle]
1850pub extern "C" fn pamoja_sx127x_rssi_centi_dbm(byte: u8, frequency_hz: u32) -> i32 {
1851    sx127x_rssi_dbm(byte, Port::for_frequency(frequency_hz)).hundredths()
1852}
1853
1854/// Decodes RegModemStat.
1855///
1856/// # Arguments
1857///
1858/// * `byte` - the register value.
1859///
1860/// # Returns
1861///
1862/// The modem's state.
1863#[no_mangle]
1864pub extern "C" fn pamoja_sx127x_modem_status_from_byte(byte: u8) -> PamojaSx127xModemStatus {
1865    let status = Sx127xModemStatus::from_byte(byte);
1866    PamojaSx127xModemStatus {
1867        coding_rate_denominator: status.coding_rate_denominator.unwrap_or(0),
1868        clear: status.clear,
1869        header_valid: status.header_valid,
1870        rx_ongoing: status.rx_ongoing,
1871        signal_synchronized: status.signal_synchronized,
1872        signal_detected: status.signal_detected,
1873    }
1874}
1875
1876/// Builds the SX127x modem settings of a link, refusing a bandwidth outside the carrier's band.
1877fn sx127x_modulation(
1878    link: PamojaLoraLink,
1879    frequency_hz: u32,
1880) -> Result<Sx127xModulation, ModulationError> {
1881    let modulation = Sx127xModulation::from_link(&settings(link))?;
1882    if modulation.bandwidth.in_band(frequency_hz) {
1883        Ok(modulation)
1884    } else {
1885        Err(ModulationError::Bandwidth(link.bandwidth_hz))
1886    }
1887}
1888
1889/// Finds a link's SX127x bandwidth, recording the error when there is none.
1890fn sx127x_bandwidth(link: PamojaLoraLink) -> Option<Sx127xBandwidth> {
1891    let bandwidth = Sx127xBandwidth::from_hz(link.bandwidth_hz);
1892    if bandwidth.is_none() {
1893        set_last_error(format!(
1894            "the SX127x has no {} Hz LoRa bandwidth",
1895            link.bandwidth_hz
1896        ));
1897    }
1898    bandwidth
1899}
1900
1901/// Says why an SX127x cannot use a link.
1902fn modulation_message(error: ModulationError) -> String {
1903    match error {
1904        ModulationError::Bandwidth(hz) => {
1905            format!("the SX127x has no {hz} Hz LoRa bandwidth at this carrier")
1906        }
1907        ModulationError::SpreadingFactor(sf) => format!("the SX127x has no SF{sf}"),
1908        ModulationError::ExplicitHeaderAtSf6 => "SF6 needs an implicit header".to_owned(),
1909    }
1910}
1911
1912/// Names the amplifier output a flag selects.
1913fn pa_output(pa_boost: bool) -> PaOutput {
1914    if pa_boost {
1915        PaOutput::PaBoost
1916    } else {
1917        PaOutput::Rfo
1918    }
1919}
1920
1921/// Flattens SX127x power settings for the boundary.
1922fn sx127x_power_of(power: Sx127xPower) -> PamojaSx127xTxPower {
1923    PamojaSx127xTxPower {
1924        pa_config: power.pa_config,
1925        pa_dac: power.pa_dac,
1926        ocp: power.ocp,
1927        output_dbm: power.output_dbm,
1928    }
1929}
1930
1931#[cfg(test)]
1932mod tests {
1933    use super::*;
1934    use crate::lora::{pamoja_lora_link_budget_default, pamoja_lora_link_default};
1935
1936    fn bytes(command: PamojaSx126xCommand) -> Vec<u8> {
1937        command.bytes[..usize::from(command.len)].to_vec()
1938    }
1939
1940    #[test]
1941    fn commands_carry_the_datasheet_bytes() {
1942        assert_eq!(
1943            bytes(pamoja_sx126x_set_rf_frequency(868_100_000)),
1944            [0x86, 0x36, 0x41, 0x99, 0x9A]
1945        );
1946        let power = pamoja_sx126x_tx_power_for_output(true, 14);
1947        assert_eq!(
1948            bytes(pamoja_sx126x_set_pa_config(power)),
1949            [0x95, 0x04, 0x07, 0x00, 0x01]
1950        );
1951        assert_eq!(
1952            bytes(pamoja_sx126x_set_tx_params(power, 40)),
1953            [0x8E, 0x0E, 0x02]
1954        );
1955        assert_eq!(
1956            bytes(pamoja_sx126x_set_rx_continuous()),
1957            [0x82, 0xFF, 0xFF, 0xFF]
1958        );
1959        let query = pamoja_sx126x_get_irq_status();
1960        assert_eq!(bytes(query.command), [0x12, 0x00]);
1961        assert_eq!(query.answer_len, 2);
1962    }
1963
1964    #[test]
1965    fn a_bandwidth_the_chip_lacks_is_refused() {
1966        let mut command = PamojaSx126xCommand {
1967            bytes: [0; PAMOJA_SX126X_COMMAND_MAX],
1968            len: 0,
1969        };
1970        let link = pamoja_lora_link_default(7, 125_000);
1971        let status = unsafe { pamoja_sx126x_set_lora_modulation_params(link, &mut command) };
1972        assert_eq!(status, PamojaStatus::Ok);
1973        assert_eq!(bytes(command), [0x8B, 0x07, 0x04, 0x01, 0x00]);
1974
1975        let narrow = pamoja_lora_link_default(7, 203_125);
1976        let status = unsafe { pamoja_sx126x_set_lora_modulation_params(narrow, &mut command) };
1977        assert_eq!(status, PamojaStatus::InvalidArgument);
1978    }
1979
1980    #[test]
1981    fn power_under_a_ceiling_takes_off_the_antenna() {
1982        let whip = PamojaLoraLinkBudget {
1983            transmit_antenna_gain_centi_dbi: 215,
1984            transmit_cable_loss_centi_db: 50,
1985            ..pamoja_lora_link_budget_default()
1986        };
1987        let power = pamoja_sx126x_tx_power_under_ceiling(true, whip, 1_600);
1988        assert_eq!(power.setting_dbm, 14);
1989        assert_eq!(
1990            pamoja_sx126x_tx_power_for_output(false, 15).pa_duty_cycle,
1991            0x06
1992        );
1993        assert_eq!(pamoja_sx126x_ramp_time_us(100), 200);
1994    }
1995
1996    #[test]
1997    fn answers_decode_as_the_datasheet_gives() {
1998        let status = pamoja_sx126x_status_from_byte(0x2C);
1999        assert_eq!(status.chip_mode, PamojaSx126xChipMode::StandbyRc);
2000        assert_eq!(status.command_status, PamojaSx126xCommandStatus::TxDone);
2001        assert!(!status.error);
2002        assert_eq!(
2003            pamoja_sx126x_irq_from_bytes(0x02, 0x62),
2004            PAMOJA_SX126X_IRQ_TIMEOUT
2005                | PAMOJA_SX126X_IRQ_CRC_ERROR
2006                | PAMOJA_SX126X_IRQ_HEADER_ERROR
2007                | PAMOJA_SX126X_IRQ_RX_DONE
2008        );
2009        let packet = pamoja_sx126x_packet_status_from_bytes(0xDB, 0xF6, 0xE0);
2010        assert_eq!(packet.rssi_centi_dbm, -10_950);
2011        assert_eq!(packet.snr_centi_db, -250);
2012        assert_eq!(pamoja_sx126x_rssi_inst_centi_dbm(0xDB), -10_950);
2013    }
2014
2015    #[test]
2016    fn a_duty_cycle_guard_holds_the_radio_silent() {
2017        let guard = pamoja_radio_duty_cycle_new(10);
2018        let link = pamoja_lora_link_default(12, 125_000);
2019        unsafe {
2020            assert_eq!(
2021                pamoja_radio_duty_cycle_transmitted(guard, 0, link, 10),
2022                991_232
2023            );
2024            assert_eq!(pamoja_radio_duty_cycle_wait_us(guard, 0), 99_123_200);
2025            assert!(!pamoja_radio_duty_cycle_ready(guard, 99_000_000));
2026            assert!(pamoja_radio_duty_cycle_ready(guard, 99_123_200));
2027            pamoja_radio_duty_cycle_free(guard);
2028            assert!(!pamoja_radio_duty_cycle_ready(std::ptr::null(), 0));
2029        }
2030    }
2031
2032    #[test]
2033    fn sx127x_register_values_follow_the_datasheet() {
2034        assert_eq!(pamoja_sx127x_frequency_word(868_100_000), 0xD9_0666);
2035        assert_eq!(pamoja_sx127x_frequency_from_word(0x6C_8000), 434_000_000);
2036        assert_eq!(pamoja_sx127x_lora_op_mode(PAMOJA_SX127X_MODE_TX), 0x8B);
2037        assert_eq!(pamoja_sx127x_fsk_op_mode(PAMOJA_SX127X_MODE_STANDBY), 0x09);
2038        assert_eq!(
2039            pamoja_sx127x_mode_from_op_mode(0x8D),
2040            PAMOJA_SX127X_MODE_RX_CONTINUOUS
2041        );
2042        assert_eq!(pamoja_sx127x_write_address(PAMOJA_SX127X_REG_OP_MODE), 0x81);
2043        assert_eq!(pamoja_sx127x_read_address(0x81), PAMOJA_SX127X_REG_OP_MODE);
2044
2045        let mut modem = PamojaSx127xModem::default();
2046        let link = pamoja_lora_link_default(7, 125_000);
2047        assert_eq!(
2048            unsafe { pamoja_sx127x_modem(link, 868_100_000, 0, &mut modem) },
2049            PamojaStatus::Ok
2050        );
2051        assert_eq!(
2052            (
2053                modem.modem_config_1,
2054                modem.modem_config_2,
2055                modem.modem_config_3
2056            ),
2057            (0x72, 0x74, 0x04)
2058        );
2059        assert_eq!(
2060            (modem.detection_optimize, modem.detection_threshold),
2061            (0x03, 0x0A)
2062        );
2063
2064        let power = pamoja_sx127x_tx_power_for_output(true, 20);
2065        assert_eq!(
2066            (power.pa_config, power.pa_dac, power.ocp),
2067            (0xFF, 0x87, 0x31)
2068        );
2069        assert_eq!(pamoja_sx127x_invert_iq(true, true), 0x66);
2070        assert_eq!(pamoja_sx127x_invert_iq_2(false), 0x1D);
2071        let packet = pamoja_sx127x_packet_status_from_bytes(0xF6, 0x30, 868_100_000);
2072        assert_eq!(
2073            (
2074                packet.rssi_centi_dbm,
2075                packet.snr_centi_db,
2076                packet.signal_rssi_centi_dbm
2077            ),
2078            (-10_900, -250, -11_150)
2079        );
2080        assert_eq!(pamoja_sx127x_rssi_centi_dbm(0x30, 433_175_000), -11_600);
2081        assert_eq!(
2082            pamoja_sx127x_modem_status_from_byte(0x30).coding_rate_denominator,
2083            5
2084        );
2085    }
2086
2087    #[test]
2088    fn sx127x_refuses_what_the_chip_cannot_do_and_the_llcc68_limits_hold() {
2089        let mut modem = PamojaSx127xModem::default();
2090        let sf5 = pamoja_lora_link_default(5, 125_000);
2091        assert_eq!(
2092            unsafe { pamoja_sx127x_modem(sf5, 868_100_000, 0, &mut modem) },
2093            PamojaStatus::InvalidArgument
2094        );
2095        let wide = pamoja_lora_link_default(7, 500_000);
2096        assert_eq!(
2097            unsafe { pamoja_sx127x_modem(wide, 169_400_000, 0, &mut modem) },
2098            PamojaStatus::InvalidArgument
2099        );
2100        assert!(pamoja_sx126x_llcc68_supports(pamoja_lora_link_default(
2101            9, 125_000
2102        )));
2103        assert!(!pamoja_sx126x_llcc68_supports(pamoja_lora_link_default(
2104            10, 125_000
2105        )));
2106
2107        let mut erratum = PamojaSx127xSpuriousReception::default();
2108        let narrow = pamoja_lora_link_default(9, 20_833);
2109        assert_eq!(
2110            unsafe { pamoja_sx127x_spurious_reception(narrow, &mut erratum) },
2111            PamojaStatus::Ok
2112        );
2113        assert!(erratum.has_if_freq_2);
2114        assert_eq!((erratum.if_freq_2, erratum.offset_hz), (0x44, 20_830));
2115        let mut optimize = PamojaSx127xHighBwOptimize::default();
2116        assert_eq!(
2117            unsafe { pamoja_sx127x_high_bw_optimize(wide, 915_000_000, &mut optimize) },
2118            PamojaStatus::Ok
2119        );
2120        assert_eq!((optimize.optimize_1, optimize.optimize_2), (0x02, 0x64));
2121        assert!(optimize.has_optimize_2);
2122    }
2123}