diff --git a/.appveyor.yml b/.appveyor.yml index c958ef0..d91d824 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,7 +7,11 @@ configuration: - --features= - --features=debug - --features=fugit + - --features=backtrace - --features=debug,fugit + - --features=fugit,backtrace + - --features=backtrace,debug + - --features=debug,fugit,backtrace # General environment vars diff --git a/rfm95/Cargo.toml b/rfm95/Cargo.toml index 19c6e57..a6e7cab 100644 --- a/rfm95/Cargo.toml +++ b/rfm95/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "embedded-lora-rfm95" -version = "0.1.3" +version = "0.2.0" edition = "2021" authors = ["KizzyCode Software Labs./Keziah Biermann "] -keywords = [] -categories = [] +keywords = ["no-std", "embedded", "hardware-support", "lora", "rfm95"] +categories = ["no-std", "embedded", "hardware-support"] description = "A `no-std`-compatible, opinionated driver for the RFM95 LoRa modem" license = "BSD-2-Clause OR MIT" repository = "https://github.com/KizzyCode/embedded-lora-rust" @@ -17,6 +17,7 @@ readme = "README.md" [features] default = [] debug = [] +backtrace = [] fugit = ["dep:fugit"] diff --git a/rfm95/README.md b/rfm95/README.md index b66ce94..6cb6933 100644 --- a/rfm95/README.md +++ b/rfm95/README.md @@ -18,6 +18,11 @@ The `fugit`-feature implements simple `From`/`Into`-conversions between the buil [`fugit`'s](https://crates.io/crates/fugit) [`HertzU32` type](https://docs.rs/fugit/latest/fugit/type.HertzU32.html). This is a comfort-feature only, and does not enable additional functionality. +### `backtrace` (disabled by default) +The `backtrace`-feature can be used to get more verbose errors. If this feature is enabled, errors will contain a human +readable description as well as file and line information about where the error occurred. This is useful for debugging +or better logging, but can be disabled if library size matters. + ### `debug` (disabled by default) The `debug` feature enables some debug functionality, namely an SPI debug callback which can be used to log all SPI transactions with the RFM95 modem, and provides some helper functions to dump the register state and FIFO contents. The diff --git a/rfm95/src/error.rs b/rfm95/src/error.rs new file mode 100644 index 0000000..5fdc855 --- /dev/null +++ b/rfm95/src/error.rs @@ -0,0 +1,136 @@ +//! The crate's error types + +/// Creates an error +#[macro_export] +macro_rules! err { + ($kind:tt, $desc:expr) => {{ + $kind { + #[cfg(feature = "backtrace")] + file: file!(), + #[cfg(feature = "backtrace")] + line: line!(), + #[cfg(feature = "backtrace")] + description: $desc, + } + }}; +} + +/// An I/O error +#[derive(Debug, Clone, Copy)] +pub struct IoError { + /// The file where the error was created + #[cfg(feature = "backtrace")] + pub file: &'static str, + /// The line at which the error was created + #[cfg(feature = "backtrace")] + pub line: u32, + /// A human readable error description + #[cfg(feature = "backtrace")] + pub description: &'static str, +} + +/// A timeout error +#[derive(Debug, Clone, Copy)] +pub struct TimeoutError { + /// The file where the error was created + #[cfg(feature = "backtrace")] + pub file: &'static str, + /// The line at which the error was created + #[cfg(feature = "backtrace")] + pub line: u32, + /// A human readable error description + #[cfg(feature = "backtrace")] + pub description: &'static str, +} + +/// A CRC-validation or format error +#[derive(Debug, Clone, Copy)] +pub struct InvalidMessageError { + /// The file where the error was created + #[cfg(feature = "backtrace")] + pub file: &'static str, + /// The line at which the error was created + #[cfg(feature = "backtrace")] + pub line: u32, + /// A human readable error description + #[cfg(feature = "backtrace")] + pub description: &'static str, +} + +/// An invalid-argument error +#[derive(Debug, Clone, Copy)] +pub struct InvalidArgumentError { + /// The file where the error was created + #[cfg(feature = "backtrace")] + pub file: &'static str, + /// The line at which the error was created + #[cfg(feature = "backtrace")] + pub line: u32, + /// A human readable error description + #[cfg(feature = "backtrace")] + pub description: &'static str, +} + +/// An TX-start error +#[derive(Debug, Clone, Copy)] +pub enum TxStartError { + /// An I/O error + IoError(IoError), + /// An invalid-argument error + InvalidArgumentError(InvalidArgumentError), +} +impl From for TxStartError { + fn from(error: IoError) -> Self { + Self::IoError(error) + } +} +impl From for TxStartError { + fn from(error: InvalidArgumentError) -> Self { + Self::InvalidArgumentError(error) + } +} + +/// An RX-start error +#[derive(Debug, Clone, Copy)] +pub enum RxStartError { + /// An I/O error + IoError(IoError), + /// An invalid-argument error + InvalidArgumentError(InvalidArgumentError), +} +impl From for RxStartError { + fn from(error: IoError) -> Self { + Self::IoError(error) + } +} +impl From for RxStartError { + fn from(error: InvalidArgumentError) -> Self { + Self::InvalidArgumentError(error) + } +} + +/// An RX-completion specific error +#[derive(Debug, Clone, Copy)] +pub enum RxCompleteError { + /// An I/O error + IoError(IoError), + /// A timeout error + TimeoutError(TimeoutError), + /// A CRC-validation or format error + InvalidMessageError(InvalidMessageError), +} +impl From for RxCompleteError { + fn from(error: IoError) -> Self { + Self::IoError(error) + } +} +impl From for RxCompleteError { + fn from(error: TimeoutError) -> Self { + Self::TimeoutError(error) + } +} +impl From for RxCompleteError { + fn from(error: InvalidMessageError) -> Self { + Self::InvalidMessageError(error) + } +} diff --git a/rfm95/src/lib.rs b/rfm95/src/lib.rs index 313d0e3..603e294 100644 --- a/rfm95/src/lib.rs +++ b/rfm95/src/lib.rs @@ -15,5 +15,6 @@ #![warn(clippy::allow_attributes_without_reason)] #![warn(clippy::cognitive_complexity)] +pub mod error; pub mod lora; pub mod rfm95; diff --git a/rfm95/src/lora/types.rs b/rfm95/src/lora/types.rs index a48c9e2..b541e06 100644 --- a/rfm95/src/lora/types.rs +++ b/rfm95/src/lora/types.rs @@ -1,5 +1,8 @@ //! Small wrappers for type safety +use crate::err; +use crate::error::IoError; + /// A LoRa spreading factor /// /// # Implementation Note @@ -25,10 +28,9 @@ pub enum SpreadingFactor { /// Spreading factor 12 aka 4096 chirps per symbol S12 = 12, } -impl TryFrom for SpreadingFactor { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl SpreadingFactor { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { sf if sf == Self::S7 as u8 => Ok(Self::S7), sf if sf == Self::S8 as u8 => Ok(Self::S8), @@ -36,7 +38,7 @@ impl TryFrom for SpreadingFactor { sf if sf == Self::S10 as u8 => Ok(Self::S10), sf if sf == Self::S11 as u8 => Ok(Self::S11), sf if sf == Self::S12 as u8 => Ok(Self::S12), - _ => Err("Invalid or unsupported spreading factor"), + _ => Err(err!(IoError, "Invalid or unsupported spreading factor")), } } } @@ -70,10 +72,9 @@ pub enum Bandwidth { /// 7.8 kHz bandwidth B7_8 = 0b0000, } -impl TryFrom for Bandwidth { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl Bandwidth { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { bw if bw == Self::B500 as u8 => Ok(Self::B500), bw if bw == Self::B250 as u8 => Ok(Self::B250), @@ -85,7 +86,7 @@ impl TryFrom for Bandwidth { bw if bw == Self::B15_6 as u8 => Ok(Self::B15_6), bw if bw == Self::B10_4 as u8 => Ok(Self::B10_4), bw if bw == Self::B7_8 as u8 => Ok(Self::B7_8), - _ => Err("Invalid or unsupported bandwidth"), + _ => Err(err!(IoError, "Invalid or unsupported bandwidth")), } } } @@ -107,16 +108,15 @@ pub enum CodingRate { /// Coding rate 4/8 aka 2x overhead C4_8 = 0b100, } -impl TryFrom for CodingRate { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl CodingRate { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { cr if cr == Self::C4_5 as u8 => Ok(Self::C4_5), cr if cr == Self::C4_6 as u8 => Ok(Self::C4_6), cr if cr == Self::C4_7 as u8 => Ok(Self::C4_7), cr if cr == Self::C4_8 as u8 => Ok(Self::C4_8), - _ => Err("Invalid coding rate"), + _ => Err(err!(IoError, "Invalid coding rate")), } } } @@ -134,14 +134,13 @@ pub enum Polarity { /// Inverted polarity, usually used for downlinks Inverted = 1, } -impl TryFrom for Polarity { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl Polarity { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { polarity if polarity == Self::Normal as u8 => Ok(Self::Normal), polarity if polarity == Self::Inverted as u8 => Ok(Self::Inverted), - _ => Err("Invalid IQ polarity value"), + _ => Err(err!(IoError, "Invalid IQ polarity value")), } } } @@ -159,14 +158,13 @@ pub enum HeaderMode { /// Implicit header mode to omit the header if decoding parameters are known Implicit = 1, } -impl TryFrom for HeaderMode { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl HeaderMode { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { mode if mode == Self::Explicit as u8 => Ok(Self::Explicit), mode if mode == Self::Implicit as u8 => Ok(Self::Implicit), - _ => Err("Invalid header mode"), + _ => Err(err!(IoError, "Invalid header mode")), } } } @@ -184,14 +182,13 @@ pub enum CrcMode { /// CRC enabled Enabled = 1, } -impl TryFrom for CrcMode { - type Error = &'static str; - - fn try_from(value: u8) -> Result { +impl CrcMode { + /// Parses `self` from a register value + pub(crate) fn parse(value: u8) -> Result { match value { mode if mode == Self::Disabled as u8 => Ok(Self::Disabled), mode if mode == Self::Enabled as u8 => Ok(Self::Enabled), - _ => Err("Invalid CRC mode"), + _ => Err(err!(IoError, "Invalid CRC mode")), } } } diff --git a/rfm95/src/rfm95/connection.rs b/rfm95/src/rfm95/connection.rs index 3caf970..9ec9947 100644 --- a/rfm95/src/rfm95/connection.rs +++ b/rfm95/src/rfm95/connection.rs @@ -1,5 +1,7 @@ //! RFM95 SPI connection +use crate::err; +use crate::error::IoError; use crate::rfm95::registers::Register; use core::fmt::{Debug, Formatter}; use embedded_hal::digital::OutputPin; @@ -32,7 +34,7 @@ where } /// Reads a RFM95 register via SPI - pub fn read(&mut self, register: T) -> Result + pub fn read(&mut self, register: T) -> Result where T: Register, { @@ -41,7 +43,7 @@ where Ok((register_value & register.mask()) >> register.offset()) } /// Updates a RFM95 register via SPI - pub fn write(&mut self, register: T, value: u8) -> Result<(), &'static str> + pub fn write(&mut self, register: T, value: u8) -> Result<(), IoError> where T: Register, { @@ -61,15 +63,15 @@ where } /// Performs RFM95-specific SPI register access - fn register(&mut self, operation: u8, address: u8, payload: u8) -> Result { + fn register(&mut self, operation: u8, address: u8, payload: u8) -> Result { // Build command let address = address & 0b0111_1111; let mut command = [operation | address, payload]; // Do transaction - self.select.set_low().map_err(|_| "Failed to pull chip-select line to low")?; - self.bus.transfer_in_place(&mut command).map_err(|_| "Failed to do SPI transaction")?; - self.select.set_high().map_err(|_| "Failed to pull chip-select line to high")?; + self.select.set_low().map_err(|_| err!(IoError, "Failed to pull chip-select line to low"))?; + self.bus.transfer_in_place(&mut command).map_err(|_| err!(IoError, "Failed to do SPI transaction"))?; + self.select.set_high().map_err(|_| err!(IoError, "Failed to pull chip-select line to high"))?; // SPI debug callback #[cfg(feature = "debug")] diff --git a/rfm95/src/rfm95/driver.rs b/rfm95/src/rfm95/driver.rs index b5e4db2..9d2c8a5 100644 --- a/rfm95/src/rfm95/driver.rs +++ b/rfm95/src/rfm95/driver.rs @@ -1,5 +1,9 @@ //! RFM95 driver for LoRa operations +use crate::err; +use crate::error::{ + InvalidArgumentError, InvalidMessageError, IoError, RxCompleteError, RxStartError, TimeoutError, TxStartError, +}; use crate::lora::airtime; use crate::lora::config::Config; use crate::lora::types::*; @@ -57,17 +61,17 @@ where /// # Important /// The RFM95 modem is initialized to LoRa-mode and put to standby. All other configurations are left untouched, so /// you probably want to configure the modem initially (also see [`Self::set_config`]). - pub fn new(bus: Bus, select: Select, mut reset: R, mut timer: T) -> Result + pub fn new(bus: Bus, select: Select, mut reset: R, mut timer: T) -> Result where R: OutputPin, T: DelayNs, { // Pull reset to low and wait until the reset is triggered - reset.set_low().map_err(|_| "Failed to pull reset line to low")?; + reset.set_low().map_err(|_| err!(IoError, "Failed to pull reset line to low"))?; timer.delay_ms(1); // Pull reset to high again and give the chip some time to boot - reset.set_high().map_err(|_| "Failed to pull reset line to high")?; + reset.set_high().map_err(|_| err!(IoError, "Failed to pull reset line to high"))?; timer.delay_ms(10); // Validate chip revision to assure the protocol matches @@ -78,7 +82,7 @@ where let silicon_revision = wire.read(RegVersion)?; let true = Self::SUPPORTED_SILICON_REVISIONS.contains(&silicon_revision) else { // Raise an error here since other revisions may be incompatible - return Err("Unsupported silicon revision"); + return Err(err!(IoError, "Unsupported silicon revision")); }; } @@ -98,7 +102,7 @@ where } /// Applies the given config (useful for initialization) - pub fn set_config(&mut self, config: &Config) -> Result<(), &'static str> { + pub fn set_config(&mut self, config: &Config) -> Result<(), IoError> { self.set_spreading_factor(config.spreading_factor())?; self.set_bandwidth(config.bandwidth())?; self.set_coding_rate(config.coding_rate())?; @@ -112,12 +116,13 @@ where } /// The current spreading factor - pub fn spreading_factor(&mut self) -> Result { - let spreading_factor = self.spi.read(RegModemConfig2SpreadingFactor)?; - SpreadingFactor::try_from(spreading_factor) + pub fn spreading_factor(&mut self) -> Result { + let spreading_factor_raw = self.spi.read(RegModemConfig2SpreadingFactor)?; + let spreading_factor = SpreadingFactor::parse(spreading_factor_raw)?; + Ok(spreading_factor) } /// Set the spreading factor - pub fn set_spreading_factor(&mut self, spreading_factor: T) -> Result<(), &'static str> + pub fn set_spreading_factor(&mut self, spreading_factor: T) -> Result<(), IoError> where T: Into, { @@ -133,12 +138,12 @@ where } /// The current bandwidth - pub fn bandwidth(&mut self) -> Result { + pub fn bandwidth(&mut self) -> Result { let bandwidth = self.spi.read(RegModemConfig1Bw)?; - Bandwidth::try_from(bandwidth) + Bandwidth::parse(bandwidth) } /// Sets the bandwidth - pub fn set_bandwidth(&mut self, bandwidth: T) -> Result<(), &'static str> + pub fn set_bandwidth(&mut self, bandwidth: T) -> Result<(), IoError> where T: Into, { @@ -154,12 +159,12 @@ where } /// The current coding rate - pub fn coding_rate(&mut self) -> Result { + pub fn coding_rate(&mut self) -> Result { let coding_rate = self.spi.read(RegModemConfig1CodingRate)?; - CodingRate::try_from(coding_rate) + CodingRate::parse(coding_rate) } /// Sets the coding rate - pub fn set_coding_rate(&mut self, coding_rate: T) -> Result<(), &'static str> + pub fn set_coding_rate(&mut self, coding_rate: T) -> Result<(), IoError> where T: Into, { @@ -168,12 +173,12 @@ where } /// The current IQ polarity - pub fn polarity(&mut self) -> Result { + pub fn polarity(&mut self) -> Result { let polarity = self.spi.read(RegInvertIQ)?; - Polarity::try_from(polarity) + Polarity::parse(polarity) } /// Sets the IQ polarity - pub fn set_polarity(&mut self, polarity: T) -> Result<(), &'static str> + pub fn set_polarity(&mut self, polarity: T) -> Result<(), IoError> where T: Into, { @@ -182,12 +187,12 @@ where } /// The current header mode - pub fn header_mode(&mut self) -> Result { + pub fn header_mode(&mut self) -> Result { let header_mode = self.spi.read(RegModemConfig1ImplicitHeaderModeOn)?; - HeaderMode::try_from(header_mode) + HeaderMode::parse(header_mode) } /// Sets the header mode - pub fn set_header_mode(&mut self, header_mode: T) -> Result<(), &'static str> + pub fn set_header_mode(&mut self, header_mode: T) -> Result<(), IoError> where T: Into, { @@ -196,12 +201,12 @@ where } /// The current CRC mode - pub fn crc_mode(&mut self) -> Result { + pub fn crc_mode(&mut self) -> Result { let crc_mode = self.spi.read(RegModemConfig2RxPayloadCrcOn)?; - CrcMode::try_from(crc_mode) + CrcMode::parse(crc_mode) } /// Sets the CRC mode - pub fn set_crc_mode(&mut self, crc: T) -> Result<(), &'static str> + pub fn set_crc_mode(&mut self, crc: T) -> Result<(), IoError> where T: Into, { @@ -210,12 +215,12 @@ where } /// The current sync word - pub fn sync_word(&mut self) -> Result { + pub fn sync_word(&mut self) -> Result { let sync_word = self.spi.read(RegSyncWord)?; Ok(SyncWord::new(sync_word)) } /// Sets the sync word - pub fn set_sync_word(&mut self, sync_word: T) -> Result<(), &'static str> + pub fn set_sync_word(&mut self, sync_word: T) -> Result<(), IoError> where T: Into, { @@ -224,7 +229,7 @@ where } /// The current preamble length - pub fn preamble_len(&mut self) -> Result { + pub fn preamble_len(&mut self) -> Result { // Read registers let preamble_len_msb = self.spi.read(RegPreambleMsb)?; let preamble_len_lsb = self.spi.read(RegPreambleLsb)?; @@ -234,7 +239,7 @@ where Ok(PreambleLength::new(preamble_len)) } /// Sets the preamble length - pub fn set_preamble_len(&mut self, len: T) -> Result<(), &'static str> + pub fn set_preamble_len(&mut self, len: T) -> Result<(), IoError> where T: Into, { @@ -244,7 +249,7 @@ where } /// The current frequency - pub fn frequency(&mut self) -> Result { + pub fn frequency(&mut self) -> Result { // Read frequency from registers let frequency_msb = self.spi.read(RegFrMsb)?; let frequency_mid = self.spi.read(RegFrMid)?; @@ -258,7 +263,7 @@ where Ok(Frequency::hz(frequency)) } /// Sets the frequency - pub fn set_frequency(&mut self, frequency: T) -> Result<(), &'static str> + pub fn set_frequency(&mut self, frequency: T) -> Result<(), IoError> where T: Into, { @@ -286,11 +291,11 @@ where /// # Non-Blocking /// This functions schedules the TX operation and returns immediately. To check if the TX operation is done, use /// [`Self::complete_tx`]. - pub fn start_tx(&mut self, data: &[u8]) -> Result<(), &'static str> { + pub fn start_tx(&mut self, data: &[u8]) -> Result<(), TxStartError> { // Validate input length let 1..=RFM95_FIFO_SIZE = data.len() else { // The message is empty or too long - return Err("Invalid TX data length"); + return Err(err!(InvalidArgumentError, "Invalid TX data length"))?; }; // Copy packet into FIFO... @@ -314,7 +319,7 @@ where /// /// # Non-Blocking /// This function is non-blocking. If the TX operation is not done yet, it returns `Ok(None)`. - pub fn complete_tx(&mut self) -> Result, &'static str> { + pub fn complete_tx(&mut self) -> Result, IoError> { // Check for TX done let 0b1 = self.spi.read(RegIrqFlagsTxDone)? else { // The TX operation has not been completed yet @@ -338,7 +343,7 @@ where /// the maximum timeout, we take the configured [`Self::spreading_factor`] and [`Self::bandwidth`], and get the /// duration of a single symbol via [`crate::lora::airtime::symbol_airtime`]. The maximum timeout is the duration of /// a single symbol, multiplied with `1023`. - pub fn rx_timeout_max(&mut self) -> Result { + pub fn rx_timeout_max(&mut self) -> Result { // Get current config let spreading_factor = self.spreading_factor()?; let bandwidth = self.bandwidth()?; @@ -356,7 +361,7 @@ where /// # Maximum Timeout /// The RFM95 timeout counter works by counting symbols, and is thus dependent on the configured spreading factor /// and bandwidth. See also [`Self::rx_timeout_max`]. - pub fn start_rx(&mut self, timeout: Duration) -> Result<(), &'static str> { + pub fn start_rx(&mut self, timeout: Duration) -> Result<(), RxStartError> { // Get the current symbol airtime in microseconds let spreading_factor = self.spreading_factor()?; let bandwidth = self.bandwidth()?; @@ -364,10 +369,11 @@ where let symbol_airtime_micros = symbol_airtime.as_micros() as i32; // Compute the raw timeout - let timeout_micros = i32::try_from(timeout.as_micros()).map_err(|_| "Timeout is too long")?; + let timeout_micros = + i32::try_from(timeout.as_micros()).map_err(|_| err!(InvalidArgumentError, "Timeout is too long"))?; let timeout_symbols @ 0..1024 = airtime::ceildiv(timeout_micros, symbol_airtime_micros) as u32 else { // This timeout is too large to be configured - return Err("Effective timeout is too large"); + return Err(err!(InvalidArgumentError, "Effective timeout is too large"))?; }; // Configure the timeout and reset the address pointer @@ -398,15 +404,15 @@ where /// # Timeout or CRC errors /// If the receive operation times out or the received message is corrupt, #[allow(clippy::missing_panics_doc, reason = "The panic should never occur during regular operation")] - pub fn complete_rx(&mut self, buf: &mut [u8]) -> Result, &'static str> { + pub fn complete_rx(&mut self, buf: &mut [u8]) -> Result, RxCompleteError> { // Check for errors let 0b0 = self.spi.read(RegIrqFlagsRxTimeout)? else { // The RX operation has timeouted - return Err("RX timeout"); + return Err(err!(TimeoutError, "RX timeout"))?; }; let 0b0 = self.spi.read(RegIrqFlagsPayloadCrcError)? else { // The RX operation has failed - return Err("RX CRC error"); + return Err(err!(InvalidMessageError, "RX CRC error"))?; }; // Check for RX done @@ -437,7 +443,7 @@ where /// Dumps all used registers; usefule for debugging purposes #[cfg(feature = "debug")] - pub fn dump_registers(&mut self) -> Result<[u8; REGISTER_MAX as usize + 1], &'static str> { + pub fn dump_registers(&mut self) -> Result<[u8; REGISTER_MAX as usize + 1], IoError> { // A dynamic register for dumping purposes struct DynamicRegister(u8); impl Register for DynamicRegister { @@ -457,7 +463,7 @@ where } /// Dumps the entire FIFO contents #[cfg(feature = "debug")] - pub fn dump_fifo(&mut self) -> Result<[u8; RFM95_FIFO_SIZE], &'static str> { + pub fn dump_fifo(&mut self) -> Result<[u8; RFM95_FIFO_SIZE], IoError> { // Save FIFO position let fifo_position = self.spi.read(RegFifoAddrPtr)?;