summaryrefslogtreecommitdiff
path: root/sound_card_init/dsm/src/lib.rs
blob: 0b3ec64ced65bf08f9fd33369ad05d8e1b00e800 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! `dsm` crate implements the required initialization workflows for smart amps.

mod datastore;
mod error;
pub mod utils;
mod vpd;
mod zero_player;

use std::{
    thread,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use libcras::{CrasClient, CrasNodeType};
use sys_util::{error, info};

use crate::datastore::Datastore;
pub use crate::error::{Error, Result};
use crate::utils::{run_time, shutdown_time};
use crate::vpd::VPD;
pub use crate::zero_player::ZeroPlayer;

#[derive(Debug, Clone, Copy)]
/// `CalibData` represents the calibration data.
pub struct CalibData {
    /// The DC resistance of the speaker is DSM unit.
    pub rdc: i32,
    /// The ambient temperature in celsius unit at which the rdc is measured.
    pub temp: f32,
}

/// `TempConverter` converts the temperature value between celsius and unit in VPD::dsm_calib_temp.
pub struct TempConverter {
    vpd_to_celsius: fn(i32) -> f32,
    celsius_to_vpd: fn(f32) -> i32,
}

impl Default for TempConverter {
    fn default() -> Self {
        let vpd_to_celsius = |x: i32| x as f32;
        let celsius_to_vpd = |x: f32| x.round() as i32;
        Self {
            vpd_to_celsius,
            celsius_to_vpd,
        }
    }
}

impl TempConverter {
    /// Creates a `TempConverter`
    ///
    /// # Arguments
    ///
    /// * `vpd_to_celsius` - function to convert VPD::dsm_calib_temp to celsius unit`
    /// * `celsius_to_vpd` - function to convert celsius unit to VPD::dsm_calib_temp`
    /// # Results
    ///
    /// * `TempConverter` - it converts the temperature value between celsius and unit in VPD::dsm_calib_temp.
    pub fn new(vpd_to_celsius: fn(i32) -> f32, celsius_to_vpd: fn(f32) -> i32) -> Self {
        Self {
            vpd_to_celsius,
            celsius_to_vpd,
        }
    }
}

/// `SpeakerStatus` are the possible return results of
/// DSM::check_speaker_over_heated_workflow.
pub enum SpeakerStatus {
    ///`SpeakerStatus::Cold` means the speakers are not overheated and the Amp can
    /// trigger the boot time calibration.
    Cold,
    /// `SpeakerStatus::Hot(Vec<CalibData>)` means the speakers may be too hot for calibration.
    /// The boot time calibration should be skipped and the Amp should use the previous
    /// calibration values returned by the enum.
    Hot(Vec<CalibData>),
}

/// `DSM`, which implements the required initialization workflows for smart amps.
pub struct DSM {
    snd_card: String,
    num_channels: usize,
    temp_converter: TempConverter,
    rdc_to_ohm: fn(i32) -> f32,
    temp_upper_limit: f32,
    temp_lower_limit: f32,
}

impl DSM {
    const SPEAKER_COOL_DOWN_TIME: Duration = Duration::from_secs(180);
    const CALI_ERROR_UPPER_LIMIT: f32 = 0.3;
    const CALI_ERROR_LOWER_LIMIT: f32 = 0.03;

    /// Creates a `DSM`
    ///
    /// # Arguments
    ///
    /// * `snd_card` - `sound card name`.
    /// * `num_channels` - `number of channels`.
    /// * `rdc_to_ohm` - `fn(rdc: i32) -> f32 to convert the CalibData::rdc to ohm unit`.
    /// * `temp_upper_limit` - the high limit of the valid ambient temperature in dsm unit.
    /// * `temp_lower_limit` - the low limit of the valid ambient temperature in dsm unit.
    ///
    /// # Results
    ///
    /// * `DSM` - It implements the required initialization workflows for smart amps.
    pub fn new(
        snd_card: &str,
        num_channels: usize,
        rdc_to_ohm: fn(i32) -> f32,
        temp_upper_limit: f32,
        temp_lower_limit: f32,
    ) -> Self {
        Self {
            snd_card: snd_card.to_owned(),
            num_channels,
            rdc_to_ohm,
            temp_converter: TempConverter::default(),
            temp_upper_limit,
            temp_lower_limit,
        }
    }

    /// Sets self.temp_converter to the given temp_converter.
    ///
    /// # Arguments
    ///
    /// * `temp_converter` - the convert function to use.
    pub fn set_temp_converter(&mut self, temp_converter: TempConverter) {
        self.temp_converter = temp_converter;
    }

    /// Checks whether the speakers are overheated or not according to the previous shutdown time.
    /// The boot time calibration should be skipped when the speakers may be too hot
    /// and the Amp should use the previous calibration value returned by the
    /// SpeakerStatus::Hot(Vec<CalibData>).
    ///
    /// # Results
    ///
    /// * `SpeakerStatus::Cold` - which means the speakers are not overheated and the Amp can
    ///    trigger the boot time calibration.
    /// * `SpeakerStatus::Hot(Vec<CalibData>)` - when the speakers may be too hot. The boot
    ///   time calibration should be skipped and the Amp should use the previous calibration values
    ///   returned by the enum.
    ///
    /// # Errors
    ///
    /// * The speakers are overheated and there are no previous calibration values stored.
    /// * Cannot determine whether the speakers are overheated as previous shutdown time record is
    ///   invalid.
    pub fn check_speaker_over_heated_workflow(&self) -> Result<SpeakerStatus> {
        if self.is_first_boot() {
            return Ok(SpeakerStatus::Cold);
        }
        match self.is_speaker_over_heated() {
            Ok(overheated) => {
                if overheated {
                    let calib: Vec<CalibData> = (0..self.num_channels)
                        .map(|ch| -> Result<CalibData> { self.get_previous_calibration_value(ch) })
                        .collect::<Result<Vec<CalibData>>>()?;
                    info!("the speakers are hot, the boot time calibration should be skipped");
                    return Ok(SpeakerStatus::Hot(calib));
                }
                Ok(SpeakerStatus::Cold)
            }
            Err(err) => {
                // We cannot assume the speakers are not replaced or not overheated
                // when the shutdown time file is invalid; therefore we can not use the datastore
                // value anymore and we can not trigger boot time calibration.
                for ch in 0..self.num_channels {
                    if let Err(e) = Datastore::delete(&self.snd_card, ch) {
                        error!("error delete datastore: {}", e);
                    }
                }
                Err(err)
            }
        }
    }

    /// Decides a good calibration value and updates the stored value according to the following
    /// logic:
    /// * Returns the previous value if the ambient temperature is not within a valid range.
    /// * Returns Error::LargeCalibrationDiff if rdc difference is larger than
    ///   `CALI_ERROR_UPPER_LIMIT`.
    /// * Returns the previous value if the rdc difference is smaller than `CALI_ERROR_LOWER_LIMIT`.
    /// * Returns the boot time calibration value and updates the datastore value if the rdc.
    ///   difference is between `CALI_ERROR_UPPER_LIMIT` and `CALI_ERROR_LOWER_LIMIT`.
    ///
    /// # Arguments
    ///
    /// * `card` - `&Card`.
    /// * `channel` - `channel number`.
    /// * `calib_data` - `boot time calibrated data`.
    ///
    /// # Results
    ///
    /// * `CalibData` - the calibration data to be applied according to the deciding logic.
    ///
    /// # Errors
    ///
    /// * VPD does not exist.
    /// * rdc difference is larger than `CALI_ERROR_UPPER_LIMIT`.
    /// * Failed to update Datastore.
    pub fn decide_calibration_value_workflow(
        &self,
        channel: usize,
        calib_data: CalibData,
    ) -> Result<CalibData> {
        if calib_data.temp < self.temp_lower_limit || calib_data.temp > self.temp_upper_limit {
            info!("invalid temperature: {}.", calib_data.temp);
            return self
                .get_previous_calibration_value(channel)
                .map_err(|_| Error::InvalidTemperature(calib_data.temp));
        }
        let (datastore_exist, previous_calib) = match self.get_previous_calibration_value(channel) {
            Ok(previous_calib) => (true, previous_calib),
            Err(e) => {
                info!("{}, use vpd as previous calibration value", e);
                (false, self.get_vpd_calibration_value(channel)?)
            }
        };

        let diff = {
            let calib_rdc_ohm = (self.rdc_to_ohm)(calib_data.rdc);
            let previous_rdc_ohm = (self.rdc_to_ohm)(previous_calib.rdc);
            (calib_rdc_ohm - previous_rdc_ohm) / previous_rdc_ohm
        };
        if diff > Self::CALI_ERROR_UPPER_LIMIT {
            Err(Error::LargeCalibrationDiff(calib_data))
        } else if diff < Self::CALI_ERROR_LOWER_LIMIT {
            if !datastore_exist {
                Datastore::UseVPD.save(&self.snd_card, channel)?;
            }
            Ok(previous_calib)
        } else {
            Datastore::DSM {
                rdc: calib_data.rdc,
                temp: (self.temp_converter.celsius_to_vpd)(calib_data.temp),
            }
            .save(&self.snd_card, channel)?;
            Ok(calib_data)
        }
    }

    /// Gets the calibration values from vpd.
    ///
    /// # Results
    ///
    /// * `Vec<CalibData>` - the calibration values in vpd.
    ///
    /// # Errors
    ///
    /// * Failed to read vpd.
    pub fn get_all_vpd_calibration_value(&self) -> Result<Vec<CalibData>> {
        (0..self.num_channels)
            .map(|ch| self.get_vpd_calibration_value(ch))
            .collect::<Result<Vec<_>>>()
    }

    /// Blocks until the internal speakers are ready.
    ///
    /// # Errors
    ///
    /// * Failed to wait the internal speakers to be ready.
    pub fn wait_for_speakers_ready(&self) -> Result<()> {
        let find_speaker = || -> Result<()> {
            let cras_client = CrasClient::new().map_err(Error::CrasClientFailed)?;
            let _node = cras_client
                .output_nodes()
                .find(|node| node.node_type == CrasNodeType::CRAS_NODE_TYPE_INTERNAL_SPEAKER)
                .ok_or(Error::InternalSpeakerNotFound)?;
            Ok(())
        };
        // TODO(b/155007305): Implement cras_client.wait_node_change and use it here.
        const RETRY: usize = 3;
        const RETRY_INTERVAL: Duration = Duration::from_millis(500);
        for _ in 0..RETRY {
            match find_speaker() {
                Ok(_) => return Ok(()),
                Err(e) => error!("retry on finding speaker: {}", e),
            };
            thread::sleep(RETRY_INTERVAL);
        }
        Err(Error::InternalSpeakerNotFound)
    }

    fn is_first_boot(&self) -> bool {
        !run_time::exists(&self.snd_card)
    }

    // If (Current time - the latest CRAS shutdown time) < cool_down_time, we assume that
    // the speakers may be overheated.
    fn is_speaker_over_heated(&self) -> Result<bool> {
        let last_run = run_time::from_file(&self.snd_card)?;
        let last_shutdown = shutdown_time::from_file()?;
        if last_shutdown < last_run {
            return Err(Error::InvalidShutDownTime);
        }

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(Error::SystemTimeError)?;

        let elapsed = now
            .checked_sub(last_shutdown)
            .ok_or(Error::InvalidShutDownTime)?;

        if elapsed < Self::SPEAKER_COOL_DOWN_TIME {
            return Ok(true);
        }
        Ok(false)
    }

    fn get_previous_calibration_value(&self, ch: usize) -> Result<CalibData> {
        let sci_calib = Datastore::from_file(&self.snd_card, ch)?;
        match sci_calib {
            Datastore::UseVPD => self.get_vpd_calibration_value(ch),
            Datastore::DSM { rdc, temp } => Ok(CalibData {
                rdc,
                temp: (self.temp_converter.vpd_to_celsius)(temp),
            }),
        }
    }

    fn get_vpd_calibration_value(&self, channel: usize) -> Result<CalibData> {
        let vpd = VPD::new(channel)?;
        Ok(CalibData {
            rdc: vpd.dsm_calib_r0,
            temp: (self.temp_converter.vpd_to_celsius)(vpd.dsm_calib_temp),
        })
    }
}