blob: 0b3ec64ced65bf08f9fd33369ad05d8e1b00e800 [file] [log] [blame]
// 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),
})
}
}