use crate::protocol::caps::ccd::CcdMeasurement;
const LEVELS: usize = u16::MAX as usize + 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Curves {
rows: Vec<Vec<u16>>,
}
impl Curves {
pub fn parse(ccd: &CcdMeasurement, words: &[u16], rows: usize, kind: usize) -> Option<Self> {
let points = ccd.points.len();
let types = usize::from(ccd.types);
if points < 2 || rows == 0 || kind >= types || words.len() < rows * types * points {
return None;
}
let curve = |row: usize| -> &[u16] {
let at = (row * types + kind) * points;
&words[at..at + points]
};
let reference = curve(rows / 2).to_vec();
Some(Self {
rows: (0..rows).map(|r| table(curve(r), &reference)).collect(),
})
}
pub fn correct(&self, row: usize, sample: u16) -> u16 {
match self.rows.get(row) {
Some(table) => table[usize::from(sample)],
None => sample,
}
}
}
fn table(from: &[u16], onto: &[u16]) -> Vec<u16> {
let mut out = vec![0u16; LEVELS];
let mut point = 0usize;
for (sample, slot) in out.iter_mut().enumerate() {
let sample = sample as u32;
while point + 2 < from.len() && u32::from(from[point + 1]) <= sample {
point += 1;
}
if sample < u32::from(from[0]) {
*slot = sample as u16;
continue;
}
let (lo, hi) = (u32::from(from[point]), u32::from(from[point + 1]));
let (a, b) = (u32::from(onto[point]), u32::from(onto[point + 1]));
*slot = match hi.checked_sub(lo).filter(|span| *span != 0) {
None => sample.min(u32::from(u16::MAX)) as u16,
Some(span) => {
let along = sample.saturating_sub(lo);
let mapped = i64::from(a)
+ i64::from(along) * (i64::from(b) - i64::from(a)) / i64::from(span);
mapped.clamp(0, i64::from(u16::MAX)) as u16
}
};
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::caps::{Page, ccd::CcdMeasurement};
fn page(points: &[u16], types: u8) -> CcdMeasurement {
let mut p = vec![0u8; 11 + points.len() * 2];
p[1] = CcdMeasurement::PAGE_CODE;
p[3] = (p.len() - 4) as u8;
p[4] = 0x07;
p[9] = types;
p[10] = points.len() as u8;
for (n, level) in points.iter().enumerate() {
p[11 + n * 2..13 + n * 2].copy_from_slice(&level.to_be_bytes());
}
CcdMeasurement::try_from(&Page::new(CcdMeasurement::PAGE_CODE, p).unwrap()).unwrap()
}
fn words(rows: &[&[u16]], types: usize, kind: usize) -> Vec<u16> {
let points = rows[0].len();
let mut out = vec![0u16; 3 * types * points];
for (r, curve) in rows.iter().enumerate() {
let at = (r * types + kind) * points;
out[at..at + points].copy_from_slice(curve);
}
out
}
#[test]
fn matching_rows_leave_every_sample_alone() {
let ccd = page(&[0, 30000, 60000], 1);
let curve: &[u16] = &[0, 30000, 60000];
let c = Curves::parse(&ccd, &words(&[curve, curve, curve], 1, 0), 3, 0).unwrap();
for v in [0u16, 1, 15000, 30000, 45000, 60000, 65535] {
for row in 0..3 {
assert_eq!(c.correct(row, v), v, "row {row} sample {v}");
}
}
}
#[test]
fn a_row_that_reads_high_is_mapped_onto_the_reference() {
let ccd = page(&[0, 30000, 60000], 1);
let high: &[u16] = &[0, 33000, 60000];
let middle: &[u16] = &[0, 30000, 60000];
let c = Curves::parse(&ccd, &words(&[high, middle, middle], 1, 0), 3, 0).unwrap();
assert_eq!(c.correct(1, 30000), 30000);
assert_eq!(c.correct(0, 33000), 30000);
assert_eq!(c.correct(0, 0), 0);
assert_eq!(c.correct(0, 60000), 60000);
assert_eq!(c.correct(0, 16500), 15000);
}
#[test]
fn the_remap_is_not_linear() {
let ccd = page(&[0, 20000, 60000], 1);
let bent: &[u16] = &[0, 30000, 60000];
let straight: &[u16] = &[0, 20000, 60000];
let c = Curves::parse(&ccd, &words(&[bent, straight, straight], 1, 0), 3, 0).unwrap();
let (a, b) = (10000u16, 50000u16);
let mean_then_correct = c.correct(0, (a + b) / 2);
let correct_then_mean = (u32::from(c.correct(0, a)) + u32::from(c.correct(0, b))) / 2;
assert_ne!(u32::from(mean_then_correct), correct_then_mean);
}
#[test]
fn samples_under_the_first_point_are_left_alone() {
let ccd = page(&[0, 30000, 60000], 1);
let high: &[u16] = &[90, 33000, 60000];
let middle: &[u16] = &[90, 30000, 60000];
let c = Curves::parse(&ccd, &words(&[high, middle, middle], 1, 0), 3, 0).unwrap();
for v in 0..90u16 {
assert_eq!(c.correct(0, v), v, "sample {v} under the dark offset");
}
assert_eq!(c.correct(0, 90), 90);
}
#[test]
fn a_reply_that_does_not_fit_is_refused() {
let ccd = page(&[0, 30000, 60000], 1);
let curve: &[u16] = &[0, 30000, 60000];
let full = words(&[curve, curve, curve], 1, 0);
assert!(Curves::parse(&ccd, &full[..full.len() - 1], 3, 0).is_none());
assert!(Curves::parse(&ccd, &full, 3, 1).is_none());
}
}