Skip to main content

input_pipeline/light_sensor/
light_sensor_handler.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::Transport;
6use crate::input_device::{
7    Handled, InputDeviceDescriptor, InputDeviceEvent, InputEvent, InputEventType,
8};
9use crate::input_handler::{Handler, InputHandler, InputHandlerStatus};
10use crate::inspect_handler::{BufferNode, CircularBuffer};
11use crate::light_sensor::calibrator::{Calibrate, Calibrator};
12use crate::light_sensor::led_watcher::{CancelableTask, LedWatcher, LedWatcherHandle};
13use crate::light_sensor::types::{AdjustmentSetting, Calibration, Rgbc, SensorConfiguration};
14use anyhow::{Context, Error, format_err};
15use async_trait::async_trait;
16use async_utils::hanging_get::server::HangingGet;
17use fidl_fuchsia_lightsensor::{
18    LightSensorData as FidlLightSensorData, Rgbc as FidlRgbc, SensorRequest, SensorRequestStream,
19    SensorWatchResponder,
20};
21use fidl_fuchsia_settings::LightProxy;
22use fidl_fuchsia_ui_brightness::ControlProxy as BrightnessControlProxy;
23use fidl_next_fuchsia_input_report::{FeatureReport, SensorFeatureReport};
24use fuchsia_inspect::NumericProperty;
25use fuchsia_inspect::health::Reporter;
26
27use futures::channel::oneshot;
28use futures::lock::Mutex;
29use futures::{Future, FutureExt, TryStreamExt};
30use std::cell::RefCell;
31use std::rc::Rc;
32use std::sync::Arc;
33
34type NotifyFn = Box<dyn Fn(&LightSensorData, SensorWatchResponder) -> bool>;
35type SensorHangingGet = HangingGet<LightSensorData, SensorWatchResponder, NotifyFn>;
36
37// Precise value is 2.78125ms, but data sheet lists 2.78ms.
38/// Number of us for each cycle of the sensor.
39const MIN_TIME_STEP_US: u32 = 2780;
40/// Maximum multiplier.
41const MAX_GAIN: u32 = 64;
42/// Maximum sensor reading per cycle for any 1 color channel.
43const MAX_COUNT_PER_CYCLE: u32 = 1024;
44/// Absolute maximum reading the sensor can return for any 1 color channel.
45const MAX_SATURATION: u32 = u16::MAX as u32;
46const MAX_ATIME: u32 = 256;
47/// Driver scales the values by max gain & atime in ms.
48const ADC_SCALING_FACTOR: f32 = 64.0 * 256.0;
49/// The gain up margin should be 10% of the saturation point.
50const GAIN_UP_MARGIN_DIVISOR: u32 = 10;
51/// The divisor for scaling uncalibrated values to transition old clients to auto gain.
52const TRANSITION_SCALING_FACTOR: f32 = 4.0;
53
54#[derive(Copy, Clone, Debug)]
55struct LightReading {
56    rgbc: Rgbc<f32>,
57    si_rgbc: Rgbc<f32>,
58    is_calibrated: bool,
59    lux: f32,
60    cct: Option<f32>,
61}
62
63fn num_cycles(atime: u32) -> u32 {
64    MAX_ATIME - atime
65}
66
67#[cfg_attr(test, derive(Debug))]
68struct ActiveSetting {
69    settings: Vec<AdjustmentSetting>,
70    idx: usize,
71}
72
73impl ActiveSetting {
74    fn new(settings: Vec<AdjustmentSetting>, idx: usize) -> Self {
75        Self { settings, idx }
76    }
77
78    /// Update sensor if it's near or past a saturation point. Returns a saturation error if the
79    /// sensor is saturated, `true` if the sensor is not saturated but still pulled up, and `false`
80    /// otherwise.
81    async fn adjust<Fut>(
82        &mut self,
83        reading: Rgbc<u16>,
84        device_proxy: &fidl_next::Client<fidl_next_fuchsia_input_report::InputDevice, Transport>,
85        track_feature_update: impl Fn(FeatureEvent) -> Fut,
86    ) -> Result<bool, SaturatedError>
87    where
88        Fut: Future<Output = ()>,
89    {
90        let saturation_point =
91            (num_cycles(self.active_setting().atime) * MAX_COUNT_PER_CYCLE).min(MAX_SATURATION);
92        let gain_up_margin = saturation_point / GAIN_UP_MARGIN_DIVISOR;
93
94        let step_change = self.step_change();
95        let mut pull_up = true;
96
97        if saturated(reading) {
98            if self.adjust_down() {
99                log::info!("adjusting down due to saturation sentinel");
100                self.update_device(&device_proxy, track_feature_update)
101                    .await
102                    .context("updating light sensor device")?;
103            }
104            return Err(SaturatedError::Saturated);
105        }
106
107        for value in [reading.red, reading.green, reading.blue, reading.clear] {
108            let value = value as u32;
109            if value >= saturation_point {
110                if self.adjust_down() {
111                    log::info!("adjusting down due to saturation point");
112                    self.update_device(&device_proxy, track_feature_update)
113                        .await
114                        .context("updating light sensor device")?;
115                }
116                return Err(SaturatedError::Saturated);
117            } else if (value * step_change + gain_up_margin) >= saturation_point {
118                pull_up = false;
119            }
120        }
121
122        if pull_up {
123            if self.adjust_up() {
124                log::info!("adjusting up");
125                self.update_device(&device_proxy, track_feature_update)
126                    .await
127                    .context("updating light sensor device")?;
128                return Ok(true);
129            }
130        }
131
132        Ok(false)
133    }
134
135    async fn update_device<Fut>(
136        &self,
137        device_proxy: &fidl_next::Client<fidl_next_fuchsia_input_report::InputDevice, Transport>,
138        track_feature_update: impl Fn(FeatureEvent) -> Fut,
139    ) -> Result<(), Error>
140    where
141        Fut: Future<Output = ()>,
142    {
143        let active_setting = self.active_setting();
144        let feature_report = device_proxy
145            .get_feature_report()
146            .await
147            .context("calling get_feature_report")?
148            .map_err(|e| {
149                format_err!(
150                    "getting feature report on light sensor device: {:?}",
151                    zx::Status::from_raw(e),
152                )
153            })?
154            .report;
155        let feature_report = FeatureReport {
156            sensor: Some(SensorFeatureReport {
157                sensitivity: Some(vec![active_setting.gain as i64]),
158                // Feature report expects sampling rate in microseconds.
159                sampling_rate: Some(to_us(active_setting.atime) as i64),
160                ..(feature_report
161                    .sensor
162                    .ok_or_else(|| format_err!("missing sensor in feature_report"))?)
163            }),
164            ..feature_report
165        };
166        device_proxy
167            .set_feature_report(&feature_report)
168            .await
169            .context("calling set_feature_report")?
170            .map_err(|e| {
171                format_err!(
172                    "updating feature report on light sensor device: {:?}",
173                    zx::Status::from_raw(e),
174                )
175            })?;
176        if let Some(feature_event) = FeatureEvent::maybe_new(feature_report) {
177            (track_feature_update)(feature_event).await;
178        }
179        Ok(())
180    }
181
182    fn active_setting(&self) -> AdjustmentSetting {
183        self.settings[self.idx]
184    }
185
186    /// Adjusts to a lower setting. Returns whether or not the setting changed.
187    fn adjust_down(&mut self) -> bool {
188        if self.idx == 0 {
189            false
190        } else {
191            self.idx -= 1;
192            true
193        }
194    }
195
196    /// Calculate the effect to saturation that occurs by moving the setting up a step.
197    fn step_change(&self) -> u32 {
198        let current = self.active_setting();
199        let new = match self.settings.get(self.idx + 1) {
200            Some(setting) => *setting,
201            // If we're at the limit, just return a coefficient of 1 since there will be no step
202            // change.
203            None => return 1,
204        };
205        div_round_up(new.gain, current.gain) * div_round_up(to_us(new.atime), to_us(current.atime))
206    }
207
208    /// Adjusts to a higher setting. Returns whether or not the setting changed.
209    fn adjust_up(&mut self) -> bool {
210        if self.idx == self.settings.len() - 1 {
211            false
212        } else {
213            self.idx += 1;
214            true
215        }
216    }
217}
218
219struct FeatureEvent {
220    event_time: zx::MonotonicInstant,
221    sampling_rate: i64,
222    sensitivity: i64,
223}
224
225impl FeatureEvent {
226    fn maybe_new(report: FeatureReport) -> Option<Self> {
227        let sensor = report.sensor?;
228        Some(FeatureEvent {
229            sampling_rate: sensor.sampling_rate?,
230            sensitivity: *sensor.sensitivity?.get(0)?,
231            event_time: zx::MonotonicInstant::get(),
232        })
233    }
234}
235
236impl BufferNode for FeatureEvent {
237    fn get_name(&self) -> &'static str {
238        "feature_report_update_event"
239    }
240
241    fn record_inspect(&self, node: &fuchsia_inspect::Node) {
242        node.record_int("sampling_rate", self.sampling_rate);
243        node.record_int("sensitivity", self.sensitivity);
244        node.record_int("event_time", self.event_time.into_nanos());
245    }
246}
247
248pub struct LightSensorHandler<T> {
249    hanging_get: RefCell<SensorHangingGet>,
250    calibrator: Option<T>,
251    active_setting: RefCell<ActiveSettingState>,
252    rgbc_to_lux_coefs: Rgbc<f32>,
253    si_scaling_factors: Rgbc<f32>,
254    vendor_id: u32,
255    product_id: u32,
256    /// The inventory of this handler's Inspect status.
257    inspect_status: InputHandlerStatus,
258    feature_updates: Arc<Mutex<CircularBuffer<FeatureEvent>>>,
259
260    // Additional inspect properties specific to LightSensorHandler
261
262    // Number of received events that were discarded because handler could not process
263    // its saturation values. These events are marked as handled in Input Pipeline so
264    // they are ignored by downstream handlers, but are not counted to events_handled_count.
265    // events_received_count >= events_handled_count + events_saturated_count
266    events_saturated_count: fuchsia_inspect::UintProperty,
267    // Number of connected clients subscribed to receive updated sensor readings from
268    // the HangingGet.
269    clients_connected_count: fuchsia_inspect::UintProperty,
270}
271
272#[cfg_attr(test, derive(Debug))]
273enum ActiveSettingState {
274    Uninitialized(Vec<AdjustmentSetting>),
275    Initialized(ActiveSetting),
276    Static(AdjustmentSetting),
277}
278
279pub type CalibratedLightSensorHandler = LightSensorHandler<Calibrator<LedWatcherHandle>>;
280pub async fn make_light_sensor_handler_and_spawn_led_watcher(
281    light_proxy: LightProxy,
282    brightness_proxy: BrightnessControlProxy,
283    calibration: Option<Calibration>,
284    configuration: SensorConfiguration,
285    input_handlers_node: &fuchsia_inspect::Node,
286) -> Result<(Rc<CalibratedLightSensorHandler>, Option<CancelableTask>), Error> {
287    let inspect_status = InputHandlerStatus::new(
288        input_handlers_node,
289        "light_sensor_handler",
290        /* generates_events */ false,
291    );
292    let (calibrator, watcher_task) = if let Some(calibration) = calibration {
293        let light_groups =
294            light_proxy.watch_light_groups().await.context("request initial light groups")?;
295        let led_watcher = LedWatcher::new(light_groups);
296        let (cancelation_tx, cancelation_rx) = oneshot::channel();
297        let light_proxy_receives_initial_response =
298            inspect_status.inspect_node.create_bool("light_proxy_receives_initial_response", false);
299        let brightness_proxy_receives_initial_response = inspect_status
300            .inspect_node
301            .create_bool("brightness_proxy_receives_initial_response", false);
302        let (led_watcher_handle, watcher_task) = led_watcher
303            .handle_light_groups_and_brightness_watch(
304                light_proxy,
305                brightness_proxy,
306                cancelation_rx,
307                light_proxy_receives_initial_response,
308                brightness_proxy_receives_initial_response,
309            );
310        let watcher_task = CancelableTask::new(cancelation_tx, watcher_task);
311        let calibrator = Calibrator::new(calibration, led_watcher_handle);
312        (Some(calibrator), Some(watcher_task))
313    } else {
314        (None, None)
315    };
316    Ok((LightSensorHandler::new(calibrator, configuration, inspect_status), watcher_task))
317}
318
319impl<T> LightSensorHandler<T> {
320    pub fn new(
321        calibrator: impl Into<Option<T>>,
322        configuration: SensorConfiguration,
323        inspect_status: InputHandlerStatus,
324    ) -> Rc<Self> {
325        let calibrator = calibrator.into();
326        let hanging_get = RefCell::new(HangingGet::new_unknown_state(Box::new(
327            |sensor_data: &LightSensorData, responder: SensorWatchResponder| -> bool {
328                if let Err(e) = responder.send(&FidlLightSensorData::from(*sensor_data)) {
329                    log::warn!("Failed to send updated data to client: {e:?}",);
330                }
331                true
332            },
333        ) as NotifyFn));
334        let feature_updates = Arc::new(Mutex::new(CircularBuffer::new(5)));
335        let active_setting =
336            RefCell::new(ActiveSettingState::Uninitialized(configuration.settings));
337        let events_saturated_count =
338            inspect_status.inspect_node.create_uint("events_saturated_count", 0);
339        let clients_connected_count =
340            inspect_status.inspect_node.create_uint("clients_connected_count", 0);
341        inspect_status.inspect_node.record_lazy_child("recent_feature_events_log", {
342            let feature_updates = Arc::clone(&feature_updates);
343            move || {
344                let feature_updates = Arc::clone(&feature_updates);
345                async move {
346                    let inspector = fuchsia_inspect::Inspector::default();
347                    Ok(feature_updates.lock().await.record_all_lazy_inspect(inspector))
348                }
349                .boxed()
350            }
351        });
352        Rc::new(Self {
353            hanging_get,
354            calibrator,
355            active_setting,
356            rgbc_to_lux_coefs: configuration.rgbc_to_lux_coefficients,
357            si_scaling_factors: configuration.si_scaling_factors,
358            vendor_id: configuration.vendor_id,
359            product_id: configuration.product_id,
360            inspect_status,
361            events_saturated_count,
362            clients_connected_count,
363            feature_updates,
364        })
365    }
366
367    pub async fn handle_light_sensor_request_stream(
368        self: &Rc<Self>,
369        mut stream: SensorRequestStream,
370    ) -> Result<(), Error> {
371        let subscriber = self.hanging_get.borrow_mut().new_subscriber();
372        self.clients_connected_count.add(1);
373        while let Some(request) =
374            stream.try_next().await.context("Error handling light sensor request stream")?
375        {
376            match request {
377                SensorRequest::Watch { responder } => {
378                    subscriber
379                        .register(responder)
380                        .context("registering responder for Watch call")?;
381                }
382            }
383        }
384        self.clients_connected_count.subtract(1);
385        Ok(())
386    }
387
388    /// Calculates the lux of a reading.
389    fn calculate_lux(&self, reading: Rgbc<f32>) -> f32 {
390        Rgbc::multi_map(reading, self.rgbc_to_lux_coefs, |reading, coef| reading * coef)
391            .fold(0.0, |lux, c| lux + c)
392    }
393}
394
395/// Normalize raw sensor counts.
396///
397/// I.e. values being read in dark lighting will be returned as their original value,
398/// but values in the brighter lighting will be returned larger, as a reading within the true
399/// output range of the light sensor.
400fn process_reading(reading: Rgbc<u16>, initial_setting: AdjustmentSetting) -> Rgbc<f32> {
401    let gain_bias = MAX_GAIN / initial_setting.gain as u32;
402
403    reading.map(|v| {
404        div_round_closest(v as u32 * gain_bias * MAX_ATIME, num_cycles(initial_setting.atime))
405            as f32
406    })
407}
408
409#[derive(Debug)]
410enum SaturatedError {
411    Saturated,
412    Anyhow(Error),
413}
414
415impl From<Error> for SaturatedError {
416    fn from(value: Error) -> Self {
417        Self::Anyhow(value)
418    }
419}
420
421impl<T> LightSensorHandler<T>
422where
423    T: Calibrate,
424{
425    async fn get_calibrated_data(
426        &self,
427        reading: Rgbc<u16>,
428        device_proxy: &fidl_next::Client<fidl_next_fuchsia_input_report::InputDevice, Transport>,
429    ) -> Result<LightReading, SaturatedError> {
430        // Update the sensor after the active setting has been used for calculations, since it may
431        // change after this call.
432        let (initial_setting, pulled_up) = {
433            let mut active_setting_state = self.active_setting.borrow_mut();
434            let track_feature_update = |feature_event| async move {
435                self.feature_updates.lock().await.push(feature_event);
436            };
437            match &mut *active_setting_state {
438                ActiveSettingState::Uninitialized(adjustment_settings) => {
439                    let active_setting = ActiveSetting::new(std::mem::take(adjustment_settings), 0);
440                    if let Err(e) =
441                        active_setting.update_device(device_proxy, track_feature_update).await
442                    {
443                        log::error!(
444                            "Unable to set initial settings for sensor. Falling back \
445                                        to static setting: {e:?}"
446                        );
447                        // Switch to a static state because this sensor cannot change its settings.
448                        let setting = active_setting.settings[0];
449                        *active_setting_state = ActiveSettingState::Static(setting);
450                        (setting, false)
451                    } else {
452                        // Initial setting is unset. Reading cannot be properly adjusted, so
453                        // override the current settings on the device and report a saturated error
454                        // so this reading is not sent to any clients.
455                        *active_setting_state = ActiveSettingState::Initialized(active_setting);
456                        return Err(SaturatedError::Saturated);
457                    }
458                }
459                ActiveSettingState::Initialized(active_setting) => {
460                    let initial_setting = active_setting.active_setting();
461                    let pulled_up = active_setting
462                        .adjust(reading, device_proxy, track_feature_update)
463                        .await
464                        .map_err(|e| match e {
465                            SaturatedError::Saturated => SaturatedError::Saturated,
466                            SaturatedError::Anyhow(e) => {
467                                SaturatedError::Anyhow(e.context("adjusting active setting"))
468                            }
469                        })?;
470                    (initial_setting, pulled_up)
471                }
472                ActiveSettingState::Static(setting) => (*setting, false),
473            }
474        };
475        let uncalibrated_rgbc = process_reading(reading, initial_setting);
476        let rgbc = self
477            .calibrator
478            .as_ref()
479            .map(|calibrator| calibrator.calibrate(uncalibrated_rgbc))
480            .unwrap_or(uncalibrated_rgbc);
481
482        let si_rgbc = (self.si_scaling_factors * rgbc).map(|c| c / ADC_SCALING_FACTOR);
483        let lux = self.calculate_lux(si_rgbc);
484        let cct = correlated_color_temperature(si_rgbc);
485        // Only return saturation error if the cct is invalid and the sensor was also adjusted. If
486        // only the cct is invalid, it means the sensor is not undersaturated but reading
487        // pitch-black at the highest sensitivity.
488        if cct.is_none() && pulled_up {
489            return Err(SaturatedError::Saturated);
490        }
491
492        let rgbc = uncalibrated_rgbc.map(|c| c as f32 / TRANSITION_SCALING_FACTOR);
493        Ok(LightReading { rgbc, si_rgbc, is_calibrated: self.calibrator.is_some(), lux, cct })
494    }
495}
496
497/// Converts atime values to microseconds.
498fn to_us(atime: u32) -> u32 {
499    num_cycles(atime) * MIN_TIME_STEP_US
500}
501
502/// Divides n by d, rounding up.
503fn div_round_up(n: u32, d: u32) -> u32 {
504    (n + d - 1) / d
505}
506
507/// Divides n by d, rounding to the closest value.
508fn div_round_closest(n: u32, d: u32) -> u32 {
509    (n + (d / 2)) / d
510}
511
512// These values are defined in //src/devices/light-sensor/ams-light/tcs3400.cc
513const MAX_SATURATION_RED: u16 = 21_067;
514const MAX_SATURATION_GREEN: u16 = 20_395;
515const MAX_SATURATION_BLUE: u16 = 20_939;
516const MAX_SATURATION_CLEAR: u16 = 65_085;
517
518// TODO(https://fxbug.dev/42143847) Update when sensor reports include saturation
519// information.
520fn saturated(reading: Rgbc<u16>) -> bool {
521    reading.red == MAX_SATURATION_RED
522        && reading.green == MAX_SATURATION_GREEN
523        && reading.blue == MAX_SATURATION_BLUE
524        && reading.clear == MAX_SATURATION_CLEAR
525}
526
527// See http://ams.com/eng/content/view/download/145158 for the detail of the
528// following calculation.
529/// Returns `None` when the reading is under or over saturated.
530fn correlated_color_temperature(reading: Rgbc<f32>) -> Option<f32> {
531    // TODO(https://fxbug.dev/42072871): Move color_temp calculation out of common code
532    let big_x = -0.7687 * reading.red + 9.7764 * reading.green + -7.4164 * reading.blue;
533    let big_y = -1.7475 * reading.red + 9.9603 * reading.green + -5.6755 * reading.blue;
534    let big_z = -3.6709 * reading.red + 4.8637 * reading.green + 4.3682 * reading.blue;
535
536    let div = big_x + big_y + big_z;
537    if div.abs() < f32::EPSILON {
538        return None;
539    }
540
541    let x = big_x / div;
542    let y = big_y / div;
543    let n = (x - 0.3320) / (0.1858 - y);
544    Some(449.0 * n.powi(3) + 3525.0 * n.powi(2) + 6823.3 * n + 5520.33)
545}
546
547impl<T> Handler for LightSensorHandler<T>
548where
549    T: Calibrate + 'static,
550{
551    fn set_handler_healthy(self: std::rc::Rc<Self>) {
552        self.inspect_status.health_node.borrow_mut().set_ok();
553    }
554
555    fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
556        self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
557    }
558
559    fn get_name(&self) -> &'static str {
560        "LightSensorHandler"
561    }
562
563    fn interest(&self) -> Vec<InputEventType> {
564        vec![InputEventType::LightSensor]
565    }
566}
567
568#[async_trait(?Send)]
569impl<T> InputHandler for LightSensorHandler<T>
570where
571    T: Calibrate + 'static,
572{
573    async fn handle_input_event(self: Rc<Self>, mut input_event: InputEvent) -> Vec<InputEvent> {
574        fuchsia_trace::duration!("input", "light_sensor_handler");
575        if let InputEvent {
576            device_event: InputDeviceEvent::LightSensor(ref light_sensor_event),
577            device_descriptor: InputDeviceDescriptor::LightSensor(ref light_sensor_descriptor),
578            event_time,
579            handled: Handled::No,
580            trace_id: _,
581        } = input_event
582        {
583            fuchsia_trace::duration!("input", "light_sensor_handler[processing]");
584            self.inspect_status.count_received_event(&event_time);
585            // Validate descriptor matches.
586            if !(light_sensor_descriptor.vendor_id == self.vendor_id
587                && light_sensor_descriptor.product_id == self.product_id)
588            {
589                // Don't handle the event.
590                log::warn!(
591                    "Unexpected device in light sensor handler: {:?}",
592                    light_sensor_descriptor,
593                );
594                return vec![input_event];
595            }
596            let LightReading { rgbc, si_rgbc, is_calibrated, lux, cct } = match self
597                .get_calibrated_data(light_sensor_event.rgbc, &light_sensor_event.device_proxy)
598                .await
599            {
600                Ok(data) => data,
601                Err(SaturatedError::Saturated) => {
602                    // Saturated data is not useful for clients so we do not publish data.
603                    self.events_saturated_count.add(1);
604                    return vec![input_event];
605                }
606                Err(SaturatedError::Anyhow(e)) => {
607                    log::warn!("Failed to get light sensor readings: {e:?}");
608                    // Don't handle the event.
609                    return vec![input_event];
610                }
611            };
612            let publisher = self.hanging_get.borrow_mut().new_publisher();
613            publisher.set(LightSensorData {
614                rgbc,
615                si_rgbc,
616                is_calibrated,
617                calculated_lux: lux,
618                correlated_color_temperature: cct,
619            });
620            input_event.handled = Handled::Yes;
621            self.inspect_status.count_handled_event();
622        }
623        vec![input_event]
624    }
625}
626
627#[derive(Copy, Clone, PartialEq)]
628struct LightSensorData {
629    rgbc: Rgbc<f32>,
630    si_rgbc: Rgbc<f32>,
631    is_calibrated: bool,
632    calculated_lux: f32,
633    correlated_color_temperature: Option<f32>,
634}
635
636impl From<LightSensorData> for FidlLightSensorData {
637    fn from(data: LightSensorData) -> Self {
638        Self {
639            rgbc: Some(FidlRgbc::from(data.rgbc)),
640            si_rgbc: Some(FidlRgbc::from(data.si_rgbc)),
641            is_calibrated: Some(data.is_calibrated),
642            calculated_lux: Some(data.calculated_lux),
643            correlated_color_temperature: data.correlated_color_temperature,
644            ..Default::default()
645        }
646    }
647}
648
649impl From<Rgbc<f32>> for FidlRgbc {
650    fn from(rgbc: Rgbc<f32>) -> Self {
651        Self {
652            red_intensity: rgbc.red,
653            green_intensity: rgbc.green,
654            blue_intensity: rgbc.blue,
655            clear_intensity: rgbc.clear,
656        }
657    }
658}
659
660#[cfg(test)]
661mod light_sensor_handler_tests;