-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[sp-sim] rudimentary simulation of sensors (#6313)
In order to develop Oximeter metrics for SP sensor readings, emitted by MGS, we would like the `sp-sim` binary to be able to simulate the protocol for reading SP sensors. This branch adds a fairly rudimentary implementation of this: components configured in the `sp-sim` config file may now include an array of one or more `sensors`, like this: ```toml # ... [[simulated_sps.gimlet.components]] id = "dev-0" device = "tmp117" description = "FAKE Southwest temperature sensor" capabilities = 2 presence = "Present" sensors = [ { name = "Southwest", kind = "Temperature", last_data.value = 41.7890625, last_data.timestamp = 1234 }, ] ``` Once this is added, the simulated SP will implement the `num_component_details`, `component_details`, and `read_sensor` functions for any such components: ```console eliza@noctis ~/Code/oxide/omicron $ curl -s http://127.0.0.1:11111/sp/sled/0/component | jq { "components": [ { "component": "sp3-host-cpu", "device": "sp3-host-cpu", "serial_number": null, "description": "FAKE host cpu", "capabilities": 0, "presence": "present" }, { "component": "dev-0", "device": "tmp117", "serial_number": null, "description": "FAKE Southwest temperature sensor", "capabilities": 2, "presence": "present" } ] } eliza@noctis ~/Code/oxide/omicron $ curl -s http://127.0.0.1:11111/sp/sled/0/component/dev-0 | jq [ { "type": "measurement", "name": "Southwest", "kind": { "kind": "temperature" }, "value": 41.789062 } ] ``` In the future, I would like to extend this functionality substantially: it would be nice to add a notion of a simulated global timestamp, and a mechanism for changing the values of sensor readings dynamically. I think this would be useful for testing the timebase synchronization code we will no doubt need to write eventually for this. But, for now, being able to hard-code sensor values is a start.
- Loading branch information
Showing
6 changed files
with
307 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
pub mod config; | ||
mod gimlet; | ||
mod helpers; | ||
mod sensors; | ||
mod server; | ||
mod sidecar; | ||
mod update; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
||
use crate::config::SpComponentConfig; | ||
use gateway_messages::measurement::MeasurementError; | ||
use gateway_messages::measurement::MeasurementKind; | ||
use gateway_messages::sp_impl::BoundsChecked; | ||
use gateway_messages::ComponentDetails; | ||
use gateway_messages::DeviceCapabilities; | ||
use gateway_messages::Measurement; | ||
use gateway_messages::SensorDataMissing; | ||
use gateway_messages::SensorError; | ||
use gateway_messages::SensorReading; | ||
use gateway_messages::SensorRequest; | ||
use gateway_messages::SensorRequestKind; | ||
use gateway_messages::SensorResponse; | ||
use gateway_messages::SpComponent; | ||
|
||
use std::collections::HashMap; | ||
|
||
pub(crate) struct Sensors { | ||
by_component: HashMap<SpComponent, Vec<u32>>, | ||
sensors: Vec<Sensor>, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct Sensor { | ||
def: SensorDef, | ||
state: SensorState, | ||
} | ||
|
||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq)] | ||
pub struct SensorDef { | ||
pub name: String, | ||
pub kind: MeasurementKind, | ||
} | ||
|
||
// TODO(eliza): note that currently, we just hardcode these in | ||
// `MeasurementConfig`. Eventually, it would be neat to allow the sensor to be | ||
// changed dynamically as part of a simulation. | ||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] | ||
pub struct SensorState { | ||
#[serde(default)] | ||
pub last_error: Option<LastError>, | ||
|
||
#[serde(default)] | ||
pub last_data: Option<LastData>, | ||
} | ||
|
||
#[derive( | ||
Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq, | ||
)] | ||
pub struct LastError { | ||
pub timestamp: u64, | ||
pub value: SensorDataMissing, | ||
} | ||
|
||
#[derive( | ||
Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq, | ||
)] | ||
pub struct LastData { | ||
pub timestamp: u64, | ||
pub value: f32, | ||
} | ||
|
||
impl SensorState { | ||
fn last_reading(&self) -> SensorReading { | ||
match self { | ||
Self { last_data: Some(data), last_error: Some(error) } => { | ||
if data.timestamp >= error.timestamp { | ||
SensorReading { | ||
value: Ok(data.value), | ||
timestamp: data.timestamp, | ||
} | ||
} else { | ||
SensorReading { | ||
value: Err(error.value), | ||
timestamp: error.timestamp, | ||
} | ||
} | ||
} | ||
Self { last_data: Some(data), last_error: None } => SensorReading { | ||
value: Ok(data.value), | ||
timestamp: data.timestamp, | ||
}, | ||
Self { last_data: None, last_error: Some(error) } => { | ||
SensorReading { | ||
value: Err(error.value), | ||
timestamp: error.timestamp, | ||
} | ||
} | ||
Self { last_data: None, last_error: None } => SensorReading { | ||
value: Err(SensorDataMissing::DeviceNotPresent), | ||
timestamp: 0, // TODO(eliza): what do? | ||
}, | ||
} | ||
} | ||
} | ||
|
||
impl Sensors { | ||
pub(crate) fn from_component_configs<'a>( | ||
cfgs: impl IntoIterator<Item = &'a SpComponentConfig>, | ||
) -> Self { | ||
let mut sensors = Vec::new(); | ||
let mut by_component = HashMap::new(); | ||
for cfg in cfgs { | ||
if cfg.sensors.is_empty() { | ||
continue; | ||
} | ||
if !cfg | ||
.capabilities | ||
.contains(DeviceCapabilities::HAS_MEASUREMENT_CHANNELS) | ||
{ | ||
panic!( | ||
"invalid component config: a device with sensors should \ | ||
have the `HAS_MEASUREMENT_CHANNELS` capability:{cfg:#?}" | ||
); | ||
} | ||
|
||
let mut ids = Vec::with_capacity(cfg.sensors.len()); | ||
for sensor in &cfg.sensors { | ||
let sensor_id = sensors.len() as u32; | ||
sensors.push(Sensor { | ||
def: sensor.def.clone(), | ||
state: sensor.state.clone(), | ||
}); | ||
ids.push(sensor_id) | ||
} | ||
|
||
let component = SpComponent::try_from(cfg.id.as_str()).unwrap(); | ||
let prev = by_component.insert(component, ids); | ||
assert!(prev.is_none(), "component ID {component} already exists!"); | ||
} | ||
Self { sensors, by_component } | ||
} | ||
|
||
fn sensor_for_component<'sensors>( | ||
&'sensors self, | ||
component: &SpComponent, | ||
index: BoundsChecked, | ||
) -> Option<&'sensors Sensor> { | ||
let &id = self.by_component.get(component)?.get(index.0 as usize)?; | ||
self.sensors.get(id as usize) | ||
} | ||
|
||
pub(crate) fn num_component_details( | ||
&self, | ||
component: &SpComponent, | ||
) -> Option<u32> { | ||
let len = self | ||
.by_component | ||
.get(component)? | ||
.len() | ||
.try_into() | ||
.expect("why would you have more than `u32::MAX` sensors?"); | ||
Some(len) | ||
} | ||
|
||
/// This method returns an `Option` because the component's details might | ||
/// be a port status rather than a measurement, if we eventually decide to | ||
/// implement port statuses in the simulated sidecar... | ||
pub(crate) fn component_details( | ||
&self, | ||
component: &SpComponent, | ||
index: BoundsChecked, | ||
) -> Option<ComponentDetails> { | ||
let sensor = self.sensor_for_component(component, index)?; | ||
let value = | ||
sensor.state.last_reading().value.map_err(|err| match err { | ||
SensorDataMissing::DeviceError => MeasurementError::DeviceError, | ||
SensorDataMissing::DeviceNotPresent => { | ||
MeasurementError::NotPresent | ||
} | ||
SensorDataMissing::DeviceOff => MeasurementError::DeviceOff, | ||
SensorDataMissing::DeviceTimeout => { | ||
MeasurementError::DeviceTimeout | ||
} | ||
SensorDataMissing::DeviceUnavailable => { | ||
MeasurementError::DeviceUnavailable | ||
} | ||
}); | ||
Some(ComponentDetails::Measurement(Measurement { | ||
name: sensor.def.name.clone(), | ||
kind: sensor.def.kind, | ||
value, | ||
})) | ||
} | ||
|
||
pub(crate) fn read_sensor( | ||
&self, | ||
SensorRequest { id, kind }: SensorRequest, | ||
) -> Result<SensorResponse, SensorError> { | ||
let sensor = | ||
self.sensors.get(id as usize).ok_or(SensorError::InvalidSensor)?; | ||
match kind { | ||
SensorRequestKind::LastReading => { | ||
Ok(SensorResponse::LastReading(sensor.state.last_reading())) | ||
} | ||
SensorRequestKind::ErrorCount => { | ||
let count = | ||
// TODO(eliza): simulate more than one error... | ||
if sensor.state.last_error.is_some() { 1 } else { 0 }; | ||
Ok(SensorResponse::ErrorCount(count)) | ||
} | ||
SensorRequestKind::LastData => { | ||
let LastData { timestamp, value } = | ||
sensor.state.last_data.ok_or(SensorError::NoReading)?; | ||
Ok(SensorResponse::LastData { value, timestamp }) | ||
} | ||
SensorRequestKind::LastError => { | ||
let LastError { timestamp, value } = | ||
sensor.state.last_error.ok_or(SensorError::NoReading)?; | ||
Ok(SensorResponse::LastError { value, timestamp }) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.