use crate::error::{CaError, CaResult};
use std::time::SystemTime;
use super::{DbFieldType, EpicsValue, PvString, WallTime};
const MAX_UNITS_SIZE: usize = 8;
const MAX_ENUM_STATES: usize = 16;
const MAX_ENUM_STRING_SIZE: usize = 26;
const EPICS_UNIX_EPOCH_OFFSET_SECS: u64 = 631_152_000;
pub fn serialize_dbr(
dbr_type: u16,
value: &EpicsValue,
status: u16,
severity: u16,
timestamp: impl Into<WallTime>,
) -> CaResult<Vec<u8>> {
let timestamp: WallTime = timestamp.into();
if dbr_type == super::DBR_CLASS_NAME {
let mut buf = [0u8; 40];
if let EpicsValue::String(s) = value {
let bytes = s.as_bytes();
let len = bytes.len().min(39);
buf[..len].copy_from_slice(&bytes[..len]);
}
return Ok(buf.to_vec());
}
let native = super::native_type_for_dbr(dbr_type)?;
let val_bytes = convert_and_serialize(native, value)?;
match dbr_type {
0..=6 => Ok(val_bytes),
7..=13 => serialize_sts(native, &val_bytes, status, severity),
14..=20 => serialize_time(native, &val_bytes, status, severity, timestamp),
21..=27 => serialize_gr_ctrl(native, &val_bytes, status, severity, false),
28..=34 => serialize_gr_ctrl(native, &val_bytes, status, severity, true),
_ => Err(CaError::UnsupportedType(dbr_type)),
}
}
fn epics_timestamp_parts(timestamp: WallTime) -> (u32, u32) {
let unix = timestamp.since_unix_epoch();
let sec_past_epoch = unix
.as_secs()
.saturating_sub(EPICS_UNIX_EPOCH_OFFSET_SECS)
.min(u32::MAX as u64) as u32;
(sec_past_epoch, unix.subsec_nanos())
}
fn convert_and_serialize(native: DbFieldType, value: &EpicsValue) -> CaResult<Vec<u8>> {
if value.dbr_type() == native {
return Ok(value.to_bytes());
}
Ok(value.convert_to(native).to_bytes())
}
const FRAC_MULTIPLIER: [i32; 9] = [
1,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
];
fn convert_value_to_dbr_string(
value: &EpicsValue,
snapshot: &crate::server::snapshot::Snapshot,
) -> EpicsValue {
let prec = snapshot
.display
.as_ref()
.map(|d| d.precision.max(0) as u16)
.unwrap_or(6);
match value {
EpicsValue::Double(v) => EpicsValue::String(cvt_double_to_string(*v, prec).into()),
EpicsValue::Float(v) => EpicsValue::String(cvt_float_to_string(*v, prec).into()),
EpicsValue::DoubleArray(a) => EpicsValue::StringArray(
a.iter()
.map(|v| cvt_double_to_string(*v, prec).into())
.collect(),
),
EpicsValue::FloatArray(a) => EpicsValue::StringArray(
a.iter()
.map(|v| cvt_float_to_string(*v, prec).into())
.collect(),
),
EpicsValue::Enum(v) => EpicsValue::String(enum_label(snapshot, *v)),
EpicsValue::EnumArray(a) => {
EpicsValue::StringArray(a.iter().map(|v| enum_label(snapshot, *v)).collect())
}
other => other.convert_to(DbFieldType::String),
}
}
fn enum_label(snapshot: &crate::server::snapshot::Snapshot, idx: u16) -> PvString {
snapshot
.enums
.as_ref()
.and_then(|e| e.strings.get(idx as usize))
.cloned()
.unwrap_or_else(|| PvString::from(idx.to_string()))
}
pub(crate) fn cvt_double_to_string(val: f64, precision: u16) -> String {
if val.is_nan() || precision > 8 || val > 1e7 || val < -1e7 {
if precision > 8 || val > 1e16 || val < -1e16 {
let p = precision.min(17) as usize;
return fmt_exp_c(val, p, p + 7);
}
return fmt_fixed_c(val, precision.min(3) as usize);
}
let mut val = val;
let mut out = String::new();
if val < 0.0 {
out.push('-');
val = -val;
}
let mut whole = val as i32;
let ftemp = val - whole as f64;
let fplace_full = FRAC_MULTIPLIER[precision as usize];
let mut fraction = (ftemp * fplace_full as f64 * 10.0) as i32;
fraction = (fraction + 5) / 10; if fraction / fplace_full >= 1 {
whole += 1;
fraction -= fplace_full;
}
push_fixed_digits(&mut out, whole, fraction, fplace_full, precision);
out
}
fn cvt_float_to_string(val: f32, precision: u16) -> String {
if val.is_nan() || precision > 8 || val > 1e7 || val < -1e7 {
if precision > 8 || val >= 1e8 || val <= -1e8 {
let p = precision.min(12) as usize;
return fmt_exp_c(val as f64, p, p + 6);
}
return fmt_fixed_c(val as f64, precision.min(3) as usize);
}
let mut val = val;
let mut out = String::new();
if val < 0.0 {
out.push('-');
val = -val;
}
let mut whole = val as i32;
let ftemp = val - whole as f32;
let fplace_full = FRAC_MULTIPLIER[precision as usize];
let mut fraction = (ftemp * fplace_full as f32 * 10.0) as i32;
fraction = (fraction + 5) / 10; if fraction / fplace_full >= 1 {
whole += 1;
fraction -= fplace_full;
}
push_fixed_digits(&mut out, whole, fraction, fplace_full, precision);
out
}
fn push_fixed_digits(
out: &mut String,
mut whole: i32,
mut fraction: i32,
fplace_full: i32,
precision: u16,
) {
let mut got_one = false;
let mut iplace = 10_000_000i32;
while iplace >= 1 {
if whole >= iplace {
got_one = true;
let number = whole / iplace;
whole -= number * iplace;
out.push((b'0' + number as u8) as char);
} else if got_one {
out.push('0');
}
iplace /= 10;
}
if !got_one {
out.push('0');
}
if precision > 0 {
out.push('.');
let mut fplace = fplace_full / 10;
for _ in 0..precision {
let number = fraction / fplace;
fraction -= number * fplace;
out.push((b'0' + number as u8) as char);
fplace /= 10;
}
}
}
fn fmt_fixed_c(val: f64, precision: usize) -> String {
if val.is_nan() {
return "nan".to_string();
}
if val.is_infinite() {
return if val < 0.0 { "-inf" } else { "inf" }.to_string();
}
format!("{val:.precision$}")
}
fn fmt_exp_c(val: f64, precision: usize, width: usize) -> String {
let body = if val.is_nan() {
"nan".to_string()
} else if val.is_infinite() {
if val < 0.0 { "-inf" } else { "inf" }.to_string()
} else {
let raw = format!("{val:.precision$e}");
let (mantissa, exp) = raw.split_once('e').unwrap_or((raw.as_str(), "0"));
let exp_num: i32 = exp.parse().unwrap_or(0);
let sign = if exp_num < 0 { '-' } else { '+' };
format!("{mantissa}e{sign}{:02}", exp_num.abs())
};
format!("{body:>width$}")
}
pub(crate) fn sts_pad(native: DbFieldType) -> &'static [u8] {
match native {
DbFieldType::Char => &[0],
DbFieldType::Double | DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
&[0, 0, 0, 0]
}
_ => &[],
}
}
pub(crate) fn time_pad(native: DbFieldType) -> &'static [u8] {
match native {
DbFieldType::Short | DbFieldType::Enum => &[0, 0],
DbFieldType::Char => &[0, 0, 0],
DbFieldType::Double | DbFieldType::ULong => &[0, 0, 0, 0],
_ => &[],
}
}
fn gr_ctrl_meta_size(native: DbFieldType, ctrl: bool) -> usize {
let n_limits = if ctrl { 8 } else { 6 };
let head = 4;
match native {
DbFieldType::String => head + sts_pad(native).len(),
DbFieldType::Enum => head + 2 + MAX_ENUM_STATES * MAX_ENUM_STRING_SIZE,
DbFieldType::Float => head + 4 + MAX_UNITS_SIZE + n_limits * 4,
DbFieldType::Double | DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
head + 4 + MAX_UNITS_SIZE + n_limits * 8
}
DbFieldType::Short => head + MAX_UNITS_SIZE + n_limits * 2,
DbFieldType::Long | DbFieldType::UShort => head + MAX_UNITS_SIZE + n_limits * 4,
DbFieldType::Char => head + MAX_UNITS_SIZE + n_limits + 1,
}
}
pub(crate) fn dbr_meta_size(dbr_type: u16, native: DbFieldType) -> usize {
match dbr_type {
0..=6 => 0,
7..=13 => 4 + sts_pad(native).len(),
14..=20 => 12 + time_pad(native).len(),
21..=27 => gr_ctrl_meta_size(native, false),
28..=34 => gr_ctrl_meta_size(native, true),
super::DBR_PUT_ACKT | super::DBR_PUT_ACKS => 0,
super::DBR_STSACK_STRING => 8,
_ => 0,
}
}
fn serialize_sts(
native: DbFieldType,
val_bytes: &[u8],
status: u16,
severity: u16,
) -> CaResult<Vec<u8>> {
let pad = sts_pad(native);
let mut buf = Vec::with_capacity(4 + pad.len() + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
buf.extend_from_slice(pad);
buf.extend_from_slice(val_bytes);
Ok(buf)
}
fn serialize_time(
native: DbFieldType,
val_bytes: &[u8],
status: u16,
severity: u16,
timestamp: WallTime,
) -> CaResult<Vec<u8>> {
let (secs, nanos) = epics_timestamp_parts(timestamp);
let pad = time_pad(native);
let mut buf = Vec::with_capacity(12 + pad.len() + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
buf.extend_from_slice(&secs.to_be_bytes());
buf.extend_from_slice(&nanos.to_be_bytes());
buf.extend_from_slice(pad);
buf.extend_from_slice(val_bytes);
Ok(buf)
}
fn serialize_gr_ctrl(
native: DbFieldType,
val_bytes: &[u8],
status: u16,
severity: u16,
ctrl: bool,
) -> CaResult<Vec<u8>> {
let mut buf = Vec::with_capacity(96 + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
match native {
DbFieldType::String => {
buf.extend_from_slice(sts_pad(native));
}
DbFieldType::Enum => {
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&[0u8; MAX_ENUM_STATES * MAX_ENUM_STRING_SIZE]);
}
DbFieldType::Float => {
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits * 4]);
}
DbFieldType::Double => {
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits * 8]);
}
DbFieldType::Short => {
buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits * 2]);
}
DbFieldType::Long | DbFieldType::UShort => {
buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits * 4]);
}
DbFieldType::Char => {
buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits]);
buf.push(0); }
DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&[0u8; MAX_UNITS_SIZE]);
let n_limits = if ctrl { 8 } else { 6 };
buf.extend_from_slice(&vec![0u8; n_limits * 8]);
}
}
buf.extend_from_slice(val_bytes);
Ok(buf)
}
pub fn encode_dbr(
dbr_type: u16,
snapshot: &crate::server::snapshot::Snapshot,
) -> CaResult<Vec<u8>> {
if dbr_type == super::DBR_CLASS_NAME {
let mut buf = [0u8; 40];
if let Some(ref name) = snapshot.class_name {
let bytes = name.as_bytes();
let len = bytes.len().min(39);
buf[..len].copy_from_slice(&bytes[..len]);
}
return Ok(buf.to_vec());
}
let native = super::native_type_for_dbr(dbr_type)?;
let val_bytes = if native == DbFieldType::String {
convert_value_to_dbr_string(&snapshot.value, snapshot).to_bytes()
} else {
convert_and_serialize(native, &snapshot.value)?
};
let status = snapshot.alarm.status;
let severity = snapshot.alarm.severity;
match dbr_type {
0..=6 => Ok(val_bytes),
7..=13 => serialize_sts(native, &val_bytes, status, severity),
14..=20 => serialize_time(native, &val_bytes, status, severity, snapshot.timestamp),
21..=27 => encode_gr(native, &val_bytes, snapshot),
28..=34 => encode_ctrl(native, &val_bytes, snapshot),
super::DBR_PUT_ACKT | super::DBR_PUT_ACKS => Ok(val_bytes),
super::DBR_STSACK_STRING => {
let ackt = snapshot.alarm.ackt.unwrap_or(0);
let acks = snapshot.alarm.acks.unwrap_or(0);
let mut buf = Vec::with_capacity(8 + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
buf.extend_from_slice(&ackt.to_be_bytes());
buf.extend_from_slice(&acks.to_be_bytes());
buf.extend_from_slice(&val_bytes);
Ok(buf)
}
_ => Err(CaError::UnsupportedType(dbr_type)),
}
}
fn encode_gr(
native: DbFieldType,
val_bytes: &[u8],
snapshot: &crate::server::snapshot::Snapshot,
) -> CaResult<Vec<u8>> {
let status = snapshot.alarm.status;
let severity = snapshot.alarm.severity;
let mut buf = Vec::with_capacity(96 + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
match native {
DbFieldType::String => {
buf.extend_from_slice(sts_pad(native));
}
DbFieldType::Enum => {
encode_enum_metadata(&mut buf, snapshot);
}
DbFieldType::Float => {
encode_prec_units_limits_f32(&mut buf, snapshot, 6);
}
DbFieldType::Double => {
encode_prec_units_limits_f64(&mut buf, snapshot, 6);
}
DbFieldType::Short => {
encode_units_limits_i16(&mut buf, snapshot, 6);
}
DbFieldType::Long | DbFieldType::UShort => {
encode_units_limits_i32(&mut buf, snapshot, 6);
}
DbFieldType::Char => {
encode_units_limits_u8(&mut buf, snapshot, 6);
buf.push(0); }
DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
encode_prec_units_limits_f64(&mut buf, snapshot, 6);
}
}
buf.extend_from_slice(val_bytes);
Ok(buf)
}
fn encode_ctrl(
native: DbFieldType,
val_bytes: &[u8],
snapshot: &crate::server::snapshot::Snapshot,
) -> CaResult<Vec<u8>> {
let status = snapshot.alarm.status;
let severity = snapshot.alarm.severity;
let mut buf = Vec::with_capacity(96 + val_bytes.len());
buf.extend_from_slice(&status.to_be_bytes());
buf.extend_from_slice(&severity.to_be_bytes());
match native {
DbFieldType::String => {
buf.extend_from_slice(sts_pad(native));
}
DbFieldType::Enum => {
encode_enum_metadata(&mut buf, snapshot);
}
DbFieldType::Float => {
encode_prec_units_limits_f32(&mut buf, snapshot, 8);
}
DbFieldType::Double => {
encode_prec_units_limits_f64(&mut buf, snapshot, 8);
}
DbFieldType::Short => {
encode_units_limits_i16(&mut buf, snapshot, 8);
}
DbFieldType::Long | DbFieldType::UShort => {
encode_units_limits_i32(&mut buf, snapshot, 8);
}
DbFieldType::Char => {
encode_units_limits_u8(&mut buf, snapshot, 8);
buf.push(0); }
DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
encode_prec_units_limits_f64(&mut buf, snapshot, 8);
}
}
buf.extend_from_slice(val_bytes);
Ok(buf)
}
fn encode_units(buf: &mut Vec<u8>, snapshot: &crate::server::snapshot::Snapshot) {
let mut units_buf = [0u8; MAX_UNITS_SIZE];
if let Some(ref disp) = snapshot.display {
let bytes = disp.units.as_bytes();
let len = bytes.len().min(MAX_UNITS_SIZE - 1);
units_buf[..len].copy_from_slice(&bytes[..len]);
}
buf.extend_from_slice(&units_buf);
}
fn get_limits(snapshot: &crate::server::snapshot::Snapshot, n_limits: usize) -> [f64; 8] {
let mut limits = [0.0f64; 8];
if let Some(ref disp) = snapshot.display {
limits[0] = disp.upper_disp_limit;
limits[1] = disp.lower_disp_limit;
limits[2] = disp.upper_alarm_limit;
limits[3] = disp.upper_warning_limit;
limits[4] = disp.lower_warning_limit;
limits[5] = disp.lower_alarm_limit;
}
if n_limits > 6 {
if let Some(ref ctrl) = snapshot.control {
limits[6] = ctrl.upper_ctrl_limit;
limits[7] = ctrl.lower_ctrl_limit;
}
}
limits
}
fn encode_prec_units_limits_f64(
buf: &mut Vec<u8>,
snapshot: &crate::server::snapshot::Snapshot,
n_limits: usize,
) {
let prec = snapshot.display.as_ref().map(|d| d.precision).unwrap_or(0);
buf.extend_from_slice(&prec.to_be_bytes());
buf.extend_from_slice(&[0, 0]); encode_units(buf, snapshot);
let limits = get_limits(snapshot, n_limits);
for l in &limits[..n_limits] {
buf.extend_from_slice(&l.to_be_bytes());
}
}
fn encode_prec_units_limits_f32(
buf: &mut Vec<u8>,
snapshot: &crate::server::snapshot::Snapshot,
n_limits: usize,
) {
let prec = snapshot.display.as_ref().map(|d| d.precision).unwrap_or(0);
buf.extend_from_slice(&prec.to_be_bytes());
buf.extend_from_slice(&[0, 0]); encode_units(buf, snapshot);
let limits = get_limits(snapshot, n_limits);
for l in &limits[..n_limits] {
buf.extend_from_slice(&(*l as f32).to_be_bytes());
}
}
fn encode_units_limits_i16(
buf: &mut Vec<u8>,
snapshot: &crate::server::snapshot::Snapshot,
n_limits: usize,
) {
encode_units(buf, snapshot);
let limits = get_limits(snapshot, n_limits);
for l in &limits[..n_limits] {
buf.extend_from_slice(&(*l as i16).to_be_bytes());
}
}
fn encode_units_limits_i32(
buf: &mut Vec<u8>,
snapshot: &crate::server::snapshot::Snapshot,
n_limits: usize,
) {
encode_units(buf, snapshot);
let limits = get_limits(snapshot, n_limits);
for l in &limits[..n_limits] {
buf.extend_from_slice(&(*l as i32).to_be_bytes());
}
}
fn encode_units_limits_u8(
buf: &mut Vec<u8>,
snapshot: &crate::server::snapshot::Snapshot,
n_limits: usize,
) {
encode_units(buf, snapshot);
let limits = get_limits(snapshot, n_limits);
for l in &limits[..n_limits] {
buf.push((*l as i8) as u8);
}
}
fn encode_enum_metadata(buf: &mut Vec<u8>, snapshot: &crate::server::snapshot::Snapshot) {
if let Some(ref ei) = snapshot.enums {
let no_str = ei.strings.len().min(MAX_ENUM_STATES) as u16;
buf.extend_from_slice(&no_str.to_be_bytes());
for i in 0..MAX_ENUM_STATES {
let mut slot = [0u8; MAX_ENUM_STRING_SIZE];
if let Some(s) = ei.strings.get(i) {
let bytes = s.as_bytes();
let len = bytes.len().min(MAX_ENUM_STRING_SIZE - 1);
slot[..len].copy_from_slice(&bytes[..len]);
}
buf.extend_from_slice(&slot);
}
} else {
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&[0u8; MAX_ENUM_STATES * MAX_ENUM_STRING_SIZE]);
}
}
use crate::server::snapshot::*;
fn read_u16(data: &[u8], off: usize) -> CaResult<u16> {
if off + 2 > data.len() {
return Err(CaError::Protocol("buffer too short for u16".into()));
}
Ok(u16::from_be_bytes([data[off], data[off + 1]]))
}
fn read_i16(data: &[u8], off: usize) -> CaResult<i16> {
if off + 2 > data.len() {
return Err(CaError::Protocol("buffer too short for i16".into()));
}
Ok(i16::from_be_bytes([data[off], data[off + 1]]))
}
fn read_u32(data: &[u8], off: usize) -> CaResult<u32> {
if off + 4 > data.len() {
return Err(CaError::Protocol("buffer too short for u32".into()));
}
Ok(u32::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
}
fn read_i32(data: &[u8], off: usize) -> CaResult<i32> {
if off + 4 > data.len() {
return Err(CaError::Protocol("buffer too short for i32".into()));
}
Ok(i32::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
}
fn read_f32(data: &[u8], off: usize) -> CaResult<f32> {
if off + 4 > data.len() {
return Err(CaError::Protocol("buffer too short for f32".into()));
}
Ok(f32::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
}
fn read_f64(data: &[u8], off: usize) -> CaResult<f64> {
if off + 8 > data.len() {
return Err(CaError::Protocol("buffer too short for f64".into()));
}
Ok(f64::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]))
}
fn read_pv_string(data: &[u8], off: usize, max_len: usize) -> PvString {
let end = data.len().min(off + max_len);
if off >= end {
return PvString::new();
}
let slice = &data[off..end];
let nul = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
PvString::from_bytes(&slice[..nul])
}
fn epics_secs_to_wall_time(secs: u32, nanos: u32) -> WallTime {
let unix_secs = secs as u64 + EPICS_UNIX_EPOCH_OFFSET_SECS;
WallTime::from_unix(unix_secs, nanos)
}
pub fn decode_dbr(dbr_type: u16, data: &[u8], count: usize) -> CaResult<Snapshot> {
if dbr_type == super::DBR_CLASS_NAME {
if data.len() < 40 {
return Err(CaError::Protocol(format!(
"DBR_CLASS_NAME requires 40-byte payload, got {}",
data.len()
)));
}
let name = read_pv_string(data, 0, 40);
let mut snap = Snapshot::new(
EpicsValue::String(name.clone()),
0,
0,
SystemTime::UNIX_EPOCH,
);
snap.class_name = Some(name.as_str_lossy().into_owned());
return Ok(snap);
}
if dbr_type == super::DBR_STSACK_STRING {
if data.len() < 48 {
return Err(CaError::Protocol(format!(
"DBR_STSACK_STRING requires 48-byte payload, got {}",
data.len()
)));
}
let status = read_u16(data, 0)?;
let severity = read_u16(data, 2)?;
let ackt = read_u16(data, 4)?;
let acks = read_u16(data, 6)?;
let value = read_pv_string(data, 8, 40);
let mut snap = Snapshot::new(
EpicsValue::String(value),
status,
severity,
SystemTime::UNIX_EPOCH,
);
snap.alarm.ackt = Some(ackt);
snap.alarm.acks = Some(acks);
return Ok(snap);
}
let native = super::native_type_for_dbr(dbr_type)?;
let meta = dbr_meta_size(dbr_type, native);
if data.len() < meta {
return Err(CaError::Protocol(format!(
"DBR type {dbr_type} requires at least {meta} metadata bytes, got {}",
data.len()
)));
}
match dbr_type {
0..=6 => {
let value = EpicsValue::from_bytes_array(native, data, count)?;
Ok(Snapshot::new(value, 0, 0, SystemTime::UNIX_EPOCH))
}
7..=13 => decode_sts(native, data, count),
14..=20 => decode_time(native, data, count),
21..=27 => decode_gr_ctrl(native, data, count, false),
28..=34 => decode_gr_ctrl(native, data, count, true),
_ => Err(CaError::UnsupportedType(dbr_type)),
}
}
fn decode_sts(native: DbFieldType, data: &[u8], count: usize) -> CaResult<Snapshot> {
let status = read_u16(data, 0)?;
let severity = read_u16(data, 2)?;
let pad_len = sts_pad(native).len();
let val_off = 4 + pad_len;
let value = EpicsValue::from_bytes_array(native, &data[val_off..], count)?;
Ok(Snapshot::new(
value,
status,
severity,
SystemTime::UNIX_EPOCH,
))
}
fn decode_time(native: DbFieldType, data: &[u8], count: usize) -> CaResult<Snapshot> {
let status = read_u16(data, 0)?;
let severity = read_u16(data, 2)?;
let secs = read_u32(data, 4)?;
let nanos = read_u32(data, 8)?;
let timestamp = epics_secs_to_wall_time(secs, nanos);
let pad_len = time_pad(native).len();
let val_off = 12 + pad_len;
let value = EpicsValue::from_bytes_array(native, &data[val_off..], count)?;
Ok(Snapshot::new(value, status, severity, timestamp))
}
fn decode_gr_ctrl(
native: DbFieldType,
data: &[u8],
count: usize,
ctrl: bool,
) -> CaResult<Snapshot> {
let status = read_u16(data, 0)?;
let severity = read_u16(data, 2)?;
let mut off = 4;
let mut display = None;
let mut control = None;
let mut enums = None;
match native {
DbFieldType::String => {
off += sts_pad(native).len();
}
DbFieldType::Enum => {
let (ei, new_off) = decode_enum_metadata(data, off)?;
enums = Some(ei);
off = new_off;
}
DbFieldType::Float => {
let precision = read_i16(data, off)?;
off += 4; let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
limits[i] = read_f32(data, off)? as f64;
off += 4;
}
display = Some(DisplayInfo {
units,
precision,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
DbFieldType::Double => {
let precision = read_i16(data, off)?;
off += 4; let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
limits[i] = read_f64(data, off)?;
off += 8;
}
display = Some(DisplayInfo {
units,
precision,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
DbFieldType::Short => {
let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
limits[i] = read_i16(data, off)? as f64;
off += 2;
}
display = Some(DisplayInfo {
units,
precision: 0,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
DbFieldType::Long | DbFieldType::UShort => {
let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
limits[i] = read_i32(data, off)? as f64;
off += 4;
}
display = Some(DisplayInfo {
units,
precision: 0,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
DbFieldType::Char => {
let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
if off < data.len() {
limits[i] = (data[off] as i8) as f64;
}
off += 1;
}
off += 1; display = Some(DisplayInfo {
units,
precision: 0,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
DbFieldType::Int64 | DbFieldType::UInt64 | DbFieldType::ULong => {
let precision = read_i16(data, off)?;
off += 4; let units = read_pv_string(data, off, MAX_UNITS_SIZE);
off += MAX_UNITS_SIZE;
let n_limits = if ctrl { 8 } else { 6 };
let mut limits = [0.0f64; 8];
for i in 0..n_limits {
limits[i] = read_f64(data, off)?;
off += 8;
}
display = Some(DisplayInfo {
units,
precision,
upper_disp_limit: limits[0],
lower_disp_limit: limits[1],
upper_alarm_limit: limits[2],
upper_warning_limit: limits[3],
lower_warning_limit: limits[4],
lower_alarm_limit: limits[5],
..Default::default()
});
if ctrl {
control = Some(ControlInfo {
upper_ctrl_limit: limits[6],
lower_ctrl_limit: limits[7],
});
}
}
}
let value = EpicsValue::from_bytes_array(native, &data[off..], count)?;
let mut snap = Snapshot::new(value, status, severity, SystemTime::UNIX_EPOCH);
snap.display = display;
snap.control = control;
snap.enums = enums;
Ok(snap)
}
fn decode_enum_metadata(data: &[u8], off: usize) -> CaResult<(EnumInfo, usize)> {
let no_str = read_u16(data, off)? as usize;
let mut pos = off + 2;
let mut strings = Vec::with_capacity(no_str.min(MAX_ENUM_STATES));
for i in 0..MAX_ENUM_STATES {
let s = read_pv_string(data, pos, MAX_ENUM_STRING_SIZE);
if i < no_str {
strings.push(s);
}
pos += MAX_ENUM_STRING_SIZE;
}
Ok((EnumInfo { strings }, pos))
}
#[cfg(test)]
mod wire_format_tests {
use super::*;
use crate::types::dbr::{
DBR_CTRL_CHAR, DBR_CTRL_DOUBLE, DBR_DOUBLE, DBR_GR_DOUBLE, DBR_GR_ENUM, DBR_STS_CHAR,
DBR_STS_DOUBLE, DBR_TIME_DOUBLE, dbr_buffer_size, native_type_for_dbr,
};
#[test]
fn decode_dbr_truncated_metadata_rejected_not_panic() {
for dbr_type in [
DBR_STS_DOUBLE,
DBR_STS_CHAR,
DBR_TIME_DOUBLE,
DBR_GR_ENUM,
DBR_CTRL_CHAR,
] {
let native = native_type_for_dbr(dbr_type).unwrap();
let meta = dbr_meta_size(dbr_type, native);
assert!(meta >= 1, "metadata types have a non-zero prefix");
for len in [0usize, meta - 1] {
let truncated = vec![0u8; len];
let r = decode_dbr(dbr_type, &truncated, 1);
assert!(
matches!(r, Err(CaError::Protocol(_))),
"dbr_type {dbr_type}: {len}-byte payload (meta={meta}) must be Protocol Err, got {r:?}"
);
}
let full = vec![0u8; meta + 64];
assert!(
decode_dbr(dbr_type, &full, 1).is_ok(),
"dbr_type {dbr_type}: full meta+value payload must decode"
);
}
}
#[test]
fn metadata_matches_encoded_length() {
let cases: &[(DbFieldType, EpicsValue, EpicsValue)] = &[
(
DbFieldType::String,
EpicsValue::String("x".into()),
EpicsValue::StringArray(vec!["x".into(), "y".into(), "z".into()]),
),
(
DbFieldType::Short,
EpicsValue::Short(7),
EpicsValue::ShortArray(vec![1, 2, 3]),
),
(
DbFieldType::Float,
EpicsValue::Float(1.5),
EpicsValue::FloatArray(vec![1.0, 2.0, 3.0]),
),
(
DbFieldType::Enum,
EpicsValue::Enum(2),
EpicsValue::EnumArray(vec![0, 1, 2]),
),
(
DbFieldType::Char,
EpicsValue::Char(9),
EpicsValue::CharArray(vec![1, 2, 3]),
),
(
DbFieldType::Long,
EpicsValue::Long(11),
EpicsValue::LongArray(vec![1, 2, 3]),
),
(
DbFieldType::Double,
EpicsValue::Double(2.5),
EpicsValue::DoubleArray(vec![1.0, 2.0, 3.0]),
),
];
let now = SystemTime::now();
for (native, scalar, array) in cases {
let base = *native as u16;
for layer in 0u16..=4 {
let dbr_type = base + 7 * layer;
for (value, count) in [(scalar, 1usize), (array, 3usize)] {
let encoded = serialize_dbr(dbr_type, value, 0, 0, now)
.expect("serialize_dbr")
.len();
let sized = dbr_buffer_size(dbr_type, *native, count);
assert_eq!(
encoded, sized,
"len mismatch dbr_type={dbr_type} native={native:?} count={count}"
);
}
}
}
}
#[test]
fn encode_dbr_length_matches_sizer() {
use crate::server::snapshot::Snapshot;
let layers = [
DBR_DOUBLE,
DBR_STS_DOUBLE,
DBR_TIME_DOUBLE,
DBR_GR_DOUBLE,
DBR_CTRL_DOUBLE,
];
let snap = Snapshot::new(EpicsValue::Double(3.25), 0, 0, SystemTime::now());
for dbr_type in layers {
let encoded = encode_dbr(dbr_type, &snap).expect("encode_dbr").len();
let sized = dbr_buffer_size(dbr_type, DbFieldType::Double, 1);
assert_eq!(
encoded, sized,
"encode_dbr len mismatch dbr_type={dbr_type}"
);
}
}
#[test]
fn sts_double_value_at_offset_8() {
let v = EpicsValue::Double(3.5);
let buf = serialize_dbr(
super::super::DBR_STS_DOUBLE,
&v,
1,
2,
SystemTime::UNIX_EPOCH,
)
.unwrap();
assert_eq!(
buf.len(),
16,
"STS_DOUBLE must be 16 bytes (4 meta + 4 pad + 8 value)"
);
assert_eq!(&buf[0..2], &1u16.to_be_bytes());
assert_eq!(&buf[2..4], &2u16.to_be_bytes());
assert_eq!(&buf[4..8], &[0, 0, 0, 0]);
assert_eq!(&buf[8..16], &3.5f64.to_be_bytes());
}
#[test]
fn sts_double_round_trip() {
let v = EpicsValue::Double(-12.75);
let buf = serialize_dbr(
super::super::DBR_STS_DOUBLE,
&v,
7,
3,
SystemTime::UNIX_EPOCH,
)
.unwrap();
let snap = decode_dbr(super::super::DBR_STS_DOUBLE, &buf, 1).unwrap();
assert_eq!(snap.value, EpicsValue::Double(-12.75));
assert_eq!(snap.alarm.status, 7);
assert_eq!(snap.alarm.severity, 3);
}
#[test]
fn sts_char_value_at_offset_5() {
let v = EpicsValue::Char(0x41);
let buf =
serialize_dbr(super::super::DBR_STS_CHAR, &v, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
assert_eq!(
buf.len(),
6,
"STS_CHAR must be 6 bytes (4 meta + 1 pad + 1 value)"
);
assert_eq!(buf[4], 0, "RISC_pad");
assert_eq!(buf[5], 0x41, "value at offset 5");
}
#[test]
fn sts_short_no_pad() {
let v = EpicsValue::Short(0x1234);
let buf = serialize_dbr(
super::super::DBR_STS_SHORT,
&v,
0,
0,
SystemTime::UNIX_EPOCH,
)
.unwrap();
assert_eq!(buf.len(), 6, "STS_SHORT is 4 meta + 2 value, no pad");
assert_eq!(&buf[4..6], &0x1234i16.to_be_bytes());
}
#[test]
fn stsack_string_decodes() {
let mut buf = Vec::with_capacity(48);
buf.extend_from_slice(&5u16.to_be_bytes()); buf.extend_from_slice(&2u16.to_be_bytes()); buf.extend_from_slice(&1u16.to_be_bytes()); buf.extend_from_slice(&3u16.to_be_bytes()); let mut value = [0u8; 40];
value[..5].copy_from_slice(b"HIHI\0");
buf.extend_from_slice(&value);
assert_eq!(buf.len(), 48);
let snap = decode_dbr(super::super::DBR_STSACK_STRING, &buf, 1).unwrap();
assert_eq!(snap.value, EpicsValue::String("HIHI".into()));
assert_eq!(snap.alarm.status, 5);
assert_eq!(snap.alarm.severity, 2);
assert_eq!(snap.alarm.ackt, Some(1));
assert_eq!(snap.alarm.acks, Some(3));
}
#[test]
fn stsack_string_short_payload_errors() {
let buf = [0u8; 16];
assert!(decode_dbr(super::super::DBR_STSACK_STRING, &buf, 1).is_err());
}
}
#[cfg(test)]
mod r57_string_precision_tests {
use super::{cvt_double_to_string, cvt_float_to_string};
#[test]
fn double_fixed_point_applies_precision() {
assert_eq!(cvt_double_to_string(3.14, 3), "3.140");
assert_eq!(cvt_double_to_string(1.0, 3), "1.000");
assert_eq!(cvt_double_to_string(1.0, 0), "1");
assert_eq!(cvt_double_to_string(0.0, 3), "0.000");
assert_eq!(cvt_double_to_string(-3.14159, 2), "-3.14");
assert_eq!(cvt_double_to_string(123.456, 2), "123.46");
assert_eq!(cvt_double_to_string(123.456, 0), "123");
}
#[test]
fn double_fast_path_rounds_half_up() {
assert_eq!(cvt_double_to_string(0.125, 2), "0.13");
assert_eq!(cvt_double_to_string(2.5, 0), "3");
assert_eq!(cvt_double_to_string(0.5, 0), "1");
}
#[test]
fn double_exp_path_for_huge_or_highprec() {
assert_eq!(cvt_double_to_string(1e20, 6), " 1.000000e+20");
assert_eq!(cvt_double_to_string(3.14159, 10), " 3.1415900000e+00");
}
#[test]
fn double_mid_range_uses_fixed_with_clamped_prec() {
assert_eq!(cvt_double_to_string(5e7, 0), "50000000");
}
#[test]
fn double_nan_inf_glibc_spelling() {
assert_eq!(cvt_double_to_string(f64::NAN, 3), "nan");
assert_eq!(cvt_double_to_string(f64::INFINITY, 6), " inf");
assert_eq!(cvt_double_to_string(f64::NEG_INFINITY, 6), " -inf");
assert_eq!(cvt_double_to_string(f64::INFINITY, 6).trim(), "inf");
}
#[test]
fn float_fixed_point_applies_precision() {
assert_eq!(cvt_float_to_string(3.5_f32, 2), "3.50");
assert_eq!(cvt_float_to_string(1.0_f32, 3), "1.000");
assert_eq!(cvt_float_to_string(-2.0_f32, 1), "-2.0");
}
}
#[cfg(test)]
mod r58_enum_label_tests {
use super::{EpicsValue, convert_value_to_dbr_string};
use crate::server::snapshot::{EnumInfo, Snapshot};
use std::time::SystemTime;
fn snap_with_enum(value: EpicsValue, labels: &[&str]) -> Snapshot {
let mut s = Snapshot::new(value, 0, 0, SystemTime::UNIX_EPOCH);
s.enums = Some(EnumInfo {
strings: labels.iter().map(|x| (*x).into()).collect(),
});
s
}
#[test]
fn enum_renders_label_not_index() {
let s = snap_with_enum(EpicsValue::Enum(1), &["Off", "On"]);
assert_eq!(
convert_value_to_dbr_string(&s.value, &s),
EpicsValue::String("On".into())
);
let s0 = snap_with_enum(EpicsValue::Enum(0), &["Off", "On"]);
assert_eq!(
convert_value_to_dbr_string(&s0.value, &s0),
EpicsValue::String("Off".into())
);
}
#[test]
fn enum_array_renders_labels() {
let s = snap_with_enum(EpicsValue::EnumArray(vec![0, 1, 0]), &["Off", "On"]);
assert_eq!(
convert_value_to_dbr_string(&s.value, &s),
EpicsValue::StringArray(vec!["Off".into(), "On".into(), "Off".into()])
);
}
#[test]
fn enum_out_of_range_falls_back_to_index() {
let s = snap_with_enum(EpicsValue::Enum(5), &["Off", "On"]);
assert_eq!(
convert_value_to_dbr_string(&s.value, &s),
EpicsValue::String("5".into())
);
}
#[test]
fn enum_without_metadata_falls_back_to_index() {
let s = Snapshot::new(EpicsValue::Enum(1), 0, 0, SystemTime::UNIX_EPOCH);
assert_eq!(
convert_value_to_dbr_string(&s.value, &s),
EpicsValue::String("1".into())
);
}
}