playerone-sdk 0.2.3

PlayerOne SDK bindings
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
use std::ffi::{c_char, c_int, c_long};

use playerone_sdk_sys::POABool::{POA_FALSE, POA_TRUE};
use playerone_sdk_sys::POAConfig::{POA_EXPOSURE, POA_GAIN};
use playerone_sdk_sys::{
    FromPOAConfigValue, POACameraProperties, POACloseCamera, POAConfigAttributes, POAConfigValue,
    POAErrors, POAGetCameraCount, POAGetCameraProperties, POAGetConfig, POAGetConfigAttributes,
    POAGetConfigsCount, POAGetImageBin, POAGetImageData, POAGetImageFormat, POAGetImageSize,
    POAGetImageStartPos, POAGetSensorMode, POAGetSensorModeCount, POAGetSensorModeInfo,
    POAImageReady, POAInitCamera, POAOpenCamera, POASensorModeInfo, POASetConfig, POASetEnableDPS,
    POASetImageBin, POASetImageFormat, POASetImageSize, POASetImageStartPos, POASetSensorMode,
    POAStartExposure, POAStopExposure, _POABool as POABool, _POAConfig as POAConfig, _POAErrors,
    _POAImgFormat as POAImgFormat,
};

use crate::{AllConfigBounds, CameraProperties, Error, ImageFormat, SensorMode};

type POAResult<T> = Result<T, Error>;

/// Region Of Interest
#[derive(Debug, Copy, Clone)]
pub struct ROI {
    pub start_x: u32,
    pub start_y: u32,
    pub width: u32,
    pub height: u32,
}

/// Description of a camera
/// Can be used to open the camera and get access to many more functionality
pub struct CameraDescription {
    camera_id: i32,
    properties: CameraProperties,
}

impl CameraDescription {
    pub fn camera_id(&self) -> i32 {
        self.camera_id
    }

    pub fn properties(&self) -> &CameraProperties {
        &self.properties
    }

    pub fn open(self) -> POAResult<Camera> {
        let mut camera = Camera {
            camera_id: self.camera_id,
            closed: false,
            properties: self.properties,
        };
        camera.open()?;
        Ok(camera)
    }
}

#[derive(Debug)]
pub struct Camera {
    camera_id: i32,
    closed: bool,
    properties: CameraProperties,
}

impl Drop for Camera {
    fn drop(&mut self) {
        if !self.closed {
            // error can be handled by calling close() manually
            let _ = unsafe { POACloseCamera(self.camera_id) };
        }
    }
}

impl Camera {
    /// Returns the list of all available cameras
    /// Call open() on the CameraDescription to get a Camera instance
    pub fn all_cameras() -> Vec<CameraDescription> {
        let camera_count = unsafe { POAGetCameraCount() };
        let mut cameras = Vec::with_capacity(camera_count as usize);

        for i in 0..camera_count {
            let mut camera_prop: POACameraProperties = POACameraProperties::default();
            let error = unsafe { POAGetCameraProperties(i, &raw mut camera_prop) };

            if error != _POAErrors::POA_OK {
                continue;
            }
            cameras.push(CameraDescription {
                camera_id: camera_prop.cameraID,
                properties: camera_prop.into(),
            });
        }

        cameras
    }

    fn open(&mut self) -> POAResult<()> {
        let error = unsafe { POAOpenCamera(self.camera_id) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }

        let error = unsafe { POAInitCamera(self.camera_id) };
        if error != _POAErrors::POA_OK {
            unsafe { POACloseCamera(self.camera_id) };

            return Err(error.into());
        }

        Ok(())
    }

    /// get a single frame, this function will block or wait for the timeout (in ms)
    ///
    /// To get continuous frames, prefer to use the stream() method
    ///
    /// the buffer size must be bigger than this: POA_RAW8: width * height, POA_RAW16: width * height * 2, POA_RGB24: width * height * 3
    pub fn capture(&mut self, buffer: &mut [u8], timeout: Option<i32>) -> POAResult<()> {
        let error = unsafe { POAStartExposure(self.camera_id, POA_TRUE) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        self.get_image_data(buffer, timeout)?;
        self.stop_exposure()?;
        Ok(())
    }

    /// Calls the callback continuously with the newest image data.
    /// Stops the stream if the callback returns false.
    pub fn stream(
        &mut self,
        timeout: Option<u32>,
        mut callback: impl FnMut(&mut Camera, &[u8]) -> bool,
    ) -> POAResult<()> {
        if let Some(timeout) = timeout {
            if timeout > i32::MAX as u32 {
                return Err(Error::OutOfBounds);
            }
        }

        let mut buffer = self.create_image_buffer();

        self.start_exposure()?;
        loop {
            match self.get_image_data(&mut buffer, timeout.map(|t| t as i32)) {
                Ok(_) => (),
                Err(e) => {
                    let _ = self.stop_exposure();
                    return Err(e);
                }
            }
            if !callback(self, &buffer) {
                break;
            }
        }

        self.stop_exposure()?;
        Ok(())
    }

    /// Creates a buffer of the proper size to hold the image data
    pub fn create_image_buffer(&self) -> Vec<u8> {
        let (w, h) = self.image_size();
        let format = self.image_format().unwrap();
        vec![0; w as usize * h as usize * format.bytes_per_pixel()]
    }

    /// start camera exposure for manual control over frame fetching
    /// Prefer to use stream() or single_frame() method for easier use.
    pub fn start_exposure(&mut self) -> POAResult<()> {
        let error = unsafe { POAStartExposure(self.camera_id, POA_FALSE) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// the image data is available? if true, you can call get_image_data to get image data
    pub fn is_image_ready(&self) -> POAResult<bool> {
        let mut is_img_data_available = POA_FALSE;
        let error = unsafe { POAImageReady(self.camera_id, &raw mut is_img_data_available) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(is_img_data_available.into())
    }

    /// get image data after exposure, this function will block or wait for the timeout (in ms)
    /// None timeout means infinite blocking
    ///
    /// the buffer size must be bigger than this: POA_RAW8: width * height, POA_RAW16: width * height * 2, POA_RGB24: width * height * 3
    pub fn get_image_data(&self, buffer: &mut [u8], timeout_ms: Option<i32>) -> POAResult<()> {
        let error = unsafe {
            POAGetImageData(
                self.camera_id,
                buffer.as_mut_ptr(),
                buffer.len() as c_long,
                timeout_ms.unwrap_or(-1),
            )
        };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Stops the exposure. Must be called before any other camera operations if start_exposure was called.
    pub fn stop_exposure(&mut self) -> POAResult<()> {
        let error = unsafe { POAStopExposure(self.camera_id) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Close the camera. This is done automatically on Camera drop but can be called manually if you wish to handle any errors
    /// that may occur.
    pub fn close(mut self) -> POAResult<()> {
        self.closed = true;

        let error = unsafe { POACloseCamera(self.camera_id) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Returns the bounds of all the configurations available for this camera
    /// This is an expensive operation and should not be called frequently
    pub fn config_bounds(&self) -> AllConfigBounds {
        let mut config_count = 0;
        safe_error(unsafe { POAGetConfigsCount(self.camera_id, &raw mut config_count) });

        let mut attributes = Vec::with_capacity(40);

        for i in 0..config_count {
            let mut conf_attributes = POAConfigAttributes::default();

            safe_error(unsafe {
                POAGetConfigAttributes(self.camera_id, i, &raw mut conf_attributes)
            });

            attributes.push(conf_attributes);
        }

        AllConfigBounds::from(attributes)
    }

    pub fn set_dps(&mut self, dps: bool) -> POAResult<()> {
        let b: POABool = dps.into();
        let error = unsafe { POASetEnableDPS(self.camera_id, &raw const b) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Sets the Region Of Interest
    pub fn set_roi(&mut self, roi_area: &ROI) -> POAResult<()> {
        self.set_image_size(roi_area.width, roi_area.height)?;
        self.set_image_start_pos(roi_area.start_x, roi_area.start_y)?;
        Ok(())
    }

    /// Gets the Region Of Interest
    pub fn roi(&self) -> ROI {
        let start_pos = self.image_start_pos().unwrap();
        let size = self.image_size();

        ROI {
            start_x: start_pos.0,
            start_y: start_pos.1,
            width: size.0,
            height: size.1,
        }
    }

    /// Must be within max_width and max_height as specified in the camera properties
    pub fn set_image_size(&mut self, width: u32, height: u32) -> POAResult<()> {
        if width > self.properties.max_width || height > self.properties.max_height {
            return Err(Error::OutOfBounds);
        }

        let error = unsafe { POASetImageSize(self.camera_id, width as c_int, height as c_int) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Returns the current image size
    /// This may change if the binning factor is changed
    pub fn image_size(&self) -> (u32, u32) {
        let mut width = 0;
        let mut height = 0;

        safe_error(unsafe { POAGetImageSize(self.camera_id, &raw mut width, &raw mut height) });

        if width < 0 || height < 0 {
            panic!("negative image size: {} {}", width, height);
        }

        (width as u32, height as u32)
    }

    /// Sets the offset/anchor/start position in the image
    pub fn set_image_start_pos(&mut self, start_x: u32, start_y: u32) -> POAResult<()> {
        if start_x > self.properties.max_width || start_y > self.properties.max_height {
            return Err(Error::OutOfBounds);
        }

        let error =
            unsafe { POASetImageStartPos(self.camera_id, start_x as c_int, start_y as c_int) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// Returns the current image start position
    /// This may change if the binning factor is changed
    pub fn image_start_pos(&self) -> POAResult<(u32, u32)> {
        let mut start_x = 0;
        let mut start_y = 0;

        safe_error(unsafe {
            POAGetImageStartPos(self.camera_id, &raw mut start_x, &raw mut start_y)
        });

        if start_x < 0 || start_y < 0 {
            panic!("negative image start position: {} {}", start_x, start_y);
        }

        Ok((start_x as u32, start_y as u32))
    }

    pub fn set_image_format(&mut self, image_format: ImageFormat) -> POAResult<()> {
        let poa_img_format = image_format.into();

        let error = unsafe { POASetImageFormat(self.camera_id, poa_img_format) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    pub fn image_format(&self) -> POAResult<ImageFormat> {
        let mut poa_img_format = POAImgFormat::POA_END;

        safe_error(unsafe { POAGetImageFormat(self.camera_id, &raw mut poa_img_format) });
        Ok(poa_img_format.into())
    }

    /// Sets the binning factor e.g 1, 2, 4  
    /// Must be a bin within the available bins in properties  
    /// The binning function can be average or sum depending on the pixel_bin_sum property (true is sum, false is average). default is average  
    ///
    /// Note: If successful, the image size (width & height) and start position will be changed (divided by the binning factor)  
    /// Call image_size() and image_start_pos() to get the updated values
    pub fn set_bin(&mut self, bin: u32) -> POAResult<()> {
        if !self.properties.bins.contains(&bin) {
            return Err(Error::OutOfBounds);
        }

        let err = unsafe { POASetImageBin(self.camera_id, bin as c_int) };
        if err != _POAErrors::POA_OK {
            return Err(err.into());
        }
        Ok(())
    }

    /// Returns the current binning factor
    pub fn bin(&self) -> u32 {
        let mut bin = 0;
        safe_error(unsafe { POAGetImageBin(self.camera_id, &raw mut bin) });
        bin as u32
    }

    /// Enumerate sensor modes advertised by this camera.
    ///
    /// Returns an empty vec when the camera does not support mode selection
    /// (e.g. most entry-level models). Each call queries the hardware.
    pub fn sensor_modes(&self) -> POAResult<Vec<SensorMode>> {
        enumerate_sensor_modes(self.camera_id)
    }

    /// Current sensor-mode index.
    ///
    /// Returns [`Error::AccessDenied`] on cameras that do not support mode
    /// selection.
    pub fn sensor_mode(&self) -> POAResult<u32> {
        let mut index: c_int = 0;
        let err = unsafe { POAGetSensorMode(self.camera_id, &raw mut index) };
        if err != _POAErrors::POA_OK {
            return Err(err.into());
        }
        if index < 0 {
            return Err(Error::OperationFailed);
        }
        Ok(index as u32)
    }

    /// Set the active sensor mode by index.
    ///
    /// The caller must stop any running exposure before calling this (matches
    /// the underlying SDK requirement).
    pub fn set_sensor_mode(&mut self, index: u32) -> POAResult<()> {
        let err = unsafe { POASetSensorMode(self.camera_id, index as c_int) };
        if err != _POAErrors::POA_OK {
            return Err(err.into());
        }
        Ok(())
    }

    pub fn properties(&self) -> &CameraProperties {
        &self.properties
    }

    pub fn id(&self) -> i32 {
        self.camera_id
    }

    /// Sets the exposure time in microseconds
    pub fn set_exposure(&mut self, exposure_micros: i64, is_auto: bool) -> POAResult<()> {
        self.set_config(POA_EXPOSURE, exposure_micros, is_auto)
    }

    pub fn set_gain(&mut self, gain: i64, is_auto: bool) -> POAResult<()> {
        self.set_config(POA_GAIN, gain, is_auto)
    }

    /// Exposure in microseconds and whether it is auto
    pub fn exposure(&self) -> POAResult<(i64, bool)> {
        unsafe { self.get_config_auto(POA_EXPOSURE) }
    }

    /// Gain and whether it is auto
    pub fn gain(&self) -> POAResult<(i64, bool)> {
        unsafe { self.get_config_auto(POA_GAIN) }
    }

    pub fn hardware_bin(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_HARDWARE_BIN) }
    }

    /// Current temperature in Celsius
    pub fn temperature(&self) -> POAResult<f64> {
        unsafe { self.get_config(POAConfig::POA_TEMPERATURE) }
    }

    /// red pixels coefficient of white balance
    pub fn wb_r(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_WB_R) }
    }

    /// green pixels coefficient of white balance
    pub fn wb_g(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_WB_G) }
    }

    /// blue pixels coefficient of white balance
    pub fn wb_b(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_WB_B) }
    }

    pub fn offset(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_OFFSET) }
    }

    /// maximum gain when auto-adjust
    pub fn auto_max_gain(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_AUTOEXPO_MAX_GAIN) }
    }

    /// maximum exposure when auto-adjust (in ms)
    pub fn auto_max_exposure_ms(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_AUTOEXPO_MAX_EXPOSURE) }
    }

    /// target brightness when auto-adjust
    pub fn auto_target_brightness(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_AUTOEXPO_BRIGHTNESS) }
    }

    /// ST4 guide north, generally, it's DEC+ on the mount
    pub fn guide_north(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_GUIDE_NORTH) }
    }

    /// ST4 guide south, generally, it's DEC- on the mount
    pub fn guide_south(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_GUIDE_SOUTH) }
    }

    /// ST4 guide east, generally, it's RA+ on the mount
    pub fn guide_east(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_GUIDE_EAST) }
    }

    /// ST4 guide west, generally, it's RA- on the mount
    pub fn guide_west(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_GUIDE_WEST) }
    }

    /// e/ADU, This value will change with gain
    pub fn egain(&self) -> POAResult<f64> {
        unsafe { self.get_config(POAConfig::POA_EGAIN) }
    }

    /// cooler power percentage[0-100%](only cool camera)
    pub fn cooler_power(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_COOLER_POWER) }
    }

    /// camera target temperature (in Celsius)
    pub fn target_temp(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_TARGET_TEMP) }
    }

    /// is cooler(and fan) on or off
    pub fn cooler(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_COOLER) }
    }

    #[deprecated]
    /// get state of lens heater(on or off)
    pub fn heater(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_HEATER) }
    }

    /// lens heater power percentage[0-100%]
    pub fn heater_power(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_HEATER_POWER) }
    }

    /// radiator fan power percentage[0-100%]
    pub fn fan_power(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_FAN_POWER) }
    }

    /// Range is [0, 2000]
    /// 0 means no limit
    pub fn frame_limit(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_FRAME_LIMIT) }
    }

    /// High Quality Image, for those without DDR camera(guide camera)
    /// if true, this will reduce the waviness and stripe of the image
    pub fn hqi(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_HQI) }
    }

    /// 0-100% usage of USB bandwidth
    pub fn usb_bandwidth_limit(&self) -> POAResult<i64> {
        unsafe { self.get_config(POAConfig::POA_USB_BANDWIDTH_LIMIT) }
    }

    /// take the sum or average of pixels after binning, true is sum and false is average, default is false
    pub fn pixel_bin_sum(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_PIXEL_BIN_SUM) }
    }

    /// only for color camera, when set to true, pixel binning will use neighbour pixels and image
    /// after binning will lose the bayer pattern
    pub fn mono_bin(&self) -> POAResult<bool> {
        unsafe { self.get_config(POAConfig::POA_MONO_BIN) }
    }

    pub fn set_hardware_bin(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_HARDWARE_BIN, value, false)
    }

    /// set the red pixels coefficient of white balance
    pub fn set_wb_r(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_WB_R, value, false)
    }

    /// set the green pixels coefficient of white balance
    pub fn set_wb_g(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_WB_G, value, false)
    }

    /// set the blue pixels coefficient of white balance
    pub fn set_wb_b(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_WB_B, value, false)
    }

    pub fn set_offset(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_OFFSET, value, false)
    }

    /// set the max gain when auto-adjust
    pub fn set_auto_max_gain(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_AUTOEXPO_MAX_GAIN, value, false)
    }

    /// set the max exposure when auto-adjust (in ms)
    pub fn set_auto_max_exposure_ms(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_AUTOEXPO_MAX_EXPOSURE, value, false)
    }

    /// set the target brightness when auto-adjust
    pub fn set_auto_target_brightness(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_AUTOEXPO_BRIGHTNESS, value, false)
    }

    /// set ST4 guide north, generally, it's DEC+ on the mount
    pub fn set_guide_north(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_GUIDE_NORTH, value, false)
    }

    /// set ST4 guide south, generally, it's DEC- on the mount
    pub fn set_guide_south(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_GUIDE_SOUTH, value, false)
    }

    /// set ST4 guide east, generally, it's RA+ on the mount
    pub fn set_guide_east(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_GUIDE_EAST, value, false)
    }

    /// set ST4 guide west, generally, it's RA- on the mount
    pub fn set_guide_west(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_GUIDE_WEST, value, false)
    }

    /// set the camera target temperature (in Celsius)
    pub fn set_target_temperature(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_TARGET_TEMP, value, false)
    }

    /// set the cooler(and fan) on or off
    pub fn set_cooler(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_COOLER, value, false)
    }

    /// set the state of lens heater(on or off)
    #[deprecated]
    pub fn set_heater(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_HEATER, value, false)
    }

    /// set the lens heater power percentage[0-100%]
    pub fn set_heater_power(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_HEATER_POWER, value, false)
    }

    /// set the radiator fan power percentage[0-100%]
    pub fn set_fan_power(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_FAN_POWER, value, false)
    }

    /// set the frame limit
    /// Range is [0, 2000]. 0 means no limit
    pub fn set_frame_limit(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_FRAME_LIMIT, value, false)
    }

    /// set High Quality Image, for those without DDR camera(guide camera)
    /// if true, this will reduce the waviness and stripe of the image but frame rate may go down
    /// note: this config has no effect on cameras with DDR
    pub fn set_hqi(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_HQI, value, false)
    }

    /// set the maximum usage of USB bandwidth [0-100%]
    pub fn set_usb_bandwidth_limit(&mut self, value: i64) -> POAResult<()> {
        self.set_config(POAConfig::POA_USB_BANDWIDTH_LIMIT, value, false)
    }

    /// set whether to take the sum or average of pixels after binning, true is sum and false is average, default is false
    pub fn set_pixel_bin_sum(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_PIXEL_BIN_SUM, value, false)
    }

    /// only for color camera: if true,  pixel binning will use neighbour pixels
    /// and image after binning will lose the bayer pattern
    pub fn set_mono_bin(&mut self, value: bool) -> POAResult<()> {
        self.set_config(POAConfig::POA_MONO_BIN, value, false)
    }

    fn set_config(
        &mut self,
        poa_config: POAConfig,

        value: impl Into<POAConfigValue>,
        is_auto: bool,
    ) -> POAResult<()> {
        let value = value.into();
        let error = unsafe { POASetConfig(self.camera_id, poa_config, value, is_auto.into()) };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }
        Ok(())
    }

    /// # Unsafe
    ///
    /// The given type must match the actual type of the config value
    unsafe fn get_config_auto<T: FromPOAConfigValue>(
        &self,
        poa_config: POAConfig,
    ) -> POAResult<(T, bool)> {
        let mut config_value = POAConfigValue::default();
        let mut is_auto = POABool::POA_FALSE;

        let error = unsafe {
            POAGetConfig(
                self.camera_id,
                poa_config,
                &raw mut config_value,
                &raw mut is_auto,
            )
        };
        if error != _POAErrors::POA_OK {
            return Err(error.into());
        }

        Ok((
            FromPOAConfigValue::from_poa_config_value(config_value),
            is_auto.into(),
        ))
    }

    /// # Unsafe
    ///
    /// The given type must match the actual type of the config value
    unsafe fn get_config<T: FromPOAConfigValue>(&self, poa_config: POAConfig) -> POAResult<T> {
        self.get_config_auto(poa_config).map(|(value, _)| value)
    }
}

/// a lot of functions should never fail by construction: pointer are not null, camera_id is valid
/// and camera is open! so we can safely ignore a lot of api errors
fn safe_error(error: POAErrors) {
    if error == _POAErrors::POA_OK {
        return;
    }
    panic!("unexpected POA error: {}", Error::from(error));
}

/// Enumerate sensor modes for an opened camera.
///
/// Returns an empty vec when the mode count is zero (camera does not support
/// mode selection). Propagates SDK errors via `POAResult`.
fn enumerate_sensor_modes(camera_id: i32) -> POAResult<Vec<SensorMode>> {
    let mut count: c_int = 0;
    let err = unsafe { POAGetSensorModeCount(camera_id, &raw mut count) };
    if err != _POAErrors::POA_OK {
        return Err(err.into());
    }
    if count <= 0 {
        return Ok(Vec::new());
    }

    let mut modes = Vec::with_capacity(count as usize);
    for index in 0..count {
        let mut info = POASensorModeInfo::default();
        let err = unsafe { POAGetSensorModeInfo(camera_id, index, &raw mut info) };
        if err != _POAErrors::POA_OK {
            return Err(err.into());
        }
        modes.push(SensorMode {
            index: index as u32,
            name: c_char_array_to_string(&info.name),
            description: c_char_array_to_string(&info.desc),
        });
    }
    Ok(modes)
}

/// Decode a fixed-size C char buffer that may or may not be NUL-terminated.
/// Safer than `CStr::from_ptr` here because the SDK fills `POASensorModeInfo`
/// fields without a length and a pathological firmware could in principle
/// write exactly `buf.len()` bytes with no trailing NUL.
fn c_char_array_to_string(buf: &[c_char]) -> String {
    let nul = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
    let bytes: Vec<u8> = buf[..nul].iter().map(|&c| c as u8).collect();
    String::from_utf8_lossy(&bytes).trim().to_string()
}