use crate::data::{DataPoint, Point2D};
use crate::error::{ChartError, ChartResult, DataError};
use heapless::Vec as HeaplessVec;
#[derive(Debug, Clone, Copy)]
pub struct RingBufferConfig {
pub overflow_mode: OverflowMode,
pub enable_events: bool,
pub preallocate: bool,
pub track_bounds: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowMode {
Overwrite,
Reject,
Callback,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RingBufferEvent {
DataAdded,
DataOverwritten,
BufferFull,
BufferEmpty,
BoundsChanged,
}
pub struct RingBuffer<T: DataPoint + Copy, const N: usize> {
data: HeaplessVec<T, N>,
write_pos: usize,
config: RingBufferConfig,
bounds: Option<DataBounds>,
event_handler: Option<fn(RingBufferEvent)>,
stats: RingBufferStats,
}
#[derive(Debug, Clone, Copy)]
struct DataBounds {
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RingBufferStats {
pub total_writes: u64,
pub total_reads: u64,
pub overflow_count: u64,
pub peak_usage: usize,
}
impl Default for RingBufferConfig {
fn default() -> Self {
Self {
overflow_mode: OverflowMode::Overwrite,
enable_events: false,
preallocate: false,
track_bounds: true,
}
}
}
impl<T: DataPoint + Copy, const N: usize> RingBuffer<T, N> {
pub fn new() -> Self {
Self::with_config(RingBufferConfig::default())
}
pub fn with_config(config: RingBufferConfig) -> Self {
let mut data = HeaplessVec::new();
if config.preallocate {
for _ in 0..N {
if let Some(default) = Self::default_value() {
let _ = data.push(default);
}
}
data.clear();
}
Self {
data,
write_pos: 0,
config,
bounds: None,
event_handler: None,
stats: RingBufferStats::default(),
}
}
fn default_value() -> Option<T> {
None
}
pub fn set_event_handler(&mut self, handler: fn(RingBufferEvent)) {
self.event_handler = Some(handler);
}
pub fn push(&mut self, value: T) -> ChartResult<()> {
self.stats.total_writes += 1;
if self.is_full() {
match self.config.overflow_mode {
OverflowMode::Reject => {
return Err(ChartError::DataError(DataError::BUFFER_FULL));
}
OverflowMode::Callback => {
self.trigger_event(RingBufferEvent::DataOverwritten);
}
OverflowMode::Overwrite => {
self.stats.overflow_count += 1;
}
}
}
let was_empty = self.is_empty();
if self.data.len() < N {
self.data
.push(value)
.map_err(|_| ChartError::DataError(DataError::BUFFER_FULL))?;
} else {
let oldest_idx = self.write_pos % self.data.len();
self.data[oldest_idx] = value;
self.write_pos = (self.write_pos + 1) % N;
}
if self.data.len() > self.stats.peak_usage {
self.stats.peak_usage = self.data.len();
}
if was_empty {
self.trigger_event(RingBufferEvent::DataAdded);
}
if self.is_full() {
self.trigger_event(RingBufferEvent::BufferFull);
}
Ok(())
}
pub fn extend<I>(&mut self, iter: I) -> ChartResult<usize>
where
I: IntoIterator<Item = T>,
{
let mut count = 0;
for value in iter {
match self.push(value) {
Ok(()) => count += 1,
Err(_) if self.config.overflow_mode == OverflowMode::Reject => break,
_ => {}
}
}
Ok(count)
}
pub fn pop(&mut self) -> Option<T> {
if self.is_empty() {
return None;
}
self.stats.total_reads += 1;
let value = self.data.remove(0);
if self.is_empty() {
self.trigger_event(RingBufferEvent::BufferEmpty);
self.bounds = None; }
Some(value)
}
pub fn peek(&self) -> Option<&T> {
self.data.first()
}
pub fn peek_newest(&self) -> Option<&T> {
self.data.last()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.data.iter()
}
pub fn iter_chronological(&self) -> ChronologicalIter<'_, T, N> {
ChronologicalIter {
buffer: self,
index: 0,
}
}
pub fn recent(&self, n: usize) -> impl Iterator<Item = &T> {
let n = n.min(self.data.len());
let start = self.data.len().saturating_sub(n);
self.data[start..].iter()
}
pub fn clear(&mut self) {
self.data.clear();
self.write_pos = 0;
self.bounds = None;
self.trigger_event(RingBufferEvent::BufferEmpty);
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn is_full(&self) -> bool {
self.data.len() >= N
}
pub fn capacity(&self) -> usize {
N
}
pub fn remaining_capacity(&self) -> usize {
N - self.data.len()
}
pub fn bounds(&self) -> Option<crate::data::bounds::DataBounds<f32, f32>> {
self.bounds.map(|b| crate::data::bounds::DataBounds {
min_x: b.min_x,
max_x: b.max_x,
min_y: b.min_y,
max_y: b.max_y,
})
}
pub fn stats(&self) -> &RingBufferStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = RingBufferStats::default();
}
pub fn for_each<F>(&self, mut f: F)
where
F: FnMut(&T),
{
for item in self.data.iter() {
f(item);
}
}
pub fn find<F>(&self, mut predicate: F) -> Option<&T>
where
F: FnMut(&T) -> bool,
{
self.data.iter().find(|item| predicate(item))
}
fn trigger_event(&self, event: RingBufferEvent) {
if self.config.enable_events {
if let Some(handler) = self.event_handler {
handler(event);
}
}
}
}
impl<T: DataPoint + Copy, const N: usize> Default for RingBuffer<T, N> {
fn default() -> Self {
Self::new()
}
}
pub type PointRingBuffer<const N: usize> = RingBuffer<Point2D, N>;
impl<const N: usize> RingBuffer<Point2D, N> {
fn update_bounds_for_point(&mut self, point: &Point2D) {
match &mut self.bounds {
Some(bounds) => {
let changed = point.x < bounds.min_x
|| point.x > bounds.max_x
|| point.y < bounds.min_y
|| point.y > bounds.max_y;
bounds.min_x = bounds.min_x.min(point.x);
bounds.max_x = bounds.max_x.max(point.x);
bounds.min_y = bounds.min_y.min(point.y);
bounds.max_y = bounds.max_y.max(point.y);
if changed {
self.trigger_event(RingBufferEvent::BoundsChanged);
}
}
None => {
self.bounds = Some(DataBounds {
min_x: point.x,
max_x: point.x,
min_y: point.y,
max_y: point.y,
});
self.trigger_event(RingBufferEvent::BoundsChanged);
}
}
}
pub fn push_point(&mut self, point: Point2D) -> ChartResult<()> {
self.push(point)?;
if self.config.track_bounds {
self.update_bounds_for_point(&point);
}
Ok(())
}
pub fn moving_average(&self, window_size: usize) -> Option<Point2D> {
let window_size = window_size.min(self.len());
if window_size == 0 {
return None;
}
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut count = 0;
for point in self.recent(window_size) {
sum_x += point.x;
sum_y += point.y;
count += 1;
}
if count > 0 {
Some(Point2D::new(sum_x / count as f32, sum_y / count as f32))
} else {
None
}
}
pub fn downsample(&self, factor: usize) -> heapless::Vec<Point2D, N> {
let mut result = heapless::Vec::new();
for (i, point) in self.iter().enumerate() {
if i % factor == 0 {
let _ = result.push(*point);
}
}
result
}
pub fn rate_of_change(&self) -> Option<f32> {
let oldest = self.peek()?;
let newest = self.peek_newest()?;
let dx = newest.x - oldest.x;
if dx.abs() < f32::EPSILON {
None
} else {
Some((newest.y - oldest.y) / dx)
}
}
}
pub struct ChronologicalIter<'a, T: DataPoint + Copy, const N: usize> {
buffer: &'a RingBuffer<T, N>,
index: usize,
}
impl<'a, T: DataPoint + Copy, const N: usize> Iterator for ChronologicalIter<'a, T, N> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.buffer.len() {
return None;
}
let item = if self.buffer.data.len() < N {
self.buffer.data.get(self.index)
} else {
let actual_index = (self.buffer.write_pos + self.index) % self.buffer.data.len();
self.buffer.data.get(actual_index)
};
self.index += 1;
item
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_buffer_basic() {
let mut buffer: RingBuffer<Point2D, 5> = RingBuffer::new();
assert!(buffer.is_empty());
assert_eq!(buffer.len(), 0);
assert_eq!(buffer.capacity(), 5);
buffer.push(Point2D::new(1.0, 2.0)).unwrap();
buffer.push(Point2D::new(2.0, 3.0)).unwrap();
assert_eq!(buffer.len(), 2);
assert!(!buffer.is_empty());
assert!(!buffer.is_full());
assert_eq!(buffer.peek().unwrap(), &Point2D::new(1.0, 2.0));
assert_eq!(buffer.peek_newest().unwrap(), &Point2D::new(2.0, 3.0));
}
#[test]
fn test_ring_buffer_overflow() {
let config = RingBufferConfig {
overflow_mode: OverflowMode::Overwrite,
..Default::default()
};
let mut buffer: RingBuffer<Point2D, 3> = RingBuffer::with_config(config);
for i in 0..5 {
buffer.push(Point2D::new(i as f32, i as f32)).unwrap();
}
assert_eq!(buffer.len(), 3);
assert!(buffer.is_full());
let values: heapless::Vec<Point2D, 3> = buffer.iter().copied().collect();
assert_eq!(values.len(), 3);
}
#[test]
fn test_ring_buffer_reject_mode() {
let config = RingBufferConfig {
overflow_mode: OverflowMode::Reject,
..Default::default()
};
let mut buffer: RingBuffer<Point2D, 2> = RingBuffer::with_config(config);
buffer.push(Point2D::new(1.0, 1.0)).unwrap();
buffer.push(Point2D::new(2.0, 2.0)).unwrap();
let result = buffer.push(Point2D::new(3.0, 3.0));
assert!(result.is_err());
assert_eq!(buffer.len(), 2);
}
#[test]
fn test_point_ring_buffer_features() {
let mut buffer: PointRingBuffer<10> = PointRingBuffer::new();
for i in 0..5 {
buffer.push(Point2D::new(i as f32, (i * 2) as f32)).unwrap();
}
let avg = buffer.moving_average(3).unwrap();
assert_eq!(avg.x, 3.0); assert_eq!(avg.y, 6.0);
let downsampled = buffer.downsample(2);
assert_eq!(downsampled.len(), 3);
let rate = buffer.rate_of_change().unwrap();
assert_eq!(rate, 2.0); }
}