Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ use_repo(qemu, "qemu_opentitan", "qemu_opentitan_src")
# ── Python deps for QEMU tooling scripts (cfggen, flashgen) ──
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
# AMD's Zscaler proxy intercepts TLS to pypi.org with its own cert; skip
# transport verification. Package integrity is still enforced via the
# sha256 hashes in requirements_lock.
extra_pip_args = [
"--trusted-host",
"pypi.org",
"--trusted-host",
"files.pythonhosted.org",
],
hub_name = "openprot_python_deps",
python_version = "3.11",
requirements_lock = "//third_party/qemu:requirements.txt",
Expand Down
6 changes: 3 additions & 3 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions services/mctp/echo/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rust_library(
visibility = ["//visibility:public"],
deps = [
"//services/mctp/api:mctp_api",
"@pigweed//pw_log/rust:pw_log",
],
)

Expand Down
111 changes: 107 additions & 4 deletions services/mctp/echo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

#![no_std]

use openprot_mctp_api::{MctpClient, MctpError, MctpRespChannel, Stack, StackListener};
use openprot_mctp_api::{
MctpClient, MctpError, MctpReqChannel, MctpRespChannel, Stack, StackListener,
};

/// Default MCTP message type used by the echo app.
pub const ECHO_MSG_TYPE: u8 = 1;
Expand All @@ -40,12 +42,113 @@ pub fn prepare_listener_with_eid_and_timeout<C: MctpClient>(
}

/// Process one echo receive/send cycle.
///
/// Returns an error from either receive or response send.
pub fn echo_once<L>(listener: &mut L, buf: &mut [u8]) -> Result<(), MctpError>
pub fn echo_once<L>(listener: &mut L, buf: &mut [u8]) -> Result<(), openprot_mctp_api::MctpError>
where
L: openprot_mctp_api::MctpListener,
{
let (_meta, msg, mut resp) = listener.recv(buf)?;
resp.send(msg)
}

/// Run the echo loop forever, echoing received messages.
pub fn run<L>(listener: &mut L) -> !
where
L: openprot_mctp_api::MctpListener,
{
let mut buf = [0u8; 255];
loop {
match listener.recv(&mut buf) {
Ok((_meta, msg, mut resp)) => {
let _ = resp.send(msg);
}
Err(e) => {
// Timeouts are expected noise from the short recv deadline; suppress them.
if !e.is_timeout() {
pw_log::error!("echo recv failed: code={}", e.code as u32);
}
}
}
}
}

/// Run the echo loop with periodic sends to a peer endpoint, forever.
pub fn run_with_peer<C: MctpClient, L: openprot_mctp_api::MctpListener>(
stack: &Stack<C>,
peer_eid: u8,
listener: &mut L,
) -> ! {
loop {
run_with_peer_round_trip_limit(stack, peer_eid, listener, u32::MAX);
}
}

/// Run the echo loop with periodic sends to a peer endpoint, returning after
/// `max_round_trips` successful request/response exchanges.
pub fn run_with_peer_round_trip_limit<C: MctpClient, L: openprot_mctp_api::MctpListener>(
stack: &Stack<C>,
peer_eid: u8,
listener: &mut L,
max_round_trips: u32,
) {
let mut buf = [0u8; 255];
let mut iteration: u32 = 0;
// 10 iterations between sends keeps the channel active without flooding it.
let send_interval = 10;

// Keep retrying until the first successful round-trip so a slow-booting
// peer (>1 min) does not cause us to give up before it is ready.
loop {
match listener.recv(&mut buf) {
Ok((_meta, msg, mut resp)) => {
let _ = resp.send(msg);
}
Err(e) => {
// Timeouts are expected noise from the short recv deadline; suppress them.
if !e.is_timeout() {
pw_log::error!("echo recv failed: code={}", e.code as u32);
}
}
}

if let Ok(mut req) = stack.req(peer_eid, 10000) {
let test_msg = b"echo_test";
let _ = req.send(ECHO_MSG_TYPE, test_msg);
if req.recv(&mut buf).is_ok() {
break;
}
}
}

// First round-trip succeeded; count it and continue with periodic sends.
let mut completed_round_trips: u32 = 1;

loop {
iteration = iteration.wrapping_add(1);

// Periodically try to send a test message to the peer.
if iteration % send_interval == 0
&& completed_round_trips < max_round_trips
&& let Ok(mut req) = stack.req(peer_eid, 100)
{
let test_msg = b"echo_test";
let _ = req.send(ECHO_MSG_TYPE, test_msg);
// Try to receive response with a short timeout.
if req.recv(&mut buf).is_ok() {
completed_round_trips = completed_round_trips.saturating_add(1);
}
}

// Listen for incoming messages and echo them back.
match listener.recv(&mut buf) {
Ok((_meta, msg, mut resp)) => {
let _ = resp.send(msg);
}
Err(e) => {
// Timeouts are expected noise from the short recv deadline; suppress them.
if !e.is_timeout() {
pw_log::error!("echo recv failed: code={}", e.code as u32);
}
}
}
}
}
2 changes: 1 addition & 1 deletion services/spdm/requester/tests/vca_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ use openprot_mctp_api::stack::Stack;
use openprot_mctp_api::MctpClient;
use openprot_mctp_server::Server;
use openprot_spdm_requester::{RequesterConfig, SpdmRequester};
use openprot_spdm_responder::{ResponderConfig, SpdmResponder};
use openprot_spdm_transport_mctp::MctpSpdmTransport;
use spdm_lib::codec::MessageBuf;
use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request;
use spdm_lib::commands::capabilities::request::generate_capabilities_request_local;
use spdm_lib::commands::version::request::generate_get_version;
use spdm_lib::commands::version::VersionReqPayload;
use spdm_lib::platform::transport::SpdmTransport;
use spdm_responder::{ResponderConfig, SpdmResponder};

use common::{
transfer, BufferSender, DemoPeerCertStore, DirectClient, MockCertStore, MockEvidence, MockHash,
Expand Down
2 changes: 1 addition & 1 deletion services/spdm/responder/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
name = "spdm_responder_lib",
srcs = glob(["src/**/*.rs"]),
crate_name = "spdm_responder",
crate_name = "openprot_spdm_responder",
edition = "2024",
visibility = ["//visibility:public"],
deps = [
Expand Down
2 changes: 1 addition & 1 deletion services/spdm/responder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

[package]
name = "spdm-responder"
name = "openprot-spdm-responder"
version = "0.1.0"
edition = "2021"
description = "SPDM responder service for OpenPRoT"
Expand Down
21 changes: 14 additions & 7 deletions target/ast10x0/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@ TARGET_COMPATIBLE_WITH = select({
})

def _system_image_test_impl(ctx):
master_elf = ctx.attr.image[SystemImageInfo].elf
image_info = ctx.attr.image[SystemImageInfo]
executable_symlink = ctx.actions.declare_file(ctx.label.name)
ctx.actions.symlink(output = executable_symlink, target_file = master_elf)
ctx.actions.symlink(output = executable_symlink, target_file = image_info.elf)

runfiles = ctx.attr.image[DefaultInfo].default_runfiles
bin_symlink = ctx.actions.declare_file(ctx.label.name + ".bin")
ctx.actions.symlink(output = bin_symlink, target_file = image_info.bin)

runfiles = ctx.runfiles(files = [bin_symlink]).merge(
ctx.attr.image[DefaultInfo].default_runfiles,
)

if ctx.attr.slave_image:
slave_elf = ctx.attr.slave_image[SystemImageInfo].elf
slave_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf")
ctx.actions.symlink(output = slave_symlink, target_file = slave_elf)
runfiles = ctx.runfiles(files = [slave_symlink]).merge(
slave_info = ctx.attr.slave_image[SystemImageInfo]
slave_elf_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf")
ctx.actions.symlink(output = slave_elf_symlink, target_file = slave_info.elf)
slave_bin_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.bin")
ctx.actions.symlink(output = slave_bin_symlink, target_file = slave_info.bin)
runfiles = ctx.runfiles(files = [slave_elf_symlink, slave_bin_symlink]).merge(
runfiles.merge(ctx.attr.slave_image[DefaultInfo].default_runfiles),
)

Expand Down
55 changes: 46 additions & 9 deletions target/ast10x0/harness/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,24 +94,59 @@ def _ssh_stream(host: str, cmd: str) -> subprocess.Popen:
def _acquire_lock(host: str, timeout: int = _LOCK_ACQUIRE_TIMEOUT) -> bool:
"""Atomically create lock file on Pi; retry until timeout."""
deadline = time.time() + timeout
create = f"set -o noclobber && echo $$ > {_LOCK_PATH}"
local_user = os.environ.get("USER", "unknown")
# On noclobber failure, cat outputs the lock file contents so we get
# owner info from the same SSH call without a second round-trip.
create = (
f"set -o noclobber && "
f"printf '%s\\t%s\\n' '{local_user}' \"$(date +%s)\" > {_LOCK_PATH} || "
f"{{ cat {_LOCK_PATH} 2>/dev/null; false; }}"
)
stale_check = (
f"mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null) && "
f"now=$(date +%s) && "
f"[ $(( now - mtime )) -gt {_LOCK_STALE_THRESHOLD} ] && "
f"rm -f {_LOCK_PATH}"
)
last_printed = 0.0
while time.time() < deadline:
r = _ssh(host, create, capture_output=True)
if r.returncode == 0:
return True
if r.stderr.strip():
print(r.stderr.decode(errors="replace").strip(), file=sys.stderr)
return False
if not r.stdout.strip():
if msg := r.stderr.decode(errors="replace").strip():
print(f"RUNNER: {msg}", file=sys.stderr)
return False
continue
parts = r.stdout.decode(errors="replace").split()
now = time.time()
if len(parts) == 2 and now - last_printed >= 30:
lock_user, acq_ts = parts[0], int(parts[1])
held_s = int(now) - acq_ts
mins, secs = divmod(held_s, 60)
held = f"{mins}m {secs}s" if mins else f"{secs}s"
stat_r = _ssh(
host,
f"now=$(date +%s); mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null); echo $((now - mtime))",
capture_output=True,
)
touch_ago = int(stat_r.stdout.decode().strip() or -1)
if touch_ago > _LOCK_STALE_THRESHOLD:
print(
f"RUNNER: removing stale lock held by {lock_user} {held} ago, last touched {touch_ago}s ago...",
file=sys.stderr,
)
else:
print(
f"RUNNER: Pi locked by {lock_user} {held} ago, last touched {touch_ago}s ago; waiting...",
file=sys.stderr,
)
last_printed = now
_ssh(host, stale_check, capture_output=True)
time.sleep(2)
print(
f"Timeout acquiring Pi lock after {timeout}s (Pi may be busy)", file=sys.stderr
f"RUNNER: timeout acquiring Pi lock after {timeout}s (Pi may be busy)",
file=sys.stderr,
)
return False

Expand Down Expand Up @@ -242,6 +277,7 @@ def _run_remote(

if not _acquire_lock(host):
return False
print("RUNNER: lock acquired, starting test", file=sys.stderr)

stop_touch = threading.Event()
touch_thread = threading.Thread(
Expand Down Expand Up @@ -383,13 +419,14 @@ def main() -> int:
if not image.suffix:
slave_symlink = image.parent / (image.name + ".slave.elf")
if slave_symlink.exists():
slave_elf = slave_symlink.resolve()
slave_elf_path = slave_elf
args.slave_firmware = str(slave_elf.with_suffix(".bin"))
slave_elf_path = slave_symlink.resolve()
args.slave_firmware = str(image.parent / (image.name + ".slave.bin"))
args.firmware = str(image.parent / (image.name + ".bin"))
image = image.resolve()
else:
args.firmware = str(image.with_suffix(".bin"))

elf_path = image.with_suffix(".elf")
args.firmware = str(image.with_suffix(".bin"))
if not elf_path.exists():
print(f"Error: ELF not found at {elf_path}", file=sys.stderr)
return 1
Expand Down
16 changes: 16 additions & 0 deletions target/ast10x0/peripherals/i2c/slave.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {

/// Configure the controller for slave mode
pub fn configure_slave(&mut self, config: &SlaveConfig) -> Result<(), I2cError> {
// Disable master mode while the slave registers are programmed so the
// shared controller is quiescent during setup; its prior state is
// restored at the end so slave-only callers keep master off.
let master_was_enabled = self.regs().i2cc00().read().enbl_master_fn().bit();
self.regs()
.i2cc00()
.modify(|_, w| w.enbl_master_fn().clear_bit());

// Set slave address
self.regs().i2cs40().write(|w| unsafe {
w.slave_dev_addr1()
Expand Down Expand Up @@ -245,6 +253,14 @@ impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {
// Enable slave interrupts
self.enable_slave_interrupts();

// Restore master mode to its prior state: dual master+slave callers
// (e.g. MCTP) get it back on; slave-only callers keep it off.
if master_was_enabled {
self.regs()
.i2cc00()
.modify(|_, w| w.enbl_master_fn().set_bit());
}

Ok(())
}

Expand Down
6 changes: 6 additions & 0 deletions target/ast10x0/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ UART, USB serial gadget, or USB networking), so the host-side protocol cannot
be specified. Omitting `AST1060_EVB_PI_HOST` should default to using a wired
connection, but currently logs an unimplemented error.

## Hardware Tests

| Test | Description |
|------|-------------|
| [`mctp/server`](mctp/server/README.md) | MCTP echo stress test between two AST1060 EVBs over I2C |

## Test Results (2026-05-12)

| Test | QEMU | Physical board |
Expand Down
1 change: 1 addition & 0 deletions target/ast10x0/tests/mctp/ipc_client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ system_image(
system_image_test(
name = "ipc_client_qemu_test",
image = ":ipc_client_image",
tags = ["qemu_only"],
target_compatible_with = TARGET_COMPATIBLE_WITH,
)

Expand Down
Loading
Loading