Skip to main content

lsl_core/
lib.rs

1//! `lsl-core` — pure-Rust Lab Streaming Layer implementation.
2//!
3//! This crate provides the Rust-native types and networking for LSL:
4//!
5//! * [`StreamInfo`](stream_info::StreamInfo) — stream metadata
6//! * [`StreamOutlet`](outlet::StreamOutlet) — publish data on the network
7//! * [`StreamInlet`](inlet::StreamInlet) — receive data from the network
8//! * [`resolver`] — discover streams via UDP multicast / broadcast
9//! * [`xml_dom`] — mutable XML tree for `<desc>` metadata
10
11pub mod clock;
12pub mod config;
13pub mod inlet;
14pub mod outlet;
15pub mod postproc;
16pub mod resolver;
17pub mod sample;
18pub mod send_buffer;
19pub mod signal_quality;
20pub mod stream_info;
21pub mod tcp_server;
22pub mod time_receiver;
23pub mod types;
24pub mod udp_server;
25pub mod xml_dom;
26
27use once_cell::sync::Lazy;
28use tokio::runtime::Runtime;
29
30/// Shared tokio runtime used by outlet / UDP servers.
31/// Inlet data-receiver threads create their own single-threaded runtimes to
32/// avoid scheduling contention with the server accept-loops.
33pub static RUNTIME: Lazy<Runtime> = Lazy::new(|| {
34    tokio::runtime::Builder::new_multi_thread()
35        .worker_threads(4)
36        .enable_all()
37        .build()
38        .expect("failed to create lsl-core tokio runtime")
39});
40
41/// Convenience re-exports.
42pub mod prelude {
43    pub use crate::clock::local_clock;
44    pub use crate::inlet::StreamInlet;
45    pub use crate::outlet::StreamOutlet;
46    pub use crate::stream_info::StreamInfo;
47    pub use crate::types::*;
48}
49
50// ── tests ────────────────────────────────────────────────────────────
51#[cfg(test)]
52mod tests {
53    use super::prelude::*;
54    use std::time::Duration;
55
56    #[test]
57    fn in_process_loopback() {
58        let info = StreamInfo::new(
59            "TestLoopback",
60            "EEG",
61            4,
62            250.0,
63            ChannelFormat::Float32,
64            "test_src_1",
65        );
66        let outlet = StreamOutlet::new(&info, 0, 360);
67        std::thread::sleep(Duration::from_millis(100));
68
69        let inlet = StreamInlet::new(&info, 360, 0, true);
70        inlet.open_stream(10.0).unwrap();
71
72        let data = [1.0f32, 2.0, 3.0, 4.0];
73        outlet.push_sample_f(&data, 0.0, true);
74
75        let mut buf = [0.0f32; 4];
76        let ts = inlet.pull_sample_f(&mut buf, 5.0).unwrap();
77        assert!(ts > 0.0);
78        assert_eq!(buf, data);
79    }
80
81    #[test]
82    fn xml_dom_operations() {
83        let info = StreamInfo::new("XMLTest", "EEG", 2, 250.0, ChannelFormat::Float32, "");
84        let desc = info.desc();
85
86        let channels = desc.append_child("channels");
87        let ch1 = channels.append_child("channel");
88        ch1.append_child_value("label", "C3");
89        ch1.append_child_value("unit", "microvolts");
90        let ch2 = channels.append_child("channel");
91        ch2.append_child_value("label", "C4");
92        ch2.append_child_value("unit", "microvolts");
93
94        let channels_read = desc.child("channels");
95        assert!(!channels_read.is_empty());
96        let ch1_read = channels_read.child("channel");
97        assert_eq!(ch1_read.child_value("label"), "C3");
98        assert_eq!(ch1_read.child_value("unit"), "microvolts");
99        let ch2_read = ch1_read.next_sibling_named("channel");
100        assert_eq!(ch2_read.child_value("label"), "C4");
101    }
102
103    #[test]
104    fn query_matching_xpath() {
105        let info = StreamInfo::new("MyEEG", "EEG", 8, 250.0, ChannelFormat::Float32, "src42");
106
107        // Simple equality
108        assert!(info.matches_query("name='MyEEG'"));
109        assert!(info.matches_query("type='EEG'"));
110        assert!(!info.matches_query("name='Other'"));
111
112        // Conjunction
113        assert!(info.matches_query("name='MyEEG' and type='EEG'"));
114        assert!(!info.matches_query("name='MyEEG' and type='Markers'"));
115
116        // Disjunction (or)
117        assert!(info.matches_query("name='MyEEG' or name='Other'"));
118        assert!(info.matches_query("name='Nope' or type='EEG'"));
119        assert!(!info.matches_query("name='A' or name='B'"));
120
121        // Inequality
122        assert!(info.matches_query("name!='Other'"));
123        assert!(!info.matches_query("name!='MyEEG'"));
124
125        // Numeric comparisons
126        assert!(info.matches_query("channel_count>4"));
127        assert!(!info.matches_query("channel_count>10"));
128        assert!(info.matches_query("channel_count>=8"));
129        assert!(info.matches_query("channel_count<100"));
130        assert!(info.matches_query("channel_count<=8"));
131        assert!(!info.matches_query("channel_count<8"));
132        assert!(info.matches_query("nominal_srate>100"));
133
134        // starts-with
135        assert!(info.matches_query("starts-with(name,'My')"));
136        assert!(!info.matches_query("starts-with(name,'Oth')"));
137
138        // contains
139        assert!(info.matches_query("contains(name,'EEG')"));
140        assert!(info.matches_query("contains(type,'EE')"));
141        assert!(!info.matches_query("contains(name,'XYZ')"));
142
143        // not(...)
144        assert!(info.matches_query("not(name='Other')"));
145        assert!(!info.matches_query("not(name='MyEEG')"));
146        assert!(info.matches_query("not(contains(name,'ZZZ'))"));
147
148        // Combined
149        assert!(info
150            .matches_query("starts-with(name,'My') and channel_count>4 and not(type='Markers')"));
151
152        // Empty query
153        assert!(info.matches_query(""));
154    }
155
156    #[test]
157    fn protocol_100_serialization() {
158        use crate::sample::Sample;
159        use std::io::Cursor;
160
161        let fmt = ChannelFormat::Float32;
162        let nch = 4u32;
163
164        // Create and serialize
165        let mut sample = Sample::new(fmt, nch, 0.0);
166        sample.timestamp = 123.456;
167        sample.assign_f32(&[1.0, 2.0, 3.0, 4.0]);
168
169        let mut buf = Vec::new();
170        sample.serialize_100(&mut buf);
171
172        // Protocol 1.00: 8 bytes timestamp + 4*4 bytes data = 24 bytes
173        assert_eq!(buf.len(), 8 + 16);
174
175        // Deserialize
176        let mut cursor = Cursor::new(&buf);
177        let decoded = Sample::deserialize_100(&mut cursor, fmt, nch).unwrap();
178        assert!((decoded.timestamp - 123.456).abs() < 1e-10);
179
180        let mut out = [0.0f32; 4];
181        decoded.retrieve_f32(&mut out);
182        assert_eq!(out, [1.0, 2.0, 3.0, 4.0]);
183
184        // String format
185        let sfmt = ChannelFormat::String;
186        let mut ssample = Sample::new(sfmt, 2, 0.0);
187        ssample.timestamp = 99.0;
188        ssample.assign_strings(&["hello".to_string(), "world".to_string()]);
189
190        let mut sbuf = Vec::new();
191        ssample.serialize_100(&mut sbuf);
192
193        let mut scursor = Cursor::new(&sbuf);
194        let sdecoded = Sample::deserialize_100(&mut scursor, sfmt, 2).unwrap();
195        assert!((sdecoded.timestamp - 99.0).abs() < 1e-10);
196        let strings = sdecoded.retrieve_strings();
197        assert_eq!(strings, vec!["hello", "world"]);
198
199        // Test pattern round-trip
200        let mut tp = Sample::new(fmt, nch, 0.0);
201        tp.assign_test_pattern(4);
202        let mut tbuf = Vec::new();
203        tp.serialize_100(&mut tbuf);
204        let mut tcursor = Cursor::new(&tbuf);
205        let tdecoded = Sample::deserialize_100(&mut tcursor, fmt, nch).unwrap();
206        assert_eq!(tp, tdecoded);
207    }
208}