From cd1991539ce2973fba401cad7241af929f063acd Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 15 Jun 2022 23:46:11 +0200 Subject: [PATCH] Prepare for release: - Cargo - use `heapless` for `Deque` - split to modules `fps` & `std_clock` - lib - add more attributes --- Cargo.toml | 17 +++-- src/fps.rs | 108 ++++++++++++++++++++++++++ src/lib.rs | 193 +++-------------------------------------------- src/std_clock.rs | 71 +++++++++++++++++ 4 files changed, 203 insertions(+), 186 deletions(-) create mode 100644 src/fps.rs create mode 100644 src/std_clock.rs diff --git a/Cargo.toml b/Cargo.toml index f175a81..439c6f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,19 +2,24 @@ name = "embedded-fps" version = "0.1.0" authors = ["Lechev.space ", "Lachezar Lechev"] -description = "Frames per second counter for embedded devices" +description = "Frames Per Second counter for embedded devices" categories = ["embedded", "no-std", "graphics"] -keywords = ["graphics", "embedded", "no_std", "frames per second", "fps"] +keywords = ["graphics", "embedded", "no_std", "frames-per-second", "fps"] license = "MIT OR Apache-2.0" repository = "https://github.com/LechevSpace/embedded-fps" documentation = "https://docs.rs/embedded-fps" +homepage = "https://github.com/LechevSpace/embedded-fps" edition = "2021" -# Because of `holodeque` and the `array_map` rust feature that was introduced in 1.55 -# and edition 2021 requires at least 1.56 + +# Edition 2021 requires at least 1.56 rust-version = "1.56" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [features] default = [] @@ -30,7 +35,9 @@ required-features = ["std"] [dependencies] embedded-time = "0.12" -holodeque = {version = "0.2", default-features = false, git = "https://github.com/elpiel/holodeque", branch = "derive-copy-for-array-deque"} +heapless = {version = "0.7", default-features = false } + +[dev-dependencies] # used for the `embedded-graphics` example embedded-graphics-simulator = "0.3" embedded-graphics = "0.7" \ No newline at end of file diff --git a/src/fps.rs b/src/fps.rs new file mode 100644 index 0000000..bf3eb26 --- /dev/null +++ b/src/fps.rs @@ -0,0 +1,108 @@ +use embedded_time::{duration::Seconds, Clock, Instant}; +use heapless::Deque; + + + +/// Measures Frames Per Second (FPS). +/// +/// `MAX_FPS` - Defines the maximum FPS that you expect to measure. +#[derive(Debug, Clone)] +pub struct FPS { + /// The last registered frames. + last_second_frames: Deque, MAX_FPS>, + /// The embedded [`Clock`] that will be used to track the passed second. + clock: C, +} + +impl FPS { + /// Creates a new Frames Per Second counter. + pub fn new(clock: C) -> FPS { + FPS { + last_second_frames: Deque::<_, MAX_FPS>::new(), + clock, + } + } + + /// Adds another frame tick and returns the current Frames Pre Second. + /// + /// # Panics + /// + /// When [`Clock::try_now()`] returns an error or if the `MAX_FPS` is reached. + pub fn tick(&mut self) -> usize { + self.try_tick().unwrap() + } + + /// Adds another frame tick and returns the current Frames Pre Second. + /// + /// This method will not panic if the `MAX_FPS` is reached, + /// instead it will just return the `MAX_FPS` value (capping it in a nutshell). + /// + /// # Panics + /// + /// If [`Clock::try_now()`] returns an error. + pub fn tick_max(&mut self) -> usize { + self.try_tick_max().unwrap() + } + + /// Adds another frame tick and returns the current Frames Pre Second. + /// + /// This method will not return an error if the `MAX_FPS` is reached, + /// instead it will just return the `MAX_FPS` value (capping it in a nutshell). + pub fn try_tick_max(&mut self) -> Result { + match self.try_tick() { + Ok(fps) => Ok(fps), + Err(Error::MaxFPS(_)) => Ok(MAX_FPS), + Err(err) => Err(err), + } + } + + /// Adds another frame tick and returns the current Frames Pre Second. + /// + /// # Panics + /// + /// When [`Clock::try_now()`] returns an error or if the `MAX_FPS` is reached. + pub fn try_tick(&mut self) -> Result { + let now = self.clock.try_now().map_err(Error::Clock)?; + let a_second_ago = now - Seconds(1); + + while self + .last_second_frames + .front() + .copied() + .map_or(false, |tick| tick < a_second_ago) + { + self.last_second_frames.pop_front(); + } + + self.last_second_frames + .push_back(now) + .map_err(|_cap_err| Error::MaxFPS(MAX_FPS))?; + + // return the frames per second + Ok(self.last_second_frames.len()) + } +} + +impl Default for FPS +where + C: Clock + Default, +{ + fn default() -> Self { + Self::new(C::default()) + } +} + +/// The errors that [`FPS`] can return. +/// +/// Keep in mind that [`Error::MaxFPS`] will trigger panic on [`FPS::tick`] +/// or be returned as an error on [`FPS::try_tick`]. +#[derive(Debug)] +pub enum Error { + /// The clock returned an error when calling [`Clock::try_now`]. + Clock(embedded_time::clock::Error), + /// The maximum reading of Frames per second was reached. + /// The internal deque reached it's capacity. + /// + /// Increase the `MAX_FPS` to avoid this problem. + MaxFPS(usize), +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 2d3d60c..62f3f59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,193 +110,24 @@ // Rustdoc lints #![deny(rustdoc::broken_intra_doc_links)] // Rustc lints -#![deny(missing_docs, unused_imports)] +#![deny( + missing_debug_implementations, + missing_docs, + rust_2018_idioms, + unreachable_pub, + unused_imports +)] // adds `#[no_std]` attribute if the `std` feature is not enabled #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] -use embedded_time::{duration::Seconds, Clock, Instant}; -use holodeque::ArrayDeque; +pub use fps::{FPS, Error}; + +mod fps; #[cfg(feature = "std")] #[doc(inline)] pub use std_clock::StdClock; -/// Measures Frames Per Second (FPS). -/// -/// `MAX_FPS` - Defines the maximum FPS that you expect to measure. -#[derive(Debug, Clone)] -pub struct FPS { - /// The last registered frames. - last_second_frames: ArrayDeque>, MAX_FPS>, - /// The embedded [`Clock`] that will be used to track the passed second. - clock: C, -} - -impl FPS { - /// Creates a new Frames Per Second counter. - pub fn new(clock: C) -> FPS { - FPS { - last_second_frames: ArrayDeque::<_, MAX_FPS>::new(), - clock, - } - } - - /// Adds another frame tick and returns the current Frames Pre Second. - /// - /// # Panics - /// - /// When [`Clock::try_now()`] returns an error or if the `MAX_FPS` is reached. - pub fn tick(&mut self) -> usize { - self.try_tick().unwrap() - } - - /// Adds another frame tick and returns the current Frames Pre Second. - /// - /// This method will not panic if the `MAX_FPS` is reached, - /// instead it will just return the `MAX_FPS` value (capping it in a nutshell). - /// - /// # Panics - /// - /// If [`Clock::try_now()`] returns an error. - pub fn tick_max(&mut self) -> usize { - self.try_tick_max().unwrap() - } - - /// Adds another frame tick and returns the current Frames Pre Second. - /// - /// This method will not return an error if the `MAX_FPS` is reached, - /// instead it will just return the `MAX_FPS` value (capping it in a nutshell). - pub fn try_tick_max(&mut self) -> Result { - match self.try_tick() { - Ok(fps) => Ok(fps), - Err(Error::MaxFPS(_)) => Ok(MAX_FPS), - Err(err) => Err(err), - } - } - - /// Adds another frame tick and returns the current Frames Pre Second. - /// - /// # Panics - /// - /// When [`Clock::try_now()`] returns an error or if the `MAX_FPS` is reached. - pub fn try_tick(&mut self) -> Result { - let now = self.clock.try_now().map_err(Error::Clock)?; - let a_second_ago = now - Seconds(1); - - while self - .last_second_frames - .front() - .copied() - .flatten() - .map_or(false, |tick| tick < a_second_ago) - { - self.last_second_frames.pop_front(); - } - - self.last_second_frames - .push_back(Some(now)) - .map_err(|_cap_err| Error::MaxFPS(MAX_FPS))?; - - // return the frames per second - Ok(self.last_second_frames.len()) - } -} - -impl Default for FPS -where - C: Clock + Default, -{ - fn default() -> Self { - Self::new(C::default()) - } -} - -#[derive(Debug)] -/// The errors that [`FPS`] can return. -/// -/// Keep in mind that [`Error::MaxFPS`] will trigger panic on [`FPS::tick`] -/// or be returned as an error on [`FPS::try_tick`]. -pub enum Error { - /// The clock returned an error when calling [`Clock::try_now`]. - Clock(embedded_time::clock::Error), - /// The maximum reading of Frames per second was reached - /// The internal deque reached it's capacity. - /// - /// Increase the `MAX_FPS` to avoid this problem. - MaxFPS(usize), -} - #[cfg(feature = "std")] -mod std_clock { - use std::time::Instant as StdInstant; - - use embedded_time::{clock::Error, rate::Fraction, Clock, Instant as EmbeddedInstant}; - - /// A Standard clock based on [`std`]. - /// - /// It takes the [`Instant::elapsed()`] time and uses nanoseconds converted to [`u64`]. - /// This still leaves us with ~594 years of representable time - /// - /// [`Instant::elapsed()`]: std::time::Instant::elapsed() - #[derive(Debug, Clone, Copy)] - pub struct StdClock(StdInstant); - - impl Default for StdClock { - fn default() -> Self { - Self::new() - } - } - - impl StdClock { - /// Creates a new [`StdClock`]. - /// Internally it calls [`Instant::now()`]. - /// - /// [`Instant::now()`]: std::time::Instant::now() - pub fn new() -> Self { - Self(StdInstant::now()) - } - } - - impl Clock for StdClock { - type T = u64; - - const SCALING_FACTOR: Fraction = Fraction::new(1, 1_000_000_000); - - fn try_now(&self) -> Result, Error> { - // discarding the upper u64 still leaves us with ~594 years of representable time - Ok(EmbeddedInstant::new(self.0.elapsed().as_nanos() as u64)) - } - } - - #[cfg(test)] - mod tests { - use std::thread::sleep; - - use embedded_time::{ - duration::{Extensions, Milliseconds}, - Clock, - }; - - use super::StdClock; - - #[test] - fn it_creates_std_instant_from_milliseconds_clock() { - let clock = StdClock::new(); - - sleep(std::time::Duration::from_millis(400)); - - let start = clock.try_now().unwrap(); - // wait 1.5 seconds - sleep(std::time::Duration::from_millis(1_600)); - let end = clock.try_now().unwrap(); - - let elapsed = Milliseconds::::try_from(end - start).unwrap(); - - let lower_bound = Milliseconds::::try_from(1_599_u32.milliseconds()).unwrap(); - assert!(elapsed > lower_bound); - - let upper_bound = Milliseconds::::try_from(2_000_u32.milliseconds()).unwrap(); - assert!(elapsed < upper_bound); - } - } -} +mod std_clock; diff --git a/src/std_clock.rs b/src/std_clock.rs new file mode 100644 index 0000000..5affb56 --- /dev/null +++ b/src/std_clock.rs @@ -0,0 +1,71 @@ +use std::time::Instant as StdInstant; + + use embedded_time::{clock::Error, rate::Fraction, Clock, Instant as EmbeddedInstant}; + + /// A Standard clock based on [`std`]. + /// + /// It takes the [`Instant::elapsed()`] time and uses nanoseconds converted to [`u64`]. + /// This still leaves us with ~594 years of representable time + /// + /// [`Instant::elapsed()`]: std::time::Instant::elapsed() + #[derive(Debug, Clone, Copy)] + pub struct StdClock(StdInstant); + + impl Default for StdClock { + fn default() -> Self { + Self::new() + } + } + + impl StdClock { + /// Creates a new [`StdClock`]. + /// Internally it calls [`Instant::now()`]. + /// + /// [`Instant::now()`]: std::time::Instant::now() + pub fn new() -> Self { + Self(StdInstant::now()) + } + } + + impl Clock for StdClock { + type T = u64; + + const SCALING_FACTOR: Fraction = Fraction::new(1, 1_000_000_000); + + fn try_now(&self) -> Result, Error> { + // discarding the upper u64 still leaves us with ~594 years of representable time + Ok(EmbeddedInstant::new(self.0.elapsed().as_nanos() as u64)) + } + } + + #[cfg(test)] + mod tests { + use std::thread::sleep; + + use embedded_time::{ + duration::{Extensions, Milliseconds}, + Clock, + }; + + use super::StdClock; + + #[test] + fn it_creates_std_instant_from_milliseconds_clock() { + let clock = StdClock::new(); + + sleep(std::time::Duration::from_millis(400)); + + let start = clock.try_now().unwrap(); + // wait 1.5 seconds + sleep(std::time::Duration::from_millis(1_600)); + let end = clock.try_now().unwrap(); + + let elapsed = Milliseconds::::try_from(end - start).unwrap(); + + let lower_bound = Milliseconds::::try_from(1_599_u32.milliseconds()).unwrap(); + assert!(elapsed > lower_bound); + + let upper_bound = Milliseconds::::try_from(2_000_u32.milliseconds()).unwrap(); + assert!(elapsed < upper_bound); + } + } \ No newline at end of file