1use core::ops::RangeInclusive;
10use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
11use std::sync::Arc;
12use std::thread::JoinHandle;
13use std::time::Duration;
14
15use crate::devices::meet2;
16use crate::discovery::DeviceInfo;
17use crate::status::{Event, EventSender};
18use crate::transport::Transport;
19use crate::types::{
20 AeMode, AiMode, AntiFlicker, AutoFramingMode, Cadence, FirmwareVersion, FovType, MediaBgColor,
21 MediaBgMode, MediaMode, ProductType, Status, WdrMode, WhiteBalanceMode,
22};
23use crate::uvc::{self, UvcGet};
24use crate::{Error, Result};
25
26pub struct Device {
32 info: DeviceInfo,
33 transport: Arc<dyn Transport>,
34 firmware: String,
35 mac: [u8; 6],
41 cadence_ms: Arc<AtomicU32>,
44 poller_stop: Arc<AtomicBool>,
46 poller: Option<JoinHandle<()>>,
47}
48
49impl Device {
50 pub(crate) fn new(
51 info: DeviceInfo,
52 transport: Arc<dyn Transport>,
53 events_tx: Option<EventSender>,
54 mac: [u8; 6],
55 ) -> Self {
56 let cadence_ms = Arc::new(AtomicU32::new(Cadence::Slow.period_ms()));
57 let poller_stop = Arc::new(AtomicBool::new(false));
58 let poller = events_tx.map(|tx| {
59 spawn_status_poller(
60 tx,
61 info.serial.clone(),
62 transport.clone(),
63 cadence_ms.clone(),
64 poller_stop.clone(),
65 )
66 });
67 Self {
68 info,
69 transport,
70 firmware: meet2::MIN_FW.to_owned(),
71 mac,
72 cadence_ms,
73 poller_stop,
74 poller,
75 }
76 }
77
78 pub fn set_status_cadence(&self, cadence: Cadence) {
80 self.cadence_ms
81 .store(cadence.period_ms(), Ordering::Relaxed);
82 }
83
84 #[must_use]
86 pub fn name(&self) -> &str {
87 match self.info.product_type {
88 ProductType::Meet2 => "OBSBOT Meet 2",
89 }
90 }
91
92 #[must_use]
94 pub fn serial(&self) -> &str {
95 &self.info.serial
96 }
97
98 #[must_use]
101 pub fn firmware_version(&self) -> &str {
102 &self.firmware
103 }
104
105 #[must_use]
107 pub fn product_type(&self) -> ProductType {
108 self.info.product_type
109 }
110
111 pub fn status(&self) -> Result<Status> {
122 let firmware = self.firmware_from_camera().unwrap_or_default();
123 let serial = self.serial_from_camera().unwrap_or_default();
124 let mut snap = sample_status(self.transport.as_ref());
125 snap.firmware = firmware;
126 snap.serial = serial;
127 Ok(snap)
128 }
129
130 pub fn firmware_from_camera(&self) -> Result<String> {
136 let mut tail = [0u8; 6];
137 tail.copy_from_slice(&self.mac);
138 let request = meet2::build_rpc_frame(0x01, 0x01, 0x0D, 0x08, 0x04, &[], 18, &tail);
139 self.rpc_request_then_reply(&request, meet2::decode_firmware_reply)
140 }
141
142 pub fn firmware(&self) -> Result<FirmwareVersion> {
147 let s = self.firmware_from_camera()?;
148 FirmwareVersion::parse(&s).ok_or_else(|| Error::BadResponse {
149 selector: meet2::XU_SEL_RPC,
150 bytes: s.into_bytes(),
151 })
152 }
153
154 pub fn serial_from_camera(&self) -> Result<String> {
156 let mut tail = [0u8; 8];
157 tail[..6].copy_from_slice(&self.mac);
158 tail[6] = 0x01;
159 tail[7] = 0x01;
160 let request = meet2::build_rpc_frame(0x01, 0x03, 0x0D, 0xC8, 0x18, &[], 24, &tail);
161 self.rpc_request_then_reply(&request, meet2::decode_serial_reply)
162 }
163
164 fn rpc_request_then_reply(
169 &self,
170 request: &[u8; meet2::RPC_FRAME_LEN],
171 decode: impl Fn(&[u8]) -> Option<String>,
172 ) -> Result<String> {
173 self.transport
174 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_RPC, request)?;
175 let mut reply = [0u8; meet2::RPC_FRAME_LEN];
176 for attempt in 0..meet2::RPC_REPLY_POLL_ATTEMPTS {
177 if attempt > 0 {
180 std::thread::sleep(std::time::Duration::from_millis(
181 meet2::RPC_REPLY_POLL_DELAY_MS,
182 ));
183 }
184 let _ = self.transport.uvc_get(
185 UvcGet::Cur,
186 meet2::XU_ENTITY_ID,
187 meet2::XU_SEL_RPC,
188 &mut reply,
189 )?;
190 if let Some(decoded) = decode(&reply) {
191 return Ok(decoded);
192 }
193 }
194 Err(Error::BadResponse {
195 selector: meet2::XU_SEL_RPC,
196 bytes: reply.to_vec(),
197 })
198 }
199
200 #[allow(clippy::cast_possible_truncation)]
209 pub fn set_pan_tilt(&self, pan: f32, tilt: f32) -> Result<()> {
210 if !(-1.0..=1.0).contains(&pan) || !(-1.0..=1.0).contains(&tilt) {
211 return Err(Error::OutOfRange);
212 }
213 let pan_i = (pan * PAN_TILT_PROVISIONAL_SCALE) as i32;
214 let tilt_i = (tilt * PAN_TILT_PROVISIONAL_SCALE) as i32;
215 let mut payload = [0u8; 8];
216 payload[..4].copy_from_slice(&pan_i.to_le_bytes());
217 payload[4..].copy_from_slice(&tilt_i.to_le_bytes());
218 self.transport
219 .uvc_set(uvc::CAMERA_TERMINAL, uvc::ct::PANTILT_ABSOLUTE, &payload)
220 }
221
222 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
228 pub fn set_zoom(&self, zoom: f32) -> Result<()> {
229 if !(0.0..=f32::from(u16::MAX)).contains(&zoom) {
230 return Err(Error::OutOfRange);
231 }
232 let v = zoom as u16;
233 self.transport.uvc_set(
234 uvc::CAMERA_TERMINAL,
235 uvc::ct::ZOOM_ABSOLUTE,
236 &v.to_le_bytes(),
237 )
238 }
239
240 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
245 pub fn set_focus(&self, focus: f32) -> Result<()> {
246 if !(0.0..=f32::from(u16::MAX)).contains(&focus) {
247 return Err(Error::OutOfRange);
248 }
249 let v = focus as u16;
250 self.transport.uvc_set(
251 uvc::CAMERA_TERMINAL,
252 uvc::ct::FOCUS_ABSOLUTE,
253 &v.to_le_bytes(),
254 )
255 }
256
257 pub fn pan_tilt(&self) -> Result<(f32, f32)> {
261 let mut buf = [0u8; 8];
262 let _ = self.transport.uvc_get(
263 UvcGet::Cur,
264 uvc::CAMERA_TERMINAL,
265 uvc::ct::PANTILT_ABSOLUTE,
266 &mut buf,
267 )?;
268 let pan = i32::from_le_bytes(buf[..4].try_into().unwrap());
269 let tilt = i32::from_le_bytes(buf[4..].try_into().unwrap());
270 Ok((normalised_pantilt(pan), normalised_pantilt(tilt)))
271 }
272
273 pub fn zoom(&self) -> Result<f32> {
276 let mut buf = [0u8; 2];
277 let _ = self.transport.uvc_get(
278 UvcGet::Cur,
279 uvc::CAMERA_TERMINAL,
280 uvc::ct::ZOOM_ABSOLUTE,
281 &mut buf,
282 )?;
283 Ok(f32::from(u16::from_le_bytes(buf)))
284 }
285
286 pub fn focus(&self) -> Result<f32> {
289 let mut buf = [0u8; 2];
290 let _ = self.transport.uvc_get(
291 UvcGet::Cur,
292 uvc::CAMERA_TERMINAL,
293 uvc::ct::FOCUS_ABSOLUTE,
294 &mut buf,
295 )?;
296 Ok(f32::from(u16::from_le_bytes(buf)))
297 }
298
299 pub fn set_auto_focus(&self, on: bool) -> Result<()> {
301 self.transport
302 .uvc_set(uvc::CAMERA_TERMINAL, uvc::ct::FOCUS_AUTO, &[u8::from(on)])
303 }
304
305 pub fn auto_focus(&self) -> Result<bool> {
307 let mut buf = [0u8; 1];
308 let _ = self.transport.uvc_get(
309 UvcGet::Cur,
310 uvc::CAMERA_TERMINAL,
311 uvc::ct::FOCUS_AUTO,
312 &mut buf,
313 )?;
314 Ok(buf[0] != 0)
315 }
316
317 pub fn set_ae_mode(&self, mode: AeMode) -> Result<()> {
323 let byte: u8 = match mode {
324 AeMode::Manual => 0x01,
325 AeMode::Auto => 0x02,
326 AeMode::ShutterPriority => 0x04,
327 AeMode::AperturePriority => 0x08,
328 };
329 self.transport
330 .uvc_set(uvc::CAMERA_TERMINAL, uvc::ct::AE_MODE, &[byte])
331 }
332
333 pub fn ae_mode(&self) -> Result<AeMode> {
335 let mut buf = [0u8; 1];
336 let _ = self.transport.uvc_get(
337 UvcGet::Cur,
338 uvc::CAMERA_TERMINAL,
339 uvc::ct::AE_MODE,
340 &mut buf,
341 )?;
342 match buf[0] {
344 0x01 => Ok(AeMode::Manual),
345 0x02 => Ok(AeMode::Auto),
346 0x04 => Ok(AeMode::ShutterPriority),
347 0x08 => Ok(AeMode::AperturePriority),
348 other => Err(Error::BadResponse {
349 selector: uvc::ct::AE_MODE,
350 bytes: vec![other],
351 }),
352 }
353 }
354
355 pub fn set_ae_lock(&self, locked: bool) -> Result<()> {
360 self.set_ae_mode(if locked { AeMode::Manual } else { AeMode::Auto })
361 }
362
363 pub fn ae_lock(&self) -> Result<bool> {
365 Ok(matches!(self.ae_mode()?, AeMode::Manual))
366 }
367
368 pub fn set_exposure_time(&self, value_100us: u32) -> Result<()> {
374 self.transport.uvc_set(
375 uvc::CAMERA_TERMINAL,
376 uvc::ct::EXPOSURE_TIME_ABSOLUTE,
377 &value_100us.to_le_bytes(),
378 )
379 }
380
381 pub fn exposure_time(&self) -> Result<u32> {
383 let mut buf = [0u8; 4];
384 let _ = self.transport.uvc_get(
385 UvcGet::Cur,
386 uvc::CAMERA_TERMINAL,
387 uvc::ct::EXPOSURE_TIME_ABSOLUTE,
388 &mut buf,
389 )?;
390 Ok(u32::from_le_bytes(buf))
391 }
392
393 pub fn exposure_time_range(&self) -> Result<RangeInclusive<u32>> {
395 let mut buf = [0u8; 4];
396 let _ = self.transport.uvc_get(
397 UvcGet::Min,
398 uvc::CAMERA_TERMINAL,
399 uvc::ct::EXPOSURE_TIME_ABSOLUTE,
400 &mut buf,
401 )?;
402 let lo = u32::from_le_bytes(buf);
403 let _ = self.transport.uvc_get(
404 UvcGet::Max,
405 uvc::CAMERA_TERMINAL,
406 uvc::ct::EXPOSURE_TIME_ABSOLUTE,
407 &mut buf,
408 )?;
409 let hi = u32::from_le_bytes(buf);
410 Ok(lo..=hi)
411 }
412
413 pub fn set_brightness(&self, value: i32) -> Result<()> {
417 let v = i16::try_from(value).map_err(|_| Error::OutOfRange)?;
418 self.transport
419 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::BRIGHTNESS, &v.to_le_bytes())
420 }
421
422 pub fn brightness(&self) -> Result<i32> {
424 let mut buf = [0u8; 2];
425 let _ = self.transport.uvc_get(
426 UvcGet::Cur,
427 uvc::PROCESSING_UNIT,
428 uvc::pu::BRIGHTNESS,
429 &mut buf,
430 )?;
431 Ok(i32::from(i16::from_le_bytes(buf)))
432 }
433
434 pub fn brightness_range(&self) -> Result<RangeInclusive<i32>> {
436 let lo = self.pu_get_i16(UvcGet::Min, uvc::pu::BRIGHTNESS)?;
437 let hi = self.pu_get_i16(UvcGet::Max, uvc::pu::BRIGHTNESS)?;
438 Ok(i32::from(lo)..=i32::from(hi))
439 }
440
441 pub fn set_contrast(&self, value: i32) -> Result<()> {
443 let v = u16::try_from(value).map_err(|_| Error::OutOfRange)?;
444 self.transport
445 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::CONTRAST, &v.to_le_bytes())
446 }
447
448 pub fn contrast(&self) -> Result<i32> {
450 Ok(i32::from(self.pu_get_u16(UvcGet::Cur, uvc::pu::CONTRAST)?))
451 }
452
453 pub fn contrast_range(&self) -> Result<RangeInclusive<i32>> {
455 let lo = self.pu_get_u16(UvcGet::Min, uvc::pu::CONTRAST)?;
456 let hi = self.pu_get_u16(UvcGet::Max, uvc::pu::CONTRAST)?;
457 Ok(i32::from(lo)..=i32::from(hi))
458 }
459
460 pub fn set_saturation(&self, value: i32) -> Result<()> {
462 let v = u16::try_from(value).map_err(|_| Error::OutOfRange)?;
463 self.transport
464 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::SATURATION, &v.to_le_bytes())
465 }
466
467 pub fn saturation(&self) -> Result<i32> {
469 Ok(i32::from(
470 self.pu_get_u16(UvcGet::Cur, uvc::pu::SATURATION)?,
471 ))
472 }
473
474 pub fn saturation_range(&self) -> Result<RangeInclusive<i32>> {
476 let lo = self.pu_get_u16(UvcGet::Min, uvc::pu::SATURATION)?;
477 let hi = self.pu_get_u16(UvcGet::Max, uvc::pu::SATURATION)?;
478 Ok(i32::from(lo)..=i32::from(hi))
479 }
480
481 pub fn set_hue(&self, value: i32) -> Result<()> {
483 let v = i16::try_from(value).map_err(|_| Error::OutOfRange)?;
484 self.transport
485 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::HUE, &v.to_le_bytes())
486 }
487
488 pub fn hue(&self) -> Result<i32> {
490 Ok(i32::from(self.pu_get_i16(UvcGet::Cur, uvc::pu::HUE)?))
491 }
492
493 pub fn hue_range(&self) -> Result<RangeInclusive<i32>> {
495 let lo = self.pu_get_i16(UvcGet::Min, uvc::pu::HUE)?;
496 let hi = self.pu_get_i16(UvcGet::Max, uvc::pu::HUE)?;
497 Ok(i32::from(lo)..=i32::from(hi))
498 }
499
500 pub fn set_sharpness(&self, value: i32) -> Result<()> {
502 let v = u16::try_from(value).map_err(|_| Error::OutOfRange)?;
503 self.transport
504 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::SHARPNESS, &v.to_le_bytes())
505 }
506
507 pub fn sharpness(&self) -> Result<i32> {
509 Ok(i32::from(self.pu_get_u16(UvcGet::Cur, uvc::pu::SHARPNESS)?))
510 }
511
512 pub fn sharpness_range(&self) -> Result<RangeInclusive<i32>> {
514 let lo = self.pu_get_u16(UvcGet::Min, uvc::pu::SHARPNESS)?;
515 let hi = self.pu_get_u16(UvcGet::Max, uvc::pu::SHARPNESS)?;
516 Ok(i32::from(lo)..=i32::from(hi))
517 }
518
519 pub fn set_gain(&self, value: i32) -> Result<()> {
521 let v = u16::try_from(value).map_err(|_| Error::OutOfRange)?;
522 self.transport
523 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::GAIN, &v.to_le_bytes())
524 }
525
526 pub fn gain(&self) -> Result<i32> {
528 Ok(i32::from(self.pu_get_u16(UvcGet::Cur, uvc::pu::GAIN)?))
529 }
530
531 pub fn gain_range(&self) -> Result<RangeInclusive<i32>> {
533 let lo = self.pu_get_u16(UvcGet::Min, uvc::pu::GAIN)?;
534 let hi = self.pu_get_u16(UvcGet::Max, uvc::pu::GAIN)?;
535 Ok(i32::from(lo)..=i32::from(hi))
536 }
537
538 pub fn set_backlight_compensation(&self, value: i32) -> Result<()> {
541 let v = u16::try_from(value).map_err(|_| Error::OutOfRange)?;
542 self.transport.uvc_set(
543 uvc::PROCESSING_UNIT,
544 uvc::pu::BACKLIGHT_COMPENSATION,
545 &v.to_le_bytes(),
546 )
547 }
548
549 pub fn backlight_compensation(&self) -> Result<i32> {
551 Ok(i32::from(self.pu_get_u16(
552 UvcGet::Cur,
553 uvc::pu::BACKLIGHT_COMPENSATION,
554 )?))
555 }
556
557 pub fn set_anti_flicker(&self, mode: AntiFlicker) -> Result<()> {
560 let v: u8 = match mode {
561 AntiFlicker::Off => 0,
562 AntiFlicker::Hz50 => 1,
563 AntiFlicker::Hz60 => 2,
564 AntiFlicker::Auto => 3,
565 };
566 self.transport
567 .uvc_set(uvc::PROCESSING_UNIT, uvc::pu::POWER_LINE_FREQUENCY, &[v])
568 }
569
570 pub fn anti_flicker(&self) -> Result<AntiFlicker> {
572 let mut buf = [0u8; 1];
573 let _ = self.transport.uvc_get(
574 UvcGet::Cur,
575 uvc::PROCESSING_UNIT,
576 uvc::pu::POWER_LINE_FREQUENCY,
577 &mut buf,
578 )?;
579 match buf[0] {
580 0 => Ok(AntiFlicker::Off),
581 1 => Ok(AntiFlicker::Hz50),
582 2 => Ok(AntiFlicker::Hz60),
583 3 => Ok(AntiFlicker::Auto),
584 other => Err(Error::BadResponse {
585 selector: uvc::pu::POWER_LINE_FREQUENCY,
586 bytes: vec![other],
587 }),
588 }
589 }
590
591 pub fn set_white_balance(&self, mode: WhiteBalanceMode, kelvin: Option<u16>) -> Result<()> {
598 match mode {
599 WhiteBalanceMode::Auto => self.transport.uvc_set(
600 uvc::PROCESSING_UNIT,
601 uvc::pu::WHITE_BALANCE_TEMPERATURE_AUTO,
602 &[1],
603 ),
604 WhiteBalanceMode::Manual => {
605 self.transport.uvc_set(
606 uvc::PROCESSING_UNIT,
607 uvc::pu::WHITE_BALANCE_TEMPERATURE_AUTO,
608 &[0],
609 )?;
610 let k = kelvin.unwrap_or(6500);
611 self.transport.uvc_set(
612 uvc::PROCESSING_UNIT,
613 uvc::pu::WHITE_BALANCE_TEMPERATURE,
614 &k.to_le_bytes(),
615 )
616 }
617 }
618 }
619
620 pub fn white_balance(&self) -> Result<(WhiteBalanceMode, u16)> {
622 let mut auto_buf = [0u8; 1];
623 let _ = self.transport.uvc_get(
624 UvcGet::Cur,
625 uvc::PROCESSING_UNIT,
626 uvc::pu::WHITE_BALANCE_TEMPERATURE_AUTO,
627 &mut auto_buf,
628 )?;
629 let kelvin = self.pu_get_u16(UvcGet::Cur, uvc::pu::WHITE_BALANCE_TEMPERATURE)?;
630 let mode = if auto_buf[0] == 0 {
631 WhiteBalanceMode::Manual
632 } else {
633 WhiteBalanceMode::Auto
634 };
635 Ok((mode, kelvin))
636 }
637
638 pub fn white_balance_range(&self) -> Result<RangeInclusive<u16>> {
640 let lo = self.pu_get_u16(UvcGet::Min, uvc::pu::WHITE_BALANCE_TEMPERATURE)?;
641 let hi = self.pu_get_u16(UvcGet::Max, uvc::pu::WHITE_BALANCE_TEMPERATURE)?;
642 Ok(lo..=hi)
643 }
644
645 pub fn set_wdr(&self, mode: WdrMode) -> Result<()> {
654 let payload = meet2::mode_register_payload(meet2::MODE_WDR, &[encode_wdr(mode)]);
655 self.transport
656 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
657 }
658
659 pub fn wdr(&self) -> Result<WdrMode> {
661 let blob = self.read_status_blob()?;
662 decode_wdr(blob[meet2::STATUS_WDR_OFFSET])
663 }
664
665 pub fn face_ae(&self) -> Result<bool> {
667 let blob = self.read_status_blob()?;
668 Ok(blob[meet2::STATUS_FACE_AE_OFFSET] != 0)
669 }
670
671 pub fn ai_mode(&self) -> Result<AiMode> {
680 let blob = self.read_status_blob()?;
681 decode_ai_mode(u16::from(blob[meet2::STATUS_AI_MODE_OFFSET]))
682 }
683
684 fn read_status_blob(&self) -> Result<[u8; meet2::MODE_REGISTER_PAYLOAD_LEN]> {
690 let mut buf = [0u8; meet2::MODE_REGISTER_PAYLOAD_LEN];
691 let _ = self.transport.uvc_get(
692 UvcGet::Cur,
693 meet2::XU_ENTITY_ID,
694 meet2::XU_SEL_MODE_REGISTER,
695 &mut buf,
696 )?;
697 if buf[0] != meet2::STATUS_BLOB_MARKER {
698 return Err(Error::BadResponse {
699 selector: meet2::XU_SEL_MODE_REGISTER,
700 bytes: buf.to_vec(),
701 });
702 }
703 Ok(buf)
704 }
705
706 pub fn set_fov(&self, fov: FovType) -> Result<()> {
710 let payload = meet2::mode_register_payload(meet2::MODE_FOV, &[encode_fov(fov)]);
711 self.transport
712 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
713 }
714
715 pub fn set_face_ae(&self, on: bool) -> Result<()> {
718 let payload = meet2::mode_register_payload(meet2::MODE_FACE_AE, &[u8::from(on)]);
719 self.transport
720 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
721 }
722
723 pub fn set_face_focus(&self, on: bool) -> Result<()> {
730 let payload = [u8::from(on), 0x00, 0x00, 0x00];
731 let mut tail = [0u8; 8];
732 tail[..6].copy_from_slice(&self.mac);
733 tail[6] = 0x01;
734 tail[7] = 0x01;
735 let request = meet2::build_rpc_frame(0x25, 0x04, 0x02, 0x02, 0x36, &payload, 26, &tail);
736 self.transport
737 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_RPC, &request)
738 }
739
740 pub fn set_media_mode(&self, mode: MediaMode) -> Result<()> {
743 let payload =
744 meet2::mode_register_payload(meet2::MODE_MEDIA_MODE, &[encode_media_mode(mode)]);
745 self.transport
746 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
747 }
748
749 pub fn set_auto_framing(&self, mode: AutoFramingMode) -> Result<()> {
753 let payload =
754 meet2::mode_register_payload(meet2::MODE_AUTO_FRAMING, &encode_auto_framing(mode));
755 self.transport
756 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
757 }
758
759 pub fn set_ai_mode(&self, mode: AiMode) -> Result<()> {
762 let value: u16 = encode_ai_mode(mode);
763 let payload = meet2::mode_register_payload(meet2::MODE_AI_MODE, &value.to_le_bytes());
764 self.transport
765 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
766 }
767
768 pub fn set_audio_agc(&self, on: bool) -> Result<()> {
774 let payload = meet2::mode_register_payload(meet2::MODE_AUDIO_AGC, &[u8::from(on)]);
775 self.transport
776 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
777 }
778
779 pub fn set_flip_horizontal(&self, on: bool) -> Result<()> {
782 let payload = meet2::mode_register_payload(meet2::MODE_FLIP_HORIZONTAL, &[u8::from(on)]);
783 self.transport
784 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
785 }
786
787 pub fn set_portrait(&self, on: bool) -> Result<()> {
792 let payload = meet2::mode_register_payload(meet2::MODE_VERTICAL, &[u8::from(on)]);
793 self.transport
794 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
795 }
796
797 pub fn set_led(&self, on: bool) -> Result<()> {
800 let payload = meet2::mode_register_payload(meet2::MODE_LED, &[u8::from(on)]);
801 self.transport
802 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
803 }
804
805 pub fn set_bg_enable(&self, on: bool) -> Result<()> {
809 let payload = meet2::mode_register_payload(meet2::MODE_BG_ENABLE, &[u8::from(on)]);
810 self.transport
811 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
812 }
813
814 pub fn set_bg_mode(&self, mode: MediaBgMode) -> Result<()> {
818 let byte: u8 = match mode {
819 MediaBgMode::Disable => 0,
820 MediaBgMode::Color => 1,
821 MediaBgMode::Replace => 17,
822 MediaBgMode::Blur => 18,
823 };
824 let payload = meet2::mode_register_payload(meet2::MODE_BG_MODE, &[byte]);
825 self.transport
826 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
827 }
828
829 #[allow(clippy::cast_sign_loss)]
835 pub fn set_bg_color(&self, color: MediaBgColor) -> Result<()> {
836 let byte: i8 = match color {
837 MediaBgColor::Disable => -2,
838 MediaBgColor::Null => -1,
839 MediaBgColor::Blue => 0,
840 MediaBgColor::Green => 1,
841 MediaBgColor::Red => 2,
842 MediaBgColor::Black => 3,
843 MediaBgColor::White => 4,
844 };
845 let payload = meet2::mode_register_payload(meet2::MODE_BG_COLOR, &[byte as u8]);
846 self.transport
847 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
848 }
849
850 pub fn set_mask_level(&self, level: u8) -> Result<()> {
852 if level > 100 {
853 return Err(Error::OutOfRange);
854 }
855 let payload = meet2::mode_register_payload(meet2::MODE_MASK_LEVEL, &[level]);
856 self.transport
857 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
858 }
859
860 pub fn set_disable_sleep_without_stream(&self, disable: bool) -> Result<()> {
863 let payload = meet2::mode_register_payload(
864 meet2::MODE_DISABLE_SLEEP_WITHOUT_STREAM,
865 &[u8::from(disable)],
866 );
867 self.transport
868 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
869 }
870
871 pub fn set_suspend_time(&self, minutes: u16) -> Result<()> {
874 let payload =
875 meet2::mode_register_payload(meet2::MODE_SUSPEND_TIME, &minutes.to_le_bytes());
876 self.transport
877 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
878 }
879
880 pub fn set_microphone_during_sleep(&self, on: bool) -> Result<()> {
883 let payload = meet2::mode_register_payload(meet2::MODE_MIC_DURING_SLEEP, &[u8::from(on)]);
884 self.transport
885 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
886 }
887
888 pub fn set_button_mode(&self, mode: u8) -> Result<()> {
893 let payload = meet2::mode_register_payload(meet2::MODE_BUTTON_MODE, &[mode]);
894 self.transport
895 .uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_MODE_REGISTER, &payload)
896 }
897
898 fn pu_get_i16(&self, req: UvcGet, selector: u8) -> Result<i16> {
901 let mut buf = [0u8; 2];
902 let _ = self
903 .transport
904 .uvc_get(req, uvc::PROCESSING_UNIT, selector, &mut buf)?;
905 Ok(i16::from_le_bytes(buf))
906 }
907
908 fn pu_get_u16(&self, req: UvcGet, selector: u8) -> Result<u16> {
909 let mut buf = [0u8; 2];
910 let _ = self
911 .transport
912 .uvc_get(req, uvc::PROCESSING_UNIT, selector, &mut buf)?;
913 Ok(u16::from_le_bytes(buf))
914 }
915}
916
917const PAN_TILT_PROVISIONAL_SCALE: f32 = 540_000.0;
921
922#[allow(clippy::cast_precision_loss)]
927fn normalised_pantilt(arc_seconds: i32) -> f32 {
928 arc_seconds as f32 / PAN_TILT_PROVISIONAL_SCALE
929}
930
931const POLLER_TICK: Duration = Duration::from_millis(25);
935
936impl Drop for Device {
937 fn drop(&mut self) {
938 self.poller_stop.store(true, Ordering::Relaxed);
939 if let Some(handle) = self.poller.take() {
940 let _ = handle.join();
941 }
942 }
943}
944
945fn spawn_status_poller(
955 tx: EventSender,
956 serial: String,
957 transport: Arc<dyn Transport>,
958 cadence_ms: Arc<AtomicU32>,
959 stop: Arc<AtomicBool>,
960) -> JoinHandle<()> {
961 std::thread::Builder::new()
962 .name(format!("libobsbot-poll-{serial}"))
963 .spawn(move || poll_loop(&tx, &serial, transport.as_ref(), &cadence_ms, &stop))
964 .expect("spawn status poller thread")
965}
966
967fn poll_loop(
968 tx: &EventSender,
969 serial: &str,
970 transport: &dyn Transport,
971 cadence_ms: &AtomicU32,
972 stop: &AtomicBool,
973) {
974 while !stop.load(Ordering::Relaxed) {
975 let snapshot = sample_status(transport);
976 if tx
977 .send(Event::Status {
978 serial: serial.to_owned(),
979 snapshot,
980 })
981 .is_err()
982 {
983 return;
984 }
985 let target = Duration::from_millis(u64::from(cadence_ms.load(Ordering::Relaxed)));
986 let mut slept = Duration::ZERO;
987 while slept < target {
988 if stop.load(Ordering::Relaxed) {
989 return;
990 }
991 std::thread::sleep(POLLER_TICK);
992 slept += POLLER_TICK;
993 }
994 }
995}
996
997fn sample_status(transport: &dyn Transport) -> Status {
1001 let mut snap = Status::default();
1002 if let Ok(v) = read_pu_i16(transport, uvc::pu::BRIGHTNESS) {
1003 snap.brightness = i32::from(v);
1004 }
1005 if let Ok(v) = read_pu_u16(transport, uvc::pu::CONTRAST) {
1006 snap.contrast = i32::from(v);
1007 }
1008 if let Ok(v) = read_pu_u16(transport, uvc::pu::SATURATION) {
1009 snap.saturation = i32::from(v);
1010 }
1011 let mut pt = [0u8; 8];
1012 if transport
1013 .uvc_get(
1014 UvcGet::Cur,
1015 uvc::CAMERA_TERMINAL,
1016 uvc::ct::PANTILT_ABSOLUTE,
1017 &mut pt,
1018 )
1019 .is_ok()
1020 {
1021 let pan = i32::from_le_bytes(pt[..4].try_into().unwrap());
1022 let tilt = i32::from_le_bytes(pt[4..].try_into().unwrap());
1023 snap.pan = normalised_pantilt(pan);
1024 snap.tilt = normalised_pantilt(tilt);
1025 }
1026 let mut zoom = [0u8; 2];
1027 if transport
1028 .uvc_get(
1029 UvcGet::Cur,
1030 uvc::CAMERA_TERMINAL,
1031 uvc::ct::ZOOM_ABSOLUTE,
1032 &mut zoom,
1033 )
1034 .is_ok()
1035 {
1036 snap.zoom = f32::from(u16::from_le_bytes(zoom));
1037 }
1038 snap
1039}
1040
1041fn read_pu_i16(transport: &dyn Transport, selector: u8) -> Result<i16> {
1042 let mut buf = [0u8; 2];
1043 let _ = transport.uvc_get(UvcGet::Cur, uvc::PROCESSING_UNIT, selector, &mut buf)?;
1044 Ok(i16::from_le_bytes(buf))
1045}
1046
1047fn read_pu_u16(transport: &dyn Transport, selector: u8) -> Result<u16> {
1048 let mut buf = [0u8; 2];
1049 let _ = transport.uvc_get(UvcGet::Cur, uvc::PROCESSING_UNIT, selector, &mut buf)?;
1050 Ok(u16::from_le_bytes(buf))
1051}
1052
1053pub(crate) fn learn_mac(transport: &dyn Transport) -> Result<[u8; 6]> {
1058 let request = meet2::build_mac_query_request();
1059 transport.uvc_set(meet2::XU_ENTITY_ID, meet2::XU_SEL_RPC, &request)?;
1060 let mut reply = [0u8; meet2::RPC_FRAME_LEN];
1061 for attempt in 0..meet2::RPC_REPLY_POLL_ATTEMPTS {
1062 if attempt > 0 {
1063 std::thread::sleep(Duration::from_millis(meet2::RPC_REPLY_POLL_DELAY_MS));
1064 }
1065 let _ = transport.uvc_get(
1066 UvcGet::Cur,
1067 meet2::XU_ENTITY_ID,
1068 meet2::XU_SEL_RPC,
1069 &mut reply,
1070 )?;
1071 if let Some(mac) = meet2::decode_mac_query_reply(&reply) {
1072 return Ok(mac);
1073 }
1074 }
1075 Err(Error::BadResponse {
1076 selector: meet2::XU_SEL_RPC,
1077 bytes: reply.to_vec(),
1078 })
1079}
1080
1081fn encode_wdr(mode: WdrMode) -> u8 {
1084 match mode {
1085 WdrMode::Off => 0,
1086 WdrMode::Dol2To1 => 1,
1087 }
1088}
1089
1090fn decode_wdr(b: u8) -> Result<WdrMode> {
1091 match b {
1092 0 => Ok(WdrMode::Off),
1093 1 => Ok(WdrMode::Dol2To1),
1094 _ => Err(Error::BadResponse {
1095 selector: 0,
1096 bytes: vec![b],
1097 }),
1098 }
1099}
1100
1101fn encode_fov(fov: FovType) -> u8 {
1102 match fov {
1103 FovType::Wide => 0,
1104 FovType::Medium => 1,
1105 FovType::Narrow => 2,
1106 }
1107}
1108
1109fn encode_media_mode(mode: MediaMode) -> u8 {
1110 match mode {
1112 MediaMode::Normal => 0,
1113 MediaMode::Background => 1,
1114 MediaMode::AutoFrame => 2,
1115 }
1116}
1117
1118fn encode_auto_framing(mode: AutoFramingMode) -> [u8; 2] {
1121 match mode {
1122 AutoFramingMode::Group => [0, 0],
1123 AutoFramingMode::SingleCloseUp => [1, 0],
1124 AutoFramingMode::SingleUpperBody => [1, 1],
1125 }
1126}
1127
1128fn encode_ai_mode(mode: AiMode) -> u16 {
1131 match mode {
1132 AiMode::None => 0,
1133 AiMode::Group => 1,
1134 AiMode::Human => 2,
1135 AiMode::Hand => 3,
1136 AiMode::WhiteBoard => 4,
1137 AiMode::Desk => 5,
1138 }
1139}
1140
1141fn decode_ai_mode(v: u16) -> Result<AiMode> {
1142 match v {
1143 0 => Ok(AiMode::None),
1144 1 => Ok(AiMode::Group),
1145 2 => Ok(AiMode::Human),
1146 3 => Ok(AiMode::Hand),
1147 4 => Ok(AiMode::WhiteBoard),
1148 5 => Ok(AiMode::Desk),
1149 _ => Err(Error::BadResponse {
1150 selector: meet2::XU_SEL_MODE_REGISTER,
1151 bytes: v.to_le_bytes().to_vec(),
1152 }),
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use crate::testing::{device_with_mock, last_set};
1160
1161 #[test]
1162 fn brightness_range_decodes_min_then_max() {
1163 use crate::testing::device_with_scripted_get;
1164 let device = device_with_scripted_get(vec![vec![0xc0, 0xff], vec![0x40, 0x00]]);
1166 let range = device.brightness_range().unwrap();
1167 assert_eq!(*range.start(), -64);
1168 assert_eq!(*range.end(), 64);
1169 }
1170
1171 #[test]
1172 fn metadata_accessors_use_info() {
1173 let (d, _) = device_with_mock();
1174 assert_eq!(d.name(), "OBSBOT Meet 2");
1175 assert_eq!(d.serial(), "MOCK");
1176 assert_eq!(d.product_type(), ProductType::Meet2);
1177 assert_eq!(d.firmware_version(), meet2::MIN_FW);
1178 }
1179
1180 #[test]
1181 fn pan_tilt_rejects_out_of_range() {
1182 let (d, _) = device_with_mock();
1183 assert!(matches!(d.set_pan_tilt(2.0, 0.0), Err(Error::OutOfRange)));
1184 assert!(matches!(d.set_pan_tilt(0.0, -1.5), Err(Error::OutOfRange)));
1185 }
1186
1187 #[test]
1188 fn pan_tilt_routes_to_camera_terminal() {
1189 let (d, mock) = device_with_mock();
1190 d.set_pan_tilt(0.0, 0.0).unwrap();
1191 let (entity, selector, payload) = last_set(&mock);
1192 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1193 assert_eq!(selector, uvc::ct::PANTILT_ABSOLUTE);
1194 assert_eq!(payload.len(), 8);
1195 }
1196
1197 #[test]
1198 fn brightness_routes_to_processing_unit_with_i16_payload() {
1199 let (d, mock) = device_with_mock();
1200 d.set_brightness(42).unwrap();
1201 let (entity, selector, payload) = last_set(&mock);
1202 assert_eq!(entity, uvc::PROCESSING_UNIT);
1203 assert_eq!(selector, uvc::pu::BRIGHTNESS);
1204 assert_eq!(payload, vec![42, 0]);
1205 }
1206
1207 #[test]
1208 fn brightness_out_of_i16_range_is_refused() {
1209 let (d, _) = device_with_mock();
1210 assert!(matches!(
1211 d.set_brightness(i32::from(i16::MAX) + 1),
1212 Err(Error::OutOfRange)
1213 ));
1214 }
1215
1216 #[test]
1217 fn hue_sharpness_gain_backlight_route_to_pu_with_correct_selectors_and_widths() {
1218 let (d, mock) = device_with_mock();
1219
1220 d.set_hue(-30).unwrap();
1221 let (entity, sel, payload) = last_set(&mock);
1222 assert_eq!(entity, uvc::PROCESSING_UNIT);
1223 assert_eq!(sel, uvc::pu::HUE);
1224 assert_eq!(payload, (-30_i16).to_le_bytes().to_vec());
1225
1226 d.set_sharpness(7).unwrap();
1227 let (_, sel, payload) = last_set(&mock);
1228 assert_eq!(sel, uvc::pu::SHARPNESS);
1229 assert_eq!(payload, 7_u16.to_le_bytes().to_vec());
1230
1231 d.set_gain(42).unwrap();
1232 let (_, sel, payload) = last_set(&mock);
1233 assert_eq!(sel, uvc::pu::GAIN);
1234 assert_eq!(payload, 42_u16.to_le_bytes().to_vec());
1235
1236 d.set_backlight_compensation(1).unwrap();
1237 let (_, sel, payload) = last_set(&mock);
1238 assert_eq!(sel, uvc::pu::BACKLIGHT_COMPENSATION);
1239 assert_eq!(payload, 1_u16.to_le_bytes().to_vec());
1240 }
1241
1242 #[test]
1243 fn auto_focus_routes_to_ct_focus_auto_with_bool_byte() {
1244 let (d, mock) = device_with_mock();
1245 d.set_auto_focus(true).unwrap();
1246 let (entity, sel, payload) = last_set(&mock);
1247 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1248 assert_eq!(sel, uvc::ct::FOCUS_AUTO);
1249 assert_eq!(payload, vec![1]);
1250
1251 d.set_auto_focus(false).unwrap();
1252 let (_, _, payload) = last_set(&mock);
1253 assert_eq!(payload, vec![0]);
1254 }
1255
1256 #[test]
1257 fn ae_mode_routes_to_ct_with_uvc_bitmap_byte() {
1258 let (d, mock) = device_with_mock();
1259 for (mode, byte) in [
1260 (AeMode::Manual, 0x01),
1261 (AeMode::Auto, 0x02),
1262 (AeMode::ShutterPriority, 0x04),
1263 (AeMode::AperturePriority, 0x08),
1264 ] {
1265 d.set_ae_mode(mode).unwrap();
1266 let (entity, sel, payload) = last_set(&mock);
1267 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1268 assert_eq!(sel, uvc::ct::AE_MODE);
1269 assert_eq!(payload, vec![byte], "wrong byte for {mode:?}");
1270 }
1271 }
1272
1273 #[test]
1274 fn ae_lock_is_thin_wrapper_over_ae_mode() {
1275 let (d, mock) = device_with_mock();
1276 d.set_ae_lock(true).unwrap();
1277 let (_, sel, payload) = last_set(&mock);
1278 assert_eq!(sel, uvc::ct::AE_MODE);
1279 assert_eq!(payload, vec![0x01]); d.set_ae_lock(false).unwrap();
1282 let (_, _, payload) = last_set(&mock);
1283 assert_eq!(payload, vec![0x02]); }
1285
1286 #[test]
1287 fn exposure_time_routes_to_ct_with_u32_le_payload() {
1288 let (d, mock) = device_with_mock();
1289 d.set_exposure_time(1234).unwrap();
1290 let (entity, sel, payload) = last_set(&mock);
1291 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1292 assert_eq!(sel, uvc::ct::EXPOSURE_TIME_ABSOLUTE);
1293 assert_eq!(payload, 1234_u32.to_le_bytes().to_vec());
1294 }
1295
1296 #[test]
1297 fn anti_flicker_encodes_each_mode() {
1298 let (d, mock) = device_with_mock();
1299 for (mode, byte) in [
1300 (AntiFlicker::Off, 0),
1301 (AntiFlicker::Hz50, 1),
1302 (AntiFlicker::Hz60, 2),
1303 (AntiFlicker::Auto, 3),
1304 ] {
1305 d.set_anti_flicker(mode).unwrap();
1306 let (entity, sel, payload) = last_set(&mock);
1307 assert_eq!(entity, uvc::PROCESSING_UNIT);
1308 assert_eq!(sel, uvc::pu::POWER_LINE_FREQUENCY);
1309 assert_eq!(payload, vec![byte], "wrong byte for {mode:?}");
1310 }
1311 }
1312
1313 #[test]
1314 fn hue_rejects_out_of_i16_range() {
1315 let (d, _) = device_with_mock();
1316 assert!(matches!(d.set_hue(i32::MAX), Err(Error::OutOfRange)));
1317 }
1318
1319 #[test]
1320 fn contrast_and_saturation_route_to_pu() {
1321 let (d, mock) = device_with_mock();
1322 d.set_contrast(100).unwrap();
1323 let (entity, sel, payload) = last_set(&mock);
1324 assert_eq!(entity, uvc::PROCESSING_UNIT);
1325 assert_eq!(sel, uvc::pu::CONTRAST);
1326 assert_eq!(payload, vec![100, 0]);
1327
1328 d.set_saturation(150).unwrap();
1329 let (entity, sel, payload) = last_set(&mock);
1330 assert_eq!(entity, uvc::PROCESSING_UNIT);
1331 assert_eq!(sel, uvc::pu::SATURATION);
1332 assert_eq!(payload, vec![150, 0]);
1333 }
1334
1335 #[test]
1336 fn zoom_routes_to_camera_terminal_with_u16_payload() {
1337 let (d, mock) = device_with_mock();
1338 d.set_zoom(2.5).unwrap();
1339 let (entity, sel, payload) = last_set(&mock);
1340 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1341 assert_eq!(sel, uvc::ct::ZOOM_ABSOLUTE);
1342 assert_eq!(payload, vec![2, 0]); }
1344
1345 #[test]
1346 fn focus_routes_to_camera_terminal() {
1347 let (d, mock) = device_with_mock();
1348 d.set_focus(100.0).unwrap();
1349 let (entity, sel, _) = last_set(&mock);
1350 assert_eq!(entity, uvc::CAMERA_TERMINAL);
1351 assert_eq!(sel, uvc::ct::FOCUS_ABSOLUTE);
1352 }
1353
1354 #[test]
1355 fn wb_auto_routes_to_pu() {
1356 let (d, mock) = device_with_mock();
1357 d.set_white_balance(WhiteBalanceMode::Auto, None).unwrap();
1358 let (entity, sel, payload) = last_set(&mock);
1359 assert_eq!(entity, uvc::PROCESSING_UNIT);
1360 assert_eq!(sel, uvc::pu::WHITE_BALANCE_TEMPERATURE_AUTO);
1361 assert_eq!(payload, vec![1]);
1362 }
1363
1364 #[test]
1365 fn wb_manual_writes_kelvin_to_pu() {
1366 let (d, mock) = device_with_mock();
1367 d.set_white_balance(WhiteBalanceMode::Manual, Some(5500))
1368 .unwrap();
1369 let (entity, sel, payload) = last_set(&mock);
1370 assert_eq!(entity, uvc::PROCESSING_UNIT);
1371 assert_eq!(sel, uvc::pu::WHITE_BALANCE_TEMPERATURE);
1372 assert_eq!(payload, 5500_u16.to_le_bytes().to_vec());
1373 }
1374
1375 #[test]
1376 fn wdr_routes_to_xu_mode_register_with_wire_bytes() {
1377 let (d, mock) = device_with_mock();
1378 d.set_wdr(WdrMode::Dol2To1).unwrap();
1379 let (entity, sel, payload) = last_set(&mock);
1380 assert_eq!(entity, meet2::XU_ENTITY_ID);
1381 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1382 assert_eq!(payload.len(), meet2::MODE_REGISTER_PAYLOAD_LEN);
1384 assert_eq!(payload[..3], [0x01, 0x01, 0x01]);
1385 assert!(payload[3..].iter().all(|&b| b == 0));
1386
1387 d.set_wdr(WdrMode::Off).unwrap();
1388 let (_, _, payload) = last_set(&mock);
1389 assert_eq!(payload[..3], [0x01, 0x01, 0x00]);
1391 }
1392
1393 #[test]
1394 fn auto_framing_routes_to_xu_mode_register_with_pair_value() {
1395 let (d, mock) = device_with_mock();
1396
1397 d.set_auto_framing(AutoFramingMode::Group).unwrap();
1399 let (entity, sel, payload) = last_set(&mock);
1400 assert_eq!(entity, meet2::XU_ENTITY_ID);
1401 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1402 assert_eq!(payload[..4], [0x0d, 0x02, 0x00, 0x00]);
1403
1404 d.set_auto_framing(AutoFramingMode::SingleCloseUp).unwrap();
1406 let (_, _, payload) = last_set(&mock);
1407 assert_eq!(payload[..4], [0x0d, 0x02, 0x01, 0x00]);
1408
1409 d.set_auto_framing(AutoFramingMode::SingleUpperBody)
1411 .unwrap();
1412 let (_, _, payload) = last_set(&mock);
1413 assert_eq!(payload[..4], [0x0d, 0x02, 0x01, 0x01]);
1414 }
1415
1416 #[test]
1417 fn ai_mode_routes_to_xu_mode_register_with_u16_value() {
1418 let (d, mock) = device_with_mock();
1420 d.set_ai_mode(AiMode::Human).unwrap();
1421 let (entity, sel, payload) = last_set(&mock);
1422 assert_eq!(entity, meet2::XU_ENTITY_ID);
1423 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1424 assert_eq!(payload[..4], [0x16, 0x02, 0x02, 0x00]);
1425
1426 d.set_ai_mode(AiMode::None).unwrap();
1427 let (_, _, payload) = last_set(&mock);
1428 assert_eq!(payload[..4], [0x16, 0x02, 0x00, 0x00]);
1430 }
1431
1432 #[test]
1433 fn fov_routes_to_xu_mode_register_with_wire_bytes() {
1434 let (d, mock) = device_with_mock();
1436 d.set_fov(FovType::Medium).unwrap();
1437 let (entity, sel, payload) = last_set(&mock);
1438 assert_eq!(entity, meet2::XU_ENTITY_ID);
1439 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1440 assert_eq!(payload.len(), meet2::MODE_REGISTER_PAYLOAD_LEN);
1441 assert_eq!(payload[..3], [0x04, 0x01, 0x01]);
1442 assert!(payload[3..].iter().all(|&b| b == 0));
1443 }
1444
1445 #[test]
1446 fn media_mode_routes_to_xu_mode_register_with_wire_bytes() {
1447 let (d, mock) = device_with_mock();
1449 d.set_media_mode(MediaMode::AutoFrame).unwrap();
1450 let (entity, sel, payload) = last_set(&mock);
1451 assert_eq!(entity, meet2::XU_ENTITY_ID);
1452 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1453 assert_eq!(payload[..3], [0x00, 0x01, 0x02]);
1454 }
1455
1456 #[test]
1457 fn microphone_during_sleep_and_button_mode_route_to_mode_register() {
1458 let (d, mock) = device_with_mock();
1459
1460 d.set_microphone_during_sleep(true).unwrap();
1461 let (entity, sel, payload) = last_set(&mock);
1462 assert_eq!(entity, meet2::XU_ENTITY_ID);
1463 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1464 assert_eq!(payload[..3], [0x13, 0x01, 0x01]);
1465
1466 d.set_button_mode(2).unwrap();
1467 let (_, _, payload) = last_set(&mock);
1468 assert_eq!(payload[..3], [0x07, 0x01, 0x02]);
1469 }
1470
1471 #[test]
1472 fn bg_mode_uses_sdk_enum_values_on_the_wire() {
1473 let (d, mock) = device_with_mock();
1474 for (mode, byte) in [
1475 (MediaBgMode::Disable, 0),
1476 (MediaBgMode::Color, 1),
1477 (MediaBgMode::Replace, 17),
1478 (MediaBgMode::Blur, 18),
1479 ] {
1480 d.set_bg_mode(mode).unwrap();
1481 let (entity, sel, payload) = last_set(&mock);
1482 assert_eq!(entity, meet2::XU_ENTITY_ID);
1483 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1484 assert_eq!(payload[..3], [0x05, 0x01, byte], "wrong byte for {mode:?}");
1485 }
1486 }
1487
1488 #[test]
1489 fn bg_color_signed_sentinels_round_trip_through_u8() {
1490 let (d, mock) = device_with_mock();
1491 d.set_bg_color(MediaBgColor::Disable).unwrap();
1493 let (_, _, payload) = last_set(&mock);
1494 assert_eq!(payload[..3], [0x10, 0x01, 0xfe]);
1495
1496 d.set_bg_color(MediaBgColor::Null).unwrap();
1497 let (_, _, payload) = last_set(&mock);
1498 assert_eq!(payload[..3], [0x10, 0x01, 0xff]);
1499
1500 d.set_bg_color(MediaBgColor::Green).unwrap();
1501 let (_, _, payload) = last_set(&mock);
1502 assert_eq!(payload[..3], [0x10, 0x01, 0x01]);
1503 }
1504
1505 #[test]
1506 fn mask_level_rejects_above_100() {
1507 let (d, _) = device_with_mock();
1508 assert!(matches!(d.set_mask_level(101), Err(Error::OutOfRange)));
1509 assert!(matches!(d.set_mask_level(255), Err(Error::OutOfRange)));
1510 }
1511
1512 #[test]
1513 fn suspend_time_encodes_u16_le() {
1514 let (d, mock) = device_with_mock();
1515 d.set_suspend_time(15).unwrap();
1516 let (_, sel, payload) = last_set(&mock);
1517 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1518 assert_eq!(payload[..4], [0x0b, 0x02, 0x0f, 0x00]);
1520 }
1521
1522 #[test]
1523 fn flip_portrait_led_route_to_xu_mode_register() {
1524 let (d, mock) = device_with_mock();
1525
1526 d.set_flip_horizontal(true).unwrap();
1527 let (entity, sel, payload) = last_set(&mock);
1528 assert_eq!(entity, meet2::XU_ENTITY_ID);
1529 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1530 assert_eq!(payload[..3], [0x14, 0x01, 0x01]);
1531
1532 d.set_portrait(true).unwrap();
1533 let (_, _, payload) = last_set(&mock);
1534 assert_eq!(payload[..3], [0x0c, 0x01, 0x01]);
1535
1536 d.set_led(false).unwrap();
1537 let (_, _, payload) = last_set(&mock);
1538 assert_eq!(payload[..3], [0x18, 0x01, 0x00]);
1539 }
1540
1541 #[test]
1542 fn audio_agc_routes_to_xu_mode_register_with_wire_bytes() {
1543 let (d, mock) = device_with_mock();
1544 d.set_audio_agc(true).unwrap();
1545 let (entity, sel, payload) = last_set(&mock);
1546 assert_eq!(entity, meet2::XU_ENTITY_ID);
1547 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1548 assert_eq!(payload[..3], [0x17, 0x01, 0x01]);
1550 assert!(payload[3..].iter().all(|&b| b == 0));
1551
1552 d.set_audio_agc(false).unwrap();
1553 let (_, _, payload) = last_set(&mock);
1554 assert_eq!(payload[..3], [0x17, 0x01, 0x00]);
1555 }
1556
1557 #[test]
1558 fn face_ae_routes_to_xu_mode_register_with_wire_bytes() {
1559 let (d, mock) = device_with_mock();
1561 d.set_face_ae(true).unwrap();
1562 let (entity, sel, payload) = last_set(&mock);
1563 assert_eq!(entity, meet2::XU_ENTITY_ID);
1564 assert_eq!(sel, meet2::XU_SEL_MODE_REGISTER);
1565 assert_eq!(payload[..3], [0x03, 0x01, 0x01]);
1566
1567 d.set_face_ae(false).unwrap();
1568 let (_, _, payload) = last_set(&mock);
1569 assert_eq!(payload[..3], [0x03, 0x01, 0x00]);
1570 }
1571
1572 #[test]
1573 fn wdr_encode_decode_round_trip() {
1574 for mode in [WdrMode::Off, WdrMode::Dol2To1] {
1575 assert_eq!(decode_wdr(encode_wdr(mode)).unwrap(), mode);
1576 }
1577 }
1578
1579 #[test]
1580 fn decode_rejects_unknown_byte() {
1581 assert!(matches!(decode_wdr(99), Err(Error::BadResponse { .. })));
1582 }
1583
1584 #[test]
1585 fn pan_tilt_getter_decodes_two_i32_le() {
1586 use crate::testing::device_with_scripted_get;
1587 let mut buf = Vec::new();
1589 buf.extend_from_slice(&540_000_i32.to_le_bytes());
1590 buf.extend_from_slice(&(-270_000_i32).to_le_bytes());
1591 let device = device_with_scripted_get(vec![buf]);
1592 let (pan, tilt) = device.pan_tilt().unwrap();
1593 assert!((pan - 1.0).abs() < 1e-3);
1594 assert!((tilt + 0.5).abs() < 1e-3);
1595 }
1596
1597 #[test]
1598 fn zoom_and_focus_getters_decode_u16_le() {
1599 use crate::testing::device_with_scripted_get;
1600 let device = device_with_scripted_get(vec![
1601 vec![0x10, 0x00], vec![0xff, 0x00], ]);
1604 assert!((device.zoom().unwrap() - 16.0).abs() < f32::EPSILON);
1605 assert!((device.focus().unwrap() - 255.0).abs() < f32::EPSILON);
1606 }
1607
1608 #[test]
1609 fn status_poller_emits_snapshots_when_sender_provided_and_stops_on_drop() {
1610 use crate::testing::{meet2_mock_info, Forward, MockTransport};
1611
1612 let mock = Arc::new(MockTransport::default());
1613 let transport: Arc<dyn Transport> = Arc::new(Forward(mock));
1614 let (tx, rx) = crossbeam_channel::unbounded::<Event>();
1615 let device = Device::new(meet2_mock_info(), transport, Some(tx), meet2::CAPTURED_MAC);
1616 device.set_status_cadence(Cadence::Fast);
1617
1618 let ev = rx
1621 .recv_timeout(Duration::from_millis(500))
1622 .expect("first status event");
1623 let Event::Status { snapshot, serial } = ev else {
1624 panic!("expected Status event, got {ev:?}");
1625 };
1626 assert_eq!(serial, "MOCK");
1627 assert_eq!(snapshot.brightness, 0);
1629 assert_eq!(snapshot.contrast, 0);
1630 assert_eq!(snapshot.saturation, 0);
1631
1632 drop(device);
1633
1634 std::thread::sleep(Duration::from_millis(100));
1637 while rx.try_recv().is_ok() {}
1638 assert!(rx.try_recv().is_err(), "poller still emitting after drop");
1639 }
1640}