Skip to main content

ci2_pylon/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4extern crate machine_vision_formats as formats;
5
6use anyhow::Context;
7use std::sync::{Arc, Mutex};
8
9use ci2::{
10    AcquisitionMode, AutoMode, DynamicFrameWithInfo, HostTimingInfo, TriggerMode, TriggerSelector,
11};
12use pylon_shimload::HasProperties;
13use strand_dynamic_frame::DynamicFrameOwned;
14
15trait ExtendedError<T> {
16    fn map_pylon_err(self) -> ci2::Result<T>;
17}
18
19impl<T> ExtendedError<T> for std::result::Result<T, pylon_shimload::PylonError> {
20    fn map_pylon_err(self) -> ci2::Result<T> {
21        self.map_err(|pylon_error| ci2::Error::BackendError(anyhow::Error::new(pylon_error)))
22    }
23}
24
25pub type Result<M> = std::result::Result<M, Error>;
26
27const BAD_FNO: usize = usize::MAX;
28
29mod feature_cache;
30use feature_cache::*;
31
32#[derive(thiserror::Error, Debug)]
33pub enum Error {
34    #[error("Pylon error: {source}")]
35    PylonError {
36        #[from]
37        source: pylon_shimload::PylonError,
38    },
39    #[error("int parse error: {source}")]
40    IntParseError {
41        #[from]
42        source: std::num::ParseIntError,
43    },
44    #[error("other error: {msg}")]
45    OtherError { msg: String },
46}
47
48impl From<Error> for ci2::Error {
49    fn from(orig: Error) -> ci2::Error {
50        ci2::Error::BackendError(orig.into())
51    }
52}
53
54pub struct WrappedModule {}
55
56fn to_name(info: &pylon_shimload::DeviceInfo) -> String {
57    // TODO: make ci2 cameras have full_name and friendly_name attributes?
58    // &info.property_value("FullName").unwrap()
59    let serial = &info.property_value("SerialNumber").unwrap();
60    let vendor = &info.property_value("VendorName").unwrap();
61    format!("{}-{}", vendor, serial)
62}
63
64pub fn new_module() -> ci2::Result<WrappedModule> {
65    Ok(WrappedModule {})
66}
67
68// This is just here for backwards compatibility. It doesn't do anything.
69// pylon-shimload handles its own runtime and lifetimes.
70pub struct PylonTerminateGuard {}
71
72pub fn make_singleton_guard(
73    _pylon_module: &dyn ci2::CameraModule<CameraType = WrappedCamera, Guard = PylonTerminateGuard>,
74) -> ci2::Result<PylonTerminateGuard> {
75    Ok(PylonTerminateGuard {})
76}
77
78impl<'a> ci2::CameraModule for &'a WrappedModule {
79    type CameraType = WrappedCamera;
80    type Guard = PylonTerminateGuard;
81
82    fn name(self: &&'a WrappedModule) -> &'static str {
83        "pylon"
84    }
85    fn camera_infos(self: &&'a WrappedModule) -> ci2::Result<Vec<Box<dyn ci2::CameraInfo>>> {
86        let pylon_infos = pylon_shimload::enumerate_devices()
87            .map_pylon_err()
88            .context("enumerate_devices")?;
89        let infos = pylon_infos
90            .into_iter()
91            .map(|info| {
92                let serial = info.property_value("SerialNumber").unwrap();
93                let model = info.property_value("ModelName").unwrap();
94                let vendor = info.property_value("VendorName").unwrap();
95                let name = to_name(&info);
96                let pci = Box::new(PylonCameraInfo {
97                    name,
98                    serial,
99                    model,
100                    vendor,
101                });
102                let ci: Box<dyn ci2::CameraInfo> = pci; // explicitly perform type erasure
103                ci
104            })
105            .collect();
106        Ok(infos)
107    }
108    fn camera(self: &mut &'a WrappedModule, name: &str) -> ci2::Result<Self::CameraType> {
109        WrappedCamera::new(name)
110    }
111    fn settings_file_extension(&self) -> &str {
112        // See https://www.baslerweb.com/en/sales-support/knowledge-base/frequently-asked-questions/saving-camera-features-or-user-sets-as-file-on-hard-disk/588482/
113        "pfs" // Pylon Feature Stream
114    }
115}
116
117#[derive(Debug)]
118struct PylonCameraInfo {
119    name: String,
120    serial: String,
121    model: String,
122    vendor: String,
123}
124
125impl ci2::CameraInfo for PylonCameraInfo {
126    fn name(&self) -> &str {
127        &self.name
128    }
129    fn serial(&self) -> &str {
130        &self.serial
131    }
132    fn model(&self) -> &str {
133        &self.model
134    }
135    fn vendor(&self) -> &str {
136        &self.vendor
137    }
138}
139
140#[derive(Clone)]
141pub struct WrappedCamera {
142    inner: Arc<Mutex<pylon_shimload::InstantCamera>>,
143    store_fno: usize,
144    name: String,
145    serial: String,
146    model: String,
147    vendor: String,
148    grab_result: Arc<Mutex<pylon_shimload::GrabResult>>,
149    is_sfnc2: bool,
150    pfs_cache: Arc<Mutex<PfsCache>>,
151}
152
153fn _test_camera_is_send() {
154    // Compile-time test to ensure WrappedCamera implements Send trait.
155    fn implements<T: Send>() {}
156    implements::<WrappedCamera>();
157}
158
159impl WrappedCamera {
160    fn new(name: &str) -> ci2::Result<Self> {
161        let max_u64_as_usize: usize = u64::MAX.try_into().unwrap();
162        assert_eq!(max_u64_as_usize, BAD_FNO);
163
164        let devices = pylon_shimload::enumerate_devices().context("enumerate_devices")?;
165
166        for device_info in devices.into_iter() {
167            let this_name = to_name(&device_info);
168            if this_name == name {
169                let serial = device_info
170                    .property_value("SerialNumber")
171                    .context("getting serial")?;
172                let model = device_info
173                    .property_value("ModelName")
174                    .context("getting model")?;
175                let vendor = device_info
176                    .property_value("VendorName")
177                    .context("getting vendor")?;
178                let store_fno = 0;
179
180                let cam = pylon_shimload::create_device(&device_info).context("creating device")?;
181                cam.open().context("opening camera")?;
182
183                let is_sfnc2 = match cam
184                    .node_map()
185                    .map_pylon_err()?
186                    .integer_node("DeviceSFNCVersionMajor")
187                    .map_pylon_err()?
188                    .value()
189                {
190                    Ok(major) => major >= 2,
191                    Err(_) => false,
192                };
193
194                let set_max_transfer_size = match std::env::var_os("DISABLE_SET_MAX_TRANSFER_SIZE")
195                {
196                    Some(v) => &v == "0",
197                    None => true,
198                };
199
200                if set_max_transfer_size {
201                    // Set stream grabber MaxTransferSize. This is a
202                    // Basler-specific quirk and so to avoid introducing a
203                    // Basler-specific API, we do this always (unless the user
204                    // sets the environment variable to disable it).
205
206                    let mut node = cam
207                        .stream_grabber_node_map()
208                        .map_pylon_err()?
209                        .integer_node("MaxTransferSize")
210                        .map_pylon_err()?;
211
212                    if let Ok(max_size) = node.max() {
213                        // If this node exists, we want to set it. If we cannot
214                        // open the node (because, e.g. the stream grabber is
215                        // for GigE not USB3), don't bother.
216                        node.set_value(max_size).map_pylon_err()?;
217                        tracing::debug!(
218                            "For camera {}, set stream grabber MaxTransferSize to {}",
219                            name,
220                            max_size
221                        );
222
223                        #[cfg(target_os = "linux")]
224                        {
225                            // This seems to be a USB camera, let's also check /sys/module/usbcore/parameters/usbfs_memory_mb
226                            let fname = "/sys/module/usbcore/parameters/usbfs_memory_mb";
227                            match std::fs::read_to_string(fname) {
228                                Ok(usbfs_memory_mb) => {
229                                    let usbfs_memory_mb: i64 =
230                                        usbfs_memory_mb.trim().parse().unwrap();
231                                    let desired_mb = 1000;
232                                    if usbfs_memory_mb < desired_mb {
233                                        tracing::warn!(
234                                            "You seem to be using a USB3 camera on linux but the file \"{}\" \
235                                        is set to only {}. For best performance, consider setting it to {}. \
236                                        For more information, see \
237                                        https://web.archive.org/web/20230318224225/https://www.baslerweb.com/en/sales-support/knowledge-base/frequently-asked-questions/how-can-i-set-the-usbfs-on-linux-or-linux-for-arm-to-prevent-image-losses-with-pylon-and-usb-cameras/29826/.",
238                                            fname,
239                                            usbfs_memory_mb,
240                                            desired_mb
241                                        );
242                                    } else {
243                                        tracing::debug!(
244                                            "File \"{}\" indicates a value of {}.",
245                                            fname,
246                                            usbfs_memory_mb
247                                        );
248                                    }
249                                }
250                                Err(e) => {
251                                    tracing::warn!(
252                                        "Could not read {} to check USB subsystem memory due to error: {}",
253                                        fname,
254                                        e
255                                    );
256                                }
257                            }
258
259                            // While we are at it, let's check max number of open file descriptors.
260                            // one greater than the maximum file descriptor number that can be opened by this process.
261                            match rlimit::Resource::NOFILE.get() {
262                                Ok((soft, _hard)) => {
263                                    let desired = 4096;
264                                    if soft < desired {
265                                        tracing::warn!(
266                                            "You seem to be using linux but you have only {} file descriptors available. \
267                                        For best performance, set this to at least {}. See https://github.com/basler/pypylon/issues/80#issuecomment-461727225 \
268                                        for more information. Hint: use 'ulimit -n 4096' to update.",
269                                            soft,
270                                            desired
271                                        );
272                                    }
273                                }
274                                Err(e) => {
275                                    tracing::warn!(
276                                        "Could not check max number of open file descriptors due to error: {}",
277                                        e
278                                    );
279                                }
280                            }
281                        }
282                    }
283                }
284
285                let pfs_cache = {
286                    let node_map = cam.node_map().map_pylon_err()?;
287                    let settings = node_map.save_to_string().map_pylon_err()?;
288                    PfsCache::new_from_string(settings)?
289                };
290                let pfs_cache = Arc::new(Mutex::new(pfs_cache));
291
292                let grab_result = Arc::new(Mutex::new(
293                    pylon_shimload::GrabResult::new().map_pylon_err()?,
294                ));
295                return Ok(Self {
296                    // pylon_auto_init: Arc::new(Mutex::new(pylon_shimload::Pylon::new())),
297                    inner: Arc::new(Mutex::new(cam)),
298                    name: name.to_string(),
299                    store_fno,
300                    serial,
301                    model,
302                    vendor,
303                    grab_result,
304                    is_sfnc2,
305                    pfs_cache,
306                });
307            }
308        }
309        Err(Error::OtherError {
310            msg: format!("requested camera '{}' was not found", name),
311        }
312        .into())
313    }
314
315    fn exposure_time_param_name(&self) -> &'static str {
316        if self.is_sfnc2 {
317            "ExposureTime"
318        } else {
319            "ExposureTimeRaw"
320        }
321    }
322
323    fn acquisition_frame_rate_name(&self) -> &'static str {
324        if self.is_sfnc2 {
325            "AcquisitionFrameRate"
326        } else {
327            "AcquisitionFrameRateAbs"
328        }
329    }
330}
331
332impl ci2::CameraInfo for WrappedCamera {
333    fn name(&self) -> &str {
334        &self.name
335    }
336    fn serial(&self) -> &str {
337        &self.serial
338    }
339    fn model(&self) -> &str {
340        &self.model
341    }
342    fn vendor(&self) -> &str {
343        &self.vendor
344    }
345}
346
347impl ci2::Camera for WrappedCamera {
348    // ----- start: weakly typed but easier to implement API -----
349
350    // fn feature_access_query(&self, name: &str) -> ci2::Result<ci2::AccessQueryResult> {
351    //     todo!();
352    // }
353
354    fn command_execute(&self, name: &str, verify: bool) -> ci2::Result<()> {
355        let camera = self.inner.lock().unwrap();
356        camera
357            .node_map()
358            .map_pylon_err()?
359            .command_node(name)
360            .map_pylon_err()?
361            .execute(verify)
362            .map_pylon_err()
363    }
364
365    fn feature_bool(&self, name: &str) -> ci2::Result<bool> {
366        let camera = self.inner.lock().unwrap();
367        camera
368            .node_map()
369            .map_pylon_err()?
370            .boolean_node(name)
371            .map_pylon_err()?
372            .value()
373            .map_pylon_err()
374    }
375
376    fn feature_bool_set(&self, name: &str, value: bool) -> ci2::Result<()> {
377        let camera = self.inner.lock().unwrap();
378        camera
379            .node_map()
380            .map_pylon_err()?
381            .boolean_node(name)
382            .map_pylon_err()?
383            .set_value(value)
384            .map_pylon_err()
385    }
386
387    fn feature_enum(&self, name: &str) -> ci2::Result<String> {
388        let camera = self.inner.lock().unwrap();
389        let node = camera
390            .node_map()
391            .map_pylon_err()?
392            .enum_node(name)
393            .map_pylon_err()?;
394        node.value().map_pylon_err()
395    }
396
397    fn feature_enum_set(&self, name: &str, value: &str) -> ci2::Result<()> {
398        let camera = self.inner.lock().unwrap();
399        let mut node = camera
400            .node_map()
401            .map_pylon_err()?
402            .enum_node(name)
403            .map_pylon_err()?;
404        node.set_value_pfs(&mut self.pfs_cache.lock().unwrap(), value)
405            .map_pylon_err()
406    }
407
408    fn feature_float(&self, name: &str) -> ci2::Result<f64> {
409        let camera = self.inner.lock().unwrap();
410        camera
411            .node_map()
412            .map_pylon_err()?
413            .float_node(name)
414            .map_pylon_err()?
415            .value()
416            .map_pylon_err()
417    }
418
419    fn feature_float_set(&self, name: &str, value: f64) -> ci2::Result<()> {
420        let camera = self.inner.lock().unwrap();
421        camera
422            .node_map()
423            .map_pylon_err()?
424            .float_node(name)
425            .map_pylon_err()?
426            .set_value(value)
427            .map_pylon_err()
428    }
429
430    fn feature_int(&self, name: &str) -> ci2::Result<i64> {
431        let camera = self.inner.lock().unwrap();
432        camera
433            .node_map()
434            .map_pylon_err()?
435            .integer_node(name)
436            .map_pylon_err()?
437            .value()
438            .map_pylon_err()
439    }
440
441    fn feature_int_set(&self, name: &str, value: i64) -> ci2::Result<()> {
442        let camera = self.inner.lock().unwrap();
443        camera
444            .node_map()
445            .map_pylon_err()?
446            .integer_node(name)
447            .map_pylon_err()?
448            .set_value(value)
449            .map_pylon_err()
450    }
451
452    // ----- end: weakly typed but easier to implement API -----
453
454    fn node_map_load(&self, settings: &str) -> ci2::Result<()> {
455        // It seems that sometimes the Pylon PFS (Pylon Feature Stream) files
456        // may have CRLF line endings but loading from a string only works with
457        // LF line endings. So here we convert line endings to LF only.
458        let settings_lf_only = settings.lines().collect::<Vec<_>>().join("\n");
459
460        let camera = self.inner.lock().unwrap();
461        camera
462            .node_map()
463            .map_pylon_err()?
464            .load_from_string(settings_lf_only, true)
465            .map_pylon_err()?;
466        Ok(())
467    }
468
469    fn node_map_save(&self) -> ci2::Result<String> {
470        // Ideally we would simply call camera.node_map().map_pylon_err()?.save_to_string() here,
471        // but this requires stopping the camera. Instead we cache the node
472        // values.
473        Ok(self.pfs_cache.lock().unwrap().to_header_string())
474    }
475
476    /// Return the sensor width in pixels
477    fn width(&self) -> ci2::Result<u32> {
478        Ok(self
479            .inner
480            .lock()
481            .unwrap()
482            .node_map()
483            .map_pylon_err()?
484            .integer_node("Width")
485            .map_pylon_err()?
486            .value()
487            .map_pylon_err()?
488            .try_into()?)
489    }
490    /// Return the sensor height in pixels
491    fn height(&self) -> ci2::Result<u32> {
492        Ok(self
493            .inner
494            .lock()
495            .unwrap()
496            .node_map()
497            .map_pylon_err()?
498            .integer_node("Height")
499            .map_pylon_err()?
500            .value()
501            .map_pylon_err()?
502            .try_into()?)
503    }
504
505    // Settings: PixFmt ----------------------------
506    fn pixel_format(&self) -> ci2::Result<formats::PixFmt> {
507        let camera = self.inner.lock().unwrap();
508        let pixel_format_node = camera
509            .node_map()
510            .map_pylon_err()?
511            .enum_node("PixelFormat")
512            .map_pylon_err()?;
513        convert_to_pixel_format(pixel_format_node.value().map_pylon_err()?.as_ref())
514    }
515    fn possible_pixel_formats(&self) -> ci2::Result<Vec<formats::PixFmt>> {
516        let camera = self.inner.lock().unwrap();
517        let pixel_format_node = camera
518            .node_map()
519            .map_pylon_err()?
520            .enum_node("PixelFormat")
521            .map_pylon_err()?;
522        // This version returns only the formats we know, silently dropping the unknowns.
523        Ok(pixel_format_node
524            .settable_values()
525            .map_pylon_err()?
526            .iter()
527            .filter_map(|string_val| convert_to_pixel_format(string_val).ok())
528            .collect::<Vec<formats::PixFmt>>())
529        // This version returns only the formats we know, returning an error if an unknown is found.
530        // Ok(pixel_format_node
531        //     .settable_values()
532        //     .map_pylon_err()?
533        //     .iter()
534        //     .map(|string_val| convert_to_pixel_format(string_val))
535        //     .collect::<ci2::Result<Vec<formats::PixFmt>>>()?)
536    }
537    fn set_pixel_format(&mut self, pixel_format: formats::PixFmt) -> ci2::Result<()> {
538        let camera = self.inner.lock().unwrap();
539        let mut pixel_format_node = camera
540            .node_map()
541            .map_pylon_err()?
542            .enum_node("PixelFormat")
543            .map_pylon_err()?;
544        // Pick whichever name this camera actually offers, since modern (ace2)
545        // and legacy Basler cameras use different names for the same format.
546        let available = pixel_format_node.settable_values().map_pylon_err()?;
547        let s = choose_pixel_format_name(pixel_format, &available)?;
548        pixel_format_node
549            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), s)
550            .map_pylon_err()
551    }
552
553    // Settings: Exposure Time ----------------------------
554    /// value given in microseconds
555    fn exposure_time(&self) -> ci2::Result<f64> {
556        let camera = self.inner.lock().unwrap();
557        let name = self.exposure_time_param_name();
558        if self.is_sfnc2 {
559            camera
560                .node_map()
561                .map_pylon_err()?
562                .float_node(name)
563                .map_pylon_err()?
564                .value()
565                .map_pylon_err()
566        } else {
567            camera
568                .node_map()
569                .map_pylon_err()?
570                .integer_node(name)
571                .map_pylon_err()?
572                .value()
573                .map_pylon_err()
574                .map(|x| x as f64)
575        }
576    }
577
578    /// value given in microseconds
579    fn exposure_time_range(&self) -> ci2::Result<(f64, f64)> {
580        let camera = self.inner.lock().unwrap();
581        let name = self.exposure_time_param_name();
582        if self.is_sfnc2 {
583            let node = camera
584                .node_map()
585                .map_pylon_err()?
586                .float_node(name)
587                .map_pylon_err()?;
588            Ok((node.min().map_pylon_err()?, node.max().map_pylon_err()?))
589        } else {
590            let node = camera
591                .node_map()
592                .map_pylon_err()?
593                .integer_node(name)
594                .map_pylon_err()?;
595            Ok((
596                node.min().map_pylon_err()? as f64,
597                node.max().map_pylon_err()? as f64,
598            ))
599        }
600    }
601
602    /// value given in microseconds
603    fn set_exposure_time(&mut self, value: f64) -> ci2::Result<()> {
604        let camera = self.inner.lock().unwrap();
605        let name = self.exposure_time_param_name();
606        if self.is_sfnc2 {
607            camera
608                .node_map()
609                .map_pylon_err()?
610                .float_node(name)
611                .map_pylon_err()?
612                .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), value)
613                .map_pylon_err()
614        } else {
615            camera
616                .node_map()
617                .map_pylon_err()?
618                .integer_node(name)
619                .map_pylon_err()?
620                .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), value.round() as i64)
621                .map_pylon_err()
622        }
623    }
624
625    // Settings: Exposure Time Auto Mode ----------------------------
626    fn exposure_auto(&self) -> ci2::Result<AutoMode> {
627        let camera = self.inner.lock().unwrap();
628        let val = camera
629            .node_map()
630            .map_pylon_err()?
631            .enum_node("ExposureAuto")
632            .map_pylon_err()?
633            .value()
634            .map_pylon_err()?;
635        str_to_auto_mode(val.as_ref())
636    }
637    fn set_exposure_auto(&mut self, value: AutoMode) -> ci2::Result<()> {
638        let sval = mode_to_str(value);
639        self.inner
640            .lock()
641            .unwrap()
642            .node_map()
643            .map_pylon_err()?
644            .enum_node("ExposureAuto")
645            .map_pylon_err()?
646            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), sval)
647            .map_pylon_err()
648    }
649
650    // Settings: Gain ----------------------------
651    /// value given in dB
652    fn gain(&self) -> ci2::Result<f64> {
653        let camera = self.inner.lock().unwrap();
654        if self.is_sfnc2 {
655            camera
656                .node_map()
657                .map_pylon_err()?
658                .float_node("Gain")
659                .map_pylon_err()?
660                .value()
661                .map_pylon_err()
662        } else {
663            let gain_raw = camera
664                .node_map()
665                .map_pylon_err()?
666                .integer_node("GainRaw")
667                .map_pylon_err()?
668                .value()
669                .map_pylon_err()?;
670
671            let gain_db = gain_raw_to_db(gain_raw)?;
672            // debug!("got gain raw {}, converted to db {}", gain_raw, gain_db);
673            Ok(gain_db as f64)
674        }
675    }
676    /// value given in dB
677    fn gain_range(&self) -> ci2::Result<(f64, f64)> {
678        let camera = self.inner.lock().unwrap();
679        if self.is_sfnc2 {
680            let gain_node = camera
681                .node_map()
682                .map_pylon_err()?
683                .float_node("Gain")
684                .map_pylon_err()?;
685            Ok((
686                gain_node.min().map_pylon_err()?,
687                gain_node.max().map_pylon_err()?,
688            ))
689        } else {
690            let gain_node = camera
691                .node_map()
692                .map_pylon_err()?
693                .integer_node("GainRaw")
694                .map_pylon_err()?;
695
696            let gain_min = gain_node.min().map_pylon_err()?;
697            let gain_max = gain_node.max().map_pylon_err()?;
698
699            let gain_min_db = gain_raw_to_db(gain_min)?;
700            let gain_max_db = gain_raw_to_db(gain_max)?;
701            Ok((gain_min_db, gain_max_db))
702        }
703    }
704
705    /// value given in dB
706    fn set_gain(&mut self, gain_db: f64) -> ci2::Result<()> {
707        let camera = self.inner.lock().unwrap();
708        if self.is_sfnc2 {
709            camera
710                .node_map()
711                .map_pylon_err()?
712                .float_node("Gain")
713                .map_pylon_err()?
714                .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), gain_db)
715                .map_pylon_err()?;
716        } else {
717            let gain_raw = gain_db_to_raw(gain_db)?;
718            camera
719                .node_map()
720                .map_pylon_err()?
721                .integer_node("GainRaw")
722                .map_pylon_err()?
723                .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), gain_raw)
724                .map_pylon_err()?;
725        }
726        Ok(())
727    }
728
729    // Settings: Gain Auto Mode ----------------------------
730    fn gain_auto(&self) -> ci2::Result<AutoMode> {
731        let camera = self.inner.lock().unwrap();
732        let val = camera
733            .node_map()
734            .map_pylon_err()?
735            .enum_node("GainAuto")
736            .map_pylon_err()?
737            .value()
738            .map_pylon_err()?;
739        str_to_auto_mode(val.as_ref())
740    }
741
742    fn set_gain_auto(&mut self, value: AutoMode) -> ci2::Result<()> {
743        let sval = mode_to_str(value);
744        self.inner
745            .lock()
746            .unwrap()
747            .node_map()
748            .map_pylon_err()?
749            .enum_node("GainAuto")
750            .map_pylon_err()?
751            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), sval)
752            .map_pylon_err()
753    }
754
755    // Settings: TriggerMode ----------------------------
756    fn trigger_mode(&self) -> ci2::Result<TriggerMode> {
757        let camera = self.inner.lock().unwrap();
758        let val = camera
759            .node_map()
760            .map_pylon_err()?
761            .enum_node("TriggerMode")
762            .map_pylon_err()?
763            .value()
764            .map_pylon_err()?;
765        match val.as_ref() {
766            "Off" => Ok(ci2::TriggerMode::Off),
767            "On" => Ok(ci2::TriggerMode::On),
768            s => Err(ci2::Error::from(format!(
769                "unexpected TriggerMode enum string: {}",
770                s
771            ))),
772        }
773    }
774    fn set_trigger_mode(&mut self, value: TriggerMode) -> ci2::Result<()> {
775        let sval = match value {
776            ci2::TriggerMode::Off => "Off",
777            ci2::TriggerMode::On => "On",
778        };
779        self.inner
780            .lock()
781            .unwrap()
782            .node_map()
783            .map_pylon_err()?
784            .enum_node("TriggerMode")
785            .map_pylon_err()?
786            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), sval)
787            .map_pylon_err()
788    }
789
790    // Settings: AcquisitionFrameRateEnable ----------------------------
791    fn acquisition_frame_rate_enable(&self) -> ci2::Result<bool> {
792        self.inner
793            .lock()
794            .unwrap()
795            .node_map()
796            .map_pylon_err()?
797            .boolean_node("AcquisitionFrameRateEnable")
798            .map_pylon_err()?
799            .value()
800            .map_pylon_err()
801    }
802    fn set_acquisition_frame_rate_enable(&mut self, value: bool) -> ci2::Result<()> {
803        self.inner
804            .lock()
805            .unwrap()
806            .node_map()
807            .map_pylon_err()?
808            .boolean_node("AcquisitionFrameRateEnable")
809            .map_pylon_err()?
810            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), value)
811            .map_pylon_err()
812    }
813
814    // Settings: AcquisitionFrameRate ----------------------------
815    fn acquisition_frame_rate(&self) -> ci2::Result<f64> {
816        let camera = self.inner.lock().unwrap();
817        let node = camera
818            .node_map()
819            .map_pylon_err()?
820            .float_node(self.acquisition_frame_rate_name())
821            .map_pylon_err()?;
822        node.value().map_pylon_err()
823    }
824    fn acquisition_frame_rate_range(&self) -> ci2::Result<(f64, f64)> {
825        let camera = self.inner.lock().unwrap();
826        let node = camera
827            .node_map()
828            .map_pylon_err()?
829            .float_node(self.acquisition_frame_rate_name())
830            .map_pylon_err()?;
831        Ok((node.min().map_pylon_err()?, node.max().map_pylon_err()?))
832    }
833    fn set_acquisition_frame_rate(&mut self, value: f64) -> ci2::Result<()> {
834        self.inner
835            .lock()
836            .unwrap()
837            .node_map()
838            .map_pylon_err()?
839            .float_node(self.acquisition_frame_rate_name())
840            .map_pylon_err()?
841            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), value)
842            .map_pylon_err()
843    }
844
845    // Settings: TriggerSelector ----------------------------
846    fn trigger_selector(&self) -> ci2::Result<TriggerSelector> {
847        let camera = self.inner.lock().unwrap();
848        let val = camera
849            .node_map()
850            .map_pylon_err()?
851            .enum_node("TriggerSelector")
852            .map_pylon_err()?
853            .value()
854            .map_pylon_err()?;
855        match val.as_ref() {
856            "AcquisitionStart" => Ok(ci2::TriggerSelector::AcquisitionStart),
857            "FrameBurstStart" => Ok(ci2::TriggerSelector::FrameBurstStart),
858            "FrameStart" => Ok(ci2::TriggerSelector::FrameStart),
859            "ExposureActive" => Ok(ci2::TriggerSelector::ExposureActive),
860            s => Err(ci2::Error::from(format!(
861                "unexpected TriggerSelector enum string: {}",
862                s
863            ))),
864        }
865    }
866    fn set_trigger_selector(&mut self, value: TriggerSelector) -> ci2::Result<()> {
867        let sval = match value {
868            ci2::TriggerSelector::AcquisitionStart => "AcquisitionStart",
869            ci2::TriggerSelector::FrameBurstStart => "FrameBurstStart",
870            ci2::TriggerSelector::FrameStart => "FrameStart",
871            ci2::TriggerSelector::ExposureActive => "ExposureActive",
872            s => {
873                return Err(ci2::Error::from(format!(
874                    "unexpected TriggerSelector: {:?}",
875                    s
876                )));
877            }
878        };
879        let camera = self.inner.lock().unwrap();
880        camera
881            .node_map()
882            .map_pylon_err()?
883            .enum_node("TriggerSelector")
884            .map_pylon_err()?
885            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), sval)
886            .map_pylon_err()
887    }
888
889    // Settings: AcquisitionMode ----------------------------
890    fn acquisition_mode(&self) -> ci2::Result<AcquisitionMode> {
891        let mode = self
892            .inner
893            .lock()
894            .unwrap()
895            .node_map()
896            .map_pylon_err()?
897            .enum_node("AcquisitionMode")
898            .map_pylon_err()?
899            .value()
900            .map_pylon_err()?;
901        Ok(match mode.as_ref() {
902            "Continuous" => ci2::AcquisitionMode::Continuous,
903            "SingleFrame" => ci2::AcquisitionMode::SingleFrame,
904            "MultiFrame" => ci2::AcquisitionMode::MultiFrame,
905            s => {
906                return Err(ci2::Error::from(format!(
907                    "unexpected AcquisitionMode: {:?}",
908                    s
909                )));
910            }
911        })
912    }
913    fn set_acquisition_mode(&mut self, value: ci2::AcquisitionMode) -> ci2::Result<()> {
914        let sval = match value {
915            ci2::AcquisitionMode::Continuous => "Continuous",
916            ci2::AcquisitionMode::SingleFrame => "SingleFrame",
917            ci2::AcquisitionMode::MultiFrame => "MultiFrame",
918        };
919        self.inner
920            .lock()
921            .unwrap()
922            .node_map()
923            .map_pylon_err()?
924            .enum_node("AcquisitionMode")
925            .map_pylon_err()?
926            .set_value_pfs(&mut self.pfs_cache.lock().unwrap(), sval)
927            .map_pylon_err()
928    }
929
930    // Acquisition ----------------------------
931    fn acquisition_start(&mut self) -> ci2::Result<()> {
932        self.inner
933            .lock()
934            .unwrap()
935            .start_grabbing(&pylon_shimload::GrabOptions::default())
936            .map_pylon_err()?;
937        Ok(())
938    }
939    fn acquisition_stop(&mut self) -> ci2::Result<()> {
940        self.inner.lock().unwrap().stop_grabbing().map_pylon_err()?;
941        Ok(())
942    }
943
944    /// synchronous (blocking) frame acquisition
945    fn next_frame(&mut self) -> ci2::Result<DynamicFrameWithInfo> {
946        let pixel_format = self.pixel_format()?;
947
948        let mut gr = self.grab_result.lock().unwrap();
949        let cam = self.inner.lock().unwrap();
950
951        // Wait for an image and then retrieve it. A timeout of 99999 ms is used.
952        cam.retrieve_result(
953            99999,
954            &mut gr,
955            pylon_shimload::TimeoutHandling::ThrowException,
956        )
957        .map_pylon_err()?;
958
959        let now = chrono::Utc::now(); // earliest possible timestamp
960
961        // Image grabbed successfully?
962        if gr.grab_succeeded().map_pylon_err()? {
963            let buffer = gr.buffer().map_pylon_err()?;
964            let block_id = gr.block_id().map_pylon_err()?;
965
966            let fno: usize = self.store_fno;
967            self.store_fno += 1;
968
969            let width = gr.width().map_pylon_err()?;
970            let height = gr.height().map_pylon_err()?;
971            let stride = gr.stride().map_pylon_err()?;
972            let image_data = buffer.to_vec();
973            let device_timestamp = gr.time_stamp().map_pylon_err()?;
974
975            let backend_data = if !(device_timestamp == 0 && block_id == u64::MAX) {
976                Some(Box::new(ci2_pylon_types::PylonExtra {
977                    block_id,
978                    device_timestamp,
979                }) as Box<dyn ci2::BackendData>)
980            } else {
981                // This happens when the Basler driver emulates a camera. Don't
982                // propagate these bad values further.
983                None
984            };
985
986            let host_timing = HostTimingInfo { fno, datetime: now };
987            let image = Arc::new(
988                DynamicFrameOwned::from_buf(width, height, stride, image_data, pixel_format)
989                    .unwrap(),
990            );
991
992            Ok(DynamicFrameWithInfo {
993                image,
994                host_timing,
995                backend_data,
996            })
997
998        // println!("Gray value of first pixel: {}\n", image_buffer[0]);
999        } else {
1000            self.store_fno += 1;
1001
1002            Err(ci2::Error::SingleFrameError(format!(
1003                "Pylon Error {}: {}",
1004                gr.error_code().map_pylon_err()?,
1005                gr.error_description().map_pylon_err()?
1006            )))
1007        }
1008    }
1009}
1010
1011pub fn convert_pixel_format(pixel_format: formats::PixFmt) -> ci2::Result<&'static str> {
1012    // Return the preferred (modern SFNC) name for each format. See
1013    // `pixel_format_name_candidates` for the full set of accepted names and
1014    // `choose_pixel_format_name` for how a camera-specific name is selected.
1015    Ok(pixel_format_name_candidates(pixel_format)?[0])
1016}
1017
1018/// Returns the candidate Basler/pylon `PixelFormat` enum names for a given
1019/// [`formats::PixFmt`], in order of preference.
1020///
1021/// Some formats have more than one possible name across Basler camera
1022/// generations: modern SFNC 2.x cameras (e.g. Basler ace2) use `RGB8` and
1023/// `YCbCr422_8`, while older cameras (and the Pylon camera emulator) use the
1024/// legacy `RGB8Packed` / `YUV422Packed`. Callers should pick whichever
1025/// candidate the camera actually offers (see [`choose_pixel_format_name`]).
1026/// See strawlab/strand-braid#29.
1027fn pixel_format_name_candidates(pixel_format: formats::PixFmt) -> ci2::Result<Vec<&'static str>> {
1028    use formats::PixFmt::*;
1029    let pixfmt = match pixel_format {
1030        Mono8 => vec!["Mono8"],
1031
1032        // MONO10 => "Mono10",
1033        // MONO10p => "Mono10p",
1034        // MONO12 => "Mono12",
1035        // MONO12p => "Mono12p",
1036        // MONO16 => "Mono16",
1037        // Modern SFNC name first, then legacy names. The exact spelling of the
1038        // legacy name varies (`RGB8Packed` on most Basler models and the Pylon
1039        // emulator; `RGB8packed` seen elsewhere), so list both.
1040        YUV422 => vec!["YCbCr422_8", "YUV422Packed", "YUV422packed"],
1041        RGB8 => vec!["RGB8", "RGB8Packed", "RGB8packed"],
1042
1043        BayerGR8 => vec!["BayerGR8"],
1044        BayerRG8 => vec!["BayerRG8"],
1045        BayerBG8 => vec!["BayerBG8"],
1046        BayerGB8 => vec!["BayerGB8"],
1047        // e => {
1048        //     return Err(ci2::Error::from(format!("Unknown PixelFormat {:?}", e)));
1049        // }
1050        unknown => {
1051            return Err(ci2::Error::from(format!("Unsuppored PixFmt {}", unknown)));
1052        }
1053    };
1054    Ok(pixfmt)
1055}
1056
1057/// Choose the pylon `PixelFormat` enum name to set for `pixel_format` given the
1058/// names the camera actually reports as settable.
1059///
1060/// This resolves the ambiguity between modern SFNC names (`RGB8`,
1061/// `YCbCr422_8`) and legacy names (`RGB8Packed`, `YUV422Packed`) by preferring
1062/// whichever candidate the camera offers. See strawlab/strand-braid#29.
1063fn choose_pixel_format_name(
1064    pixel_format: formats::PixFmt,
1065    available: &[String],
1066) -> ci2::Result<&'static str> {
1067    let candidates = pixel_format_name_candidates(pixel_format)?;
1068    candidates
1069        .iter()
1070        .copied()
1071        .find(|candidate| available.iter().any(|a| a == candidate))
1072        .ok_or_else(|| {
1073            ci2::Error::from(format!(
1074                "camera does not support pixel format {pixel_format} \
1075                 (tried {candidates:?}, camera offers {available:?})"
1076            ))
1077        })
1078}
1079
1080pub fn convert_to_pixel_format(orig: &str) -> ci2::Result<formats::PixFmt> {
1081    use formats::PixFmt::*;
1082    let pixfmt = match orig {
1083        "Mono8" => Mono8,
1084        // "Mono10" => MONO10,
1085        // "Mono10p" => MONO10p,
1086        // "Mono12" => MONO12,
1087        // "Mono12p" => MONO12p,
1088        // "Mono16" => MONO16,
1089        // Accept both the legacy and modern SFNC names (Basler ace2 color
1090        // cameras report the modern names; the Pylon emulator and older models
1091        // report `RGB8Packed` / `YUV422Packed`). See strawlab/strand-braid#29.
1092        "YUV422packed" | "YUV422Packed" | "YCbCr422_8" => YUV422,
1093        "RGB8packed" | "RGB8Packed" | "RGB8" => RGB8,
1094
1095        "BayerGR8" => BayerGR8,
1096        "BayerRG8" => BayerRG8,
1097        "BayerGB8" => BayerGB8,
1098        "BayerBG8" => BayerBG8,
1099
1100        e => {
1101            return Err(ci2::Error::from(format!(
1102                "Unknown pixel format string: {:?}",
1103                e
1104            )));
1105        }
1106    };
1107    Ok(pixfmt)
1108}
1109
1110fn gain_raw_to_db(raw: i64) -> ci2::Result<f64> {
1111    // TODO check name of camera model with "Gain Properties" table
1112    // in Basler Product Documentation to ensure this is correct for
1113    // this particular camera model.
1114    Ok(0.0359 * raw as f64)
1115}
1116
1117fn gain_db_to_raw(db: f64) -> ci2::Result<i64> {
1118    // TODO check name of camera model with "Gain Properties" table
1119    // in Basler Product Documentation to ensure this is correct for
1120    // this particular camera model.
1121    Ok((db / 0.0359) as i64)
1122}
1123
1124fn str_to_auto_mode(val: &str) -> ci2::Result<ci2::AutoMode> {
1125    match val {
1126        "Off" => Ok(ci2::AutoMode::Off),
1127        "Once" => Ok(ci2::AutoMode::Once),
1128        "Continuous" => Ok(ci2::AutoMode::Continuous),
1129        s => Err(ci2::Error::from(format!(
1130            "unexpected AutoMode enum string: {}",
1131            s
1132        ))),
1133    }
1134}
1135
1136fn mode_to_str(value: AutoMode) -> &'static str {
1137    match value {
1138        ci2::AutoMode::Off => "Off",
1139        ci2::AutoMode::Once => "Once",
1140        ci2::AutoMode::Continuous => "Continuous",
1141    }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146    use super::*;
1147
1148    // Regression test for strawlab/strand-braid#29.
1149    //
1150    // Basler ace2 (ace 2) color cameras report and accept the modern SFNC
1151    // pixel-format names `RGB8` and `YCbCr422_8` instead of the legacy
1152    // `RGB8packed` / `YUV422packed` used by older Basler models. When these
1153    // modern names are not recognized, `possible_pixel_formats` silently drops
1154    // color formats and the ffmpeg/y4m recording fallback cannot be used on
1155    // such cameras.
1156    #[test]
1157    fn test_modern_pixel_format_names_recognized() {
1158        assert_eq!(
1159            convert_to_pixel_format("RGB8").unwrap(),
1160            formats::PixFmt::RGB8
1161        );
1162        assert_eq!(
1163            convert_to_pixel_format("YCbCr422_8").unwrap(),
1164            formats::PixFmt::YUV422
1165        );
1166    }
1167
1168    fn names(vals: &[&str]) -> Vec<String> {
1169        vals.iter().map(|s| s.to_string()).collect()
1170    }
1171
1172    // A modern (ace2) color camera only offers the modern SFNC names, so
1173    // setting RGB8 / YUV422 must resolve to those (not the legacy names that
1174    // ace2 cameras reject).
1175    #[test]
1176    fn test_choose_pixel_format_name_ace2() {
1177        let offered = names(&["Mono8", "BayerRG8", "RGB8", "YCbCr422_8"]);
1178        assert_eq!(
1179            choose_pixel_format_name(formats::PixFmt::RGB8, &offered).unwrap(),
1180            "RGB8"
1181        );
1182        assert_eq!(
1183            choose_pixel_format_name(formats::PixFmt::YUV422, &offered).unwrap(),
1184            "YCbCr422_8"
1185        );
1186    }
1187
1188    // A legacy camera only offers the legacy names.
1189    #[test]
1190    fn test_choose_pixel_format_name_legacy() {
1191        let offered = names(&["Mono8", "BayerRG8", "RGB8packed", "YUV422packed"]);
1192        assert_eq!(
1193            choose_pixel_format_name(formats::PixFmt::RGB8, &offered).unwrap(),
1194            "RGB8packed"
1195        );
1196        assert_eq!(
1197            choose_pixel_format_name(formats::PixFmt::YUV422, &offered).unwrap(),
1198            "YUV422packed"
1199        );
1200    }
1201
1202    // The Pylon camera emulator (PYLON_CAMEMU, used by the smoke tests) offers
1203    // the capital-P legacy name `RGB8Packed`. This is the exact set of names it
1204    // reports, and it must resolve to `RGB8`. Regression guard for the
1205    // convert_pixel_format bug that emitted only lowercase `RGB8packed`.
1206    #[test]
1207    fn test_choose_pixel_format_name_emulator() {
1208        let offered = names(&[
1209            "Mono8",
1210            "Mono10",
1211            "Mono12",
1212            "Mono16",
1213            "BGRA8Packed",
1214            "BGR8Packed",
1215            "RGB8Packed",
1216            "RGB16Packed",
1217            "BayerGR8",
1218            "BayerRG8",
1219            "BayerGB8",
1220            "BayerBG8",
1221        ]);
1222        assert_eq!(
1223            choose_pixel_format_name(formats::PixFmt::RGB8, &offered).unwrap(),
1224            "RGB8Packed"
1225        );
1226        assert_eq!(
1227            convert_to_pixel_format("RGB8Packed").unwrap(),
1228            formats::PixFmt::RGB8
1229        );
1230    }
1231
1232    // If the camera offers none of the candidate names, we get an error rather
1233    // than blindly setting an unsupported value.
1234    #[test]
1235    fn test_choose_pixel_format_name_unsupported() {
1236        let offered = names(&["Mono8", "BayerRG8"]);
1237        assert!(choose_pixel_format_name(formats::PixFmt::RGB8, &offered).is_err());
1238    }
1239
1240    // Every candidate name must round-trip back through
1241    // `convert_to_pixel_format` (guards against the old "RGB8Packed" typo).
1242    #[test]
1243    fn test_pixel_format_roundtrip() {
1244        for pixfmt in [
1245            formats::PixFmt::Mono8,
1246            formats::PixFmt::RGB8,
1247            formats::PixFmt::YUV422,
1248            formats::PixFmt::BayerGR8,
1249            formats::PixFmt::BayerRG8,
1250            formats::PixFmt::BayerGB8,
1251            formats::PixFmt::BayerBG8,
1252        ] {
1253            for name in pixel_format_name_candidates(pixfmt).unwrap() {
1254                assert_eq!(
1255                    convert_to_pixel_format(name).unwrap(),
1256                    pixfmt,
1257                    "round-trip failed for name {name:?}"
1258                );
1259            }
1260            // The public single-name accessor must also round-trip.
1261            assert_eq!(
1262                convert_to_pixel_format(convert_pixel_format(pixfmt).unwrap()).unwrap(),
1263                pixfmt
1264            );
1265        }
1266    }
1267}