Skip to content

Commit

Permalink
[sp-sim] rudimentary simulation of sensors (#6313)
Browse files Browse the repository at this point in the history
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
hawkw authored Aug 20, 2024
1 parent 6bd999b commit 256c066
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 15 deletions.
21 changes: 21 additions & 0 deletions sp-sim/examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ capabilities = 0
presence = "Present"
serial_console = "[::1]:33312"

[[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 },
]

[[simulated_sps.gimlet]]
multicast_addr = "ff15:0:1de::2"
bind_addrs = ["[::]:33320", "[::]:33321"]
Expand All @@ -39,6 +49,17 @@ capabilities = 0
presence = "Present"
serial_console = "[::1]:33322"

[[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 },
]


[log]
# Show log messages of this level and more severe
level = "debug"
Expand Down
14 changes: 14 additions & 0 deletions sp-sim/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Interfaces for parsing configuration files and working with a simulated SP
//! configuration
use crate::sensors;
use dropshot::ConfigLogging;
use gateway_messages::DeviceCapabilities;
use gateway_messages::DevicePresence;
Expand Down Expand Up @@ -59,6 +60,9 @@ pub struct SpComponentConfig {
///
/// Only supported for components inside a [`GimletConfig`].
pub serial_console: Option<SocketAddrV6>,

#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub sensors: Vec<SensorConfig>,
}

/// Configuration of a simulated sidecar SP
Expand Down Expand Up @@ -93,6 +97,16 @@ pub struct Config {
pub log: ConfigLogging,
}

/// Configuration for a component's sensor readings.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SensorConfig {
#[serde(flatten)]
pub def: sensors::SensorDef,

#[serde(flatten)]
pub state: sensors::SensorState,
}

impl Config {
/// Load a `Config` from the given TOML file
///
Expand Down
33 changes: 26 additions & 7 deletions sp-sim/src/gimlet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::config::GimletConfig;
use crate::config::SpComponentConfig;
use crate::helpers::rot_slot_id_from_u16;
use crate::helpers::rot_slot_id_to_u16;
use crate::sensors::Sensors;
use crate::serial_number_padded;
use crate::server;
use crate::server::SimSpHandler;
Expand Down Expand Up @@ -630,6 +631,7 @@ struct Handler {
startup_options: StartupOptions,
update_state: SimSpUpdate,
reset_pending: Option<SpComponent>,
sensors: Sensors,

last_request_handled: Option<SimSpHandledRequest>,

Expand Down Expand Up @@ -665,9 +667,12 @@ impl Handler {
.push(&*Box::leak(c.description.clone().into_boxed_str()));
}

let sensors = Sensors::from_component_configs(&components);

Self {
log,
components,
sensors,
leaked_component_device_strings,
leaked_component_description_strings,
serial_number,
Expand Down Expand Up @@ -1206,23 +1211,37 @@ impl SpHandler for Handler {
port: SpPort,
component: SpComponent,
) -> Result<u32, SpError> {
let num_details =
self.sensors.num_component_details(&component).unwrap_or(0);
debug!(
&self.log, "asked for component details (returning 0 details)";
&self.log, "asked for number of component details";
"sender" => %sender,
"port" => ?port,
"component" => ?component,
"num_details" => num_details
);
Ok(0)
Ok(num_details)
}

fn component_details(
&mut self,
component: SpComponent,
index: BoundsChecked,
) -> ComponentDetails {
// We return 0 for all components, so we should never be called (`index`
// would have to have been bounds checked to live in 0..0).
unreachable!("asked for {component:?} details index {index:?}")
let Some(sensor_details) =
self.sensors.component_details(&component, index)
else {
unreachable!(
"this is a gimlet, so it should have no port status details"
);
};
debug!(
&self.log, "asked for component details for a sensor";
"component" => ?component,
"index" => index.0,
"details" => ?sensor_details
);
sensor_details
}

fn component_clear_status(
Expand Down Expand Up @@ -1445,9 +1464,9 @@ impl SpHandler for Handler {

fn read_sensor(
&mut self,
_request: gateway_messages::SensorRequest,
request: gateway_messages::SensorRequest,
) -> std::result::Result<gateway_messages::SensorResponse, SpError> {
Err(SpError::RequestUnsupportedForSp)
self.sensors.read_sensor(request).map_err(SpError::Sensor)
}

fn current_time(&mut self) -> std::result::Result<u64, SpError> {
Expand Down
1 change: 1 addition & 0 deletions sp-sim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub mod config;
mod gimlet;
mod helpers;
mod sensors;
mod server;
mod sidecar;
mod update;
Expand Down
218 changes: 218 additions & 0 deletions sp-sim/src/sensors.rs
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 })
}
}
}
}
Loading

0 comments on commit 256c066

Please sign in to comment.