Skip to main content

testioc/
lib.rs

1//! A pure-Rust CA and PVA IOC serving a subset of the epics-containers
2//! example-services BL01T beamline PVs.
3
4mod cam;
5pub mod image;
6
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use asyn_rs::interfaces::motor::AsynMotor;
12use epics_base_rs::error::{CaError, CaResult};
13use epics_base_rs::server::database::db_access::DbChannel;
14use epics_base_rs::server::device_support::DeviceSupport;
15use epics_base_rs::server::records::busy::BusyRecord;
16use epics_base_rs::server::scan::ScanOwner;
17use epics_ca_rs::server::CaServer;
18use epics_pva_rs::server::native_source::PvDatabaseSource;
19use epics_pva_rs::server_native::{CompositeSource, PvaServer, PvaServerConfig, SharedSource};
20use motor_rs::poll_loop::PollCommand;
21use motor_rs::sim_motor::SimMotor;
22use motor_rs::{MotorBuilder, MotorRecord};
23
24const EA_DB: &str = include_str!("db/ea.db");
25const MOTOR_DB: &str = include_str!("db/motor.db");
26const CAM_DB: &str = include_str!("db/cam.db");
27const PLUGIN_DB: &str = include_str!("db/plugin.db");
28
29pub const MOTOR_P: &str = "BL01T-MO-SIMC-01:";
30pub const CAM_P: &str = "BL01T-DI-CAM-01";
31const AXES: [(&str, &str); 2] = [("M1", "Motor 1"), ("M2", "Motor 2")];
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Part {
35    Ea,
36    Motor,
37    Cam,
38}
39
40impl Part {
41    pub const ALL: [Part; 3] = [Part::Ea, Part::Motor, Part::Cam];
42
43    pub fn parse(s: &str) -> Option<Part> {
44        match s {
45            "ea" => Some(Part::Ea),
46            "motor" => Some(Part::Motor),
47            "cam" => Some(Part::Cam),
48            _ => None,
49        }
50    }
51
52    pub fn describe(self) -> &'static str {
53        match self {
54            Part::Ea => "bl01t:A, bl01t:B, bl01t:SUM",
55            Part::Motor => "BL01T-MO-SIMC-01:M1, :M2",
56            Part::Cam => "BL01T-DI-CAM-01:DET:, :ARR:, :PVA:",
57        }
58    }
59}
60
61/// A running IOC: the CA server, and a PVA server that is already serving.
62/// Dropping it stops scanning and PVA but not the CA server task.
63pub struct Ioc {
64    pub server: Arc<CaServer>,
65    pub pva: PvaServer,
66    _scan: ScanOwner,
67}
68
69impl Ioc {
70    /// The bound PVA TCP and UDP (search) ports.
71    pub fn pva_ports(&self) -> (u16, u16) {
72        (self.pva.tcp_addr().port(), self.pva.config().udp_port)
73    }
74
75    /// Serve until either server stops or SIGINT.
76    pub async fn run(self) -> Result<(), String> {
77        let Ioc { server, pva, _scan } = self;
78        tokio::select! {
79            r = server.run() => r.map_err(|e| format!("CA: {e}")),
80            r = pva.run() => r.map_err(|e| format!("PVA: {e}")),
81        }
82    }
83}
84
85fn macros(pairs: &[(&str, &str)]) -> HashMap<String, String> {
86    pairs
87        .iter()
88        .map(|(k, v)| (k.to_string(), v.to_string()))
89        .collect()
90}
91
92/// Build the IOC for `parts` and run its startup actions. `ca_port` None
93/// means EPICS_CA_SERVER_PORT or 5064; `pva_port` None means the
94/// EPICS_PVA(S)_* variables or 5075/5076, Some(p) means TCP p and UDP p+1
95/// (0: both ephemeral). PVA serves from here on; call `run()` for CA.
96pub async fn build(parts: &[Part], ca_port: Option<u16>, pva_port: Option<u16>) -> CaResult<Ioc> {
97    let mut b = CaServer::builder();
98    if let Some(port) = ca_port {
99        b = b.port(port);
100    }
101    let mut poll_txs = Vec::new();
102
103    if parts.contains(&Part::Ea) {
104        b = b.db_string(EA_DB, &HashMap::new())?;
105    }
106    if parts.contains(&Part::Motor) {
107        b = b.register_record_type("motor", || Box::new(MotorRecord::default()));
108        for (m, desc) in AXES {
109            let sim: Arc<Mutex<dyn AsynMotor>> =
110                Arc::new(Mutex::new(SimMotor::new().with_limits(-110.0, 110.0)));
111            let setup = MotorBuilder::new(sim)
112                .moving_poll_interval(Duration::from_millis(100))
113                .idle_poll_interval(Duration::from_secs(1))
114                .build();
115            let dtyp = format!("simMotor_{m}");
116            let ds = Mutex::new(Some(setup.device_support.with_dtyp_name(dtyp.clone())));
117            tokio::spawn(setup.poll_loop.run());
118            poll_txs.push(setup.poll_cmd_tx);
119            b = b.register_device_support(&dtyp, move || {
120                let ds = ds.lock().unwrap().take();
121                Box::new(ds.expect("one record per sim axis")) as Box<dyn DeviceSupport>
122            });
123            b = b.db_string(
124                MOTOR_DB,
125                &macros(&[("P", MOTOR_P), ("M", m), ("DESC", desc)]),
126            )?;
127        }
128    }
129    if parts.contains(&Part::Cam) {
130        b = b.register_record_type("busy", || Box::new(BusyRecord::new()));
131        b = b.db_string(CAM_DB, &macros(&[("P", CAM_P), ("R", ":DET:")]))?;
132        for r in cam::PLUGINS {
133            b = b.db_string(PLUGIN_DB, &macros(&[("P", CAM_P), ("R", r)]))?;
134        }
135        b = b.db_string(cam::PVA_NAME_DB, &macros(&[("P", CAM_P)]))?;
136    }
137
138    let server = Arc::new(b.build().await?);
139    let db = server.database().clone();
140    // CaServerBuilder stops short of these iocInit steps; CP links need them.
141    db.initialize_link_locality().await;
142    db.setup_cp_links().await;
143    let scan = ScanOwner::start(db.clone());
144
145    if parts.contains(&Part::Ea) {
146        // As ioc.yaml: dbpf bl01t:A 2.54, dbpf bl01t:B 2.61
147        DbChannel::new(&db, "bl01t:A").put_f64_process(2.54).await?;
148        DbChannel::new(&db, "bl01t:B").put_f64_process(2.61).await?;
149    }
150    for tx in poll_txs {
151        let _ = tx.send(PollCommand::StartPolling).await;
152    }
153
154    // Records over PVA as QSRV serves them, plus the camera's image PV.
155    let source = CompositeSource::new();
156    let records = Arc::new(PvDatabaseSource::new(db.clone()));
157    source
158        .add_source("records", records, 0)
159        .map_err(CaError::InvalidValue)?;
160    if parts.contains(&Part::Cam) {
161        let output = image::open_pv();
162        let shared = Arc::new(SharedSource::new());
163        shared.add(format!("{CAM_P}:PVA:OUTPUT"), output.clone());
164        source
165            .add_source("cam", shared, 1)
166            .map_err(CaError::InvalidValue)?;
167        cam::start(&db, output).await?;
168    }
169    let mut config = PvaServerConfig::default().with_env();
170    if let Some(port) = pva_port {
171        config.tcp_port = port;
172        config.udp_port = if port == 0 { 0 } else { port + 1 };
173    }
174    let pva = PvaServer::start(source, config).map_err(|e| CaError::InvalidValue(e.to_string()))?;
175
176    Ok(Ioc {
177        server,
178        pva,
179        _scan: scan,
180    })
181}