Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,259 @@
//! LSL service for managing stream discovery and connections
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::buffer::StreamBuffer;
use crate::error::{LslError, Result};
use crate::stream::{LslSample, LslStreamInfo};
/// Handle to an active LSL stream
#[derive(Debug, Clone)]
pub struct LslHandle {
/// Unique identifier for this connection
pub id: String,
/// Stream information
pub info: LslStreamInfo,
/// Whether currently connected
pub connected: bool,
/// Samples received since connection
pub samples_received: usize,
/// Samples dropped due to buffer overflow
pub samples_dropped: usize,
}
/// Internal state for a connected stream
struct StreamConnection {
handle: LslHandle,
buffer: Arc<RwLock<StreamBuffer>>,
// In a real implementation, this would hold the LSL inlet
// and background task handles
}
/// LSL service for managing stream discovery and connections
pub struct LslService {
/// Active stream connections
connections: HashMap<String, StreamConnection>,
/// Counter for generating unique IDs
counter: usize,
}
impl LslService {
/// Create a new LSL service
pub fn new() -> Self {
Self {
connections: HashMap::new(),
counter: 0,
}
}
/// Generate a unique stream ID
fn next_id(&mut self) -> String {
self.counter += 1;
format!("lsl_{}", self.counter)
}
/// Discover available LSL streams
///
/// # Arguments
/// * `timeout` - Discovery timeout in seconds
///
/// # Returns
/// List of discovered stream information
///
/// # Note
/// This is a mock implementation. Real discovery requires native LSL library.
pub async fn discover_streams(&self, _timeout: f64) -> Result<Vec<LslStreamInfo>> {
// In a real implementation, this would use lsl::resolve_streams()
// For now, return an empty list as we don't have native LSL
Ok(Vec::new())
}
/// Connect to a stream by name
///
/// # Arguments
/// * `stream_name` - Name of the stream to connect to
/// * `buffer_duration` - Duration of data to buffer in seconds
///
/// # Returns
/// Handle to the connected stream
pub async fn connect(
&mut self,
stream_info: LslStreamInfo,
buffer_duration: f64,
) -> Result<LslHandle> {
let id = self.next_id();
let buffer = StreamBuffer::for_duration(buffer_duration, stream_info.nominal_srate);
let handle = LslHandle {
id: id.clone(),
info: stream_info,
connected: true,
samples_received: 0,
samples_dropped: 0,
};
let connection = StreamConnection {
handle: handle.clone(),
buffer: Arc::new(RwLock::new(buffer)),
};
self.connections.insert(id, connection);
Ok(handle)
}
/// Disconnect from a stream
pub async fn disconnect(&mut self, stream_id: &str) -> Result<()> {
self.connections
.remove(stream_id)
.ok_or_else(|| LslError::StreamNotFound {
name: stream_id.to_string(),
})?;
Ok(())
}
/// Get recent data from a stream
///
/// # Arguments
/// * `stream_id` - ID of the connected stream
/// * `duration` - Duration of data to retrieve in seconds
///
/// # Returns
/// Tuple of (data, timestamps) where data is [samples][channels]
pub async fn get_data(
&self,
stream_id: &str,
duration: f64,
) -> Result<(Vec<Vec<f64>>, Vec<f64>)> {
let conn = self
.connections
.get(stream_id)
.ok_or_else(|| LslError::StreamNotFound {
name: stream_id.to_string(),
})?;
let buffer = conn.buffer.read().await;
Ok(buffer.get_recent(duration))
}
/// Push a sample to a stream buffer (for simulation/testing)
pub async fn push_sample(&self, stream_id: &str, sample: LslSample) -> Result<()> {
let conn = self
.connections
.get(stream_id)
.ok_or_else(|| LslError::StreamNotFound {
name: stream_id.to_string(),
})?;
let mut buffer = conn.buffer.write().await;
buffer.push(sample);
Ok(())
}
/// Get stream info for a connected stream
pub fn get_stream_info(&self, stream_id: &str) -> Option<&LslStreamInfo> {
self.connections.get(stream_id).map(|c| &c.handle.info)
}
/// List all active stream connections
pub fn list_active(&self) -> Vec<LslHandle> {
self.connections
.values()
.map(|c| c.handle.clone())
.collect()
}
/// Check if a stream is connected
pub fn is_connected(&self, stream_id: &str) -> bool {
self.connections.contains_key(stream_id)
}
/// Get sample count for a stream
pub async fn get_sample_count(&self, stream_id: &str) -> Result<usize> {
let conn = self
.connections
.get(stream_id)
.ok_or_else(|| LslError::StreamNotFound {
name: stream_id.to_string(),
})?;
let buffer = conn.buffer.read().await;
Ok(buffer.total_received())
}
/// Clear buffer for a stream
pub async fn clear_buffer(&self, stream_id: &str) -> Result<()> {
let conn = self
.connections
.get(stream_id)
.ok_or_else(|| LslError::StreamNotFound {
name: stream_id.to_string(),
})?;
let mut buffer = conn.buffer.write().await;
buffer.clear();
Ok(())
}
}
impl Default for LslService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_service_creation() {
let service = LslService::new();
assert!(service.list_active().is_empty());
}
#[tokio::test]
async fn test_connect_disconnect() {
let mut service = LslService::new();
let info = LslStreamInfo::new("TestEEG", "EEG", 64, 256.0);
let handle = service.connect(info, 5.0).await.unwrap();
assert!(service.is_connected(&handle.id));
assert_eq!(service.list_active().len(), 1);
service.disconnect(&handle.id).await.unwrap();
assert!(!service.is_connected(&handle.id));
assert!(service.list_active().is_empty());
}
#[tokio::test]
async fn test_push_and_get_data() {
let mut service = LslService::new();
let info = LslStreamInfo::new("TestEEG", "EEG", 4, 100.0);
let handle = service.connect(info, 2.0).await.unwrap();
// Push 100 samples (1 second at 100 Hz)
for i in 0..100 {
let sample = LslSample::new(vec![i as f64; 4], i as f64 * 0.01);
service.push_sample(&handle.id, sample).await.unwrap();
}
// Get last 0.5 seconds (50 samples)
let (data, times) = service.get_data(&handle.id, 0.5).await.unwrap();
assert_eq!(data.len(), 50);
assert_eq!(times.len(), 50);
}
#[tokio::test]
async fn test_discover_streams() {
let service = LslService::new();
let streams = service.discover_streams(1.0).await.unwrap();
// Without native LSL, this returns empty
assert!(streams.is_empty());
}
}