///|
pub enum TelemetryQuality {
Good
Suspect
Invalid
} derive(Debug, Eq)
///|
pub struct TelemetrySample {
time_s : Double
name : String
value : Double
unit : String
quality : TelemetryQuality
} derive(Debug, Eq)
///|
pub struct TelemetrySummary {
name : String
count : Int
valid_count : Int
minimum : Double
maximum : Double
mean : Double
latest_time_s : Double
} derive(Debug, Eq)
///|
pub fn TelemetrySample::new(
time_s : Double,
name : String,
value : Double,
unit : String,
) -> TelemetrySample {
{
time_s,
name,
value,
unit,
quality: if value.is_nan() || value.is_inf() {
Invalid
} else {
Good
},
}
}
///|
pub fn TelemetrySample::with_quality(
sample : TelemetrySample,
quality : TelemetryQuality,
) -> TelemetrySample {
{ ..sample, quality, }
}
///|
pub fn telemetry_is_valid(sample : TelemetrySample) -> Bool {
sample.quality is Good
}
///|
pub fn validate_range(
sample : TelemetrySample,
lower : Double,
upper : Double,
) -> TelemetrySample {
if !telemetry_is_valid(sample) {
sample
} else if sample.value < lower || sample.value > upper {
{ ..sample, quality: Suspect }
} else {
sample
}
}
///|
pub fn summarize_telemetry(
name : String,
samples : Array[TelemetrySample],
) -> TelemetrySummary {
let selected = samples.filter(sample => sample.name == name)
let valid = selected.filter(telemetry_is_valid)
let values = valid.map(sample => sample.value)
let stats = summarize_samples(values)
let latest = selected.fold(init=0.0, (time, sample) => time.max(sample.time_s))
{
name,
count: selected.length(),
valid_count: valid.length(),
minimum: stats.minimum,
maximum: stats.maximum,
mean: stats.mean,
latest_time_s: latest,
}
}
///|
pub fn resample_linear(
samples : Array[TelemetrySample],
times : Array[Double],
) -> Array[TelemetrySample] {
let ordered = samples.filter(telemetry_is_valid)
let result : Array[TelemetrySample] = []
for time in times {
if ordered.length() == 0 {
continue
}
let mut before : TelemetrySample? = None
let mut after : TelemetrySample? = None
for sample in ordered {
if sample.time_s <= time {
before = Some(sample)
}
if sample.time_s >= time && after is None {
after = Some(sample)
}
}
match (before, after) {
(Some(a), Some(b)) => {
let value = if a.time_s == b.time_s {
a.value
} else {
lerp_scalar(a.value, b.value, inverse_lerp(a.time_s, b.time_s, time))
}
result.push(TelemetrySample::new(time, a.name, value, a.unit))
}
_ => ()
}
}
result
}
///|
pub fn detect_step_changes(
samples : Array[TelemetrySample],
threshold : Double,
) -> Array[TelemetrySample] {
let result : Array[TelemetrySample] = []
if samples.length() == 0 {
return result
}
let mut previous = samples[0]
for sample in samples {
if sample.name == previous.name &&
(sample.value - previous.value).abs() >= threshold {
result.push(sample.with_quality(Suspect))
}
previous = sample
}
result
}
///|
pub fn telemetry_channel_names(
samples : Array[TelemetrySample],
) -> Array[String] {
let names : Array[String] = []
for sample in samples {
if !names.contains(sample.name) {
names.push(sample.name)
}
}
names
}
///|
pub fn telemetry_missing_intervals(
samples : Array[TelemetrySample],
max_gap_s : Double,
) -> Int {
let mut count = 0
if samples.length() < 2 {
return count
}
for i in 0..<(samples.length() - 1) {
if samples[i + 1].time_s - samples[i].time_s > max_gap_s {
count += 1
}
}
count
}
///|
pub fn convert_unit(value : Double, from : String, to : String) -> Double {
if from == to {
value
} else if from == "km" && to == "m" {
value * 1000.0
} else if from == "m" && to == "km" {
value / 1000.0
} else if from == "deg" && to == "rad" {
radians(value)
} else if from == "rad" && to == "deg" {
degrees(value)
} else if from == "km_s" && to == "m_s" {
value * 1000.0
} else if from == "m_s" && to == "km_s" {
value / 1000.0
} else {
value
}
}
///|
pub fn telemetry_health(samples : Array[TelemetrySample]) -> Double {
if samples.length() == 0 {
0.0
} else {
Double::from_int(samples.filter(telemetry_is_valid).length()) /
Double::from_int(samples.length())
}
}