use crate::config::{Column, InterpretorConfig};
use crate::modbus::WordOrder;
use crate::register::RegisterCellValue;
use chrono::{DateTime, Local};
use std::fmt::Write as _;
#[derive(Debug, Clone)]
pub struct Interpretor {
config: InterpretorConfig,
word_order: WordOrder,
header: String,
}
const INDEX_W: usize = 5;
const TIME_W: usize = 12;
const AGO_W: usize = 9;
const INSPECT_W: usize = 21;
struct ColumnSpec {
name: &'static str,
width: usize,
enabled: fn(&InterpretorConfig) -> bool,
render: fn(&RowCtx, usize) -> String,
}
struct RowCtx {
value: u16,
next: [Option<u16>; 3],
word: u32,
dword: u64,
custom: String,
}
impl RowCtx {
fn new(order: WordOrder, value: u16, next: [Option<u16>; 3], custom: Option<&str>) -> Self {
let word = order.make_word(value, next[0].unwrap_or_default());
let second = order.make_word(next[1].unwrap_or_default(), next[2].unwrap_or_default());
Self {
value,
next,
word,
dword: order.make_dword(word, second),
custom: custom.unwrap_or("--").to_string(),
}
}
fn two(&self) -> bool {
self.next[0].is_some()
}
fn four(&self) -> bool {
self.next.iter().all(Option::is_some)
}
}
#[rustfmt::skip]
const COLUMNS: &[ColumnSpec] = &[
ColumnSpec { name: "u16", width: 5, enabled: |c| c.u16, render: |c, _| c.value.to_string() },
ColumnSpec { name: "i16", width: 6, enabled: |c| c.i16, render: |c, _| (c.value as i16).to_string() },
ColumnSpec { name: "u8s", width: 8, enabled: |c| c.u8s, render: |c, _| format!("{}/{}", (c.value >> 8) as u8, (c.value & 0xFF) as u8) },
ColumnSpec { name: "i8s", width: 9, enabled: |c| c.i8s, render: |c, _| format!("{}/{}", (c.value >> 8) as u8 as i8, (c.value & 0xFF) as u8 as i8) },
ColumnSpec { name: "hex", width: 4, enabled: |c| c.hex, render: |c, _| format!("{:04X}", c.value) },
ColumnSpec { name: "hex32", width: 9, enabled: |c| c.hex32, render: |c, _| if c.two() { format!("{:08X}", c.word) } else { "-".to_string() } },
ColumnSpec { name: "f16", width: 10, enabled: |c| c.f16, render: |c, w| float_cell(f16_to_f32(c.value), w) },
ColumnSpec { name: "bcd", width: 6, enabled: |c| c.bcd, render: |c, _| bcd_to_decimal(c.value).map_or_else(|| "--".to_string(), |n| n.to_string()) },
ColumnSpec { name: "bcd32", width: 10, enabled: |c| c.bcd32, render: |c, _| if c.two() { bcd_to_decimal_u32(c.word).map_or_else(|| "--".to_string(), |n| n.to_string()) } else { "-".to_string() } },
ColumnSpec { name: "u32", width: 10, enabled: |c| c.u32, render: |c, _| if c.two() { c.word.to_string() } else { "-".to_string() } },
ColumnSpec { name: "i32", width: 11, enabled: |c| c.i32, render: |c, _| if c.two() { (c.word as i32).to_string() } else { "-".to_string() } },
ColumnSpec { name: "u32m10k", width: 11, enabled: |c| c.u32_m10k, render: |c, _| if c.two() { m10k_to_u32(c.word).map_or_else(|| "--".to_string(), |(h, l)| format!("{h}/{l}")) } else { "-".to_string() } },
ColumnSpec { name: "i32m10k", width: 14, enabled: |c| c.i32_m10k, render: |c, _| if c.two() { m10k_to_i32(c.word).map_or_else(|| "--".to_string(), |(h, l)| format!("{h}/{l}")) } else { "-".to_string() } },
ColumnSpec { name: "u64", width: 20, enabled: |c| c.u64, render: |c, _| if c.four() { c.dword.to_string() } else { "-".to_string() } },
ColumnSpec { name: "i64", width: 21, enabled: |c| c.i64, render: |c, _| if c.four() { (c.dword as i64).to_string() } else { "-".to_string() } },
ColumnSpec { name: "f32", width: 10, enabled: |c| c.f32, render: |c, w| if c.two() { float_cell(f32::from_bits(c.word), w) } else { "-".to_string() } },
ColumnSpec { name: "f64", width: 12, enabled: |c| c.f64, render: |c, w| if c.four() { float_cell(f64::from_bits(c.dword), w) } else { "-".to_string() } },
ColumnSpec { name: "ascii", width: 5, enabled: |c| c.ascii, render: |c, _| ascii_cell(c.value, c.next[0].unwrap_or_default()) },
ColumnSpec { name: "bits", width: 19, enabled: |c| c.bits, render: |c, _| bits_cell(c.value) },
ColumnSpec { name: "custom", width: 18, enabled: |c| c.custom, render: |c, _| c.custom.clone() },
];
impl Interpretor {
pub fn new(interpretation: InterpretorConfig, word_order: WordOrder) -> Self {
let mut interpretor = Self {
config: interpretation,
word_order,
header: String::new(),
};
interpretor.rebuild_header();
interpretor
}
fn rebuild_header(&mut self) {
let mut header = String::new();
if self.config.time {
let _ = write!(header, "{:<w$} ", "time", w = TIME_W);
}
if self.config.ago {
let _ = write!(header, "{:<w$} ", "ago", w = AGO_W);
}
let _ = write!(header, "{:>w$}: ", "index", w = INDEX_W);
for col in COLUMNS {
if (col.enabled)(&self.config) {
let _ = write!(header, "{:<w$} ", col.name, w = col.width);
}
}
if self.config.label {
header.push_str("label");
}
self.header = header;
}
pub fn toggle(&mut self, column: Column) {
self.config.toggle(column);
self.rebuild_header();
}
pub fn is_enabled(&self, column: Column) -> bool {
self.config.get(column)
}
pub fn config(&self) -> InterpretorConfig {
self.config.clone()
}
pub fn set_word_order(&mut self, word_order: WordOrder) {
self.word_order = word_order;
}
pub fn header(&self) -> &str {
&self.header
}
pub fn prefix_width(&self) -> u16 {
let mut width = (INDEX_W + 2) as u16; if self.config.time {
width += (TIME_W + 1) as u16; }
if self.config.ago {
width += (AGO_W + 1) as u16;
}
width
}
pub fn shows_ascii(&self) -> bool {
self.config.ascii
}
pub fn ascii_string(&self, data: &[RegisterCellValue]) -> String {
data.iter()
.flat_map(|&(_, v)| [(v >> 8) as u8, (v & 0xFF) as u8])
.map(|b| {
let c = b as char;
if c.is_ascii_graphic() {
c
} else {
'ยท'
}
})
.collect()
}
pub fn placeholder(&self, index: u16, label: Option<&str>) -> String {
let dash = "--";
let mut row = String::new();
if self.config.time {
let _ = write!(row, "{dash:<w$} ", w = TIME_W);
}
if self.config.ago {
let _ = write!(row, "{dash:<w$} ", w = AGO_W);
}
if self.config.index_hex {
let _ = write!(row, "{index:>w$X}: ", w = INDEX_W);
} else {
let _ = write!(row, "{index:>w$}: ", w = INDEX_W);
}
for col in COLUMNS {
if (col.enabled)(&self.config) {
let _ = write!(row, "{dash:<w$} ", w = col.width);
}
}
if self.config.label {
if let Some(text) = label {
row.push_str(text);
}
}
row
}
#[allow(clippy::too_many_arguments)]
pub fn format_row(
&self,
address: u16,
value: u16,
next: [Option<u16>; 3],
read_at: DateTime<Local>,
now: DateTime<Local>,
custom: Option<&str>,
label: Option<&str>,
) -> String {
let mut row = String::new();
if self.config.time {
let formatted = read_at.format("%H:%M:%S.%3f").to_string();
let _ = write!(row, "{formatted:<w$} ", w = TIME_W);
}
if self.config.ago {
let ago = format_ago(now.signed_duration_since(read_at));
let _ = write!(row, "{ago:<w$} ", w = AGO_W);
}
if self.config.index_hex {
let _ = write!(row, "{address:>w$X}: ", w = INDEX_W);
} else {
let _ = write!(row, "{address:>w$}: ", w = INDEX_W);
}
let ctx = RowCtx::new(self.word_order, value, next, custom);
for col in COLUMNS {
if (col.enabled)(&self.config) {
let cell = (col.render)(&ctx, col.width);
let _ = write!(row, "{cell:<w$} ", w = col.width);
}
}
if self.config.label {
if let Some(t) = label {
row.push_str(t);
}
}
row
}
pub fn interpret_all(
&self,
value: u16,
next: [Option<u16>; 3],
custom: Option<&str>,
label: Option<&str>,
) -> Vec<(&'static str, String)> {
let ctx = RowCtx::new(self.word_order, value, next, custom);
let mut entries: Vec<(&'static str, String)> = COLUMNS
.iter()
.map(|col| (col.name, (col.render)(&ctx, INSPECT_W)))
.collect();
entries.push(("label", label.unwrap_or("").to_string()));
entries
}
}
fn float_cell<T: std::fmt::Display + std::fmt::LowerExp>(x: T, width: usize) -> String {
let s = format!("{x}");
if s.len() > width {
format!("{x:.3e}")
} else {
s
}
}
fn ascii_cell(a: u16, b: u16) -> String {
[a, b]
.iter()
.flat_map(|n| [(n >> 8) as u8, (n & 0xFF) as u8])
.map(|byte| {
let ch = byte as char;
if ch.is_ascii_graphic() {
ch
} else {
'ยท'
}
})
.collect()
}
fn bits_cell(value: u16) -> String {
let b = format!("{value:016b}");
format!("{} {} {} {}", &b[0..4], &b[4..8], &b[8..12], &b[12..16])
}
pub(crate) fn format_ago(elapsed: chrono::Duration) -> String {
let secs = elapsed.num_seconds();
if secs <= 0 {
"now".to_string()
} else if secs < 60 {
format!("{secs}s ago")
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else {
">1h ago".to_string()
}
}
fn bcd_to_decimal(value: u16) -> Option<u16> {
let mut result = 0u16;
for shift in [12, 8, 4, 0] {
let nibble = (value >> shift) & 0xF;
if nibble > 9 {
return None;
}
result = result * 10 + nibble;
}
Some(result)
}
fn bcd_to_decimal_u32(value: u32) -> Option<u32> {
let mut result = 0u32;
for shift in [28, 24, 20, 16, 12, 8, 4, 0] {
let nibble = (value >> shift) & 0xF;
if nibble > 9 {
return None;
}
result = result * 10 + nibble;
}
Some(result)
}
fn m10k_to_u32(value: u32) -> Option<(u16, u16)> {
let high = (value >> 16) as u16;
let low = (value & 0xFFFF) as u16;
Some((high, low))
}
fn m10k_to_i32(value: u32) -> Option<(i16, i16)> {
let high = (value >> 16) as i16;
let low = (value & 0xFFFF) as i16;
Some((high, low))
}
pub(crate) fn f16_to_f32(bits: u16) -> f32 {
let sign = if bits & 0x8000 != 0 { -1.0 } else { 1.0 };
let exponent = (bits >> 10) & 0x1f;
let mantissa = bits & 0x3ff;
let magnitude = match exponent {
0 => (mantissa as f32) * 2f32.powi(-24),
0x1f => {
if mantissa == 0 {
f32::INFINITY
} else {
f32::NAN
}
}
_ => (1.0 + (mantissa as f32) / 1024.0) * 2f32.powi(exponent as i32 - 15),
};
sign * magnitude
}