diff --git a/faux-mgs/src/main.rs b/faux-mgs/src/main.rs index 48950ec..85bc803 100644 --- a/faux-mgs/src/main.rs +++ b/faux-mgs/src/main.rs @@ -2231,6 +2231,7 @@ enum Output { fn component_details_to_json(details: SpComponentDetails) -> serde_json::Value { use gateway_messages::measurement::{MeasurementError, MeasurementKind}; use gateway_messages::monorail_port_status::{PortStatus, PortStatusError}; + use gateway_messages::vpd::{self, MfgVpd, OxideVpd, Vpd, VpdReadError}; // SpComponentDetails and Measurement from gateway_messages intentionally do // not derive `Serialize` to avoid accidental misuse in MGS / the SP, so we @@ -2240,6 +2241,22 @@ fn component_details_to_json(details: SpComponentDetails) -> serde_json::Value { enum ComponentDetails { PortStatus(Result), Measurement(Measurement), + Vpd(Result), + } + + #[derive(serde::Serialize)] + #[serde(tag = "kind")] + enum VpdDetails { + Oxide { serial: String, part_number: String, rev: u32 }, + Mfg { mfg: String, part_number: String, rev: String, serial: String }, + Tmp117(vpd::Tmp117Vpd), + } + + #[derive(serde::Serialize)] + #[serde(tag = "kind", rename = "snake_case")] + enum VpdError { + Read(VpdReadError), + InvalidString(String), } #[derive(serde::Serialize)] @@ -2249,22 +2266,62 @@ fn component_details_to_json(details: SpComponentDetails) -> serde_json::Value { pub value: Result, } - let entries = details - .entries - .into_iter() - .map(|d| match d { - gateway_messages::ComponentDetails::PortStatus(r) => { - ComponentDetails::PortStatus(r) - } - gateway_messages::ComponentDetails::Measurement(m) => { - ComponentDetails::Measurement(Measurement { - name: m.name, - kind: m.kind, - value: m.value, - }) - } - }) - .collect::>(); + let entries = + details + .entries + .into_iter() + .map(|d| match d { + gateway_messages::ComponentDetails::PortStatus(r) => { + ComponentDetails::PortStatus(r) + } + gateway_messages::ComponentDetails::Measurement(m) => { + ComponentDetails::Measurement(Measurement { + name: m.name, + kind: m.kind, + value: m.value, + }) + } + gateway_messages::ComponentDetails::Vpd(Vpd::Oxide( + OxideVpd { serial, part_number, rev }, + )) => { + + let res = str::from_utf8(&serial) + .map(str::to_owned).map_err(|e| { + VpdError::InvalidString( + format!("serial number {serial:?} not UTF-8: {e}") + ) + }).and_then(|serial| { + let part_number = str::from_utf8(&part_number) + .map(str::to_owned) + .map_err(|e| { + VpdError::InvalidString( + format!("part number {part_number:?} not UTF-8: {e}") + ) + })?; + Ok(VpdDetails::Oxide { serial, part_number, rev }) + + }); + ComponentDetails::Vpd(res) + } + gateway_messages::ComponentDetails::Vpd(Vpd::Mfg(MfgVpd { + mfg, + mpn, + mfg_rev, + serial, + })) => ComponentDetails::Vpd(Ok(VpdDetails::Mfg { + mfg, + part_number: mpn, + rev: mfg_rev, + serial, + })), + gateway_messages::ComponentDetails::Vpd(Vpd::Tmp117(vpd)) => { + ComponentDetails::Vpd(Ok(VpdDetails::Tmp117(vpd))) + }, + gateway_messages::ComponentDetails::Vpd(Vpd::Err(err)) => { + ComponentDetails::Vpd(Err(VpdError::Read(err))) + }, + }) + .collect::>(); json!({ "entries": entries }) } diff --git a/gateway-messages/src/sp_impl.rs b/gateway-messages/src/sp_impl.rs index 9bd5e76..78ee62c 100644 --- a/gateway-messages/src/sp_impl.rs +++ b/gateway-messages/src/sp_impl.rs @@ -51,6 +51,7 @@ use crate::UpdateId; use crate::UpdateStatus; use crate::HF_PAGE_SIZE; use crate::ROT_PAGE_SIZE; +use core::ops::ControlFlow; use hubpack::error::Error as HubpackError; use hubpack::error::Result as HubpackResult; @@ -282,7 +283,7 @@ pub trait SpHandler { &mut self, component: SpComponent, index: BoundsChecked, - ) -> ComponentDetails; + ) -> ComponentDetails<&'_ str>; fn component_clear_status( &mut self, @@ -510,14 +511,20 @@ pub fn handle_message( component, offset, total, - }) => encode_tlv_structs( - &mut out[n..], - (offset..total).map(|i| { + }) => { + let mut encoder = TlvEncoder::new(&mut out[n..]); + for i in offset..total { let details = handler.component_details(component, BoundsChecked(i)); - (details.tag(), move |buf: &mut [u8]| details.serialize(buf)) - }), - ), + if encoder + .encode(details.tag(), move |buf| details.serialize(buf)) + .is_break() + { + break; + } + } + encoder.total_tlv_len() + } Some(OutgoingTrailingData::BulkIgnitionState(iter)) => { encode_tlv_structs( &mut out[n..], @@ -554,32 +561,56 @@ pub fn handle_message( /// many TLV triples from `iter` as we can into `out`. /// /// Returns the total number of bytes written into `out`. -fn encode_tlv_structs(mut out: &mut [u8], iter: I) -> usize +fn encode_tlv_structs(out: &mut [u8], iter: I) -> usize where I: Iterator, F: FnOnce(&mut [u8]) -> HubpackResult, { - let mut total_tlv_len = 0; - + let mut encoder = TlvEncoder::new(out); for (tag, encode) in iter { - match tlv::encode(out, tag, encode) { + if encoder.encode(tag, encode).is_break() { + break; + } + } + + encoder.total_tlv_len() +} + +struct TlvEncoder<'out> { + out: &'out mut [u8], + total_tlv_len: usize, +} + +impl<'out> TlvEncoder<'out> { + fn new(out: &'out mut [u8]) -> Self { + Self { out, total_tlv_len: 0 } + } + + fn total_tlv_len(&self) -> usize { + self.total_tlv_len + } + + fn encode( + &mut self, + tag: tlv::Tag, + encode: impl FnOnce(&mut [u8]) -> Result, + ) -> core::ops::ControlFlow<()> { + match tlv::encode(&mut self.out[self.total_tlv_len..], tag, encode) { Ok(n) => { - total_tlv_len += n; - out = &mut out[n..]; + self.total_tlv_len += n; + ControlFlow::Continue(()) } // If either the `encode` closure or the TLV header doesn't fit, // we've packed as much as we can into `out` and we're done. Err(tlv::EncodeError::Custom(HubpackError::Overrun)) - | Err(tlv::EncodeError::BufferTooSmall) => break, + | Err(tlv::EncodeError::BufferTooSmall) => ControlFlow::Break(()), // Other hubpack errors are impossible with all serialization types // we use. Err(tlv::EncodeError::Custom(_)) => panic!(), } } - - total_tlv_len } // We could use a combination of Option/Result/tuples to represent the results @@ -1261,11 +1292,11 @@ mod tests { unimplemented!() } - fn component_details( - &mut self, + fn component_details<'a>( + &'a mut self, _component: SpComponent, _index: BoundsChecked, - ) -> ComponentDetails { + ) -> ComponentDetails<&'a str> { unimplemented!() } diff --git a/gateway-messages/src/sp_to_mgs.rs b/gateway-messages/src/sp_to_mgs.rs index 17f007f..b73abf9 100644 --- a/gateway-messages/src/sp_to_mgs.rs +++ b/gateway-messages/src/sp_to_mgs.rs @@ -25,9 +25,11 @@ use serde_repr::Serialize_repr; pub mod ignition; pub mod measurement; pub mod monorail_port_status; +pub mod vpd; pub use ignition::IgnitionState; pub use measurement::Measurement; +pub use vpd::Vpd; use ignition::IgnitionError; use measurement::MeasurementHeader; @@ -711,20 +713,25 @@ pub struct TlvPage { /// possible types contained in a component details message. Each TLV-encoded /// struct corresponds to one of these cases. #[derive(Debug, Clone)] -pub enum ComponentDetails { +pub enum ComponentDetails { PortStatus(Result), Measurement(Measurement), + Vpd(vpd::Vpd), } -impl ComponentDetails { +impl ComponentDetails +where + S: AsRef, +{ pub fn tag(&self) -> tlv::Tag { match self { ComponentDetails::PortStatus(_) => PortStatus::TAG, ComponentDetails::Measurement(_) => MeasurementHeader::TAG, + ComponentDetails::Vpd(_) => Vpd::::TAG, } } - pub fn serialize(&self, buf: &mut [u8]) -> hubpack::error::Result { + pub fn serialize(&self, buf: &mut [u8]) -> hubpack::Result { match self { ComponentDetails::PortStatus(p) => hubpack::serialize(buf, p), ComponentDetails::Measurement(m) => { @@ -742,6 +749,7 @@ impl ComponentDetails { Ok(n + m.name.len()) } } + ComponentDetails::Vpd(vpd) => vpd.encode(buf), } } } @@ -868,6 +876,10 @@ bitflags! { const HAS_MEASUREMENT_CHANNELS = 1 << 1; const HAS_SERIAL_CONSOLE = 1 << 2; const IS_LED = 1 << 3; + /// Indicates that this device has its own vital product data (e.g. part + /// number/serial number) which can be read by requesting the device's + /// details. + const HAS_VPD = 1 << 4; // MGS has a placeholder API for powering off an individual component; // do we want to keep that? If so, add a bit for "can be powered on and // off". diff --git a/gateway-messages/src/sp_to_mgs/vpd.rs b/gateway-messages/src/sp_to_mgs/vpd.rs new file mode 100644 index 0000000..016664e --- /dev/null +++ b/gateway-messages/src/sp_to_mgs/vpd.rs @@ -0,0 +1,393 @@ +// 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::tlv; +use core::fmt; +use core::str; +use hubpack::SerializedSize; +use serde::Deserialize; +use serde::Serialize; + +pub const MAX_STR_LEN: usize = 32; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Vpd { + Oxide(OxideVpd), + Mfg(MfgVpd), + Tmp117(Tmp117Vpd), + Err(VpdReadError), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct OxideVpd { + pub serial: [u8; 11], + pub rev: u32, + pub part_number: [u8; 11], +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct MfgVpd { + pub mfg: S, + pub serial: S, + pub mfg_rev: S, + pub mpn: S, +} + +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, SerializedSize, +)] +pub struct Tmp117Vpd { + /// Device ID (register 0x0F) + pub id: u16, + /// 48-bit NIST traceability data + pub eeprom1: u16, + pub eeprom2: u16, + pub eeprom3: u16, +} + +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, SerializedSize, +)] +pub enum VpdReadError { + DeviceNotPresent, + I2cError, + InvalidContents, + BadRead, +} + +#[cfg(feature = "std")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OwnedOxideVpd { + pub serial: String, + pub rev: u32, + pub part_number: String, +} + +#[cfg(feature = "std")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OwnedMfgVpd { + pub mfg: String, + pub serial: String, + pub mfg_rev: String, + pub mpn: String, +} + +#[cfg(feature = "std")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OwnedVpd { + Oxide(OwnedOxideVpd), + Mfg(OwnedMfgVpd), +} + +// TLV tags for FRUID VPD. +// See: https://rfd.shared.oxide.computer/rfd/308#_fruid_data + +const SERIAL_TAG: tlv::Tag = tlv::Tag(*b"SER0"); +/// Tag for Oxide part numbers. The value of this tag is a UTF-8-encoded string. +const CPN_TAG: tlv::Tag = tlv::Tag(*b"CPN0"); +/// Tag for Oxide revisions. The value of this tag is always a little-endian `u32`. +const OXIDE_REV_TAG: tlv::Tag = tlv::Tag(*b"REV0"); + +/// Tag for manufacturer names. The value of this tag is a UTF-8-encoded string. +const MFG_TAG: tlv::Tag = tlv::Tag(*b"MFG0"); +/// Tag for manufacturer part numbers. The value of this tag is a UTF-8-encoded string. +const MPN_TAG: tlv::Tag = tlv::Tag(*b"MPN0"); +/// Manufacturer revision tag. Unlike `OXIDE_REV_TAG`, this is a byte array rather than a `u32`. +const MFG_REV_TAG: tlv::Tag = tlv::Tag(*b"MRV0"); + +impl Vpd +where + S: AsRef, +{ + pub const TAG: tlv::Tag = tlv::Tag(*b"FRU0"); + + pub fn tlv_len(&self) -> usize { + match self { + Vpd::Oxide(vpd) => tlv::tlv_len(vpd.tlv_len()), + Vpd::Mfg(vpd) => tlv::tlv_len(vpd.tlv_len()), + Vpd::Tmp117(_) => tlv::tlv_len(Tmp117Vpd::MAX_SIZE), + Vpd::Err(_) => tlv::tlv_len(VpdReadError::MAX_SIZE), + } + } + + pub fn encode(&self, out: &mut [u8]) -> Result { + if out.len() < self.tlv_len() { + return Err(hubpack::Error::Overrun); + } + match self { + Vpd::Oxide(vpd) => vpd.encode(out), + Vpd::Mfg(vpd) => vpd.encode(out), + Vpd::Tmp117(vpd) => vpd.encode(out), + Vpd::Err(err) => err.encode(out), + } + .map_err(|e| match e { + tlv::EncodeError::BufferTooSmall => hubpack::Error::Overrun, + tlv::EncodeError::Custom(e) => e, + }) + } +} + +impl<'buf> Vpd<&'buf str> { + pub fn decode_body(buf: &'buf [u8]) -> Result { + let mut tags = tlv::decode_iter(buf); + + fn expect_tag<'a, T>( + tags: &mut impl Iterator< + Item = Result<(tlv::Tag, &'a [u8]), tlv::DecodeError>, + >, + expected_tag: tlv::Tag, + decode: impl Fn(&'a [u8]) -> Result, + ) -> Result { + match tags.next() { + Some(Ok((tag, value))) if tag == expected_tag => decode(value), + Some(Ok((tag, _))) => Err(DecodeError::UnexpectedTag(tag)), + Some(Err(err)) => Err(DecodeError::Tlv(expected_tag, err)), + None => Err(DecodeError::MissingTag(expected_tag)), + } + } + + fn expect_str_tag<'a>( + tags: &mut impl Iterator< + Item = Result<(tlv::Tag, &'a [u8]), tlv::DecodeError>, + >, + expected_tag: tlv::Tag, + ) -> Result<&'a str, DecodeError> { + expect_tag(tags, expected_tag, |value| { + core::str::from_utf8(value) + .map_err(DecodeError::invalid_str(expected_tag)) + }) + } + + match tags.next() { + Some(Ok((VpdReadError::TAG, value))) => { + let (vpd, _rest) = + hubpack::deserialize(value).map_err(|error| { + DecodeError::Hubpack(VpdReadError::TAG, error) + })?; + Ok(Self::Err(vpd)) + } + Some(Ok((Tmp117Vpd::TAG, value))) => { + let (vpd, _rest) = + hubpack::deserialize(value).map_err(|error| { + DecodeError::Hubpack(Tmp117Vpd::TAG, error) + })?; + Ok(Self::Tmp117(vpd)) + } + Some(Ok((MFG_TAG, mfg))) => { + let mfg = core::str::from_utf8(mfg) + .map_err(DecodeError::invalid_str(MFG_TAG))?; + let mpn = expect_str_tag(&mut tags, MPN_TAG)?; + let serial = expect_str_tag(&mut tags, SERIAL_TAG)?; + let mfg_rev = expect_str_tag(&mut tags, MFG_REV_TAG)?; + Ok(Self::Mfg(MfgVpd { mfg, mpn, mfg_rev, serial })) + } + Some(Ok((CPN_TAG, cpn))) => { + let part_number: [u8; 11] = cpn + .try_into() + .map_err(|_| DecodeError::BadLength(CPN_TAG, cpn.len()))?; + let serial: [u8; 11] = + expect_tag(&mut tags, SERIAL_TAG, |val| { + val.try_into().map_err(|_| { + DecodeError::BadLength(CPN_TAG, cpn.len()) + }) + })?; + let rev = expect_tag(&mut tags, OXIDE_REV_TAG, |value| { + let rev_bytes: [u8; 4] = value + .try_into() + .map_err(|_| DecodeError::InvalidU32(OXIDE_REV_TAG))?; + Ok(u32::from_le_bytes(rev_bytes)) + })?; + Ok(Self::Oxide(OxideVpd { part_number, rev, serial })) + } + Some(Ok((tag, _))) => Err(DecodeError::UnexpectedTag(tag)), + Some(Err(e)) => Err(DecodeError::TlvUntyped(e)), + None => Err(DecodeError::UnexpectedEnd), + } + } + + #[cfg(feature = "std")] + pub fn into_owned(self) -> Vpd { + match self { + Self::Oxide(vpd) => Vpd::Oxide(vpd), + Self::Tmp117(vpd) => Vpd::Tmp117(vpd), + Self::Mfg(vpd) => Vpd::Mfg(vpd.into_owned()), + Self::Err(err) => Vpd::Err(err), + } + } +} + +impl OxideVpd { + pub fn tlv_len(&self) -> usize { + tlv::tlv_len(self.part_number.len()) + + tlv::tlv_len(self.serial.len()) + + tlv::tlv_len(4) // revision number (u32) + } + + pub fn encode( + &self, + out: &mut [u8], + ) -> Result> { + if out.len() < self.tlv_len() { + return Err(tlv::EncodeError::Custom(hubpack::Error::Overrun)); + } + let mut total = 0; + total += encode_bytes(&mut out[total..], CPN_TAG, &self.part_number)?; + total += encode_bytes(&mut out[total..], SERIAL_TAG, &self.serial)?; + total += encode_bytes( + &mut out[total..], + OXIDE_REV_TAG, + &self.rev.to_le_bytes()[..], + )?; + Ok(total) + } +} + +impl MfgVpd +where + S: AsRef, +{ + pub fn tlv_len(&self) -> usize { + tlv::tlv_len(self.mfg.as_ref().len()) + + tlv::tlv_len(self.mpn.as_ref().len()) + + tlv::tlv_len(self.mfg_rev.as_ref().len()) + + tlv::tlv_len(self.serial.as_ref().len()) + } + + pub fn encode( + &self, + out: &mut [u8], + ) -> Result> { + if out.len() < self.tlv_len() { + return Err(tlv::EncodeError::Custom(hubpack::Error::Overrun)); + } + let mut total = 0; + total += encode_str(&mut out[total..], MFG_TAG, &self.mfg)?; + total += encode_str(&mut out[total..], MPN_TAG, &self.mpn)?; + total += encode_str(&mut out[total..], SERIAL_TAG, &self.serial)?; + total += encode_str(&mut out[total..], MFG_REV_TAG, &self.mfg_rev)?; + Ok(total) + } + + #[cfg(feature = "std")] + pub fn into_owned(self) -> MfgVpd { + let Self { serial, mfg_rev, mpn, mfg } = self; + MfgVpd { + serial: serial.as_ref().to_owned(), + mfg_rev: mfg_rev.as_ref().to_owned(), + mpn: mpn.as_ref().to_owned(), + mfg: mfg.as_ref().to_owned(), + } + } +} + +impl Tmp117Vpd { + pub const TAG: tlv::Tag = tlv::Tag(*b"TMP1"); + pub const TLV_LEN: usize = tlv::tlv_len(Self::MAX_SIZE); + + pub fn encode( + &self, + out: &mut [u8], + ) -> Result> { + tlv::encode(out, Self::TAG, |out| hubpack::serialize(out, self)) + } +} + +impl VpdReadError { + pub const TAG: tlv::Tag = tlv::Tag(*b"ERR0"); + pub const TLV_LEN: usize = tlv::tlv_len(Self::MAX_SIZE); + + pub fn encode( + &self, + out: &mut [u8], + ) -> Result> { + tlv::encode(out, Self::TAG, |out| hubpack::serialize(out, self)) + } +} + +fn encode_str( + out: &mut [u8], + tag: tlv::Tag, + value: &impl AsRef, +) -> Result> { + encode_bytes(out, tag, value.as_ref().as_bytes()) +} + +fn encode_bytes( + out: &mut [u8], + tag: tlv::Tag, + value: &[u8], +) -> Result> { + tlv::encode(out, tag, |out| { + if out.len() < value.len() { + return Err(hubpack::Error::Overrun); + } + out[..value.len()].copy_from_slice(value); + Ok(value.len()) + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + UnexpectedEnd, + MissingTag(tlv::Tag), + UnexpectedTag(tlv::Tag), + InvalidUtf8(tlv::Tag, str::Utf8Error), + InvalidU32(tlv::Tag), + Tlv(tlv::Tag, tlv::DecodeError), + TlvUntyped(tlv::DecodeError), + BadLength(tlv::Tag, usize), + Hubpack(tlv::Tag, hubpack::Error), +} + +impl DecodeError { + fn invalid_str(tag: tlv::Tag) -> impl Fn(str::Utf8Error) -> Self { + move |err| DecodeError::InvalidUtf8(tag, err) + } +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingTag(tag) => write!( + f, + "unexpected end of input while expecting TLV tag {tag:?}" + ), + Self::UnexpectedTag(tag) => { + write!(f, "unexpected TLV tag {tag:?}") + } + Self::InvalidUtf8(tag, err) => { + write!(f, "value for tag {tag:?} was not UTF-8: {err}") + } + Self::InvalidU32(tag) => { + write!(f, "value for tag {tag:?} was not a u32") + } + Self::Tlv(tag, error) => { + write!(f, "TLV decode error while decoding {tag:?}: {error}") + } + Self::TlvUntyped(error) => { + write!( + f, + "TLV decode error while expecting {MFG_TAG:?} or \ + {CPN_TAG:?}: {error}" + ) + } + Self::UnexpectedEnd => { + write!(f, "unexpected end of input") + } + Self::BadLength(tag, size) => { + write!( + f, + "expected value for tag {tag:?} to be 11 bytes, but got \ + {size} bytes" + ) + } + Self::Hubpack(tag, error) => { + write!( + f, + "failed to decode hubpack-encoded VPD tag {tag:?}: {error}" + ) + } + } + } +} diff --git a/gateway-sp-comms/src/error.rs b/gateway-sp-comms/src/error.rs index 8c45978..bdae993 100644 --- a/gateway-sp-comms/src/error.rs +++ b/gateway-sp-comms/src/error.rs @@ -55,6 +55,8 @@ pub enum CommunicationError { VersionMismatch { sp: u32, mgs: u32 }, #[error("failed to deserialize TLV value for tag {tag:?}: {err}")] TlvDeserialize { tag: tlv::Tag, err: gateway_messages::HubpackError }, + #[error("failed to deserialize TLV-encoded VPD: {err}")] + VpdDeserialize { err: gateway_messages::vpd::DecodeError }, #[error("failed to decode TLV triple")] TlvDecode(#[from] tlv::DecodeError), #[error("invalid pagination: {reason}")] diff --git a/gateway-sp-comms/src/single_sp.rs b/gateway-sp-comms/src/single_sp.rs index 8ca5573..d233f0a 100644 --- a/gateway-sp-comms/src/single_sp.rs +++ b/gateway-sp-comms/src/single_sp.rs @@ -142,7 +142,7 @@ pub struct SpDevice { #[derive(Debug, Clone)] pub struct SpComponentDetails { - pub entries: Vec, + pub entries: Vec>, } #[derive(Debug, Clone, Copy)] @@ -1550,7 +1550,7 @@ struct ComponentDetailsTlvRpc<'a> { } impl TlvRpc for ComponentDetailsTlvRpc<'_> { - type Item = ComponentDetails; + type Item = ComponentDetails; const LOG_NAME: &'static str = "component details"; @@ -1576,6 +1576,7 @@ impl TlvRpc for ComponentDetailsTlvRpc<'_> { use gateway_messages::measurement::MeasurementHeader; use gateway_messages::monorail_port_status::PortStatus; use gateway_messages::monorail_port_status::PortStatusError; + use gateway_messages::vpd::Vpd; match tag { PortStatus::TAG => { @@ -1622,6 +1623,13 @@ impl TlvRpc for ComponentDetailsTlvRpc<'_> { value: header.value, }))) } + Vpd::::TAG => { + let vpd = gateway_messages::Vpd::decode_body(value).map_err( + |err| CommunicationError::VpdDeserialize { err }, + )?; + + Ok(Some(ComponentDetails::Vpd(vpd.into_owned()))) + } _ => { info!( self.log,