use alec::classifier::Classifier;
use alec::context::Context;
use alec::protocol::{ChannelInput, EncodingType, MessageHeader, Priority, RawData};
use alec::{Decoder, Encoder};
fn make_channels(base: &[f64; 5], drift: &[f64; 5]) -> Vec<ChannelInput> {
(0..5)
.map(|i| ChannelInput {
name_id: i as u8,
source_id: (i as u32) + 1, value: base[i] + drift[i],
})
.collect()
}
fn warmup(ctx: &mut Context, base: &[f64; 5], rounds: usize) {
for r in 0..rounds {
for (i, &val) in base.iter().enumerate() {
let rd = RawData::with_source(i as u32, val, r as u64);
ctx.observe(&rd);
}
}
}
#[test]
fn test_encode_multi_adaptive() {
let mut encoder = Encoder::new();
let classifier = Classifier::default();
let mut context = Context::new();
let base: [f64; 5] = [22.5, 65.0, 1013.25, 3.3, 48.0];
warmup(&mut context, &base, 20);
let drift: [f64; 5] = [0.5, -1.5, 15.0, 0.0, 1.0];
let channels = make_channels(&base, &drift);
let (message, _classifications) =
encoder.encode_multi_adaptive(&channels, 100, &context, &classifier);
let payload = &message.payload;
let mut pos = 0;
while pos < payload.len() && payload[pos] & 0x80 != 0 {
pos += 1;
}
pos += 1;
assert_eq!(
payload[pos],
EncodingType::Multi as u8,
"Expected Multi tag"
);
pos += 1;
let count = payload[pos];
pos += 1;
println!("Included channels: {}", count);
let mut saw_delta_or_repeated = false;
for ch_idx in 0..count {
let name_id = payload[pos];
pos += 1;
let enc_byte = payload[pos];
pos += 1;
let enc_type = EncodingType::from_u8(enc_byte).unwrap();
let value_size = enc_type.typical_size();
pos += value_size;
println!(
" ch[{}] name_id={} encoding={:?} value_bytes={}",
ch_idx, name_id, enc_type, value_size
);
match enc_type {
EncodingType::Delta8 | EncodingType::Delta16 | EncodingType::Repeated => {
saw_delta_or_repeated = true;
}
_ => {}
}
}
assert!(
saw_delta_or_repeated,
"Expected at least one Delta8/Delta16/Repeated after warmup, but all were raw"
);
}
#[test]
fn test_encode_multi_p5_suppression() {
let mut encoder = Encoder::new();
let classifier = Classifier::default();
let mut context = Context::new();
let base: [f64; 5] = [22.5, 65.0, 1013.25, 3.3, 48.0];
warmup(&mut context, &base, 20);
let channels = make_channels(&base, &[0.0001, 0.0001, 0.0001, 0.0001, 0.0001]);
let (message, classifications) =
encoder.encode_multi_adaptive(&channels, 200, &context, &classifier);
let p5_count = classifications
.iter()
.filter(|c| c.priority == Priority::P5Disposable)
.count();
println!("P5 count: {} / {}", p5_count, classifications.len());
let payload = &message.payload;
let mut pos = 0;
while pos < payload.len() && payload[pos] & 0x80 != 0 {
pos += 1;
}
pos += 1; pos += 1; let included_count = payload[pos] as usize;
println!("Included in frame: {}", included_count);
println!("Total channels: {}", channels.len());
assert!(
included_count < channels.len() || p5_count == 0,
"P5 channels should be excluded from frame, but included_count={} total={}",
included_count,
channels.len()
);
if p5_count > 0 {
assert_eq!(
included_count,
channels.len() - p5_count,
"Included count should be total minus P5 count"
);
}
}
#[test]
fn test_encode_multi_shared_header() {
let mut encoder_multi = Encoder::new();
let mut encoder_single = Encoder::new();
let classifier = Classifier::default();
let mut context = Context::new();
let base: [f64; 5] = [22.5, 65.0, 1013.25, 3.3, 48.0];
warmup(&mut context, &base, 20);
let drift: [f64; 5] = [0.5, -1.5, 15.0, 0.1, -1.0];
let channels = make_channels(&base, &drift);
let (multi_msg, _) = encoder_multi.encode_multi_adaptive(&channels, 300, &context, &classifier);
let multi_bytes = multi_msg.to_bytes();
let mut single_total = 0usize;
for ch in &channels {
let raw = RawData::with_source(ch.source_id, ch.value, 300);
let cls = classifier.classify(&raw, &context);
let msg = encoder_single.encode(&raw, &cls, &context);
single_total += msg.to_bytes().len();
}
println!(
"Multi: {} bytes vs 5x single: {} bytes (ratio: {:.1}%)",
multi_bytes.len(),
single_total,
(multi_bytes.len() as f64 / single_total as f64) * 100.0
);
assert!(
multi_bytes.len() < single_total,
"Multi ({}) should be smaller than sum of singles ({})",
multi_bytes.len(),
single_total
);
let saved = single_total - multi_bytes.len();
println!(
"Saved: {} bytes ({:.0}%)",
saved,
(saved as f64 / single_total as f64) * 100.0
);
assert!(
saved >= 2 * MessageHeader::SIZE,
"Should save at least 2 headers (20B), only saved {}",
saved
);
}
#[test]
fn test_encode_multi_adaptive_decode_roundtrip() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
let classifier = Classifier::default();
let mut enc_ctx = Context::new();
let mut dec_ctx = Context::new();
let base: [f64; 5] = [22.5, 65.0, 1013.25, 3.3, 48.0];
for r in 0..20 {
for (i, &val) in base.iter().enumerate() {
let rd = RawData::with_source((i as u32) + 1, val, r as u64);
enc_ctx.observe(&rd);
dec_ctx.observe(&rd);
}
}
let drift: [f64; 5] = [0.5, -1.5, 15.0, 0.1, -1.0];
let channels = make_channels(&base, &drift);
let (message, _) = encoder.encode_multi_adaptive(&channels, 100, &enc_ctx, &classifier);
let decoded = decoder.decode_multi(&message, &dec_ctx).unwrap();
assert!(!decoded.is_empty(), "Expected at least one decoded channel");
for (name_id, decoded_val) in &decoded {
let ch = &channels[*name_id as usize];
let error = (decoded_val - ch.value).abs();
println!(
"ch[{}] expected={:.4} decoded={:.4} error={:.6}",
name_id, ch.value, decoded_val, error
);
assert!(
error < 0.5,
"Decode error too large for ch[{}]: {}",
name_id,
error
);
}
}