1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
//! Data collection Rust library for embedded systems, such as the Arduino.
//!
//! ## Overview
//! This library enables the creation of an array of a generic type with a maximum capacity of rows.
//! This crate is meant to be used with the `ufmt` crate in a `no_std` environment. It was specifically
//! created for sensor data collection on small microcrontrollers, such as the Arduino.
//!
//! All data is saved on the stack, so no heap allocations are required. Column names are defined for the row type
//! when the data table is created. The data table can be appended to up to the maximum number
//! of rows defined at compile time. The data table contents can be erased to reset the length to zero.
//!
//! A [`uDataTable`](crate::uDataTable) structure can be displayed with `ufmt` using the `uDisplay` or `uDebug` trait.
//! The intent is to use the `uDisplay` trait to print the data in a csv format and the `uDebug`
//! trait to print the headers and the length of the table. You must define the `uDisplay` and
//! `uDebug` traits for the row type if your row type is not a primitive type.
//!
//! The [`uDataTable`](crate::uDataTable) structure can also be plotted with the optional `plot` feature. The [`plot`](crate::uDataTable::plot) method
//! requires a function that takes a reference to the row type and returns an `i32`. The
//! `plot` method will plot the values returned by the function for each row in the data table.
//!
//! ## Usage
//! Add the following to your `Cargo.toml` file to use the `udatatable` crate.
//! ```toml
//! [dependencies]
//! udatatable = "0.1"
//! ```
//! ### Features
//! * `plot` - Enables the [`plot`](crate::uDataTable::plot) method. This was made an option feature to allow you to keep your
//! code size small if you don't need the [`plot`](crate::uDataTable::plot) method.
//! ## Example
//! Create a data table, append rows, and display the contents. Note that the row type must
//! implement the `Copy`, `Default`, `uDebug`, and `uDisplay` traits.
//! ```rust
//! use ufmt::{uDebug, uDisplay, uWrite, uwrite, uwriteln, Formatter};
//! use udatatable::uDataTable;
//!
//! // Define the row type
//! #[derive(Copy, Clone, Default)]
//! struct Row {
//! a: u32,
//! b: u32,
//! c: u32,
//! }
//!
//! // Define the uDisplay and uDebug traits for the row type
//! impl uDebug for Row {
//! fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
//! where
//! W: uWrite + ?Sized,
//! {
//! uwrite!(f, "Row {{ a: {}, b: {}, c: {} }}", self.a, self.b, self.c)
//! }
//! }
//!
//! impl uDisplay for Row {
//! fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
//! where
//! W: uWrite + ?Sized,
//! {
//! // The uDisplay trait is meant to print the data in a csv format
//! uwrite!(f, "{}, {}, {}", self.a, self.b, self.c)
//! }
//! }
//!
//! // Create the data table
//! const N: usize = 10;
//! const M: usize = 3;
//! let mut table = uDataTable::<Row, N, M>::new(["a", "b", "c"]);
//!
//! // Append rows to the data table
//! for i in 0..5 {
//! let row = Row {
//! a: i as u32,
//! b: i as u32 * 2,
//! c: i as u32 * 3,
//! };
//! if let Err(error) = table.append(row) {
//! // handle the error
//! }
//! }
//!
//! assert_eq!(table.length(), 5);
//! assert_eq!(*table.headers(), ["a", "b", "c"]);
//! assert_eq!(table.get(0).unwrap().a, 0);
//! assert_eq!(table.get(0).unwrap().b, 0);
//! assert_eq!(table.get(0).unwrap().c, 0);
//! assert_eq!(table.get(1).unwrap().a, 1);
//! assert_eq!(table.get(1).unwrap().b, 2);
//! assert_eq!(table.get(1).unwrap().c, 3);
//! assert_eq!(table.get(2).unwrap().a, 2);
//! assert_eq!(table.get(2).unwrap().b, 4);
//! assert_eq!(table.get(2).unwrap().c, 6);
//!
//! // Display the data table
//! let mut s = String::new();
//! ufmt::uwrite!(&mut s, "{}", table).ok();
//! assert_eq!(s, "\"a\",\"b\",\"c\"\n0, 0, 0\n1, 2, 3\n2, 4, 6\n3, 6, 9\n4, 8, 12\n");
//!
//! // Display the data table with uDebug
//! let mut s = String::new();
//! ufmt::uwrite!(&mut s, "{:?}", table).ok();
//! assert_eq!(s, "uDataTable<[\"a\", \"b\", \"c\"], length: 5>");
//!
//! #[cfg(feature = "plot")]
//! {
//! // graph the data table for value `a`
//! let mut s = String::new();
//! table.plot(&mut s, |row| row.a as i32);
//! assert_eq!(s, "4 | *\n | *.\n | *..\n | *...\n0 |*....\n");
//! }
//! ```
//!
#![no_std]
use ufmt::{uDebug, uDisplay, uWrite, uwrite, uwriteln, Formatter};
/// The [`uDataTable`] structure.
/// # Generic Parameters
/// * `T` - The row type. This type must implement the `Copy`, `Default`, `uDebug`, and `uDisplay` traits.
/// * `N` - The maximum number of rows in the data table. This value must be greater than zero. Note that the
/// data table will be stored on the stack, so the maximum number of rows should be kept small.
/// * `M` - The number of columns in the data table, or more specifically, the number of column names that will be passed
/// to the `new` method's `headers` parameter. This value must be greater than zero.
/// # Fields
/// * `headers` - An array of `M` strings that are the column names for the data table.
/// * `data` - An array of `N` rows of type `T`.
/// * `length` - The number of rows of data that has been inserted into the able. This value will be between 0..N.
#[allow(non_camel_case_types)]
pub struct uDataTable<'a, T: Copy + Default + uDebug + uDisplay, const N: usize, const M: usize> {
headers: [&'a str; M],
data: [T; N],
length: usize,
}
#[allow(dead_code)]
impl<'a, T: Copy + Default + uDebug + uDisplay, const N: usize, const M: usize>
uDataTable<'a, T, N, M>
{
/// Create a new data table with the specified headers passed in `headers`. There should be M headers in the passed `headers` array.
pub fn new(headers: [&'a str; M]) -> Self {
Self {
headers,
data: [T::default(); N],
length: 0,
}
}
/// Get a reference to the row at the specified `index`. The `index`` must be less than the length of the table.
pub fn get(&self, index: usize) -> Result<&T, uDataTableError> {
if index < self.length {
Result::Ok(&self.data[index])
} else {
Result::Err(uDataTableError::RowIndexOutOfBounds)
}
}
/// Append a row to the data table. The length of the table will be increased by one. The row
/// will be copied into the data table. The row must implement the Copy trait. If the length
/// of the table is equal to N, then the row will not be appended and an error will be returned.
pub fn append(&mut self, row: T) -> Result<&T, uDataTableError> {
if self.length < N {
self.data[self.length] = row;
self.length += 1;
Result::Ok(&self.data[self.length - 1])
} else {
Result::Err(uDataTableError::CannotGrowTable)
}
}
/// Erase the data table. The length of the table will be set to zero.
pub fn erase(&mut self) {
self.length = 0;
for i in 0..N {
self.data[i] = T::default();
}
}
/// Get the length of the data table.
pub fn length(&self) -> usize {
self.length
}
/// Get a reference to the headers of the data table.
pub fn headers(&self) -> &[&'a str; M] {
&self.headers
}
/// Plots the data table. The data table will be plotted with rows on the horizontal axis and values
/// on the vertical axis. The plot method will scan thrugh all the rows in th data table
/// with the passed `value` function to determine the range of values to be plotted. The plot method will then
/// scale the values to fit in the display area. The plot method will display the range of values
/// on the vertical axis and the row index on the horizontal axis.
///
/// # Arguments
///
/// * `f` - The `ufmt::uWrite` object that the grph should be printed to.
/// * `value` - A function that gets called on each row in the data table to determine the value from that row to plot.
/// This function must take a reference to the row type and return an `i32`. The mapping of the desired
/// row value to the `i32` is for display purposes.
#[cfg(any(feature = "plot", doc))]
pub fn plot<W>(&self, f: &mut W, value: fn(&T) -> i32)
where
W: uWrite + ?Sized,
{
// first we need to scan through the data to find the range of
// values that we need to plot
let mut min = i32::MAX;
let mut max = i32::MIN;
for row in self.data.iter() {
let value = value(row);
if value < min {
min = value;
}
if value > max {
max = value;
}
}
let min_digits = Self::count_digits(min);
let max_digits = Self::count_digits(max);
let digits = if min_digits > max_digits {
min_digits
} else {
max_digits
};
// now we can calculate the scale factor
let scale = 1.0 / (max - min) as f32;
const MAX_HEIGHT: i32 = 23;
let display_height = if (max - min) as i32 > MAX_HEIGHT {
MAX_HEIGHT
} else {
(max - min) as i32
};
// now we can plot the data with rows on horizontal axis and values on vertical axis
for h in (0..display_height + 1).rev() {
if h == (display_height as f32 * (0 - min) as f32 / (max - min) as f32) as i32 {
Self::write_n_spaces(digits - 1, f);
uwrite!(f, "0 |").ok();
} else if h == display_height {
Self::write_n_spaces(digits - max_digits, f);
uwrite!(f, "{} |", max).ok();
} else if h == 0 {
Self::write_n_spaces(digits - min_digits, f);
uwrite!(f, "{} |", min).ok();
} else {
Self::write_n_spaces(digits, f);
uwrite!(f, " |").ok();
}
for r in 0..self.length() {
if let Result::Ok(row) = self.get(r) {
let value = value(row);
let scaled_value =
((value - min) as f32 * scale * display_height as f32) as i32;
if scaled_value == h {
uwrite!(f, "*").ok();
} else if scaled_value > h {
uwrite!(f, ".").ok();
} else {
uwrite!(f, " ").ok();
}
}
}
uwriteln!(f, "").ok();
}
}
fn count_digits(value: i32) -> u32 {
let mut n = value;
let mut count = 0;
if n < 0 {
n = -n;
count += 1; // for the '-' sign
}
loop {
count += 1;
n /= 10;
if n == 0 {
break;
}
}
count
}
#[cfg(feature = "plot")]
fn write_n_spaces<W>(n: u32, f: &mut W)
where
W: uWrite + ?Sized,
{
for _ in 0..n {
uwrite!(f, " ").ok();
}
}
}
impl<'a, T: Copy + Default + uDebug + uDisplay, const N: usize, const M: usize> uDebug
for uDataTable<'a, T, N, M>
{
fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
where
W: uWrite + ?Sized,
{
uwrite!(f, "uDataTable<[\"")?;
for i in 0..M {
uwrite!(f, "{}", self.headers[i])?;
if i < M - 1 {
uwrite!(f, "\", \"")?;
}
}
uwrite!(f, "\"], length: {}>", self.length)?;
Result::Ok(())
}
}
impl<'a, T: Copy + Default + uDebug + uDisplay, const N: usize, const M: usize> uDisplay
for uDataTable<'a, T, N, M>
{
fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
where
W: uWrite + ?Sized,
{
for i in 0..M {
uwrite!(f, "\"{}\"", self.headers[i])?;
if i < M - 1 {
uwrite!(f, ",")?;
}
}
uwrite!(f, "\n")?;
for i in 0..self.length {
uwriteln!(f, "{}", self.data[i])?;
}
Ok(())
}
}
/// Errors that can occur when using the uDataTable structure.
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, Debug)]
pub enum uDataTableError {
/// The passed row index is out of bounds.
RowIndexOutOfBounds,
/// The data table cannot grow any larger.
CannotGrowTable,
}
impl uDebug for uDataTableError {
fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
where
W: uWrite + ?Sized,
{
match self {
uDataTableError::RowIndexOutOfBounds => uwrite!(f, "RowIndexOutOfBounds"),
uDataTableError::CannotGrowTable => uwrite!(f, "CannotGrowTable"),
}
}
}
impl uDisplay for uDataTableError {
fn fmt<W>(&self, f: &mut Formatter<'_, W>) -> Result<(), W::Error>
where
W: uWrite + ?Sized,
{
uDebug::fmt(self, f)
}
}