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