Typed Command Interfaces โ Request Determines Response ๐ก
What you'll learn: How associated types on a command trait create a compile-time binding between request and response, eliminating mismatched parsing, unit confusion, and silent type coercion across IPMI, Redfish, and NVMe protocols.
Cross-references: ch01 (philosophy), ch06 (dimensional types), ch07 (validated boundaries), ch10 (integration)
The Untyped Swamp
Most hardware management stacks โ IPMI, Redfish, NVMe Admin, PLDM โ start life as
raw bytes in โ raw bytes out. This creates a category of bugs that tests can only
partially find:
use std::io;
struct BmcRaw { /* ipmitool handle */ }
impl BmcRaw {
fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result<Vec<u8>> {
// ... shells out to ipmitool ...
Ok(vec![0x00, 0x19, 0x00]) // stub
}
}
fn diagnose_thermal(bmc: &BmcRaw) -> io::Result<()> {
let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?;
let cpu_temp = raw[0] as f64; // ๐ค is byte 0 the reading?
let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?;
let fan_rpm = raw[0] as u32; // ๐ fan speed is 2 bytes LE
let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?;
let voltage = raw[0] as f64; // ๐ need to divide by 1000
if cpu_temp > fan_rpm as f64 { // ๐ comparing ยฐC to RPM
println!("uh oh");
}
log_temp(voltage); // ๐ passing Volts as temperature
Ok(())
}
fn log_temp(t: f64) { println!("Temp: {t}ยฐC"); }
| # | Bug | Discovered |
|---|---|---|
| 1 | Fan RPM parsed as 1 byte instead of 2 | Production, 3 AM |
| 2 | Voltage not scaled | Every PSU flagged as overvoltage |
| 3 | Comparing ยฐC to RPM | Maybe never |
| 4 | Volts passed to temp logger | 6 months later, reading historical data |
Root cause: Everything is Vec<u8> โ f64 โ pray.
The Typed Command Pattern
Step 1 โ Domain newtypes
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32); // u32: raw IPMI sensor value (integer RPM)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);
Note on
Rpm(u32)vsRpm(f64): In this chapter the inner type isu32because IPMI sensor readings are integer values. In ch06 (Dimensional Analysis),Rpmusesf64to support arithmetic operations (averaging, scaling). Both are valid โ the newtype prevents cross-unit confusion regardless of inner type.
Step 2 โ The command trait (type-indexed dispatch)
The associated type Response is the key โ it binds each command struct to its
return type. Each implementing struct pins Response to a specific domain type,
so execute() always returns exactly the right type:
pub trait IpmiCmd {
/// The "type index" โ determines what execute() returns.
type Response;
fn net_fn(&self) -> u8;
fn cmd_byte(&self) -> u8;
fn payload(&self) -> Vec<u8>;
/// Parsing encapsulated here โ each command knows its own byte layout.
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
Step 3 โ One struct per command
pub struct ReadTemp { pub sensor_id: u8 }
impl IpmiCmd for ReadTemp {
type Response = Celsius;
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
if raw.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "empty response"));
}
// Note: ch01's untyped example uses `raw[0] as i8 as f64` (signed)
// because that function was demonstrating generic parsing without
// SDR metadata. Here we use unsigned (`as f64`) because the SDR
// linearization formula in IPMI spec ยง35.5 converts the unsigned
// raw reading to a calibrated value. In production, apply the
// full SDR formula: result = (M ร raw + B) ร 10^(R_exp).
Ok(Celsius(raw[0] as f64)) // unsigned raw byte, converted per SDR formula
}
}
pub struct ReadFanSpeed { pub fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
type Response = Rpm;
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
if raw.len() < 2 {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("fan speed needs 2 bytes, got {}", raw.len())));
}
Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32))
}
}
pub struct ReadVoltage { pub rail: u8 }
impl IpmiCmd for ReadVoltage {
type Response = Volts;
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.rail] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Volts> {
if raw.len() < 2 {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("voltage needs 2 bytes, got {}", raw.len())));
}
Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0))
}
}
Step 4 โ The executor (zero dyn, monomorphised)
pub struct BmcConnection { pub timeout_secs: u32 }
impl BmcConnection {
pub fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
let raw = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
cmd.parse_response(&raw)
}
fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
Ok(vec![0x19, 0x00]) // stub
}
}
Step 5 โ All four bugs become compile errors
fn diagnose_thermal_typed(bmc: &BmcConnection) -> io::Result<()> {
let cpu_temp: Celsius = bmc.execute(&ReadTemp { sensor_id: 0x20 })?;
let fan_rpm: Rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?;
let voltage: Volts = bmc.execute(&ReadVoltage { rail: 0x40 })?;
// Bug #1 โ IMPOSSIBLE: parsing lives in ReadFanSpeed::parse_response
// Bug #2 โ IMPOSSIBLE: unit scaling lives in ReadVoltage::parse_response
// Bug #3 โ COMPILE ERROR:
// if cpu_temp > fan_rpm { }
// ^^^^^^^^ ^^^^^^^ Celsius vs Rpm โ "mismatched types" โ
// Bug #4 โ COMPILE ERROR:
// log_temperature(voltage);
// ^^^^^^^ Volts, expected Celsius โ
if cpu_temp > Celsius(85.0) { println!("CPU overheating: {:?}", cpu_temp); }
if fan_rpm < Rpm(4000) { println!("Fan too slow: {:?}", fan_rpm); }
Ok(())
}
fn log_temperature(t: Celsius) { println!("Temp: {:?}", t); }
fn log_voltage(v: Volts) { println!("Voltage: {:?}", v); }
IPMI: Sensor Reads That Can't Be Confused
Adding a new sensor is one struct + one impl โ no scattered parsing:
pub struct ReadPowerDraw { pub domain: u8 }
impl IpmiCmd for ReadPowerDraw {
type Response = Watts;
fn net_fn(&self) -> u8 { 0x04 }
fn cmd_byte(&self) -> u8 { 0x2D }
fn payload(&self) -> Vec<u8> { vec![self.domain] }
fn parse_response(&self, raw: &[u8]) -> io::Result<Watts> {
if raw.len() < 2 {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("power draw needs 2 bytes, got {}", raw.len())));
}
Ok(Watts(u16::from_le_bytes([raw[0], raw[1]]) as f64))
}
}
// Every caller that uses bmc.execute(&ReadPowerDraw { domain: 0 })
// automatically gets Watts back โ no parsing code elsewhere
Testing Each Command in Isolation
#[cfg(test)]
mod tests {
use super::*;
struct StubBmc {
responses: std::collections::HashMap<u8, Vec<u8>>,
}
impl StubBmc {
fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
let key = cmd.payload()[0];
let raw = self.responses.get(&key)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no stub"))?;
cmd.parse_response(raw)
}
}
#[test]
fn read_temp_parses_raw_byte() {
let bmc = StubBmc {
responses: [(0x20, vec![0x19])].into(), // 25 decimal = 0x19
};
let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap();
assert_eq!(temp, Celsius(25.0));
}
#[test]
fn read_fan_parses_two_byte_le() {
let bmc = StubBmc {
responses: [(0x30, vec![0x00, 0x19])].into(), // 0x1900 = 6400
};
let rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 }).unwrap();
assert_eq!(rpm, Rpm(6400));
}
#[test]
fn read_voltage_scales_millivolts() {
let bmc = StubBmc {
responses: [(0x40, vec![0xE8, 0x2E])].into(), // 0x2EE8 = 12008 mV
};
let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap();
assert!((v.0 - 12.008).abs() < 0.001);
}
}
Redfish: Schema-Typed REST Endpoints
Redfish is an even better fit โ each endpoint returns a DMTF-defined JSON schema:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct ThermalResponse {
#[serde(rename = "Temperatures")]
pub temperatures: Vec<RedfishTemp>,
#[serde(rename = "Fans")]
pub fans: Vec<RedfishFan>,
}
#[derive(Debug, Deserialize)]
pub struct RedfishTemp {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "ReadingCelsius")]
pub reading: f64,
#[serde(rename = "UpperThresholdCritical")]
pub critical_hi: Option<f64>,
#[serde(rename = "Status")]
pub status: RedfishHealth,
}
#[derive(Debug, Deserialize)]
pub struct RedfishFan {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Reading")]
pub rpm: u32,
#[serde(rename = "Status")]
pub status: RedfishHealth,
}
#[derive(Debug, Deserialize)]
pub struct PowerResponse {
#[serde(rename = "Voltages")]
pub voltages: Vec<RedfishVoltage>,
#[serde(rename = "PowerSupplies")]
pub psus: Vec<RedfishPsu>,
}
#[derive(Debug, Deserialize)]
pub struct RedfishVoltage {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "ReadingVolts")]
pub reading: f64,
#[serde(rename = "Status")]
pub status: RedfishHealth,
}
#[derive(Debug, Deserialize)]
pub struct RedfishPsu {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "PowerOutputWatts")]
pub output_watts: Option<f64>,
#[serde(rename = "Status")]
pub status: RedfishHealth,
}
#[derive(Debug, Deserialize)]
pub struct ProcessorResponse {
#[serde(rename = "Model")]
pub model: String,
#[serde(rename = "TotalCores")]
pub cores: u32,
#[serde(rename = "Status")]
pub status: RedfishHealth,
}
#[derive(Debug, Deserialize)]
pub struct RedfishHealth {
#[serde(rename = "State")]
pub state: String,
#[serde(rename = "Health")]
pub health: Option<String>,
}
/// Typed Redfish endpoint โ each knows its response type.
pub trait RedfishEndpoint {
type Response: serde::de::DeserializeOwned;
fn method(&self) -> &'static str;
fn path(&self) -> String;
}
pub struct GetThermal { pub chassis_id: String }
impl RedfishEndpoint for GetThermal {
type Response = ThermalResponse;
fn method(&self) -> &'static str { "GET" }
fn path(&self) -> String {
format!("/redfish/v1/Chassis/{}/Thermal", self.chassis_id)
}
}
pub struct GetPower { pub chassis_id: String }
impl RedfishEndpoint for GetPower {
type Response = PowerResponse;
fn method(&self) -> &'static str { "GET" }
fn path(&self) -> String {
format!("/redfish/v1/Chassis/{}/Power", self.chassis_id)
}
}
pub struct GetProcessor { pub system_id: String, pub proc_id: String }
impl RedfishEndpoint for GetProcessor {
type Response = ProcessorResponse;
fn method(&self) -> &'static str { "GET" }
fn path(&self) -> String {
format!("/redfish/v1/Systems/{}/Processors/{}", self.system_id, self.proc_id)
}
}
pub struct RedfishClient {
pub base_url: String,
pub auth_token: String,
}
impl RedfishClient {
pub fn execute<E: RedfishEndpoint>(&self, endpoint: &E) -> io::Result<E::Response> {
let url = format!("{}{}", self.base_url, endpoint.path());
let json_bytes = self.http_request(endpoint.method(), &url)?;
serde_json::from_slice(&json_bytes)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn http_request(&self, _method: &str, _url: &str) -> io::Result<Vec<u8>> {
Ok(vec![]) // stub โ real impl uses reqwest/hyper
}
}
// Usage โ fully typed, self-documenting
fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> {
let thermal: ThermalResponse = client.execute(&GetThermal {
chassis_id: "1".into(),
})?;
let power: PowerResponse = client.execute(&GetPower {
chassis_id: "1".into(),
})?;
// โ Compile error โ can't pass PowerResponse to a thermal check:
// check_thermals(&power); โ "expected ThermalResponse, found PowerResponse"
for temp in &thermal.temperatures {
if let Some(crit) = temp.critical_hi {
if temp.reading > crit {
println!("CRITICAL: {} at {}ยฐC (threshold: {}ยฐC)",
temp.name, temp.reading, crit);
}
}
}
Ok(())
}
NVMe Admin: Identify Doesn't Return Log Pages
NVMe admin commands follow the same shape. The controller distinguishes command opcodes, but in C the caller must know which struct to overlay on the 4 KB completion buffer. The typed-command pattern makes this impossible to get wrong:
use std::io;
/// The NVMe Admin command trait โ same shape as IpmiCmd.
pub trait NvmeAdminCmd {
type Response;
fn opcode(&self) -> u8;
fn parse_completion(&self, data: &[u8]) -> io::Result<Self::Response>;
}
// โโ Identify (opcode 0x06) โโ
#[derive(Debug, Clone)]
pub struct IdentifyResponse {
pub model_number: String, // bytes 24โ63
pub serial_number: String, // bytes 4โ23
pub firmware_rev: String, // bytes 64โ71
pub total_capacity_gb: u64,
}
pub struct Identify {
pub nsid: u32, // 0 = controller, >0 = namespace
}
impl NvmeAdminCmd for Identify {
type Response = IdentifyResponse;
fn opcode(&self) -> u8 { 0x06 }
fn parse_completion(&self, data: &[u8]) -> io::Result<IdentifyResponse> {
if data.len() < 4096 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "short identify"));
}
Ok(IdentifyResponse {
serial_number: String::from_utf8_lossy(&data[4..24]).trim().to_string(),
model_number: String::from_utf8_lossy(&data[24..64]).trim().to_string(),
firmware_rev: String::from_utf8_lossy(&data[64..72]).trim().to_string(),
total_capacity_gb: u64::from_le_bytes(
data[280..288].try_into().unwrap()
) / (1024 * 1024 * 1024),
})
}
}
// โโ Get Log Page (opcode 0x02) โโ
#[derive(Debug, Clone)]
pub struct SmartLog {
pub critical_warning: u8,
pub temperature_kelvin: u16,
pub available_spare_pct: u8,
pub data_units_read: u128,
}
pub struct GetLogPage {
pub log_id: u8, // 0x02 = SMART/Health
}
impl NvmeAdminCmd for GetLogPage {
type Response = SmartLog;
fn opcode(&self) -> u8 { 0x02 }
fn parse_completion(&self, data: &[u8]) -> io::Result<SmartLog> {
if data.len() < 512 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "short log page"));
}
Ok(SmartLog {
critical_warning: data[0],
temperature_kelvin: u16::from_le_bytes([data[1], data[2]]),
available_spare_pct: data[3],
data_units_read: u128::from_le_bytes(data[32..48].try_into().unwrap()),
})
}
}
// โโ Executor โโ
pub struct NvmeController { /* fd, BAR, etc. */ }
impl NvmeController {
pub fn admin_cmd<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
let raw = self.submit_and_wait(cmd.opcode())?;
cmd.parse_completion(&raw)
}
fn submit_and_wait(&self, _opcode: u8) -> io::Result<Vec<u8>> {
Ok(vec![0u8; 4096]) // stub โ real impl issues doorbell + waits for CQ entry
}
}
// โโ Usage โโ
fn nvme_health_check(ctrl: &NvmeController) -> io::Result<()> {
let id: IdentifyResponse = ctrl.admin_cmd(&Identify { nsid: 0 })?;
let smart: SmartLog = ctrl.admin_cmd(&GetLogPage { log_id: 0x02 })?;
// โ Compile error โ Identify returns IdentifyResponse, not SmartLog:
// let smart: SmartLog = ctrl.admin_cmd(&Identify { nsid: 0 })?;
println!("{} (FW {}): {}ยฐC, {}% spare",
id.model_number, id.firmware_rev,
smart.temperature_kelvin.saturating_sub(273),
smart.available_spare_pct);
Ok(())
}
The three-protocol progression now follows a graduated arc (the same technique ch07 uses for validated boundaries):
| Beat | Protocol | Complexity | What it adds |
|---|---|---|---|
| 1 | IPMI | Simple: sensor ID โ reading | Core pattern: trait + associated type |
| 2 | Redfish | REST: endpoint โ typed JSON | Serde integration, schema-typed responses |
| 3 | NVMe | Binary: opcode โ 4 KB struct overlay | Raw buffer parsing, multi-struct completion data |
Extension: Macro DSL for Command Scripts
/// Execute a series of typed IPMI commands, returning a tuple of results.
macro_rules! diag_script {
($bmc:expr; $($cmd:expr),+ $(,)?) => {{
( $( $bmc.execute(&$cmd)?, )+ )
}};
}
fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> {
let (temp, rpm, volts) = diag_script!(bmc;
ReadTemp { sensor_id: 0x20 },
ReadFanSpeed { fan_id: 0x30 },
ReadVoltage { rail: 0x40 },
);
// Type: (Celsius, Rpm, Volts) โ fully inferred, swap = compile error
assert!(temp < Celsius(95.0), "CPU too hot");
assert!(rpm > Rpm(3000), "Fan too slow");
assert!(volts > Volts(11.4), "12V rail sagging");
Ok(())
}
Extension: Enum Dispatch for Dynamic Scripts
When commands come from JSON config at runtime:
pub enum AnyReading {
Temp(Celsius),
Rpm(Rpm),
Volt(Volts),
Watt(Watts),
}
pub enum AnyCmd {
Temp(ReadTemp),
Fan(ReadFanSpeed),
Voltage(ReadVoltage),
Power(ReadPowerDraw),
}
impl AnyCmd {
pub fn execute(&self, bmc: &BmcConnection) -> io::Result<AnyReading> {
match self {
AnyCmd::Temp(c) => Ok(AnyReading::Temp(bmc.execute(c)?)),
AnyCmd::Fan(c) => Ok(AnyReading::Rpm(bmc.execute(c)?)),
AnyCmd::Voltage(c) => Ok(AnyReading::Volt(bmc.execute(c)?)),
AnyCmd::Power(c) => Ok(AnyReading::Watt(bmc.execute(c)?)),
}
}
}
fn run_dynamic_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result<Vec<AnyReading>> {
script.iter().map(|cmd| cmd.execute(bmc)).collect()
}
The Pattern Family
This pattern applies to every hardware management protocol:
| Protocol | Request Type | Response Type |
|---|---|---|
| IPMI Sensor Reading | ReadTemp | Celsius |
| Redfish REST | GetThermal | ThermalResponse |
| NVMe Admin | Identify | IdentifyResponse |
| PLDM | GetFwParams | FwParamsResponse |
| MCTP | GetEid | EidResponse |
| PCIe Config Space | ReadCapability | CapabilityHeader |
| SMBIOS/DMI | ReadType17 | MemoryDeviceInfo |
The request type determines the response type โ the compiler enforces it everywhere.
Typed Command Flow
flowchart LR
subgraph "Compile Time"
RT["ReadTemp"] -->|"type Response = Celsius"| C[Celsius]
RF["ReadFanSpeed"] -->|"type Response = Rpm"| R[Rpm]
RV["ReadVoltage"] -->|"type Response = Volts"| V[Volts]
end
subgraph "Runtime"
E["bmc.execute(&cmd)"] -->|"monomorphised"| P["cmd.parse_response(raw)"]
end
style RT fill:#e1f5fe,color:#000
style RF fill:#e1f5fe,color:#000
style RV fill:#e1f5fe,color:#000
style C fill:#c8e6c9,color:#000
style R fill:#c8e6c9,color:#000
style V fill:#c8e6c9,color:#000
style E fill:#fff3e0,color:#000
style P fill:#fff3e0,color:#000
Exercise: PLDM Typed Commands
Design a PldmCmd trait (same shape as IpmiCmd) for two PLDM commands:
GetFwParamsโFwParamsResponse { active_version: String, pending_version: Option<String> }QueryDeviceIdsโDeviceIdResponse { descriptors: Vec<Descriptor> }
Requirements: static dispatch, parse_response returns io::Result<Self::Response>.
use std::io;
pub trait PldmCmd {
type Response;
fn pldm_type(&self) -> u8;
fn command_code(&self) -> u8;
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
#[derive(Debug, Clone)]
pub struct FwParamsResponse {
pub active_version: String,
pub pending_version: Option<String>,
}
pub struct GetFwParams;
impl PldmCmd for GetFwParams {
type Response = FwParamsResponse;
fn pldm_type(&self) -> u8 { 0x05 } // Firmware Update
fn command_code(&self) -> u8 { 0x02 }
fn parse_response(&self, raw: &[u8]) -> io::Result<FwParamsResponse> {
// Simplified โ real impl decodes PLDM FW Update spec fields
if raw.len() < 4 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "too short"));
}
Ok(FwParamsResponse {
active_version: String::from_utf8_lossy(&raw[..4]).to_string(),
pending_version: None,
})
}
}
#[derive(Debug, Clone)]
pub struct Descriptor { pub descriptor_type: u16, pub data: Vec<u8> }
#[derive(Debug, Clone)]
pub struct DeviceIdResponse { pub descriptors: Vec<Descriptor> }
pub struct QueryDeviceIds;
impl PldmCmd for QueryDeviceIds {
type Response = DeviceIdResponse;
fn pldm_type(&self) -> u8 { 0x05 }
fn command_code(&self) -> u8 { 0x04 }
fn parse_response(&self, raw: &[u8]) -> io::Result<DeviceIdResponse> {
Ok(DeviceIdResponse { descriptors: vec![] }) // stub
}
}
Key Takeaways
- Associated type = compile-time contract โ
type Responseon the command trait locks each request to exactly one response type. - Parsing is encapsulated โ byte-layout knowledge lives in
parse_response, not scattered across callers. - Zero-cost dispatch โ generic
execute<C: IpmiCmd>monomorphises to direct calls with no vtable. - One pattern, many protocols โ IPMI, Redfish, NVMe, PLDM, MCTP all fit the same
trait Cmd { type Response; }shape. - Enum dispatch bridges static and dynamic โ wrap typed commands in an enum for runtime-driven scripts without losing type safety inside each arm.
- Graduated complexity strengthens intuition โ IPMI (sensor ID โ reading), Redfish (endpoint โ JSON schema), and NVMe (opcode โ 4 KB struct overlay) all use the same trait shape, but each beat adds a layer of parsing complexity.