use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use epics_base_rs::error::{CaError, CaResult};
use epics_base_rs::server::record::{FieldDesc, Record, RecordProcessResult};
use epics_base_rs::types::{DbFieldType, EpicsValue};
use crate::port_handle::PortHandle;
use crate::trace::{TraceFile, TraceInfoMask, TraceIoMask, TraceMask, TraceManager};
#[derive(Clone)]
pub struct PortEntry {
pub handle: PortHandle,
pub trace: Arc<TraceManager>,
}
static PORT_REGISTRY: OnceLock<Mutex<HashMap<String, PortEntry>>> = OnceLock::new();
fn get_port_registry() -> &'static Mutex<HashMap<String, PortEntry>> {
PORT_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn register_port(name: &str, handle: PortHandle, trace: Arc<TraceManager>) {
let mut reg = get_port_registry().lock().unwrap();
reg.insert(name.to_string(), PortEntry { handle, trace });
}
fn lookup_port(name: &str) -> Option<PortEntry> {
let reg = get_port_registry().lock().ok()?;
reg.get(name).cloned()
}
pub fn register_asyn_record_type() {
epics_base_rs::server::db_loader::register_record_type(
"asyn",
Box::new(|| Box::new(AsynRecord::default())),
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
enum TransferMode {
WriteRead = 0,
Write = 1,
Read = 2,
Flush = 3,
NoIo = 4,
}
impl TransferMode {
fn from_u16(v: u16) -> Self {
match v {
0 => Self::WriteRead,
1 => Self::Write,
2 => Self::Read,
3 => Self::Flush,
4 => Self::NoIo,
_ => Self::WriteRead,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
enum InterfaceType {
Octet = 0,
Int32 = 1,
UInt32Digital = 2,
Float64 = 3,
}
impl InterfaceType {
fn from_u16(v: u16) -> Self {
match v {
0 => Self::Octet,
1 => Self::Int32,
2 => Self::UInt32Digital,
3 => Self::Float64,
_ => Self::Octet,
}
}
}
fn baud_rate_to_menu_index(baud: i32) -> i32 {
match baud {
300 => 1,
600 => 2,
1200 => 3,
2400 => 4,
4800 => 5,
9600 => 6,
19200 => 7,
38400 => 8,
57600 => 9,
115200 => 10,
230400 => 11,
460800 => 12,
576000 => 13,
921600 => 14,
1152000 => 15,
_ => 0, }
}
fn menu_index_to_baud_rate(idx: i32) -> i32 {
match idx {
1 => 300,
2 => 600,
3 => 1200,
4 => 2400,
5 => 4800,
6 => 9600,
7 => 19200,
8 => 38400,
9 => 57600,
10 => 115200,
11 => 230400,
12 => 460800,
13 => 576000,
14 => 921600,
15 => 1152000,
_ => 0,
}
}
pub struct AsynRecord {
pub port: String,
pub addr: i32,
pub pcnct: i32, pub drvinfo: String,
pub reason: i32,
pub tmod: i32, pub tmot: f64, pub iface: i32, pub octetiv: i32, pub optioniv: i32, pub gpibiv: i32, pub i32iv: i32, pub ui32iv: i32, pub f64iv: i32,
pub aout: String,
pub oeos: String,
pub bout: Vec<u8>,
pub omax: i32,
pub nowt: i32,
pub nawt: i32,
pub ofmt: i32,
pub ainp: String,
pub tinp: String,
pub ieos: String,
pub binp: Vec<u8>,
pub imax: i32,
pub nrrd: i32,
pub nord: i32,
pub ifmt: i32, pub eomr: i32,
pub i32inp: i32,
pub i32out: i32,
pub ui32inp: u32,
pub ui32out: u32,
pub ui32mask: u32,
pub f64inp: f64,
pub f64out: f64,
pub baud: i32,
pub lbaud: i32,
pub prty: i32,
pub dbit: i32,
pub sbit: i32,
pub mctl: i32,
pub fctl: i32,
pub ixon: i32,
pub ixoff: i32,
pub ixany: i32,
pub hostinfo: String,
pub drto: i32,
pub ucmd: i32,
pub acmd: i32,
pub spr: i32,
pub tmsk: i32,
pub tb0: i32,
pub tb1: i32,
pub tb2: i32,
pub tb3: i32,
pub tb4: i32,
pub tb5: i32,
pub tiom: i32,
pub tib0: i32,
pub tib1: i32,
pub tib2: i32,
pub tinm: i32,
pub tinb0: i32,
pub tinb1: i32,
pub tinb2: i32,
pub tinb3: i32,
pub tsiz: i32,
pub tfil: String,
pub auct: i32, pub cnct: i32, pub enbl: i32,
pub val: i32,
pub errs: String,
pub aqr: i32,
port_entry: Option<PortEntry>,
resolved_reason: usize,
}
impl Default for AsynRecord {
fn default() -> Self {
Self {
port: String::new(),
addr: 0,
pcnct: 0,
drvinfo: String::new(),
reason: 0,
tmod: 0,
tmot: 1.0,
iface: 0,
octetiv: 0,
optioniv: 0,
gpibiv: 0,
i32iv: 0,
ui32iv: 0,
f64iv: 0,
aout: String::new(),
oeos: String::new(),
bout: Vec::new(),
omax: 80,
nowt: 80,
nawt: 0,
ofmt: 0,
ainp: String::new(),
tinp: String::new(),
ieos: String::new(),
binp: Vec::new(),
imax: 80,
nrrd: 0,
nord: 0,
ifmt: 0,
eomr: 0,
i32inp: 0,
i32out: 0,
ui32inp: 0,
ui32out: 0,
ui32mask: 0xFFFFFFFF,
f64inp: 0.0,
f64out: 0.0,
baud: 0,
lbaud: 0,
prty: 0,
dbit: 0,
sbit: 0,
mctl: 0,
fctl: 0,
ixon: 0,
ixoff: 0,
ixany: 0,
hostinfo: String::new(),
drto: 0,
ucmd: 0,
acmd: 0,
spr: 0,
tmsk: 0,
tb0: 0,
tb1: 0,
tb2: 0,
tb3: 0,
tb4: 0,
tb5: 0,
tiom: 0,
tib0: 0,
tib1: 0,
tib2: 0,
tinm: 0,
tinb0: 0,
tinb1: 0,
tinb2: 0,
tinb3: 0,
tsiz: 80,
tfil: String::new(),
auct: 1,
cnct: 0,
enbl: 1,
val: 0,
errs: String::new(),
aqr: 0,
port_entry: None,
resolved_reason: 0,
}
}
}
static FIELD_LIST: &[FieldDesc] = &[
FieldDesc { name: "PORT", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "ADDR", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "PCNCT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "DRVINFO", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "REASON", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "TMOD", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TMOT", dbf_type: DbFieldType::Double, read_only: false },
FieldDesc { name: "IFACE", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "OCTETIV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "OPTIONIV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "GPIBIV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "I32IV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "UI32IV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "F64IV", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "AOUT", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "OEOS", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "BOUT", dbf_type: DbFieldType::Char, read_only: false },
FieldDesc { name: "OMAX", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "NOWT", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "NAWT", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "OFMT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "AINP", dbf_type: DbFieldType::String, read_only: true },
FieldDesc { name: "TINP", dbf_type: DbFieldType::String, read_only: true },
FieldDesc { name: "IEOS", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "BINP", dbf_type: DbFieldType::Char, read_only: true },
FieldDesc { name: "IMAX", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "NRRD", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "NORD", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "IFMT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "EOMR", dbf_type: DbFieldType::Short, read_only: true },
FieldDesc { name: "I32INP", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "I32OUT", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "UI32INP", dbf_type: DbFieldType::Long, read_only: true },
FieldDesc { name: "UI32OUT", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "UI32MASK", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "F64INP", dbf_type: DbFieldType::Double, read_only: true },
FieldDesc { name: "F64OUT", dbf_type: DbFieldType::Double, read_only: false },
FieldDesc { name: "BAUD", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "LBAUD", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "PRTY", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "DBIT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "SBIT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "MCTL", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "FCTL", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "IXON", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "IXOFF", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "IXANY", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "HOSTINFO", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "DRTO", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "UCMD", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "ACMD", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "SPR", dbf_type: DbFieldType::Char, read_only: true },
FieldDesc { name: "TMSK", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "TB0", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TB1", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TB2", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TB3", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TB4", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TB5", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TIOM", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "TIB0", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TIB1", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TIB2", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TINM", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "TINB0", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TINB1", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TINB2", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TINB3", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "TSIZ", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "TFIL", dbf_type: DbFieldType::String, read_only: false },
FieldDesc { name: "AUCT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "CNCT", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "ENBL", dbf_type: DbFieldType::Short, read_only: false },
FieldDesc { name: "VAL", dbf_type: DbFieldType::Long, read_only: false },
FieldDesc { name: "ERRS", dbf_type: DbFieldType::String, read_only: true },
FieldDesc { name: "AQR", dbf_type: DbFieldType::Char, read_only: false },
];
impl AsynRecord {
fn update_trace_bits_from_mask(&mut self) {
let mask = self.tmsk as u32;
self.tb0 = if mask & TraceMask::ERROR.bits() != 0 { 1 } else { 0 };
self.tb1 = if mask & TraceMask::IO_DEVICE.bits() != 0 { 1 } else { 0 };
self.tb2 = if mask & TraceMask::IO_FILTER.bits() != 0 { 1 } else { 0 };
self.tb3 = if mask & TraceMask::IO_DRIVER.bits() != 0 { 1 } else { 0 };
self.tb4 = if mask & TraceMask::FLOW.bits() != 0 { 1 } else { 0 };
self.tb5 = if mask & TraceMask::WARNING.bits() != 0 { 1 } else { 0 };
}
fn update_mask_from_trace_bits(&mut self) {
let mut mask: u32 = 0;
if self.tb0 != 0 { mask |= TraceMask::ERROR.bits(); }
if self.tb1 != 0 { mask |= TraceMask::IO_DEVICE.bits(); }
if self.tb2 != 0 { mask |= TraceMask::IO_FILTER.bits(); }
if self.tb3 != 0 { mask |= TraceMask::IO_DRIVER.bits(); }
if self.tb4 != 0 { mask |= TraceMask::FLOW.bits(); }
if self.tb5 != 0 { mask |= TraceMask::WARNING.bits(); }
self.tmsk = mask as i32;
}
fn update_io_bits_from_mask(&mut self) {
let mask = self.tiom as u32;
self.tib0 = if mask & TraceIoMask::ASCII.bits() != 0 { 1 } else { 0 };
self.tib1 = if mask & TraceIoMask::ESCAPE.bits() != 0 { 1 } else { 0 };
self.tib2 = if mask & TraceIoMask::HEX.bits() != 0 { 1 } else { 0 };
}
fn update_mask_from_io_bits(&mut self) {
let mut mask: u32 = 0;
if self.tib0 != 0 { mask |= TraceIoMask::ASCII.bits(); }
if self.tib1 != 0 { mask |= TraceIoMask::ESCAPE.bits(); }
if self.tib2 != 0 { mask |= TraceIoMask::HEX.bits(); }
self.tiom = mask as i32;
}
fn update_info_bits_from_mask(&mut self) {
let mask = self.tinm as u32;
self.tinb0 = if mask & TraceInfoMask::TIME.bits() != 0 { 1 } else { 0 };
self.tinb1 = if mask & TraceInfoMask::PORT.bits() != 0 { 1 } else { 0 };
self.tinb2 = if mask & TraceInfoMask::SOURCE.bits() != 0 { 1 } else { 0 };
self.tinb3 = if mask & TraceInfoMask::THREAD.bits() != 0 { 1 } else { 0 };
}
fn update_mask_from_info_bits(&mut self) {
let mut mask: u32 = 0;
if self.tinb0 != 0 { mask |= TraceInfoMask::TIME.bits(); }
if self.tinb1 != 0 { mask |= TraceInfoMask::PORT.bits(); }
if self.tinb2 != 0 { mask |= TraceInfoMask::SOURCE.bits(); }
if self.tinb3 != 0 { mask |= TraceInfoMask::THREAD.bits(); }
self.tinm = mask as i32;
}
fn apply_trace_mask(&self) {
if let Some(ref entry) = self.port_entry {
let mask = TraceMask::from_bits_truncate(self.tmsk as u32);
entry.trace.set_trace_mask(Some(&self.port), mask);
}
}
fn apply_trace_io_mask(&self) {
if let Some(ref entry) = self.port_entry {
let mask = TraceIoMask::from_bits_truncate(self.tiom as u32);
entry.trace.set_trace_io_mask(Some(&self.port), mask);
}
}
fn apply_trace_info_mask(&self) {
if let Some(ref entry) = self.port_entry {
let mask = TraceInfoMask::from_bits_truncate(self.tinm as u32);
entry.trace.set_trace_info_mask(Some(&self.port), mask);
}
}
fn apply_trace_truncate_size(&self) {
if let Some(ref entry) = self.port_entry {
entry.trace.set_io_truncate_size(Some(&self.port), self.tsiz as usize);
}
}
fn apply_trace_file(&self) {
if let Some(ref entry) = self.port_entry {
let file = match self.tfil.as_str() {
"" | "stderr" => TraceFile::Stderr,
"stdout" => TraceFile::Stdout,
path => {
match std::fs::File::create(path) {
Ok(f) => TraceFile::File(Arc::new(std::sync::Mutex::new(f))),
Err(_) => {
eprintln!("asynRecord: cannot open trace file '{path}', using stderr");
TraceFile::Stderr
}
}
}
};
entry.trace.set_trace_file(Some(&self.port), file);
}
}
fn read_trace_state(&mut self) {
let (trace_mask, io_mask) = match self.port_entry {
Some(ref entry) => {
let port = &self.port;
(
entry.trace.get_trace_mask(Some(port)).bits(),
entry.trace.get_trace_io_mask(Some(port)).bits(),
)
}
None => return,
};
self.tmsk = trace_mask as i32;
self.update_trace_bits_from_mask();
self.tiom = io_mask as i32;
self.update_io_bits_from_mask();
}
fn read_options_from_driver(&mut self, handle: &PortHandle) {
if let Ok(val) = handle.get_option_blocking("baud") {
self.lbaud = val.parse::<i32>().unwrap_or(0);
self.baud = baud_rate_to_menu_index(self.lbaud);
}
if let Ok(val) = handle.get_option_blocking("parity") {
self.prty = match val.as_str() {
"none" => 1,
"even" => 2,
"odd" => 3,
_ => 0, };
}
if let Ok(val) = handle.get_option_blocking("csize") {
self.dbit = match val.as_str() {
"5" => 1,
"6" => 2,
"7" => 3,
"8" => 4,
_ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("stop") {
self.sbit = match val.as_str() {
"1" => 1,
"2" => 2,
_ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("crtscts") {
self.fctl = match val.as_str() {
"Y" | "Yes" => 2, "N" | "No" | "none" => 1, _ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("clocal") {
self.mctl = match val.as_str() {
"Y" | "Yes" => 1, "N" | "No" => 2, _ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("ixon") {
self.ixon = match val.as_str() {
"Y" | "Yes" => 2,
"N" | "No" => 1,
_ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("ixoff") {
self.ixoff = match val.as_str() {
"Y" | "Yes" => 2,
"N" | "No" => 1,
_ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("ixany") {
self.ixany = match val.as_str() {
"Y" | "Yes" => 2,
"N" | "No" => 1,
_ => 0,
};
}
if let Ok(val) = handle.get_option_blocking("hostinfo") {
self.hostinfo = val;
}
if let Ok(val) = handle.get_option_blocking("disconnectOnReadTimeout") {
self.drto = match val.as_str() {
"Y" | "Yes" => 2,
"N" | "No" => 1,
_ => 0,
};
}
}
fn write_option(&mut self, key: &str, value: &str) {
if let Some(ref entry) = self.port_entry {
if let Err(e) = entry.handle.set_option_blocking(key, value) {
self.errs = format!("set_option({key}): {e}");
}
}
}
fn connect_device(&mut self) {
if self.port.is_empty() {
self.pcnct = 0;
self.cnct = 0;
self.port_entry = None;
return;
}
match lookup_port(&self.port) {
Some(entry) => {
if !self.drvinfo.is_empty() {
match entry.handle.drv_user_create_blocking(&self.drvinfo) {
Ok(r) => {
self.resolved_reason = r;
self.reason = r as i32;
}
Err(e) => {
self.errs = format!("drvUserCreate failed: {e}");
self.resolved_reason = 0;
}
}
} else {
self.resolved_reason = self.reason as usize;
}
self.octetiv = 1;
self.i32iv = 1;
self.ui32iv = 1;
self.f64iv = 1;
self.optioniv = 1;
self.gpibiv = 0;
self.port_entry = Some(entry.clone());
self.read_trace_state();
self.read_options_from_driver(&entry.handle);
self.pcnct = 1;
self.cnct = 1;
self.enbl = 1;
self.auct = 1;
self.errs.clear();
}
None => {
self.errs = format!("port '{}' not found", self.port);
self.pcnct = 0;
self.cnct = 0;
self.port_entry = None;
}
}
}
fn perform_io(&mut self) -> CaResult<()> {
let entry = match &self.port_entry {
Some(e) => e.clone(),
None => {
self.errs = "not connected".to_string();
return Ok(());
}
};
let tmod = TransferMode::from_u16(self.tmod as u16);
let iface = InterfaceType::from_u16(self.iface as u16);
if matches!(tmod, TransferMode::Write | TransferMode::WriteRead) {
match iface {
InterfaceType::Octet => {
let data = self.aout.as_bytes().to_vec();
match entry.handle.submit_blocking(
crate::request::RequestOp::OctetWrite { data: data.clone() },
crate::user::AsynUser::new(self.resolved_reason).with_addr(self.addr),
) {
Ok(_) => { self.nawt = data.len() as i32; }
Err(e) => { self.errs = format!("write: {e}"); }
}
}
InterfaceType::Int32 => {
match entry.handle.write_int32_blocking(self.resolved_reason, self.addr, self.i32out) {
Ok(_) => {}
Err(e) => { self.errs = format!("write: {e}"); }
}
}
InterfaceType::UInt32Digital => {
match entry.handle.submit_blocking(
crate::request::RequestOp::UInt32DigitalWrite {
value: self.ui32out,
mask: self.ui32mask,
},
crate::user::AsynUser::new(self.resolved_reason).with_addr(self.addr),
) {
Ok(_) => {}
Err(e) => { self.errs = format!("write: {e}"); }
}
}
InterfaceType::Float64 => {
match entry.handle.write_float64_blocking(self.resolved_reason, self.addr, self.f64out) {
Ok(_) => {}
Err(e) => { self.errs = format!("write: {e}"); }
}
}
}
}
if matches!(tmod, TransferMode::Read | TransferMode::WriteRead) {
match iface {
InterfaceType::Octet => {
let buf_size = if self.nrrd > 0 { self.nrrd as usize } else { self.imax as usize };
match entry.handle.submit_blocking(
crate::request::RequestOp::OctetRead { buf_size },
crate::user::AsynUser::new(self.resolved_reason).with_addr(self.addr),
) {
Ok(result) => {
if let Some(data) = result.data {
self.nord = data.len() as i32;
self.ainp = String::from_utf8_lossy(&data).to_string();
self.tinp = crate::trace::format_io_data(&data, TraceIoMask::ESCAPE);
self.binp = data;
}
}
Err(e) => { self.errs = format!("read: {e}"); }
}
}
InterfaceType::Int32 => {
match entry.handle.read_int32_blocking(self.resolved_reason, self.addr) {
Ok(v) => { self.i32inp = v; }
Err(e) => { self.errs = format!("read: {e}"); }
}
}
InterfaceType::UInt32Digital => {
match entry.handle.submit_blocking(
crate::request::RequestOp::UInt32DigitalRead { mask: self.ui32mask },
crate::user::AsynUser::new(self.resolved_reason).with_addr(self.addr),
) {
Ok(result) => {
if let Some(v) = result.uint_val {
self.ui32inp = v;
}
}
Err(e) => { self.errs = format!("read: {e}"); }
}
}
InterfaceType::Float64 => {
match entry.handle.read_float64_blocking(self.resolved_reason, self.addr) {
Ok(v) => { self.f64inp = v; }
Err(e) => { self.errs = format!("read: {e}"); }
}
}
}
}
if matches!(tmod, TransferMode::Flush) {
match entry.handle.submit_blocking(
crate::request::RequestOp::Flush,
crate::user::AsynUser::new(self.resolved_reason).with_addr(self.addr),
) {
Ok(_) => {}
Err(e) => { self.errs = format!("flush: {e}"); }
}
}
Ok(())
}
}
impl Record for AsynRecord {
fn record_type(&self) -> &'static str {
"asyn"
}
fn field_list(&self) -> &'static [FieldDesc] {
FIELD_LIST
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"PORT" => Some(EpicsValue::String(self.port.clone())),
"ADDR" => Some(EpicsValue::Long(self.addr)),
"PCNCT" => Some(EpicsValue::Short(self.pcnct as i16)),
"DRVINFO" => Some(EpicsValue::String(self.drvinfo.clone())),
"REASON" => Some(EpicsValue::Long(self.reason)),
"TMOD" => Some(EpicsValue::Short(self.tmod as i16)),
"TMOT" => Some(EpicsValue::Double(self.tmot)),
"IFACE" => Some(EpicsValue::Short(self.iface as i16)),
"OCTETIV" => Some(EpicsValue::Long(self.octetiv)),
"OPTIONIV" => Some(EpicsValue::Long(self.optioniv)),
"GPIBIV" => Some(EpicsValue::Long(self.gpibiv)),
"I32IV" => Some(EpicsValue::Long(self.i32iv)),
"UI32IV" => Some(EpicsValue::Long(self.ui32iv)),
"F64IV" => Some(EpicsValue::Long(self.f64iv)),
"AOUT" => Some(EpicsValue::String(self.aout.clone())),
"OEOS" => Some(EpicsValue::String(self.oeos.clone())),
"BOUT" => Some(EpicsValue::CharArray(self.bout.clone())),
"OMAX" => Some(EpicsValue::Long(self.omax)),
"NOWT" => Some(EpicsValue::Long(self.nowt)),
"NAWT" => Some(EpicsValue::Long(self.nawt)),
"OFMT" => Some(EpicsValue::Short(self.ofmt as i16)),
"AINP" => Some(EpicsValue::String(self.ainp.clone())),
"TINP" => Some(EpicsValue::String(self.tinp.clone())),
"IEOS" => Some(EpicsValue::String(self.ieos.clone())),
"BINP" => Some(EpicsValue::CharArray(self.binp.clone())),
"IMAX" => Some(EpicsValue::Long(self.imax)),
"NRRD" => Some(EpicsValue::Long(self.nrrd)),
"NORD" => Some(EpicsValue::Long(self.nord)),
"IFMT" => Some(EpicsValue::Short(self.ifmt as i16)),
"EOMR" => Some(EpicsValue::Short(self.eomr as i16)),
"I32INP" => Some(EpicsValue::Long(self.i32inp)),
"I32OUT" => Some(EpicsValue::Long(self.i32out)),
"UI32INP" => Some(EpicsValue::Long(self.ui32inp as i32)),
"UI32OUT" => Some(EpicsValue::Long(self.ui32out as i32)),
"UI32MASK" => Some(EpicsValue::Long(self.ui32mask as i32)),
"F64INP" => Some(EpicsValue::Double(self.f64inp)),
"F64OUT" => Some(EpicsValue::Double(self.f64out)),
"BAUD" => Some(EpicsValue::Short(self.baud as i16)),
"LBAUD" => Some(EpicsValue::Long(self.lbaud)),
"PRTY" => Some(EpicsValue::Short(self.prty as i16)),
"DBIT" => Some(EpicsValue::Short(self.dbit as i16)),
"SBIT" => Some(EpicsValue::Short(self.sbit as i16)),
"MCTL" => Some(EpicsValue::Short(self.mctl as i16)),
"FCTL" => Some(EpicsValue::Short(self.fctl as i16)),
"IXON" => Some(EpicsValue::Short(self.ixon as i16)),
"IXOFF" => Some(EpicsValue::Short(self.ixoff as i16)),
"IXANY" => Some(EpicsValue::Short(self.ixany as i16)),
"HOSTINFO" => Some(EpicsValue::String(self.hostinfo.clone())),
"DRTO" => Some(EpicsValue::Short(self.drto as i16)),
"UCMD" => Some(EpicsValue::Short(self.ucmd as i16)),
"ACMD" => Some(EpicsValue::Short(self.acmd as i16)),
"SPR" => Some(EpicsValue::Char(self.spr as u8)),
"TMSK" => Some(EpicsValue::Long(self.tmsk)),
"TB0" => Some(EpicsValue::Short(self.tb0 as i16)),
"TB1" => Some(EpicsValue::Short(self.tb1 as i16)),
"TB2" => Some(EpicsValue::Short(self.tb2 as i16)),
"TB3" => Some(EpicsValue::Short(self.tb3 as i16)),
"TB4" => Some(EpicsValue::Short(self.tb4 as i16)),
"TB5" => Some(EpicsValue::Short(self.tb5 as i16)),
"TIOM" => Some(EpicsValue::Long(self.tiom)),
"TIB0" => Some(EpicsValue::Short(self.tib0 as i16)),
"TIB1" => Some(EpicsValue::Short(self.tib1 as i16)),
"TIB2" => Some(EpicsValue::Short(self.tib2 as i16)),
"TINM" => Some(EpicsValue::Long(self.tinm)),
"TINB0" => Some(EpicsValue::Short(self.tinb0 as i16)),
"TINB1" => Some(EpicsValue::Short(self.tinb1 as i16)),
"TINB2" => Some(EpicsValue::Short(self.tinb2 as i16)),
"TINB3" => Some(EpicsValue::Short(self.tinb3 as i16)),
"TSIZ" => Some(EpicsValue::Long(self.tsiz)),
"TFIL" => Some(EpicsValue::String(self.tfil.clone())),
"AUCT" => Some(EpicsValue::Short(self.auct as i16)),
"CNCT" => Some(EpicsValue::Short(self.cnct as i16)),
"ENBL" => Some(EpicsValue::Short(self.enbl as i16)),
"VAL" => Some(EpicsValue::Long(self.val)),
"ERRS" => Some(EpicsValue::String(self.errs.clone())),
"AQR" => Some(EpicsValue::Char(self.aqr as u8)),
_ => None,
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
let to_i32 = |v: &EpicsValue| -> i32 { v.to_f64().unwrap_or(0.0) as i32 };
let to_u32 = |v: &EpicsValue| -> u32 { v.to_f64().unwrap_or(0.0) as u32 };
let to_f64 = |v: &EpicsValue| -> f64 { v.to_f64().unwrap_or(0.0) };
let to_str = |v: &EpicsValue| -> String { format!("{v}") };
let to_bytes = |v: &EpicsValue| -> Vec<u8> {
match v {
EpicsValue::CharArray(b) => b.clone(),
EpicsValue::String(s) => s.as_bytes().to_vec(),
_ => Vec::new(),
}
};
match name {
"PORT" => { self.port = to_str(&value); }
"ADDR" => { self.addr = to_i32(&value); }
"PCNCT" => { self.pcnct = to_i32(&value); }
"DRVINFO" => { self.drvinfo = to_str(&value); }
"REASON" => { self.reason = to_i32(&value); }
"TMOD" => { self.tmod = to_i32(&value); }
"TMOT" => { self.tmot = to_f64(&value); }
"IFACE" => { self.iface = to_i32(&value); }
"OCTETIV" => { self.octetiv = to_i32(&value); }
"OPTIONIV" => { self.optioniv = to_i32(&value); }
"GPIBIV" => { self.gpibiv = to_i32(&value); }
"I32IV" => { self.i32iv = to_i32(&value); }
"UI32IV" => { self.ui32iv = to_i32(&value); }
"F64IV" => { self.f64iv = to_i32(&value); }
"AOUT" => { self.aout = to_str(&value); }
"OEOS" => { self.oeos = to_str(&value); }
"BOUT" => { self.bout = to_bytes(&value); }
"OMAX" => { self.omax = to_i32(&value); }
"NOWT" => { self.nowt = to_i32(&value); }
"NAWT" => { self.nawt = to_i32(&value); }
"OFMT" => { self.ofmt = to_i32(&value); }
"AINP" => { self.ainp = to_str(&value); }
"TINP" => { self.tinp = to_str(&value); }
"IEOS" => { self.ieos = to_str(&value); }
"BINP" => { self.binp = to_bytes(&value); }
"IMAX" => { self.imax = to_i32(&value); }
"NRRD" => { self.nrrd = to_i32(&value); }
"NORD" => { self.nord = to_i32(&value); }
"IFMT" => { self.ifmt = to_i32(&value); }
"EOMR" => { self.eomr = to_i32(&value); }
"I32INP" => { self.i32inp = to_i32(&value); }
"I32OUT" => { self.i32out = to_i32(&value); }
"UI32INP" => { self.ui32inp = to_u32(&value); }
"UI32OUT" => { self.ui32out = to_u32(&value); }
"UI32MASK" => { self.ui32mask = to_u32(&value); }
"F64INP" => { self.f64inp = to_f64(&value); }
"F64OUT" => { self.f64out = to_f64(&value); }
"BAUD" => { self.baud = to_i32(&value); }
"LBAUD" => { self.lbaud = to_i32(&value); }
"PRTY" => { self.prty = to_i32(&value); }
"DBIT" => { self.dbit = to_i32(&value); }
"SBIT" => { self.sbit = to_i32(&value); }
"MCTL" => { self.mctl = to_i32(&value); }
"FCTL" => { self.fctl = to_i32(&value); }
"IXON" => { self.ixon = to_i32(&value); }
"IXOFF" => { self.ixoff = to_i32(&value); }
"IXANY" => { self.ixany = to_i32(&value); }
"HOSTINFO" => { self.hostinfo = to_str(&value); }
"DRTO" => { self.drto = to_i32(&value); }
"UCMD" => { self.ucmd = to_i32(&value); }
"ACMD" => { self.acmd = to_i32(&value); }
"SPR" => { self.spr = to_i32(&value); }
"TMSK" => { self.tmsk = to_i32(&value); }
"TB0" => { self.tb0 = to_i32(&value); }
"TB1" => { self.tb1 = to_i32(&value); }
"TB2" => { self.tb2 = to_i32(&value); }
"TB3" => { self.tb3 = to_i32(&value); }
"TB4" => { self.tb4 = to_i32(&value); }
"TB5" => { self.tb5 = to_i32(&value); }
"TIOM" => { self.tiom = to_i32(&value); }
"TIB0" => { self.tib0 = to_i32(&value); }
"TIB1" => { self.tib1 = to_i32(&value); }
"TIB2" => { self.tib2 = to_i32(&value); }
"TINM" => { self.tinm = to_i32(&value); }
"TINB0" => { self.tinb0 = to_i32(&value); }
"TINB1" => { self.tinb1 = to_i32(&value); }
"TINB2" => { self.tinb2 = to_i32(&value); }
"TINB3" => { self.tinb3 = to_i32(&value); }
"TSIZ" => { self.tsiz = to_i32(&value); }
"TFIL" => { self.tfil = to_str(&value); }
"AUCT" => { self.auct = to_i32(&value); }
"CNCT" => { self.cnct = to_i32(&value); }
"ENBL" => { self.enbl = to_i32(&value); }
"VAL" => { self.val = to_i32(&value); }
"ERRS" => { self.errs = to_str(&value); }
"AQR" => { self.aqr = to_i32(&value); }
_ => {
return Err(CaError::InvalidValue(format!("unknown field: {name}")));
}
}
Ok(())
}
fn init_record(&mut self, pass: u8) -> CaResult<()> {
if pass == 1 && !self.port.is_empty() {
self.connect_device();
}
Ok(())
}
fn special(&mut self, field: &str, after: bool) -> CaResult<()> {
if !after {
return Ok(());
}
match field {
"PORT" | "ADDR" | "DRVINFO" => {
self.connect_device();
}
"TMSK" => {
self.update_trace_bits_from_mask();
self.apply_trace_mask();
}
"TB0" | "TB1" | "TB2" | "TB3" | "TB4" | "TB5" => {
self.update_mask_from_trace_bits();
self.apply_trace_mask();
}
"TIOM" => {
self.update_io_bits_from_mask();
self.apply_trace_io_mask();
}
"TIB0" | "TIB1" | "TIB2" => {
self.update_mask_from_io_bits();
self.apply_trace_io_mask();
}
"TINM" => {
self.update_info_bits_from_mask();
self.apply_trace_info_mask();
}
"TINB0" | "TINB1" | "TINB2" | "TINB3" => {
self.update_mask_from_info_bits();
self.apply_trace_info_mask();
}
"TSIZ" => {
self.apply_trace_truncate_size();
}
"TFIL" => {
self.apply_trace_file();
}
"CNCT" => {
if self.cnct != 0 {
self.connect_device();
} else {
self.pcnct = 0;
self.port_entry = None;
}
}
"PCNCT" => {
if self.pcnct != 0 {
self.connect_device();
} else {
self.cnct = 0;
self.port_entry = None;
}
}
"IFACE" => {
}
"REASON" => {
self.resolved_reason = self.reason as usize;
}
"BAUD" => {
let rate = menu_index_to_baud_rate(self.baud);
if rate > 0 {
self.lbaud = rate;
self.write_option("baud", &rate.to_string());
}
}
"LBAUD" => {
if self.lbaud > 0 {
self.baud = baud_rate_to_menu_index(self.lbaud);
self.write_option("baud", &self.lbaud.to_string());
}
}
"PRTY" => {
let val = match self.prty {
1 => "none",
2 => "even",
3 => "odd",
_ => return Ok(()),
};
self.write_option("parity", val);
}
"DBIT" => {
let val = match self.dbit {
1 => "5",
2 => "6",
3 => "7",
4 => "8",
_ => return Ok(()),
};
self.write_option("csize", val);
}
"SBIT" => {
let val = match self.sbit {
1 => "1",
2 => "2",
_ => return Ok(()),
};
self.write_option("stop", val);
}
"MCTL" => {
let val = match self.mctl {
1 => "Y", 2 => "N", _ => return Ok(()),
};
self.write_option("clocal", val);
}
"FCTL" => {
let val = match self.fctl {
1 => "N", 2 => "Y", _ => return Ok(()),
};
self.write_option("crtscts", val);
}
"IXON" => {
let val = match self.ixon {
1 => "N",
2 => "Y",
_ => return Ok(()),
};
self.write_option("ixon", val);
}
"IXOFF" => {
let val = match self.ixoff {
1 => "N",
2 => "Y",
_ => return Ok(()),
};
self.write_option("ixoff", val);
}
"IXANY" => {
let val = match self.ixany {
1 => "N",
2 => "Y",
_ => return Ok(()),
};
self.write_option("ixany", val);
}
"HOSTINFO" => {
if !self.hostinfo.is_empty() {
self.write_option("hostinfo", &self.hostinfo.clone());
}
}
"DRTO" => {
let val = match self.drto {
1 => "N",
2 => "Y",
_ => return Ok(()),
};
self.write_option("disconnectOnReadTimeout", val);
}
"UCMD" | "ACMD" => {
if self.gpibiv == 0 && (self.ucmd != 0 || self.acmd != 0) {
self.errs = "GPIB not supported on this port".to_string();
}
}
"AQR" => {
}
"OEOS" => {
self.write_option("oeos", &self.oeos.clone());
}
"IEOS" => {
self.write_option("ieos", &self.ieos.clone());
}
"UI32MASK" => {
}
_ => {}
}
Ok(())
}
fn process(&mut self) -> CaResult<RecordProcessResult> {
let tmod = TransferMode::from_u16(self.tmod as u16);
if tmod == TransferMode::NoIo {
return Ok(RecordProcessResult::Complete);
}
self.errs.clear();
self.perform_io()?;
Ok(RecordProcessResult::Complete)
}
fn clears_udf(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_fields() {
let rec = AsynRecord::default();
assert_eq!(rec.record_type(), "asyn");
assert_eq!(rec.cnct, 0);
assert_eq!(rec.tmot, 1.0);
assert_eq!(rec.omax, 80);
assert_eq!(rec.imax, 80);
assert_eq!(rec.tsiz, 80);
assert_eq!(rec.ui32mask, 0xFFFFFFFF);
assert_eq!(rec.auct, 1);
assert_eq!(rec.enbl, 1);
}
#[test]
fn test_field_list_count() {
let rec = AsynRecord::default();
assert_eq!(rec.field_list().len(), 76);
}
#[test]
fn test_get_put_roundtrip() {
let mut rec = AsynRecord::default();
rec.put_field("PORT", EpicsValue::String("SIM1".into())).unwrap();
assert_eq!(rec.get_field("PORT"), Some(EpicsValue::String("SIM1".into())));
rec.put_field("ADDR", EpicsValue::Long(3)).unwrap();
assert_eq!(rec.get_field("ADDR"), Some(EpicsValue::Long(3)));
rec.put_field("TMOT", EpicsValue::Double(2.5)).unwrap();
assert_eq!(rec.get_field("TMOT"), Some(EpicsValue::Double(2.5)));
rec.put_field("F64OUT", EpicsValue::Double(3.14)).unwrap();
assert_eq!(rec.get_field("F64OUT"), Some(EpicsValue::Double(3.14)));
}
#[test]
fn test_trace_bit_sync() {
let mut rec = AsynRecord::default();
rec.tmsk = (TraceMask::ERROR | TraceMask::FLOW).bits() as i32;
rec.update_trace_bits_from_mask();
assert_eq!(rec.tb0, 1); assert_eq!(rec.tb4, 1); assert_eq!(rec.tb1, 0);
assert_eq!(rec.tb2, 0);
assert_eq!(rec.tb3, 0);
assert_eq!(rec.tb5, 0);
rec.tb0 = 1;
rec.tb1 = 1;
rec.tb2 = 0;
rec.tb3 = 0;
rec.tb4 = 0;
rec.tb5 = 1;
rec.update_mask_from_trace_bits();
let expected = TraceMask::ERROR | TraceMask::IO_DEVICE | TraceMask::WARNING;
assert_eq!(rec.tmsk, expected.bits() as i32);
}
#[test]
fn test_io_bit_sync() {
let mut rec = AsynRecord::default();
rec.tiom = (TraceIoMask::ASCII | TraceIoMask::HEX).bits() as i32;
rec.update_io_bits_from_mask();
assert_eq!(rec.tib0, 1); assert_eq!(rec.tib1, 0); assert_eq!(rec.tib2, 1); }
#[test]
fn test_info_bit_sync() {
let mut rec = AsynRecord::default();
rec.tinm = (TraceInfoMask::TIME | TraceInfoMask::THREAD).bits() as i32;
rec.update_info_bits_from_mask();
assert_eq!(rec.tinb0, 1); assert_eq!(rec.tinb1, 0); assert_eq!(rec.tinb2, 0); assert_eq!(rec.tinb3, 1); }
#[test]
fn test_connect_nonexistent_port() {
let mut rec = AsynRecord::default();
rec.port = "NONEXISTENT".to_string();
rec.connect_device();
assert_eq!(rec.cnct, 0);
assert!(rec.errs.contains("not found"));
}
#[test]
fn test_connect_empty_port() {
let mut rec = AsynRecord::default();
rec.connect_device();
assert_eq!(rec.cnct, 0);
assert!(rec.port_entry.is_none());
}
#[test]
fn test_process_no_io_mode() {
let mut rec = AsynRecord::default();
rec.tmod = TransferMode::NoIo as i32;
let result = rec.process().unwrap();
assert_eq!(result, RecordProcessResult::Complete);
}
#[test]
fn test_process_not_connected() {
let mut rec = AsynRecord::default();
rec.tmod = TransferMode::Read as i32;
rec.process().unwrap();
assert_eq!(rec.errs, "not connected");
}
#[test]
fn test_special_trace_mask() {
let mut rec = AsynRecord::default();
rec.tmsk = (TraceMask::ERROR | TraceMask::WARNING | TraceMask::FLOW).bits() as i32;
rec.special("TMSK", true).unwrap();
assert_eq!(rec.tb0, 1); assert_eq!(rec.tb4, 1); assert_eq!(rec.tb5, 1); }
#[test]
fn test_special_trace_bits() {
let mut rec = AsynRecord::default();
rec.tb0 = 1;
rec.tb3 = 1;
rec.special("TB0", true).unwrap();
assert_eq!(rec.tmsk as u32, (TraceMask::ERROR | TraceMask::IO_DRIVER).bits());
}
#[test]
fn test_register_and_lookup_port() {
use crate::interrupt::InterruptManager;
use crate::port::{PortDriverBase, PortDriver, PortFlags};
use crate::port_actor::PortActor;
use tokio::sync::mpsc;
struct TestDriver(PortDriverBase);
impl TestDriver {
fn new() -> Self {
Self(PortDriverBase::new("test_asyn_rec", 1, PortFlags::default()))
}
}
impl PortDriver for TestDriver {
fn base(&self) -> &PortDriverBase { &self.0 }
fn base_mut(&mut self) -> &mut PortDriverBase { &mut self.0 }
}
let interrupts = Arc::new(InterruptManager::new(256));
let (tx, rx) = mpsc::channel(256);
let actor = PortActor::new(Box::new(TestDriver::new()), rx);
std::thread::spawn(move || actor.run());
let handle = PortHandle::new(tx, "test_asyn_rec".into(), interrupts);
let trace = Arc::new(TraceManager::new());
register_port("test_asyn_rec", handle, trace);
let entry = lookup_port("test_asyn_rec");
assert!(entry.is_some());
assert_eq!(entry.unwrap().handle.port_name(), "test_asyn_rec");
}
#[test]
fn test_register_asyn_record_type() {
register_asyn_record_type();
let rec = epics_base_rs::server::db_loader::create_record("asyn").unwrap();
assert_eq!(rec.record_type(), "asyn");
assert!(rec.field_list().len() > 3);
}
}