From 565b65adcb9aa6111b60f92dbe4b22e8567836de Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Tue, 19 May 2026 12:57:24 -0400 Subject: [PATCH] Introduce `humility-vpd-lib` It's useful to be able to call VPD functions from Rust code --- Cargo.lock | 20 +- Cargo.toml | 2 + cmd/vpd/Cargo.toml | 3 +- cmd/vpd/src/lib.rs | 502 +++++------------------------- humility-vpd-lib/Cargo.toml | 23 ++ humility-vpd-lib/examples/list.rs | 35 +++ humility-vpd-lib/examples/read.rs | 37 +++ humility-vpd-lib/src/lib.rs | 445 ++++++++++++++++++++++++++ 8 files changed, 644 insertions(+), 423 deletions(-) create mode 100644 humility-vpd-lib/Cargo.toml create mode 100644 humility-vpd-lib/examples/list.rs create mode 100644 humility-vpd-lib/examples/read.rs create mode 100644 humility-vpd-lib/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 36db12544..824727158 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2557,8 +2557,7 @@ dependencies = [ "humility-cli", "humility-core", "humility-hexdump", - "humility-hiffy", - "humility-idol", + "humility-vpd-lib", "idol", "indexmap 2.14.0", "indicatif", @@ -2787,6 +2786,23 @@ dependencies = [ "regex", ] +[[package]] +name = "humility-vpd-lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "hif", + "humility-core", + "humility-hiffy", + "humility-idol", + "humility-probes-core", + "idol", + "indicatif", + "thiserror 2.0.18", + "tlvc", + "tlvc-text", +] + [[package]] name = "humility_load_derive" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3ea6b7608..d790821b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "humility-probes-core", "humility-spd", "humility-stack", + "humility-vpd-lib", "cmd/auxflash", "cmd/console-proxy", "cmd/counters", @@ -129,6 +130,7 @@ humility-pmbus = { path = "./humility-pmbus" } humility-probes-core = { path = "./humility-probes-core" } humility-spd = { path = "./humility-spd" } humility-stack = { path = "./humility-stack" } +humility-vpd-lib = { path = "./humility-vpd-lib" } cmd-auxflash = { path = "./cmd/auxflash", package = "humility-cmd-auxflash" } cmd-console-proxy = { path = "./cmd/console-proxy", package = "humility-cmd-console-proxy" } cmd-counters = { path = "./cmd/counters", package = "humility-cmd-counters" } diff --git a/cmd/vpd/Cargo.toml b/cmd/vpd/Cargo.toml index 0c48d723a..2c68ce17a 100644 --- a/cmd/vpd/Cargo.toml +++ b/cmd/vpd/Cargo.toml @@ -18,8 +18,7 @@ indicatif.workspace = true serde.workspace = true ron.workspace = true +humility-vpd-lib.workspace = true humility-cli.workspace = true humility-hexdump.workspace = true -humility-hiffy.workspace = true -humility-idol.workspace = true humility.workspace = true diff --git a/cmd/vpd/src/lib.rs b/cmd/vpd/src/lib.rs index 166a58b93..d790e5209 100644 --- a/cmd/vpd/src/lib.rs +++ b/cmd/vpd/src/lib.rs @@ -102,18 +102,17 @@ //! - If all devices are present but some devices fail to lock, other devices //! will be locked and the command will return with a *non-zero* exit status. -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use clap::{ArgGroup, Parser}; -use hif::*; use humility::core::Core; use humility::hubris::*; use humility_cli::{ExecutionContext, humility_cmd}; use humility_hexdump::Dumper; -use humility_hiffy::*; -use humility_idol::{self as idol, HubrisIdol}; -use indicatif::{ProgressBar, ProgressStyle}; +use humility_vpd_lib::VpdTarget; use std::fs; -use std::io::{Read, Seek, SeekFrom, Write}; +use std::io::Write; +use std::path::PathBuf; +use std::time::Duration; #[derive(Parser, Debug)] #[clap( @@ -144,7 +143,7 @@ pub struct VpdArgs { /// write the contents of the specified file into the designated VPD #[clap(long, short, value_name = "filename", group = "command")] - write: Option, + write: Option, /// read the contents of the designated VPD (or of all with --list) #[clap(long, short, group = "command")] @@ -183,11 +182,6 @@ pub struct VpdArgs { allow_missing: bool, } -enum VpdTarget { - Device(usize), - Loopback(fs::File), -} - fn vpd_devices( hubris: &HubrisArchive, ) -> impl Iterator { @@ -198,90 +192,48 @@ fn vpd_devices( .filter(|device| device.device == "at24csw080") } -fn list( +pub fn list( hubris: &HubrisArchive, core: &mut dyn Core, - subargs: &VpdArgs, + timeout: Duration, + read: bool, ) -> Result<()> { - let devices = vpd_devices(hubris).collect::>(); - let timeout = std::time::Duration::from_millis(subargs.timeout); - let mut context = HiffyContext::new(hubris, core, timeout)?; - let read_op = hubris.get_idol_command("Vpd.read")?; - - let locked_op = hubris.get_idol_command("Vpd.is_locked").ok(); - let results = if let Some(locked_op) = &locked_op { - let mut ops = vec![]; - - for ndx in 0..devices.len() { - let payload = locked_op.payload(&[( - "index", - idol::IdolArgument::Scalar(ndx as u64), - )])?; - - context.idol_call_ops(locked_op, &payload, &mut ops)?; - } - - ops.push(Op::Done); - - Some(context.run(core, ops.as_slice(), None)?) - } else { - println!( - "Note: firmware does not support the Vpd.is_locked operation." - ); - println!("Lock status will be missing from the table below."); - None - }; + let results = humility_vpd_lib::vpd_list(hubris, core, timeout, read)?; println!( "{:2} {:>2} {:2} {:3} {:4} {:13} {:25} LOCKED", "ID", "C", "P", "MUX", "ADDR", "DEVICE", "DESCRIPTION", ); - for (ndx, device) in devices.iter().enumerate() { - use humility::reflect::Base::Bool; - use humility::reflect::Value::Base; - - let mux = match (device.mux, device.segment) { + for device in results { + let mux = match (device.device.mux, device.device.segment) { (Some(m), Some(s)) => format!("{}:{}", m, s), (None, None) => "-".to_string(), (_, _) => "?:?".to_string(), }; - let result = if let (Some(lop), Some(rs)) = (&locked_op, &results) { - Some(lop.decode(&rs[ndx])) - } else { - None + let locked = match device.locked { + Ok(true) => "locked".to_string(), + Ok(false) => "unlocked".to_string(), + Err(s) => format!("<{s}>"), }; println!( "{:2} {:2} {:2} {:3} 0x{:02x} {:13} {:25} {}", - ndx, - device.controller, - device.port.name, + device.ndx, + device.device.controller, + device.device.port.name, mux, - device.address, - device.device, - device.description, - match result { - Some(Ok(Base(Bool(val)))) => match val { - false => "unlocked", - true => "locked", - } - .to_string(), - Some(Ok(r)) => format!("<{r:?}>").to_string(), - Some(Err(err)) => format!("<{}>", err).to_string(), - None => "(too old)".to_string(), - }, + device.device.address, + device.device.device, + device.device.description, + locked ); - if subargs.read { - let mut target = VpdTarget::Device(ndx); - - let rval = vpd_slurp(core, &mut context, &read_op, &mut target); - + if let humility_vpd_lib::VpdData::Data(d) = device.data { print!(" |\n +--> "); - match rval { + match d { Ok(vpd) => { match tlvc::TlvcReader::begin(&vpd[..]) { Ok(reader) => { @@ -340,196 +292,27 @@ fn target(hubris: &HubrisArchive, subargs: &VpdArgs) -> Result { } else { bail!("device index {} invalid; --list to list", id) } - } else if let Some(loopback) = &subargs.loopback { - if subargs.write.is_some() { - Ok(VpdTarget::Loopback(fs::File::create(loopback)?)) - } else { - Ok(VpdTarget::Loopback(fs::File::open(loopback)?)) - } + } else if subargs.loopback.is_some() { + bail!("loopback support is deprecated"); } else { bail!("must specify either device ID or device description"); } } -fn vpd_write( - hubris: &HubrisArchive, - core: &mut dyn Core, - subargs: &VpdArgs, -) -> Result<()> { - let timeout = std::time::Duration::from_millis(subargs.timeout); - let mut context = HiffyContext::new(hubris, core, timeout)?; - let op = hubris.get_idol_command("Vpd.write")?; - let target = target(hubris, subargs)?; - - let bytes = if let Some(ref filename) = subargs.write { - let file = fs::File::open(filename)?; - - let p = tlvc_text::load(file).with_context(|| { - format!("failed to parse {} as VPD input", filename) - })?; - - tlvc_text::pack(&p) - } else { - vec![0xffu8; 1024] - }; - - let target = match target { - VpdTarget::Device(target) => target, - VpdTarget::Loopback(mut file) => { - file.write_all(&bytes)?; - return Ok(()); - } - }; - - let mut all_ops = vec![]; - - for (offset, b) in bytes.iter().enumerate() { - let mut ops = vec![]; - let payload = op.payload(&[ - ("index", idol::IdolArgument::Scalar(target as u64)), - ("offset", idol::IdolArgument::Scalar(offset as u64)), - ("contents", idol::IdolArgument::Scalar(*b as u64)), - ])?; - - context.idol_call_ops(&op, &payload, &mut ops)?; - all_ops.push(ops); - } - - let nops = (context.text_size() / context.ops_size(&all_ops[0])?) - 1; - - if nops == 0 { - bail!("text size is too small for a single write!"); - } - - let mut offset = 0; - - let bar = ProgressBar::new(bytes.len() as u64); - - bar.set_style(ProgressStyle::default_bar().template(if subargs.erase { - "humility: erasing VPD [{bar:30}] {bytes}/{total_bytes}" - } else { - "humility: writing VPD [{bar:30}] {bytes}/{total_bytes}" - })?); - - for chunk in all_ops.chunks(nops) { - let mut ops = chunk.iter().flatten().copied().collect::>(); - ops.push(Op::Done); - - let results = context.run(core, ops.as_slice(), None)?; - - for (o, result) in results.iter().enumerate() { - op.decode::<()>(result).with_context(|| { - format!("failed to write VPD at offset {}", offset + o) - })?; - } - - offset += results.len(); - - bar.set_position(offset as u64); - } - - bar.finish_and_clear(); - - if subargs.erase { - humility::msg!("successfully erased VPD"); - } else { - humility::msg!("successfully wrote {offset} bytes of VPD"); - } - - Ok(()) -} - -fn vpd_read_at( - core: &mut dyn Core, - context: &mut HiffyContext, - op: &idol::IdolOperation, - target: &mut VpdTarget, - offset: usize, -) -> Result> { - let target = match target { - VpdTarget::Device(target) => *target, - VpdTarget::Loopback(file) => { - let mut buffer = vec![]; - file.seek(SeekFrom::Start(offset as u64))?; - file.read_to_end(&mut buffer)?; - return Ok(buffer); - } - }; - - let payload = op.payload(&[ - ("index", idol::IdolArgument::Scalar(target as u64)), - ("offset", idol::IdolArgument::Scalar(offset as u64)), - ])?; - - let mut ops = vec![]; - - context.idol_call_ops(op, &payload, &mut ops)?; - ops.push(Op::Done); - - let results = context.run(core, ops.as_slice(), None)?; - - op.decode::>(&results[0]) - .with_context(|| format!("failed to read at offset {offset}")) -} - -fn vpd_slurp( - core: &mut dyn Core, - context: &mut HiffyContext, - op: &idol::IdolOperation, - target: &mut VpdTarget, -) -> Result> { - // - // First, read in enough to read just the header. - // - let mut vpd = vpd_read_at(core, context, op, target, 0)?; - - let reader = match tlvc::TlvcReader::begin(&vpd[..]) { - Ok(reader) => reader, - Err(err) => bail!("{:?}", err), - }; - - // - // If this isn't a header, see if it's all 0xff -- in which case we - // will suggest that the part is unprogrammed. - // - let header = match reader.read_header() { - Ok(header) => header, - Err(err) => match vpd.iter().find(|&b| *b != 0xffu8) { - Some(_) => { - bail!("bad header: {:x?}", err); - } - None => { - bail!("VPD appears to be unprogrammed"); - } - }, - }; - - // - // And now go back and read everything. - // - let total = header.total_len_in_bytes(); - - while vpd.len() < total { - vpd.extend( - vpd_read_at(core, context, op, target, vpd.len()) - .with_context(|| format!("failed to read {total} bytes"))?, - ); - } - - Ok(vpd[..total].to_vec()) +enum OutputOption { + Tlvc, + Raw, + Binary(String), } fn vpd_read( hubris: &HubrisArchive, core: &mut dyn Core, - subargs: &VpdArgs, + target: VpdTarget, + timeout: Duration, + output: OutputOption, ) -> Result<()> { - let timeout = std::time::Duration::from_millis(subargs.timeout); - let mut context = HiffyContext::new(hubris, core, timeout)?; - let op = hubris.get_idol_command("Vpd.read")?; - let mut target = target(hubris, subargs)?; - - let vpd = vpd_slurp(core, &mut context, &op, &mut target)?; + let vpd = humility_vpd_lib::vpd_read(hubris, core, target, timeout)?; // // Now we should have the whole thing! @@ -541,188 +324,69 @@ fn vpd_read( } }; - if subargs.lock { - // - // We can only get here because we are doing the read as part of - // a `--lock` operation. - // - assert!(!subargs.read); - } else if subargs.raw { - let dumper = Dumper::new(); - dumper.dump(&vpd, 0); - } else if let Some(output) = &subargs.binary { - let mut file = fs::File::create(output)?; - file.write_all(&vpd)?; - } else { - let p = tlvc_text::dump(reader); - tlvc_text::save(std::io::stdout(), &p)?; - println!(); - } - - Ok(()) -} - -fn vpd_lock( - hubris: &HubrisArchive, - core: &mut dyn Core, - subargs: &VpdArgs, -) -> Result<()> { - let timeout = std::time::Duration::from_millis(subargs.timeout); - let mut context = HiffyContext::new(hubris, core, timeout)?; - - let op = hubris.get_idol_command("Vpd.permanently_lock")?; - let index = match target(hubris, subargs)? { - VpdTarget::Device(index) => index, - _ => { - bail!("can only lock a physical device"); + match output { + OutputOption::Raw => { + let dumper = Dumper::new(); + dumper.dump(&vpd, 0); } - }; - - vpd_read(hubris, core, subargs).context("can't lock VPD")?; - - let payload = - op.payload(&[("index", idol::IdolArgument::Scalar(index as u64))])?; - - let mut ops = vec![]; - - context.idol_call_ops(&op, &payload, &mut ops)?; - ops.push(Op::Done); - - let results = context.run(core, ops.as_slice(), None)?; - - op.decode::<()>(&results[0]) - .with_context(|| format!("failed to lock {index}"))?; - - humility::msg!("successfully locked VPD"); - - Ok(()) -} - -fn vpd_lock_all( - hubris: &HubrisArchive, - core: &mut dyn Core, - subargs: &VpdArgs, -) -> Result<()> { - let timeout = std::time::Duration::from_millis(subargs.timeout); - let mut context = HiffyContext::new(hubris, core, timeout)?; - let op = hubris.get_idol_command("Vpd.is_locked")?; - let read_op = hubris.get_idol_command("Vpd.read")?; - let lock_op = hubris.get_idol_command("Vpd.permanently_lock")?; - let devices = vpd_devices(hubris).collect::>(); - - let mut ops = vec![]; - let mut locking = vec![]; - - // - // First, determine those devices that are already locked... - // - for ndx in 0..devices.len() { - let payload = - op.payload(&[("index", idol::IdolArgument::Scalar(ndx as u64))])?; - - context.idol_call_ops(&op, &payload, &mut ops)?; - } - - ops.push(Op::Done); - - let results = context.run(core, ops.as_slice(), None)?; - let mut locked = 0; - let mut any_missing = false; - - for ((ndx, _), r) in devices.iter().enumerate().zip(results.iter()) { - use humility::reflect::Base::Bool; - use humility::reflect::Value::Base; - - let result = op.decode(r); - - match result { - Ok(Base(Bool(true))) => { - humility::msg!("skipping VPD {ndx}: already locked"); - locked += 1; - } - - Err(err) => { - humility::warn!("skipping VPD {ndx}: {err:?}"); - any_missing = true; - } - - Ok(Base(Bool(false))) => { - let mut target = VpdTarget::Device(ndx); - match vpd_slurp(core, &mut context, &read_op, &mut target) { - Ok(_) => { - humility::msg!("will lock VPD {ndx}"); - locking.push(ndx); - } - - Err(err) => { - humility::warn!("skipping VPD {ndx}: {err:?}"); - any_missing = true; - } - } - } - - Ok(r) => { - humility::warn!("skipping {ndx}: unknown result: {r:?}"); - any_missing = true; - } + OutputOption::Binary(output) => { + let mut file = fs::File::create(output)?; + file.write_all(&vpd)?; } - } - - let mut ops = vec![]; - - if locking.is_empty() { - if locked == devices.len() { - humility::msg!("all VPDs are already locked"); - return Ok(()); + OutputOption::Tlvc => { + let p = tlvc_text::dump(reader); + tlvc_text::save(std::io::stdout(), &p)?; + println!(); } - - bail!("no VPDs to lock"); - } else if any_missing && !subargs.allow_missing { - bail!("some VPDs are missing; use `--allow-missing` to continue") - } - - for ndx in &locking { - let payload = lock_op - .payload(&[("index", idol::IdolArgument::Scalar(*ndx as u64))])?; - - context.idol_call_ops(&lock_op, &payload, &mut ops)?; - } - - ops.push(Op::Done); - - let results = context.run(core, ops.as_slice(), None)?; - let mut success = 0; - - for (ndx, r) in locking.iter().zip(results.iter()) { - lock_op - .decode::<()>(r) - .with_context(|| format!("failed to lock VPD {ndx}"))?; - success += 1; } - if success != locking.len() { - bail!("failed to lock some VPDs"); - } - - humility::msg!("successfully locked {} VPDs", success); - Ok(()) } fn vpd(subargs: VpdArgs, context: &mut ExecutionContext) -> Result<()> { let hubris = &context.cli.archive()?; let core = &mut *context.cli.attach_live_booted(hubris)?; + let timeout = Duration::from_millis(subargs.timeout); + // These commands don't take a target so check them first if subargs.list { - list(hubris, core, &subargs)?; - } else if subargs.write.is_some() || subargs.erase { - vpd_write(hubris, core, &subargs)?; + list(hubris, core, timeout, subargs.read)?; + return Ok(()); + } else if subargs.lock_all { + if subargs.allow_missing { + bail!("--allow-missing is deprecated"); + } + match humility_vpd_lib::vpd_lock_all(hubris, core, timeout)? { + humility_vpd_lib::VpdLockStatus::AlreadyLocked => { + humility::msg!("all VPDs are already locked"); + } + humility_vpd_lib::VpdLockStatus::Count(count) => { + humility::msg!("successfully locked {count} VPDs"); + } + } + return Ok(()); + } + + let target = target(hubris, &subargs)?; + + if let Some(path) = subargs.write { + humility_vpd_lib::vpd_write(hubris, core, target, timeout, &path)?; + humility::msg!("successfully wrote VPD"); + } else if subargs.erase { + humility_vpd_lib::vpd_erase(hubris, core, target, timeout)?; + humility::msg!("successfully erased VPD"); } else if subargs.read { - vpd_read(hubris, core, &subargs)?; + let options = if subargs.raw { + OutputOption::Raw + } else if let Some(s) = subargs.binary { + OutputOption::Binary(s) + } else { + OutputOption::Tlvc + }; + vpd_read(hubris, core, target, timeout, options)?; } else if subargs.lock { - vpd_lock(hubris, core, &subargs)?; - } else if subargs.lock_all { - vpd_lock_all(hubris, core, &subargs)?; + humility_vpd_lib::vpd_lock(hubris, core, target, timeout)?; + humility::msg!("successfully locked VPD"); } else { bail!("expected a command"); } diff --git a/humility-vpd-lib/Cargo.toml b/humility-vpd-lib/Cargo.toml new file mode 100644 index 000000000..a3285b554 --- /dev/null +++ b/humility-vpd-lib/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "humility-vpd-lib" +version = "0.1.0" +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# anyhow is only for compatibiltiy +anyhow.workspace = true +hif.workspace = true +idol.workspace = true +tlvc.workspace = true +tlvc-text.workspace = true +humility-hiffy.workspace = true +humility-idol.workspace = true +humility.workspace = true +thiserror.workspace = true +indicatif.workspace = true + +[dev-dependencies] +humility.workspace = true +humility-probes-core.workspace = true diff --git a/humility-vpd-lib/examples/list.rs b/humility-vpd-lib/examples/list.rs new file mode 100644 index 000000000..dd76ce721 --- /dev/null +++ b/humility-vpd-lib/examples/list.rs @@ -0,0 +1,35 @@ +// 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 humility::hubris::HubrisArchive; +use humility_probes_core::HubrisAttach; + +fn main() { + let hubris = std::env::var("HUMILITY_ARCHIVE").unwrap(); + let probe = std::env::var("HUMILITY_PROBE").unwrap(); + + let hubris = HubrisArchive::load_from_path(&hubris).unwrap(); + + let core = &mut *hubris.attach_probe(&probe).unwrap(); + + let results = humility_vpd_lib::vpd_list( + &hubris, + core, + std::time::Duration::from_millis(10000), + false, + ) + .unwrap(); + + for r in results { + println!( + "index {} lock status {}", + r.ndx, + match r.locked { + Ok(true) => "locked", + Ok(false) => "unlocked", + Err(_) => "error", + } + ); + } +} diff --git a/humility-vpd-lib/examples/read.rs b/humility-vpd-lib/examples/read.rs new file mode 100644 index 000000000..178a3a0d3 --- /dev/null +++ b/humility-vpd-lib/examples/read.rs @@ -0,0 +1,37 @@ +// 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 humility::hubris::HubrisArchive; +use humility_probes_core::HubrisAttach; + +fn main() { + let hubris = std::env::var("HUMILITY_ARCHIVE").unwrap(); + let probe = std::env::var("HUMILITY_PROBE").unwrap(); + + let hubris = HubrisArchive::load_from_path(&hubris).unwrap(); + + let core = &mut *hubris.attach_probe(&probe).unwrap(); + + // Read the first VPD + let target = humility_vpd_lib::VpdTarget::Device(0); + + let vpd = humility_vpd_lib::vpd_read( + &hubris, + core, + target, + std::time::Duration::from_millis(10000), + ) + .unwrap(); + + let reader = match tlvc::TlvcReader::begin(&vpd[..]) { + Ok(reader) => reader, + Err(err) => { + panic!("{:?}", err); + } + }; + + let p = tlvc_text::dump(reader); + tlvc_text::save(std::io::stdout(), &p).unwrap(); + println!(); +} diff --git a/humility-vpd-lib/src/lib.rs b/humility-vpd-lib/src/lib.rs new file mode 100644 index 000000000..cb8787600 --- /dev/null +++ b/humility-vpd-lib/src/lib.rs @@ -0,0 +1,445 @@ +// 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 hif::Op; +use humility::core::Core; +use humility::hubris::{HubrisArchive, HubrisI2cDevice}; +use humility_hiffy::HiffyContext; +use humility_idol::{HubrisIdol, IdolArgument, IdolDecodeError}; +use indicatif::{ProgressBar, ProgressStyle}; +use std::fs; +use std::path::Path; +use tlvc::TlvcReadError; + +#[derive(Debug, thiserror::Error)] +pub enum VpdError { + #[error("hiffy error")] + Hiffy(#[source] anyhow::Error), + #[error("idol error")] + Idol(#[source] anyhow::Error), + #[error("I/O error")] + Io { + #[source] + err: std::io::Error, + }, + #[error("tlvc: {0:?}")] + Tlvc(TlvcReadError), + #[error("VPD region is unprogrammed")] + Unprogrammed, + #[error("reflect: {0}")] + Reflect(String), + #[error("No VPD entries found")] + NoVpd, + #[error("Errors in VPD entry prevent locking")] + VpdEntry, + #[error("indicatif template")] + TemplateError(#[source] indicatif::style::TemplateError), + #[error("lock failure at index {ndx}")] + LockFailure { + ndx: usize, + #[source] + err: IdolDecodeError, + }, + #[error("text size too small for a single write")] + TextTooSmall, + #[error("failed to write VPD at offset {offset}")] + WriteFailure { + offset: usize, + #[source] + err: IdolDecodeError, + }, + #[error("failed to read VPD at offset {offset}")] + ReadFailure { + offset: usize, + #[source] + err: IdolDecodeError, + }, +} + +pub enum VpdTarget { + Device(usize), +} + +pub enum VpdData { + /// Data was not requested to be read + NotRead, + /// Result of reading the VPD data. These are the raw bytes + /// and can be formatted with the `tlvc` crate. + Data(Result, VpdError>), +} + +/// Represents a single VPD from the hubris archive +pub struct VpdEntry<'a> { + /// Index into the vpd devices list. This is expected to be stable + /// across hubris releases + pub ndx: usize, + /// Raw i2c data about the device + pub device: &'a HubrisI2cDevice, + /// Whether the `VpdLock` command has been successfully issued to this + /// device. + pub locked: Result, + /// Reading out the VPD can be very slow and can be omitted + pub data: VpdData, +} + +/// Name we expect to see from hubris +const VPD_EEPROM_NAME: &str = "at24csw080"; + +fn vpd_devices( + hubris: &HubrisArchive, +) -> impl Iterator { + hubris + .manifest + .i2c_devices + .iter() + .filter(|device| device.device == VPD_EEPROM_NAME) +} + +/// List all available VPD devices +pub fn vpd_list<'a>( + hubris: &'a HubrisArchive, + core: &mut dyn Core, + timeout: std::time::Duration, + read_data: bool, +) -> Result>, VpdError> { + let devices = vpd_devices(hubris).collect::>(); + let mut context = + HiffyContext::new(hubris, core, timeout).map_err(VpdError::Hiffy)?; + + let locked_op = + hubris.get_idol_command("Vpd.is_locked").map_err(VpdError::Idol)?; + + let mut ops = vec![]; + + for ndx in 0..devices.len() { + let payload = locked_op + .payload(&[( + "index", + humility_idol::IdolArgument::Scalar(ndx as u64), + )]) + .map_err(VpdError::Idol)?; + + context + .idol_call_ops(&locked_op, &payload, &mut ops) + .map_err(VpdError::Idol)?; + } + + ops.push(Op::Done); + + let mut items = vec![]; + + let results = + context.run(core, ops.as_slice(), None).map_err(VpdError::Idol)?; + + for (ndx, device) in devices.into_iter().enumerate() { + let locked = locked_op + .decode::(&results[ndx]) + .map_err(|e| format!("{e:#}")); + + let mut target = VpdTarget::Device(ndx); + + let data = if read_data { + VpdData::Data(vpd_slurp(core, &mut context, hubris, &mut target)) + } else { + VpdData::NotRead + }; + + items.push(VpdEntry { ndx, locked, data, device }); + } + + if items.is_empty() { Err(VpdError::NoVpd) } else { Ok(items) } +} + +/// Writes the VPD data from the file +pub fn vpd_write( + hubris: &HubrisArchive, + core: &mut dyn Core, + target: VpdTarget, + timeout: std::time::Duration, + write: &Path, +) -> Result<(), VpdError> { + vpd_erase_write(hubris, core, target, timeout, Some(write)) +} + +/// Erase the entire VPD (write as `0xff`) +pub fn vpd_erase( + hubris: &HubrisArchive, + core: &mut dyn Core, + target: VpdTarget, + timeout: std::time::Duration, +) -> Result<(), VpdError> { + vpd_erase_write(hubris, core, target, timeout, None) +} + +pub enum VpdLockStatus { + AlreadyLocked, + Count(usize), +} + +/// Permanently lock all VPDs +pub fn vpd_lock_all( + hubris: &HubrisArchive, + core: &mut dyn Core, + timeout: std::time::Duration, +) -> Result { + let devices = vpd_list(hubris, core, timeout, true)?; + + let mut any_missing = false; + let mut locking = vec![]; + for d in devices { + match (d.locked, d.data) { + (Ok(true), _) => { + // Do nothing, this is already locked + } + (Ok(false), VpdData::Data(Ok(_))) => { + locking.push(d.ndx); + } + (Err(_), _) | (_, VpdData::Data(Err(_))) => { + any_missing = true; + } + (_, VpdData::NotRead) => unreachable!(), + } + } + + let mut context = + HiffyContext::new(hubris, core, timeout).map_err(VpdError::Idol)?; + + let mut ops = vec![]; + let lock_op = hubris + .get_idol_command("Vpd.permanently_lock") + .map_err(VpdError::Idol)?; + + if any_missing { + return Err(VpdError::VpdEntry); + } + + // We aren't missing any VPD and don't have any VPD to lock. + // vpd_list returns an error on an empty VPD list so there's + // nothing to do and we can return success. + if locking.is_empty() { + return Ok(VpdLockStatus::AlreadyLocked); + } + + for ndx in &locking { + let payload = lock_op + .payload(&[("index", IdolArgument::Scalar(*ndx as u64))]) + .map_err(VpdError::Idol)?; + + context + .idol_call_ops(&lock_op, &payload, &mut ops) + .map_err(VpdError::Idol)?; + } + + ops.push(Op::Done); + + let results = + context.run(core, ops.as_slice(), None).map_err(VpdError::Idol)?; + + for (ndx, r) in locking.iter().zip(results.iter()) { + lock_op + .decode::<()>(r) + .map_err(|err| VpdError::LockFailure { err, ndx: *ndx })?; + } + + Ok(VpdLockStatus::Count(locking.len())) +} + +/// Permanently lock a single VPD +pub fn vpd_lock( + hubris: &HubrisArchive, + core: &mut dyn Core, + mut target: VpdTarget, + timeout: std::time::Duration, +) -> Result<(), VpdError> { + let mut context = + HiffyContext::new(hubris, core, timeout).map_err(VpdError::Hiffy)?; + + let op = hubris + .get_idol_command("Vpd.permanently_lock") + .map_err(VpdError::Idol)?; + // Make sure we can read the VPD + vpd_slurp(core, &mut context, hubris, &mut target)?; + + let VpdTarget::Device(index) = target; + + let payload = op + .payload(&[("index", IdolArgument::Scalar(index as u64))]) + .map_err(VpdError::Idol)?; + + let mut ops = vec![]; + + context.idol_call_ops(&op, &payload, &mut ops).map_err(VpdError::Idol)?; + ops.push(Op::Done); + + let results = + context.run(core, ops.as_slice(), None).map_err(VpdError::Idol)?; + + op.decode::<()>(&results[0]) + .map_err(|err| VpdError::LockFailure { err, ndx: index })?; + + Ok(()) +} + +/// Read the raw bytes out of the VPD +pub fn vpd_read( + hubris: &HubrisArchive, + core: &mut dyn Core, + mut target: VpdTarget, + timeout: std::time::Duration, +) -> Result, VpdError> { + let mut context = + HiffyContext::new(hubris, core, timeout).map_err(VpdError::Hiffy)?; + + vpd_slurp(core, &mut context, hubris, &mut target) +} + +// Erasing is just writing everything to `0xff` +fn vpd_erase_write( + hubris: &HubrisArchive, + core: &mut dyn Core, + target: VpdTarget, + timeout: std::time::Duration, + write: Option<&Path>, +) -> Result<(), VpdError> { + let mut context = + HiffyContext::new(hubris, core, timeout).map_err(VpdError::Hiffy)?; + let op = hubris.get_idol_command("Vpd.write").map_err(VpdError::Idol)?; + + let (bytes, erase) = if let Some(filename) = write { + let file = + fs::File::open(filename).map_err(|err| VpdError::Io { err })?; + + let p = tlvc_text::load(file).map_err(|err| VpdError::Io { err })?; + + (tlvc_text::pack(&p), false) + } else { + // All our EEPROMs are 1KiB so this will erase the whole space + (vec![0xffu8; 1024], true) + }; + + let VpdTarget::Device(target) = target; + + let mut all_ops = vec![]; + + for (offset, b) in bytes.iter().enumerate() { + let mut ops = vec![]; + let payload = op + .payload(&[ + ("index", IdolArgument::Scalar(target as u64)), + ("offset", IdolArgument::Scalar(offset as u64)), + ("contents", IdolArgument::Scalar(*b as u64)), + ]) + .map_err(VpdError::Idol)?; + + context + .idol_call_ops(&op, &payload, &mut ops) + .map_err(VpdError::Idol)?; + all_ops.push(ops); + } + + let nops = (context.text_size() + / context.ops_size(&all_ops[0]).map_err(VpdError::Idol)?) + - 1; + + if nops == 0 { + return Err(VpdError::TextTooSmall); + } + + let mut offset = 0; + + let bar = ProgressBar::new(bytes.len() as u64); + + bar.set_style( + ProgressStyle::default_bar() + .template(if erase { + "humility: erasing VPD [{bar:30}] {bytes}/{total_bytes}" + } else { + "humility: writing VPD [{bar:30}] {bytes}/{total_bytes}" + }) + .map_err(VpdError::TemplateError)?, + ); + + for chunk in all_ops.chunks(nops) { + let mut ops = chunk.iter().flatten().copied().collect::>(); + ops.push(Op::Done); + + let results = + context.run(core, ops.as_slice(), None).map_err(VpdError::Idol)?; + + for (o, result) in results.iter().enumerate() { + op.decode::<()>(result).map_err(|err| VpdError::WriteFailure { + err, + offset: offset + o, + })?; + } + + offset += results.len(); + + bar.set_position(offset as u64); + } + + bar.finish_and_clear(); + + Ok(()) +} + +fn vpd_read_at( + core: &mut dyn Core, + context: &mut HiffyContext, + op: &humility_idol::IdolOperation, + target: &mut VpdTarget, + offset: usize, +) -> Result, VpdError> { + let VpdTarget::Device(target) = *target; + + let payload = op + .payload(&[ + ("index", humility_idol::IdolArgument::Scalar(target as u64)), + ("offset", humility_idol::IdolArgument::Scalar(offset as u64)), + ]) + .map_err(VpdError::Idol)?; + + let mut ops = vec![]; + + context.idol_call_ops(op, &payload, &mut ops).map_err(VpdError::Idol)?; + ops.push(Op::Done); + + let results = + context.run(core, ops.as_slice(), None).map_err(VpdError::Hiffy)?; + + op.decode::>(&results[0]) + .map_err(|err| VpdError::ReadFailure { err, offset }) +} + +fn vpd_slurp( + core: &mut dyn Core, + context: &mut HiffyContext, + hubris: &HubrisArchive, + target: &mut VpdTarget, +) -> Result, VpdError> { + let op = hubris.get_idol_command("Vpd.read").map_err(VpdError::Idol)?; + + // First, read in enough to read just the header. + let mut vpd = vpd_read_at(core, context, &op, target, 0)?; + + let reader = tlvc::TlvcReader::begin(&vpd[..]).map_err(VpdError::Tlvc)?; + + // If this isn't a header, see if it's all 0xff -- in which case we + // will suggest that the part is unprogrammed. + let header = reader.read_header().map_err(|e| { + match vpd.iter().find(|&b| *b != 0xffu8) { + Some(_) => VpdError::Tlvc(e), + None => VpdError::Unprogrammed, + } + })?; + + // And now go back and read everything. + let total = header.total_len_in_bytes(); + + while vpd.len() < total { + vpd.extend(vpd_read_at(core, context, &op, target, vpd.len())?); + } + + Ok(vpd[..total].to_vec()) +}