sinusoidal-core 0.1.1

Core definitions used by Sinusoidal Systems
Documentation
use std::fmt;
use std::num::{NonZeroU16, NonZeroU32};
use std::str::FromStr;

use anyhow::{Result, anyhow};
use serde::{Deserialize, Deserializer};

use crate::consts::NOMINAL_FREQUENCY_HZ;

/// A stream provided by the framework.
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub enum SysStream {
  Time,
}

/// An SV stream identifier qualified by application id.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SvStreamId {
  pub appid: u16,
  pub svid: String,
  pub simulated: bool,
}

/// Uniquely identifies a data stream within the simulator.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StreamId {
  Sv {
    id: SvStreamId,
  },
  App {
    app_name: String,
    stream_name: String,
  },
  Sys {
    stream: SysStream,
  },
  Goose {
    go_id: String,
  },
}

impl StreamId {
  pub const SYS_TIME: Self = Self::Sys {
    stream: SysStream::Time,
  };
}

impl FromStr for StreamId {
  type Err = String;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    let parts: Vec<&str> = s.split(':').collect();
    match parts.as_slice() {
      ["App", app_name, stream_name] => {
        let app_name = app_name.to_string();
        let stream_name = stream_name.to_string();
        Ok(StreamId::App {
          app_name,
          stream_name,
        })
      }
      ["SV", appid, svid] | ["SV", appid, svid, "test"] => {
        let appid = u16::from_str_radix(appid, 16)
          .map_err(|_| "Invalid hex integer for appid".to_string())?;
        let simulated = parts.len() > 3;
        Ok(StreamId::Sv {
          id: SvStreamId {
            appid,
            svid: svid.to_string(),
            simulated,
          },
        })
      }
      ["Sys", "Time"] => Ok(StreamId::Sys {
        stream: SysStream::Time,
      }),
      ["GOOSE", go_id @ ..] => Ok(StreamId::Goose {
        go_id: go_id.join(":"),
      }),
      _ => Err(format!("Invalid format StreamId: {s}").to_string()),
    }
  }
}

impl fmt::Display for SysStream {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      SysStream::Time => write!(f, "Time"),
    }
  }
}

impl fmt::Display for SvStreamId {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(
      f,
      "SV:{:x}:{}{}",
      self.appid,
      self.svid,
      if self.simulated { ":test" } else { "" }
    )
  }
}

impl fmt::Display for StreamId {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      StreamId::App {
        app_name,
        stream_name,
      } => {
        write!(f, "App:{app_name}:{stream_name}")
      }
      StreamId::Sv { id } => fmt::Display::fmt(id, f),
      StreamId::Sys { stream } => {
        write!(f, "Sys:{stream}")
      }
      StreamId::Goose { go_id } => {
        write!(f, "GOOSE:{go_id}")
      }
    }
  }
}

impl<'de> Deserialize<'de> for StreamId {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let s: String = Deserialize::deserialize(deserializer)?;
    StreamId::from_str(&s).map_err(serde::de::Error::custom)
  }
}

/// Sample rate of a data stream, expressed in one of several supported units.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SampleRate {
  SamplesPerSecond(NonZeroU32),
  SecondsPerSample(NonZeroU32),
  SamplesPerPeriod(NonZeroU32),

  // TODO Remove
  SamplesPerCycle(NonZeroU32),
  Variable(String),
}

impl SampleRate {
  /// Convert this sample rate to an integer number of samples per second.
  ///
  /// Returns an error if the rate cannot be expressed as a non-zero `u16`, or
  /// if the variant has no fixed rate (e.g. `Variable` or `SecondsPerSample`).
  pub fn to_samples_per_second(&self) -> Result<NonZeroU16> {
    fn to_nonzero_u16(val: u32, field: &'static str) -> Result<NonZeroU16> {
      let rate = u16::try_from(val).map_err(|_| anyhow!("{field} must fit in non-zero u16"))?;
      Ok(NonZeroU16::new(rate).expect("NonZeroU32 converted to u16 cannot become zero"))
    }

    match self {
      SampleRate::SamplesPerSecond(val) => {
        to_nonzero_u16(val.get(), "sample_rate.samples_per_second")
      }
      // This isn't entirely correct for `SamplesPerCycle` since the actual rate depends on
      // the actual frequency
      SampleRate::SamplesPerPeriod(val) | SampleRate::SamplesPerCycle(val) => {
        let rate = val
          .get()
          .checked_mul(u32::from(NOMINAL_FREQUENCY_HZ.get()))
          .ok_or_else(|| {
            anyhow!("overflow in sample_rate.samples_per_period * nominal_frequency")
          })?;
        to_nonzero_u16(rate, "sample_rate.samples_per_period * nominal_frequency")
      }
      SampleRate::SecondsPerSample(_) => Err(anyhow!(
        "sample_rate.seconds_per_sample cannot be converted to samples per second"
      )),
      SampleRate::Variable(_) => Err(anyhow!("sample_rate.variable has no fixed rate")),
    }
  }
}

/// Physical quantity type carried by a stream field.
#[derive(Debug, Clone, Deserialize)]
pub enum StreamValueType {
  Voltage,
  Current,
  Energy,
  Power,
  Frequency,
  Other,
}

/// SI unit used by a stream field.
#[derive(Debug, Clone, Deserialize)]
pub enum SIUnit {
  Ampere,
  Volt,
  Radian,
  Watt,
  Joule,
  Hz,
  VArs,
  SIOther,
}

/// Describes a single field within a stream's layout.
#[derive(Debug, Clone, Deserialize)]
pub struct StreamLayoutValue {
  pub name: String,
  pub r#type: StreamValueType,
  pub unit: SIUnit,
  pub mag: i64,
}

/// Metadata about a Sampled Values data stream.
#[derive(Debug, Clone, Deserialize)]
pub struct DataStreamInfo {
  pub name: String,
  pub sample_rate: SampleRate,
  pub fields: Vec<StreamLayoutValue>,
}

impl DataStreamInfo {
  /// Find a field by name, returning its index and a reference to the field.
  pub fn find_field(&self, name: &str) -> Result<(usize, &StreamLayoutValue), std::io::Error> {
    self
      .fields
      .iter()
      .enumerate()
      .find(|(_, f)| f.name == name)
      .ok_or_else(|| {
        std::io::Error::other(format!(
          "Field '{}' not found in stream '{}'",
          name, self.name
        ))
      })
  }

  /// Find a field by name, returning its index.
  pub fn find_field_idx(&self, name: &str) -> Result<usize, std::io::Error> {
    self.find_field(name).map(|(idx, _)| idx)
  }
}

/// Metadata about a GOOSE stream.
#[derive(Debug, Clone, Deserialize)]
pub struct GooseStreamInfo {
  pub name: String,
}

/// Metadata about a system (framework-provided) stream.
#[derive(Debug, Clone, Deserialize)]
pub struct SysStreamInfo {
  pub name: String,
}

// This is what you get from the framework when registering.
/// Stream metadata returned by the framework upon registration.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StreamInfo {
  DataStreamInfo(DataStreamInfo),
  GooseStreamInfo(GooseStreamInfo),
  SysStreamInfo(SysStreamInfo),
}

impl StreamInfo {
  /// Returns the human-readable name of the stream.
  pub fn name(&self) -> String {
    match self {
      Self::DataStreamInfo(info) => info.name.clone(),
      Self::GooseStreamInfo(info) => info.name.clone(),
      Self::SysStreamInfo(info) => info.name.clone(),
    }
  }
}

// This is what you put in your settings.
/// An input stream reference used in simulator settings.
#[derive(Debug, Clone, Deserialize)]
pub struct InputStream {
  #[serde(rename = "$input_stream")]
  pub name: StreamId,
}

/// An output stream reference used in simulator settings.
#[derive(Debug, Clone, Deserialize)]
pub struct OutputStream {
  #[serde(rename = "$output_stream")]
  pub name: String,
  pub sample_rate: SampleRate,
  pub fields: Vec<StreamLayoutValue>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct OwnedTrigger {
  #[serde(rename = "$trigger")]
  pub id: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SendTrigger {
  #[serde(rename = "$send_trigger")]
  pub id: String,
}