embedded_charts/
heapless_utils.rs

1//! Heapless utilities and enhanced no_std support
2//!
3//! This module provides enhanced heapless integration for no_std embedded systems,
4//! including configurable sizing, utility macros, and specialized data structures.
5
6use crate::error::{DataError, DataResult};
7use heapless::{String, Vec};
8
9/// Common heapless sizes optimized for different embedded system constraints
10pub mod sizes {
11    /// Ultra-constrained systems (≤1KB RAM)
12    pub mod ultra {
13        /// 8 character strings for labels
14        pub type LabelString = heapless::String<8>;
15        /// 4 element vectors for colors/positions
16        pub type SmallVec<T> = heapless::Vec<T, 4>;
17        /// 16 point data series
18        pub type DataVec<T> = heapless::Vec<T, 16>;
19        /// 2 series maximum
20        pub type SeriesVec<T> = heapless::Vec<T, 2>;
21    }
22
23    /// Small embedded systems (1-4KB RAM)
24    pub mod small {
25        /// 16 character strings for labels
26        pub type LabelString = heapless::String<16>;
27        /// 8 element vectors for colors/positions
28        pub type SmallVec<T> = heapless::Vec<T, 8>;
29        /// 64 point data series
30        pub type DataVec<T> = heapless::Vec<T, 64>;
31        /// 4 series maximum
32        pub type SeriesVec<T> = heapless::Vec<T, 4>;
33    }
34
35    /// Medium embedded systems (4-16KB RAM)
36    pub mod medium {
37        /// 32 character strings for labels
38        pub type LabelString = heapless::String<32>;
39        /// 16 element vectors for colors/positions
40        pub type SmallVec<T> = heapless::Vec<T, 16>;
41        /// 256 point data series
42        pub type DataVec<T> = heapless::Vec<T, 256>;
43        /// 8 series maximum
44        pub type SeriesVec<T> = heapless::Vec<T, 8>;
45    }
46
47    /// Large embedded systems (≥16KB RAM)
48    pub mod large {
49        /// 64 character strings for labels
50        pub type LabelString = heapless::String<64>;
51        /// 32 element vectors for colors/positions
52        pub type SmallVec<T> = heapless::Vec<T, 32>;
53        /// 512 point data series
54        pub type DataVec<T> = heapless::Vec<T, 512>;
55        /// 16 series maximum
56        pub type SeriesVec<T> = heapless::Vec<T, 16>;
57    }
58
59    /// Default sizes based on feature configuration
60    #[cfg(feature = "minimal-memory")]
61    pub use ultra::*;
62
63    #[cfg(all(feature = "static-only", not(feature = "minimal-memory")))]
64    pub use small::*;
65
66    #[cfg(all(
67        not(feature = "static-only"),
68        not(feature = "minimal-memory"),
69        feature = "no_std"
70    ))]
71    pub use medium::*;
72
73    #[cfg(all(
74        feature = "std",
75        not(any(feature = "static-only", feature = "minimal-memory"))
76    ))]
77    pub use large::*;
78
79    #[cfg(not(any(
80        feature = "minimal-memory",
81        feature = "static-only",
82        feature = "no_std",
83        feature = "std"
84    )))]
85    pub use medium::*;
86}
87
88/// Heapless string utilities
89pub mod string {
90    use super::*;
91
92    /// Create a heapless string from a string slice with error handling
93    pub fn try_from_str<const N: usize>(s: &str) -> DataResult<String<N>> {
94        String::try_from(s).map_err(|_| DataError::buffer_full("create heapless string", N))
95    }
96
97    /// Create a heapless string from a string slice, truncating if necessary
98    pub fn from_str_truncate<const N: usize>(s: &str) -> String<N> {
99        let truncated = if s.len() > N { &s[..N] } else { s };
100        String::try_from(truncated).unwrap_or_else(|_| String::new())
101    }
102
103    /// Safely push a string slice to a heapless string
104    pub fn push_str_safe<const N: usize>(string: &mut String<N>, s: &str) -> DataResult<()> {
105        string
106            .push_str(s)
107            .map_err(|_| DataError::buffer_full("push string", N))
108    }
109
110    /// Push a character with error handling
111    pub fn push_char_safe<const N: usize>(string: &mut String<N>, c: char) -> DataResult<()> {
112        string
113            .push(c)
114            .map_err(|_| DataError::buffer_full("push character", N))
115    }
116
117    /// Format a number into a heapless string
118    pub fn format_number<const N: usize>(value: f32, precision: usize) -> String<N> {
119        let mut result = String::new();
120
121        // Handle negative numbers
122        if value < 0.0 {
123            let _ = result.push('-');
124        }
125
126        let abs_value = if value < 0.0 { -value } else { value };
127
128        // Integer part
129        let integer_part = abs_value as u32;
130        let _ = format_integer(&mut result, integer_part);
131
132        // Decimal part if precision > 0
133        if precision > 0 {
134            let _ = result.push('.');
135            let mut fractional = abs_value - integer_part as f32;
136
137            for _ in 0..precision {
138                fractional *= 10.0;
139                let digit = fractional as u32 % 10;
140                let _ = result.push((b'0' + digit as u8) as char);
141            }
142        }
143
144        result
145    }
146
147    /// Format an integer into a heapless string
148    fn format_integer<const N: usize>(string: &mut String<N>, mut value: u32) -> DataResult<()> {
149        if value == 0 {
150            return push_char_safe(string, '0');
151        }
152
153        let mut digits = Vec::<u8, 16>::new();
154        while value > 0 {
155            digits
156                .push((value % 10) as u8)
157                .map_err(|_| DataError::buffer_full("format integer", 16))?;
158            value /= 10;
159        }
160
161        // Reverse digits and push to string
162        for &digit in digits.iter().rev() {
163            push_char_safe(string, (b'0' + digit) as char)?;
164        }
165
166        Ok(())
167    }
168}
169
170/// Heapless vector utilities
171pub mod vec {
172    use super::*;
173
174    /// Safely push an item to a heapless vector
175    pub fn push_safe<T, const N: usize>(vec: &mut Vec<T, N>, item: T) -> DataResult<()> {
176        vec.push(item)
177            .map_err(|_| DataError::buffer_full("push item", N))
178    }
179
180    /// Extend a heapless vector from an iterator with error handling
181    pub fn extend_safe<T, I, const N: usize>(vec: &mut Vec<T, N>, iter: I) -> DataResult<()>
182    where
183        I: IntoIterator<Item = T>,
184    {
185        for item in iter {
186            push_safe(vec, item)?;
187        }
188        Ok(())
189    }
190
191    /// Create a heapless vector from a slice with error handling
192    pub fn try_from_slice<T: Clone, const N: usize>(slice: &[T]) -> DataResult<Vec<T, N>> {
193        if slice.len() > N {
194            return Err(DataError::buffer_full("create from slice", N));
195        }
196        Vec::from_slice(slice).map_err(|_| DataError::buffer_full("create from slice", N))
197    }
198
199    /// Sort a heapless vector using insertion sort (good for small vecs)
200    pub fn insertion_sort<T: Ord + Clone, const N: usize>(vec: &mut Vec<T, N>) {
201        let len = vec.len();
202        for i in 1..len {
203            let key = vec[i].clone();
204            let mut j = i;
205
206            while j > 0 && vec[j - 1] > key {
207                vec[j] = vec[j - 1].clone();
208                j -= 1;
209            }
210            vec[j] = key;
211        }
212    }
213
214    /// Find the index of an item in a heapless vector
215    pub fn find_index<T: PartialEq, const N: usize>(vec: &Vec<T, N>, item: &T) -> Option<usize> {
216        vec.iter().position(|x| x == item)
217    }
218
219    /// Remove an item by value from a heapless vector
220    pub fn remove_item<T: PartialEq + Clone, const N: usize>(
221        vec: &mut Vec<T, N>,
222        item: &T,
223    ) -> bool {
224        if let Some(index) = find_index(vec, item) {
225            vec.remove(index);
226            true
227        } else {
228            false
229        }
230    }
231}
232
233/// Memory pool for reusable heapless data structures
234pub struct HeaplessPool<T, const N: usize> {
235    pool: Vec<Option<T>, N>,
236    free_list: Vec<usize, N>,
237}
238
239impl<T, const N: usize> HeaplessPool<T, N> {
240    /// Create a new memory pool
241    pub fn new() -> Self {
242        let mut pool = Vec::new();
243        let mut free_list = Vec::new();
244
245        // Initialize pool with None values
246        for i in 0..N {
247            let _ = pool.push(None);
248            let _ = free_list.push(i);
249        }
250
251        Self { pool, free_list }
252    }
253
254    /// Allocate an item from the pool
255    pub fn allocate(&mut self, item: T) -> Option<usize> {
256        if let Some(index) = self.free_list.pop() {
257            self.pool[index] = Some(item);
258            Some(index)
259        } else {
260            None
261        }
262    }
263
264    /// Deallocate an item back to the pool
265    pub fn deallocate(&mut self, index: usize) -> Option<T> {
266        if index < N {
267            if let Some(item) = self.pool[index].take() {
268                let _ = self.free_list.push(index);
269                Some(item)
270            } else {
271                None
272            }
273        } else {
274            None
275        }
276    }
277
278    /// Get a reference to an allocated item
279    pub fn get(&self, index: usize) -> Option<&T> {
280        self.pool.get(index)?.as_ref()
281    }
282
283    /// Get a mutable reference to an allocated item
284    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
285        self.pool.get_mut(index)?.as_mut()
286    }
287
288    /// Get the number of allocated items
289    pub fn allocated_count(&self) -> usize {
290        N - self.free_list.len()
291    }
292
293    /// Check if the pool is full
294    pub fn is_full(&self) -> bool {
295        self.free_list.is_empty()
296    }
297
298    /// Check if the pool is empty
299    pub fn is_empty(&self) -> bool {
300        self.free_list.len() == N
301    }
302}
303
304impl<T, const N: usize> Default for HeaplessPool<T, N> {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310/// Circular buffer implementation using heapless
311pub struct CircularBuffer<T: Copy, const N: usize> {
312    buffer: [Option<T>; N],
313    head: usize,
314    tail: usize,
315    full: bool,
316}
317
318impl<T: Copy, const N: usize> CircularBuffer<T, N> {
319    /// Create a new circular buffer
320    pub const fn new() -> Self {
321        Self {
322            buffer: [None; N],
323            head: 0,
324            tail: 0,
325            full: false,
326        }
327    }
328
329    /// Push an item to the buffer (overwrites oldest if full)
330    pub fn push(&mut self, item: T) {
331        self.buffer[self.head] = Some(item);
332
333        if self.full {
334            self.tail = (self.tail + 1) % N;
335        }
336
337        self.head = (self.head + 1) % N;
338
339        if self.head == self.tail {
340            self.full = true;
341        }
342    }
343
344    /// Pop the oldest item from the buffer
345    pub fn pop(&mut self) -> Option<T> {
346        if self.is_empty() {
347            return None;
348        }
349
350        let item = self.buffer[self.tail].take();
351        self.tail = (self.tail + 1) % N;
352        self.full = false;
353        item
354    }
355
356    /// Get the current length
357    pub fn len(&self) -> usize {
358        if self.full {
359            N
360        } else if self.head >= self.tail {
361            self.head - self.tail
362        } else {
363            N - self.tail + self.head
364        }
365    }
366
367    /// Check if the buffer is empty
368    pub fn is_empty(&self) -> bool {
369        !self.full && self.head == self.tail
370    }
371
372    /// Check if the buffer is full
373    pub fn is_full(&self) -> bool {
374        self.full
375    }
376
377    /// Get capacity
378    pub const fn capacity(&self) -> usize {
379        N
380    }
381
382    /// Clear the buffer
383    pub fn clear(&mut self) {
384        self.buffer = [None; N];
385        self.head = 0;
386        self.tail = 0;
387        self.full = false;
388    }
389
390    /// Iterate over items in chronological order
391    pub fn iter(&self) -> CircularBufferIter<T, N> {
392        CircularBufferIter {
393            buffer: &self.buffer,
394            current: self.tail,
395            remaining: self.len(),
396        }
397    }
398}
399
400impl<T: Copy, const N: usize> Default for CircularBuffer<T, N> {
401    fn default() -> Self {
402        Self::new()
403    }
404}
405
406/// Iterator for circular buffer
407pub struct CircularBufferIter<'a, T: Copy, const N: usize> {
408    buffer: &'a [Option<T>; N],
409    current: usize,
410    remaining: usize,
411}
412
413impl<'a, T: Copy, const N: usize> Iterator for CircularBufferIter<'a, T, N> {
414    type Item = T;
415
416    fn next(&mut self) -> Option<Self::Item> {
417        if self.remaining == 0 {
418            return None;
419        }
420
421        let item = self.buffer[self.current]?;
422        self.current = (self.current + 1) % N;
423        self.remaining -= 1;
424        Some(item)
425    }
426
427    fn size_hint(&self) -> (usize, Option<usize>) {
428        (self.remaining, Some(self.remaining))
429    }
430}
431
432impl<'a, T: Copy, const N: usize> ExactSizeIterator for CircularBufferIter<'a, T, N> {}
433
434/// Create a heapless string with error handling
435///
436/// # Examples
437///
438/// ```rust
439/// use embedded_charts::heapless_string;
440///
441/// // Create with explicit size  
442/// let result = heapless_string!("Hello", 16);
443/// assert!(result.is_ok());
444/// ```
445#[macro_export]
446macro_rules! heapless_string {
447    ($s:expr) => {
448        $crate::heapless_utils::string::try_from_str($s)
449    };
450    ($s:expr, $n:expr) => {
451        heapless::String::<$n>::try_from($s)
452    };
453}
454
455/// Create a heapless vector from items
456///
457/// # Examples
458///
459/// ```rust
460/// use embedded_charts::heapless_vec;
461///
462/// // Create with explicit capacity
463/// let vec: heapless::Vec<i32, 8> = heapless_vec![1, 2, 3];
464/// assert_eq!(vec.len(), 3);
465/// ```
466#[macro_export]
467macro_rules! heapless_vec {
468    ($($item:expr),* $(,)?) => {{
469        let mut vec = heapless::Vec::new();
470        $(
471            let _ = vec.push($item);
472        )*
473        vec
474    }};
475    ($item:expr; $count:expr) => {{
476        let mut vec = heapless::Vec::new();
477        for _ in 0..$count {
478            let _ = vec.push($item);
479        }
480        vec
481    }};
482}
483
484/// Configuration for heapless usage based on system constraints
485pub struct HeaplessConfig {
486    /// Maximum string length for labels
487    pub max_string_length: usize,
488    /// Maximum vector capacity for small collections
489    pub max_small_vec_capacity: usize,
490    /// Maximum data points per series
491    pub max_data_points: usize,
492    /// Maximum number of series
493    pub max_series_count: usize,
494}
495
496impl HeaplessConfig {
497    /// Ultra-constrained configuration (≤1KB RAM)
498    pub const ULTRA: Self = Self {
499        max_string_length: 8,
500        max_small_vec_capacity: 4,
501        max_data_points: 16,
502        max_series_count: 2,
503    };
504
505    /// Small embedded configuration (1-4KB RAM)
506    pub const SMALL: Self = Self {
507        max_string_length: 16,
508        max_small_vec_capacity: 8,
509        max_data_points: 64,
510        max_series_count: 4,
511    };
512
513    /// Medium embedded configuration (4-16KB RAM)
514    pub const MEDIUM: Self = Self {
515        max_string_length: 32,
516        max_small_vec_capacity: 16,
517        max_data_points: 256,
518        max_series_count: 8,
519    };
520
521    /// Large embedded configuration (≥16KB RAM)
522    pub const LARGE: Self = Self {
523        max_string_length: 64,
524        max_small_vec_capacity: 32,
525        max_data_points: 512,
526        max_series_count: 16,
527    };
528
529    /// Get the default configuration based on enabled features
530    pub const fn default() -> &'static Self {
531        #[cfg(feature = "minimal-memory")]
532        return &Self::ULTRA;
533
534        #[cfg(all(feature = "static-only", not(feature = "minimal-memory")))]
535        return &Self::SMALL;
536
537        #[cfg(all(
538            not(feature = "static-only"),
539            not(feature = "minimal-memory"),
540            feature = "no_std"
541        ))]
542        return &Self::MEDIUM;
543
544        #[cfg(all(
545            feature = "std",
546            not(any(feature = "static-only", feature = "minimal-memory"))
547        ))]
548        return &Self::LARGE;
549
550        #[cfg(not(any(
551            feature = "minimal-memory",
552            feature = "static-only",
553            feature = "no_std",
554            feature = "std"
555        )))]
556        return &Self::MEDIUM;
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_string_utilities() {
566        // Test safe string creation
567        let result: DataResult<String<16>> = string::try_from_str("Hello");
568        assert!(result.is_ok());
569        assert_eq!(result.unwrap().as_str(), "Hello");
570
571        // Test string too long
572        let result: DataResult<String<8>> = string::try_from_str("This is too long");
573        assert!(result.is_err());
574
575        // Test truncation
576        let truncated = string::from_str_truncate::<8>("This is too long");
577        assert_eq!(truncated.as_str(), "This is ");
578
579        // Test number formatting
580        let number_str = string::format_number::<16>(123.45, 2);
581        // Allow for floating point precision variations
582        assert!(number_str.as_str() == "123.45" || number_str.as_str() == "123.44");
583    }
584
585    #[test]
586    fn test_vec_utilities() {
587        let mut vec: Vec<i32, 8> = Vec::new();
588
589        // Test safe push
590        assert!(vec::push_safe(&mut vec, 42).is_ok());
591        assert_eq!(vec.len(), 1);
592        assert_eq!(vec[0], 42);
593
594        // Test extend
595        assert!(vec::extend_safe(&mut vec, [1, 2, 3]).is_ok());
596        assert_eq!(vec.len(), 4);
597
598        // Test sorting
599        vec::insertion_sort(&mut vec);
600        assert_eq!(vec.as_slice(), &[1, 2, 3, 42]);
601    }
602
603    #[test]
604    fn test_circular_buffer() {
605        let mut buffer: CircularBuffer<i32, 4> = CircularBuffer::new();
606
607        assert!(buffer.is_empty());
608        assert_eq!(buffer.len(), 0);
609
610        // Push items
611        buffer.push(1);
612        buffer.push(2);
613        buffer.push(3);
614        assert_eq!(buffer.len(), 3);
615
616        // Fill buffer
617        buffer.push(4);
618        assert!(buffer.is_full());
619        assert_eq!(buffer.len(), 4);
620
621        // Overflow (should overwrite oldest)
622        buffer.push(5);
623        assert_eq!(buffer.len(), 4);
624
625        // Test iteration
626        let items: Vec<i32, 4> = buffer.iter().collect();
627        assert_eq!(items.as_slice(), &[2, 3, 4, 5]);
628    }
629
630    #[test]
631    fn test_memory_pool() {
632        let mut pool: HeaplessPool<String<16>, 4> = HeaplessPool::new();
633
634        assert!(pool.is_empty());
635        assert!(!pool.is_full());
636
637        // Allocate items
638        let idx1 = pool.allocate(String::try_from("Hello").unwrap()).unwrap();
639        let idx2 = pool.allocate(String::try_from("World").unwrap()).unwrap();
640
641        assert_eq!(pool.allocated_count(), 2);
642        assert_eq!(pool.get(idx1).unwrap().as_str(), "Hello");
643        assert_eq!(pool.get(idx2).unwrap().as_str(), "World");
644
645        // Deallocate
646        let item = pool.deallocate(idx1).unwrap();
647        assert_eq!(item.as_str(), "Hello");
648        assert_eq!(pool.allocated_count(), 1);
649    }
650
651    #[test]
652    fn test_heapless_config() {
653        let config = HeaplessConfig::default();
654        assert!(config.max_string_length > 0);
655        assert!(config.max_data_points > 0);
656    }
657}