use std::fs::File;
use std::io::{BufRead, BufReader, Result, Write};
use fixed::types::I32F32;
use crate::result::CompilerResult;
pub type Frac = I32F32;
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum Calibration {
Numeric(NumericCalibration),
Boolean(BooleanCalibration),
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct NumericCalibration {
unit: String,
points: Vec<Point>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Point {
pub x_val: u16,
pub y_val: Frac,
pub interpolate_next: bool,
}
fn check_increasing(points: &[Point]) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Check that y values are increasing");
let mut last = None;
for (i, point) in points.iter().enumerate() {
if let Some(last_val) = last {
res.assert(
point.y_val > last_val,
format!(
"points[{}].y_val = {}, points[{}].y_val = {}",
i - 1,
last_val,
i,
point.y_val
),
)
}
last = Some(point.y_val);
}
res
}
impl NumericCalibration {
const MAXIMUM_POINTS: usize = u16::pow(2, 12) as usize;
pub fn try_new(unit: String, points: Vec<Point>) -> CompilerResult<Self> {
let mut res = CompilerResult::new("Creating Calibration from points");
res.assert(
points.len() <= NumericCalibration::MAXIMUM_POINTS,
format!(
"Found {} points, maximum allowed is {}",
points.len(),
NumericCalibration::MAXIMUM_POINTS
),
);
res.assert(
!points.is_empty(),
"Tables with zero points are not supported",
);
res.require(check_increasing(&points));
res.with_value(NumericCalibration { unit, points })
}
pub fn read_from_file(file: &mut File) -> CompilerResult<Self> {
let mut res = CompilerResult::new("Creating Calibration from File");
let lines_result: Result<Vec<String>> = BufReader::new(file).lines().collect();
let lines: Vec<String> = check!(res, lines_result);
let mut lines_iter = lines.into_iter();
let type_line = check!(res, lines_iter.next(), "Found EOF while looking for type");
res.assert(
type_line.as_str().trim_end() == "type: numeric",
r#"Numeric file must begin with "type: numeric""#,
);
let unit_line = check!(res, lines_iter.next(), "Found EOF while looking for unit");
let unit = check!(res, NumericCalibration::parse_unit(unit_line.clone()));
let points: Vec<Point> = check!(res, NumericCalibration::parse_numeric_points(lines_iter));
res.assert(
points.len() <= NumericCalibration::MAXIMUM_POINTS,
format!(
"Found {} points, maximum allowed is {}",
points.len(),
NumericCalibration::MAXIMUM_POINTS
),
);
let table = check!(res, NumericCalibration::try_new(unit, points));
res.with_value(table)
}
pub fn write_to_file(&self, file: &mut File) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Writing Calibration to file");
check!(res, write!(file, "type: numeric\n"));
check!(res, write!(file, "unit: {}\n", self.unit));
for point in self.points.iter() {
check!(
res,
write!(file, "0x{:03x}, {}\n", point.x_val, point.y_val)
);
if !point.interpolate_next {
check!(res, write!(file, "break\n"));
}
}
res
}
fn parse_numeric_points<Iter: Iterator<Item = String>>(
mut lines: Iter,
) -> CompilerResult<Vec<Point>> {
let mut res = CompilerResult::new("Parsing numeric Calibration points");
let mut points: Vec<Point> = Vec::new();
while let Some(line) = lines.next() {
if line == "break" {
if let Some(last) = points.last_mut() {
if last.interpolate_next {
last.interpolate_next = false;
} else {
res.error("Back to back break statements are illegal");
}
} else {
res.error("break not allowed as first statement");
}
} else {
if line.is_empty() || line.starts_with("#") {
continue;
}
let point: Point = check!(res, NumericCalibration::parse_point(&line));
points.push(point);
}
}
res.assert(
points
.last()
.map(|point| point.interpolate_next)
.unwrap_or(true),
"ended with a break",
);
return res.with_value(points);
}
fn parse_unit(unit_line: String) -> CompilerResult<String> {
let mut res = CompilerResult::new("Parsing unit value");
match unit_line
.trim_end()
.split(" ")
.collect::<Vec<&str>>()
.as_slice()
{
&["unit:", value] if value.chars().all(char::is_alphanumeric) => {
res.set_value(String::from(value))
}
_ => {
res.error(r#"Invalid unit string found, must be "unit: <unit-name>""#);
}
};
res
}
fn parse_point(line: &str) -> CompilerResult<Point> {
let mut res = CompilerResult::new("Parsing Calibration point");
match line
.trim_end()
.split(", ")
.collect::<Vec<&str>>()
.as_slice()
{
&[x, y] => {
let x_val = check!(res, parse_x_val(x));
let y_val = check!(res, parse_y_val(y));
res.set_value(Point {
x_val,
y_val,
interpolate_next: true,
});
}
_ => res.error("Points must contain two values separated by a comma and space"),
}
res
}
pub fn lookup(&self, x_val: u16) -> Option<Frac> {
match self
.points
.binary_search_by(|probe| probe.x_val.cmp(&x_val))
{
Ok(index) => Some(self.points[index].y_val),
Err(index) => {
if index == 0 {
None
} else {
self.interpolate(index - 1, x_val)
}
}
}
}
fn interpolate(&self, index: usize, middle_x: u16) -> Option<Frac> {
let lower_point = self.points.get(index);
let upper_point = self.points.get(index + 1);
match (lower_point, upper_point) {
(Some(lower), Some(upper)) if lower.interpolate_next => {
let delta_x = upper.x_val - lower.x_val;
let delta_y = upper.y_val - lower.y_val;
let span_x = middle_x - lower.x_val;
let span_ratio = Frac::from(span_x) / Frac::from(delta_x);
Some(delta_y * span_ratio + lower.y_val)
}
_ => None,
}
}
pub fn inverse_lookup(&self, y_val: Frac) -> Option<u16> {
match self
.points
.binary_search_by(|probe| probe.y_val.cmp(&y_val))
{
Ok(index) => Some(self.points[index].x_val),
Err(index) => {
if index == 0 {
None
} else {
self.interpolate_inv(index - 1, y_val)
}
}
}
}
fn interpolate_inv(&self, index: usize, middle_y: Frac) -> Option<u16> {
let lower_point = self.points.get(index);
let upper_point = self.points.get(index + 1);
match (lower_point, upper_point) {
(Some(lower), Some(upper)) if lower.interpolate_next => {
let delta_x = upper.x_val - lower.x_val;
let delta_y = upper.y_val - lower.y_val;
let span_y = middle_y - lower.y_val;
let span_ratio = span_y / delta_y;
Some((Frac::from(delta_x) * span_ratio).to_num::<u16>() + lower.x_val)
}
_ => None,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct BooleanCalibration {
false_lower: u16,
false_upper: u16,
true_lower: u16,
true_upper: u16,
}
impl BooleanCalibration {
pub fn read_from_file(file: &mut File) -> CompilerResult<Self> {
let mut res = CompilerResult::new("Creating Calibration from File");
let lines_result: Result<Vec<String>> = BufReader::new(file).lines().collect();
let lines: Vec<String> = check!(res, lines_result);
let mut lines_iter = lines.into_iter();
let type_line = check!(res, lines_iter.next(), "Found EOF while looking for type");
res.assert(
type_line.trim_end() == "type: boolean",
"boolean table must have boolean type",
);
let false_line = check!(
res,
lines_iter.next(),
"Found EOF while looking for false case"
);
let (false_lower, false_upper) = check!(
res,
BooleanCalibration::parse_bool_case("false", false_line)
);
res.assert(
false_upper > false_lower,
"Lower bound must be below upper bound",
);
let true_line = check!(
res,
lines_iter.next(),
"Found EOF while looking for true case"
);
let (true_lower, true_upper) =
check!(res, BooleanCalibration::parse_bool_case("true", true_line));
res.assert(
true_upper > true_lower,
"Lower bound must be below upper bound",
);
if false_lower == true_lower {
res.error("False and True regions overlap")
} else if false_lower < true_lower {
res.assert(false_upper <= true_lower, "False and True regions overlap");
} else {
res.assert(true_upper <= false_lower, "False and True regions overlap");
}
let remaining_lines = lines_iter.count();
res.assert(
remaining_lines == 0,
"Found extra lines at end of boolean table",
);
let table = BooleanCalibration {
false_lower,
false_upper,
true_lower,
true_upper,
};
res.with_value(table)
}
pub fn write_to_file(&self, file: &mut File) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Writing Calibration to file");
check!(res, write!(file, "type: boolean\n"));
check!(
res,
write!(
file,
"false from {:03x} to {:03x}\n",
self.false_lower, self.false_upper
)
);
check!(
res,
write!(
file,
"true from {:03x} to {:03x}\n",
self.true_lower, self.true_upper
)
);
res
}
fn parse_bool_case(expected_label: &str, line: String) -> CompilerResult<(u16, u16)> {
let mut res = CompilerResult::new("Parsing case");
match line.trim_end().split(" ").collect::<Vec<&str>>().as_slice() {
&[label, "from", start, "to", end] if label == expected_label => {
let start_value = check!(res, parse_x_val(start));
let end_value = check!(res, parse_x_val(end));
res.set_value((start_value, end_value));
}
_ => {
res.error(format!(
"Invalid {} case, did not match pattern",
expected_label
));
}
}
res
}
pub fn lookup(&self, x_val: u16) -> Option<bool> {
if x_val >= self.true_lower && x_val <= self.true_upper {
return Some(true);
}
if x_val >= self.false_lower && x_val <= self.false_upper {
return Some(false);
}
None
}
pub fn lower_bound(&self, y_val: bool) -> u16 {
if y_val {
self.true_lower
} else {
self.false_lower
}
}
pub fn upper_bound(&self, y_val: bool) -> u16 {
if y_val {
self.true_upper
} else {
self.false_upper
}
}
}
fn parse_x_val(s: &str) -> CompilerResult<u16> {
let mut res = CompilerResult::new("Parsing X value");
if s.starts_with("0x") {
if let Ok(value) = u16::from_str_radix(&s[2..], 16) {
res.set_value(value);
} else {
res.error("Could not parse limit");
}
} else {
res.error("Limit value must start with '0x'");
}
res
}
fn parse_y_val(s: &str) -> CompilerResult<Frac> {
let mut res = CompilerResult::new("Parsing Y value");
if s.starts_with("-") {
match &s[1..].parse::<Frac>() {
Ok(parsed) => {
res.set_value(-parsed);
}
Err(error) => res.error(error.to_string()),
}
} else {
match s.parse::<Frac>() {
Ok(parsed) => {
res.set_value(parsed);
}
Err(error) => res.error(error.to_string()),
}
}
res
}
#[cfg(test)]
mod test {
use super::*;
use codespan_reporting::files::SimpleFiles;
use fixed_macro::fixed;
use std::io::{Read, Seek, SeekFrom};
const SAMPLE_POINTS: [Point; 4] = [
Point {
x_val: 0,
y_val: fixed!(12.75: I32F32),
interpolate_next: true,
},
Point {
x_val: 2,
y_val: fixed!(13.33: I32F32),
interpolate_next: true,
},
Point {
x_val: 4,
y_val: fixed!(14.92: I32F32),
interpolate_next: true,
},
Point {
x_val: 8,
y_val: fixed!(19.32: I32F32),
interpolate_next: true,
},
];
#[test]
fn exact_lookups_are_exact() {
let table = NumericCalibration::try_new("C".into(), Vec::from(SAMPLE_POINTS))
.to_option()
.unwrap();
for point in SAMPLE_POINTS.iter() {
assert_eq!(Some(point.y_val), table.lookup(point.x_val), "{:?}", point);
assert_eq!(
Some(point.x_val),
table.inverse_lookup(point.y_val),
"{:?}",
point
);
}
}
#[test]
fn lookuptable_round_trip_through_file() {
let original = NumericCalibration::try_new("C".into(), Vec::from(SAMPLE_POINTS))
.to_option()
.unwrap();
let mut file = tempfile::tempfile().unwrap();
original.write_to_file(&mut file);
file.seek(SeekFrom::Start(0)).unwrap();
let mut files: SimpleFiles<String, String> = SimpleFiles::new();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
let _file_id = files.add("temp_file".to_string(), content);
file.seek(SeekFrom::Start(0)).unwrap();
let result = NumericCalibration::read_from_file(&mut file);
result.print(&files).unwrap();
assert_eq!(original, result.to_option().unwrap());
}
}