pfeiffer_tpg36x/
lib.rs

1//! A rust driver for the Pfeiffer/Inficon TPG36x vacuum gauge controller.
2//!
3//! This driver is by default set up to work with the dual gauge model (TPG362), but can also be
4//! set up to be used with the single gauge model (TPG361).
5//!
6//! # Example
7//!
8//! This example shows the usage via the TCP/IP interface. An example for the serial port is shown
9//! in the [`Tpg36x`] documentation.
10//!
11//! ```no_run
12//! use instrumentrs::TcpIpInterface;
13//! use pfeiffer_tpg36x::{SensorStatus, Tpg36x};
14//!
15//! // IP address and port of the instrument. Adjust to your setup!
16//! let addr = "192.168.1.10:8000";
17//!
18//! // Create a new TCP/IP instrument interface and use it to create a new Tpg36x instance.
19//! let tcpip_inst = TcpIpInterface::simple(addr).unwrap();
20//! let mut inst = Tpg36x::try_new(tcpip_inst).unwrap();
21//!
22//! // Check if the the first pressure sensor is on and if so, read the pressure and print it.
23//! let mut sensor1 = inst.get_channel(0).unwrap();
24//! let status = sensor1.get_status().unwrap();
25//!
26//! if status == SensorStatus::On {
27//!     println!("Current pressure: {}", sensor1.get_pressure().unwrap());
28//! }
29
30#![deny(warnings, missing_docs)]
31
32mod ethernet_conf;
33mod status;
34mod units;
35
36pub use ethernet_conf::{DhcpConfig, EthernetConfig};
37pub use status::SensorStatus;
38pub use units::{PressureUnit, Tpg36xMeasurement};
39
40use std::sync::{Arc, Mutex};
41
42use instrumentrs::{InstrumentError, InstrumentInterface};
43
44use status::PressMsrDatStat;
45
46/// A rust driver for the TPG36x.
47///
48/// This driver provides functionality to control the Pfeiffer/Inficon TPG361 and TPG362 vacuum gauge
49/// controllers.
50///
51/// # Example via serial port connection
52/// ```no_run
53/// use instrumentrs::SerialInterface;
54/// use pfeiffer_tpg36x::Tpg36x;
55///
56/// let port = "/dev/ttyACM0";
57/// let baud = 9600;
58/// let inst_interface = SerialInterface::simple(port, baud).unwrap();
59/// let mut inst = Tpg36x::try_new(inst_interface).unwrap();
60///
61/// println!("Instrument name: {}", inst.get_name().unwrap());
62/// ```
63///
64/// This would print the type of unit, model number, serial number, firmware, and hardware version
65/// of the vacuum gauge controller to `stdout`.
66pub struct Tpg36x<T: InstrumentInterface> {
67    interface: Arc<Mutex<T>>,
68    unit: Arc<Mutex<PressureUnit>>,
69    num_channels: usize,
70}
71
72impl<T: InstrumentInterface> Tpg36x<T> {
73    /// Create a new TPG36x instance with the given instrument interface.
74    ///
75    /// This function can fail if the instrument is not answering, as the function queries the
76    /// instrument upon initialization in order to set the correct pressure unit that is currently
77    /// displayed.
78    ///
79    /// # Arguments
80    /// - `interface`: An instrument interface that implements the [`InstrumentInterface`] trait.
81    pub fn try_new(interface: T) -> Result<Self, InstrumentError> {
82        let mut intf = interface;
83        intf.set_terminator("\r\n");
84        let interface = Arc::new(Mutex::new(intf));
85        let mut instrument = Tpg36x {
86            interface,
87            unit: Arc::new(Mutex::new(PressureUnit::default())),
88            num_channels: 2, // Default for the TPG362 model, can be changed later
89        };
90        instrument.update_unit()?;
91        Ok(instrument)
92    }
93
94    /// Get a new channel with a given index for the Channel.
95    ///
96    /// Please note that channels are zero-indexed.
97    pub fn get_channel(&mut self, idx: usize) -> Result<Channel<T>, InstrumentError> {
98        if idx >= self.num_channels {
99            return Err(InstrumentError::ChannelIndexOutOfRange {
100                idx,
101                nof_channels: self.num_channels,
102            });
103        }
104        Ok(Channel::new(
105            idx,
106            Arc::clone(&self.interface),
107            Arc::clone(&self.unit),
108        ))
109    }
110
111    /// Get the ethernet configuration of the TPG36x.
112    ///
113    /// This returns the current ethernet configuration of the TPG36x as an [`EthernetConfig`]
114    pub fn get_ethernet_config(&mut self) -> Result<EthernetConfig, InstrumentError> {
115        let response = self.query("ETH")?;
116        EthernetConfig::from_cmd_str(response.as_str())
117            .map_err(|_| InstrumentError::ResponseParseError(response))
118    }
119
120    /// Set the ethernet configuration for the TPG36x.
121    ///
122    /// # Arguments
123    /// - `ethernet_config`: An ethernet configuration.
124    pub fn set_ethernet_config(
125        &mut self,
126        ethernet_config: EthernetConfig,
127    ) -> Result<(), InstrumentError> {
128        self.sendcmd(&ethernet_config.to_command_string())
129    }
130
131    /// Query the name, hard, and firmware version of the device as a string.
132    ///
133    /// This returns, separated by commas, the following information as a string:
134    /// - Type of the unit, e.g. TPG362
135    /// - Model No. of the unit, e.g. PTG28290
136    /// - Serial No. of the unit, e.g. 44990000
137    /// - Firmware version of the unit, e.g.. 010100
138    /// - Hardware version of the unit, e.g. 010100
139    pub fn get_name(&mut self) -> Result<String, InstrumentError> {
140        Ok(self.query("AYT")?.trim().to_string())
141    }
142
143    /// Set the number of channels for the TPG36x.
144    pub fn set_num_channels(&mut self, num: usize) -> Result<(), InstrumentError> {
145        if !(1..3).contains(&num) {
146            let num: i64 = num.try_into().unwrap_or(i64::MAX);
147            return Err(InstrumentError::IntValueOutOfRange {
148                value: num,
149                min: 1,
150                max: 2,
151            });
152        }
153        self.num_channels = num;
154        Ok(())
155    }
156
157    /// Get the MAC address of the instrument.
158    ///
159    /// This returns a string that you can put into your own mac address converter if you like.
160    /// However, as this is a niche feature and MAC address handling is not in `std`, we decided
161    /// to return a String instead.
162    pub fn get_mac_address(&mut self) -> Result<String, InstrumentError> {
163        self.query("MAC")
164    }
165
166    /// Get the current unit from the instrument.
167    ///
168    /// This updates the internally kept unit and returns a copy of it.
169    pub fn get_unit(&mut self) -> Result<PressureUnit, InstrumentError> {
170        self.update_unit()?;
171        let unit = self.unit.lock().expect("Mutex should not be poisoned");
172        Ok(*unit)
173    }
174
175    /// Set the unit for the instrument.
176    ///
177    /// This sets a new unit for the instrument and, if successful, updates the internal unit
178    /// representation to match the new unit.
179    ///
180    /// # Arguments
181    /// - `unit`: The new unit to set for the instrument.
182    pub fn set_unit(&mut self, unit: PressureUnit) -> Result<(), InstrumentError> {
183        self.sendcmd(&format!("UNI,{}", unit.as_str()))?;
184        {
185            let mut current_unit = self.unit.lock().expect("Mutex should not be poisoned");
186            *current_unit = unit;
187        }
188        Ok(())
189    }
190
191    /// Update the unit by querying the instrument for the current unit setting.
192    pub fn update_unit(&mut self) -> Result<(), InstrumentError> {
193        let response = self.query("UNI")?;
194        {
195            let mut unit = self.unit.lock().expect("Mutex should not be poisoned");
196            *unit = PressureUnit::from_cmd_str(response.as_str())?;
197        }
198        Ok(())
199    }
200
201    /// Send a command to the instrument.
202    fn sendcmd(&mut self, cmd: &str) -> Result<(), InstrumentError> {
203        let mut intf = self.interface.lock().expect("Mutex should not be poisoned");
204        intf.sendcmd(cmd)?;
205        intf.check_acknowledgment("\u{6}") // check for "ACK"
206    }
207
208    fn query(&mut self, cmd: &str) -> Result<String, InstrumentError> {
209        self.sendcmd(cmd)?;
210        let mut intf = self.interface.lock().expect("Mutex should not be poisoned");
211        intf.write("\u{5}")?; // send "ENQ"
212        intf.read_until_terminator()
213    }
214}
215
216impl<T: InstrumentInterface> Clone for Tpg36x<T> {
217    fn clone(&self) -> Self {
218        Self {
219            interface: self.interface.clone(),
220            unit: self.unit.clone(),
221            num_channels: self.num_channels,
222        }
223    }
224}
225
226/// Channel structure representing a single channel of the TPG36x.
227///
228/// **This structure can only be created through the [`Tpg36x`] struct.**
229///
230/// Implementation of an individual channel and commands that go to it.
231pub struct Channel<T: InstrumentInterface> {
232    idx: usize,
233    interface: Arc<Mutex<T>>,
234    unit: Arc<Mutex<PressureUnit>>,
235}
236
237impl<T: InstrumentInterface> Channel<T> {
238    /// Get the pressure of this channel in the given unit.
239    ///
240    /// This will return a [`Tpg36xMeasurement`] struct containing the value either as a pressure or
241    /// as a voltage, depending on the setup of the unit.
242    ///
243    /// **Note**: If the unit on the instrument was changed manually, this may not return the
244    /// correct value! In this case, make sure that the `update_unit` function on the [`Tpg36x`]
245    /// struct prior to calling this function!
246    pub fn get_pressure(&mut self) -> Result<Tpg36xMeasurement, InstrumentError> {
247        let resp = self.query(&format!("PR{}", self.idx + 1))?;
248        println!("Response: {resp}");
249        let parts = resp.split(',').collect::<Vec<&str>>();
250        if parts.len() != 2 {
251            return Err(InstrumentError::ResponseParseError(resp));
252        }
253
254        let status = PressMsrDatStat::from_cmd_str(parts[0])?;
255        if status != PressMsrDatStat::Ok {
256            return Err(InstrumentError::InstrumentStatus(format!("{status}")));
257        }
258
259        let val = parts[1]
260            .parse::<f64>()
261            .map_err(|_| InstrumentError::ResponseParseError(resp.to_string()))?;
262        let ret_val = {
263            let unit = self.unit.lock().expect("Mutex should not be poisoned");
264            units::from_value_unit(val, &unit)
265        };
266        Ok(ret_val)
267    }
268
269    /// Get the status of the channel.
270    ///
271    /// This routine returns the status of the channel, i.e., whether the channel is on, off, or in
272    /// a stat that cannot be changed.
273    pub fn get_status(&mut self) -> Result<SensorStatus, InstrumentError> {
274        let resp = self.query("SEN")?;
275        let parts = split_check_resp(&resp, 2)?;
276        // This should be infallible for two reasons:
277        // - We check the length of the vector before in the `split_check_resp` function.
278        // - If it's a one channel gauge, `self.idx = 1` cannot be accessed from the get go.
279        // So if this panics, it is a bug in the code!
280        SensorStatus::from_cmd_str(parts[self.idx])
281    }
282
283    /// Set the status of the channel.
284    ///
285    /// This routine sets the status of the channel, i.e., whether the channel should be on, off,
286    /// or left unchanged.
287    ///
288    /// Note: The manual does not specify different commands for the one or two channel models,
289    /// even though it does for other commands. We thus assume that sending two channels always is
290    /// not a problem, as the second channel on the one channel model is simply ignored. This is an
291    /// assumption, as we currently have no one channel model to test this with.
292    pub fn set_status(&mut self, status: SensorStatus) -> Result<(), InstrumentError> {
293        let mut to_send = [SensorStatus::NoChange, SensorStatus::NoChange];
294        to_send[self.idx] = status; // infallible, `self.idx` can at most be 1
295        self.sendcmd(&format!(
296            "SEN,{},{}",
297            to_send[0].to_cmd_str(),
298            to_send[1].to_cmd_str()
299        ))?;
300        Ok(())
301    }
302
303    /// Get a new channel for the given instrument interface.
304    ///
305    /// This function can only be called from inside of the [`Tpg36x`] struct.
306    fn new(idx: usize, interface: Arc<Mutex<T>>, unit: Arc<Mutex<PressureUnit>>) -> Self {
307        Channel {
308            idx,
309            interface,
310            unit,
311        }
312    }
313
314    /// Send a command for this instrument to an interface.
315    fn sendcmd(&mut self, cmd: &str) -> Result<(), InstrumentError> {
316        let mut intf = self.interface.lock().expect("Mutex should not be poisoned");
317        intf.sendcmd(cmd)?;
318        intf.check_acknowledgment("\u{6}") // check for "ACK"
319    }
320
321    /// Query the instrument with a command and return the response as a String.
322    fn query(&mut self, cmd: &str) -> Result<String, InstrumentError> {
323        self.sendcmd(cmd)?;
324        let mut intf = self.interface.lock().expect("Mutex should not be poisoned");
325        intf.write("\u{5}")?; // send "ENQ"
326        intf.read_until_terminator()
327    }
328}
329
330impl<T: InstrumentInterface> Clone for Channel<T> {
331    fn clone(&self) -> Self {
332        Self {
333            idx: self.idx,
334            interface: self.interface.clone(),
335            unit: self.unit.clone(),
336        }
337    }
338}
339
340/// Split a string slice into its parts by commas, check if of correct length, and return the parts
341/// as a vector.
342fn split_check_resp(resp: &str, exp_len: usize) -> Result<Vec<&str>, InstrumentError> {
343    let parts = resp.split(',').collect::<Vec<&str>>();
344    if parts.len() != exp_len {
345        return Err(InstrumentError::ResponseParseError(resp.to_string()));
346    }
347    Ok(parts)
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use rstest::*;
354
355    /// Ensure that the split really splits by commas and checks the length.
356    #[rstest]
357    fn test_split_check_resp() {
358        let resp = "part1,part2,part3";
359        let parts = split_check_resp(resp, 3).unwrap();
360        assert_eq!(parts.len(), 3);
361        assert_eq!(parts[0], "part1");
362        assert_eq!(parts[1], "part2");
363        assert_eq!(parts[2], "part3");
364
365        // Test with incorrect length
366        assert!(split_check_resp(resp, 2).is_err());
367        assert!(split_check_resp(resp, 4).is_err());
368    }
369
370    /// Ensure that any response without comma returns one part, which is the response itself.
371    #[rstest]
372    #[case("")]
373    #[case("asdf")]
374    fn test_split_check_resp_empty(#[case] resp: &str) {
375        let parts = split_check_resp(resp, 1).unwrap();
376
377        assert_eq!(parts.len(), 1);
378        assert_eq!(parts[0], resp);
379
380        // Test with incorrect length
381        assert!(split_check_resp(resp, 0).is_err());
382        assert!(split_check_resp(resp, 2).is_err());
383    }
384}