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(ðernet_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}