use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
use rigless::emulator::NO_DEADLINE;
use rigless::time::VirtualNanos;
#[cfg(not(emulator_absent))]
use anyhow::bail;
#[cfg(not(emulator_absent))]
use rigless::chip::uart::Host;
#[cfg(not(emulator_absent))]
use rigless::machine::Board;
#[cfg(not(emulator_absent))]
use std::io::{self, Write};
const NANOS_PER_MICRO: u64 = 1_000;
const NANOS_PER_MILLI: u64 = 1_000_000;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
const UNITS: [(&str, u64); 4] = [
("ns", 1),
("us", NANOS_PER_MICRO),
("ms", NANOS_PER_MILLI),
("s", NANOS_PER_SECOND),
];
#[derive(Parser)]
#[command(version = rigless::version::RUNTIME, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Svd {
#[command(subcommand)]
command: Svd,
},
Run(Run),
}
#[derive(Subcommand)]
enum Svd {
Generate {
soc_directory: PathBuf,
},
}
#[derive(clap::Args)]
struct Run {
#[arg(long)]
machine: String,
#[arg(long, value_name = "MCU")]
target: Option<String>,
#[arg(long, value_parser = parse_virtual_time)]
until: Option<VirtualNanos>,
#[arg(long, value_name = "[HOST:]PORT", value_parser = parse_debugger_address)]
gdb: Option<SocketAddrV4>,
#[arg(long, requires = "gdb")]
wait_for_debugger: bool,
#[arg(long, value_name = "PATH")]
coverage: Option<PathBuf>,
#[arg(long, value_parser = parse_period, value_name = "PERIOD")]
outputs: Option<VirtualNanos>,
#[arg(long, conflicts_with_all = ["until", "outputs", "coverage", "host"])]
link: bool,
#[arg(long, value_name = "WIRE=PORT")]
host: Vec<String>,
image: PathBuf,
}
fn main() -> anyhow::Result<()> {
match Cli::parse().command {
Command::Svd {
command: Svd::Generate { soc_directory },
} => rigless::svd::write_module(&soc_directory)
.with_context(|| format!("generating {}", soc_directory.display())),
Command::Run(run) => run_firmware(&run),
}
}
#[cfg(not(emulator_absent))]
fn attach_hosts(machine: &mut dyn Board, name: &str, ports: &[String]) -> anyhow::Result<()> {
let mut chosen: Vec<(&str, u16)> = Vec::with_capacity(ports.len());
for override_ in ports {
let (wire, port) = override_
.split_once('=')
.with_context(|| format!("{override_} is not WIRE=PORT"))?;
let port = port
.parse()
.with_context(|| format!("{port} is not a port"))?;
chosen.push((wire, port));
}
for wire in machine.hosts() {
let port = chosen
.iter()
.find(|(named, _)| *named == wire.name)
.map_or(wire.port, |(_, port)| *port);
let host = Host::listen(port)
.with_context(|| format!("listening for {} on :{port}", wire.name))?;
let port = host.port()?;
if !machine.attach_host(wire.name, Box::new(host)) {
bail!("{name} names no wire called {}", wire.name);
}
writeln!(io::stderr(), "{} on :{port}", wire.name)?;
}
Ok(())
}
#[cfg(not(emulator_absent))]
fn target_index(machine: &dyn Board, named: Option<&str>) -> anyhow::Result<usize> {
let names = machine.target_names();
match (named, &names[..]) {
(Some(name), _) => machine.target(name).with_context(|| {
format!(
"{} has no MCU named {name}, only {}",
machine.name(),
names.join(", ")
)
}),
(None, [_only]) => Ok(0),
(None, _) => bail!(
"{} has the MCUs {}, so name the one to run with --target",
machine.name(),
names.join(", ")
),
}
}
fn parse_virtual_time(text: &str) -> Result<VirtualNanos, String> {
let (digits, scale) = UNITS
.iter()
.find_map(|(unit, scale)| text.strip_suffix(unit).map(|digits| (digits, *scale)))
.ok_or_else(|| format!("{text} has no unit: use ns, us, ms, or s"))?;
let count: u64 = digits
.parse()
.map_err(|error| format!("{digits} is not a whole number: {error}"))?;
let nanos = count
.checked_mul(scale)
.ok_or_else(|| format!("{text} does not fit the virtual clock"))?;
if nanos > NO_DEADLINE.0 {
return Err(format!(
"{text} is longer than the virtual clock runs: the most is {} ns",
NO_DEADLINE.0
));
}
Ok(VirtualNanos(nanos))
}
fn parse_period(text: &str) -> Result<VirtualNanos, String> {
let period = parse_virtual_time(text)?;
if period.0 == 0 {
return Err(format!(
"{text} would read the outputs forever at one instant. Give a period above zero"
));
}
Ok(period)
}
fn parse_debugger_address(text: &str) -> Result<SocketAddrV4, String> {
if let Ok(port) = text.parse::<u16>() {
return Ok(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port));
}
text.parse().map_err(|error| {
format!("{text} is neither a port nor an IPv4 address and port like 0.0.0.0:3333: {error}")
})
}
#[cfg(emulator_absent)]
fn run_firmware(_run: &Run) -> anyhow::Result<()> {
Err(rigless::emulator::Error::NoEmulator.into())
}
#[cfg(not(emulator_absent))]
fn run_firmware(run: &Run) -> anyhow::Result<()> {
if run.link {
linked::run(run)
} else {
standalone::run(run)
}
}
#[cfg(not(emulator_absent))]
mod standalone {
use std::fs;
use std::io::{self, Write};
use anyhow::{Context, bail};
use rigless::emulator::{NO_DEADLINE, Start, run_printed};
use rigless::session::Session;
use super::{Run, attach_hosts, target_index};
pub(super) fn run(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::all();
let Some((name, build)) = machines.iter().find(|(name, _)| *name == run.machine) else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {}: this build has {}",
run.machine,
names.join(", ")
);
};
let mut board = build().with_context(|| format!("building {name}"))?;
let target = target_index(&*board, run.target.as_deref())?;
attach_hosts(&mut *board, name, &run.host)?;
if let Some(address) = run.gdb {
let state = if run.wait_for_debugger {
"waiting for a debugger"
} else {
"serving GDB"
};
writeln!(io::stderr(), "{state} on {address}")?;
}
let start = Start {
firmware: &run.image,
debugger: run.gdb,
wait_for_debugger: run.wait_for_debugger,
};
let session = Session::new(board, target, run.until.unwrap_or(NO_DEADLINE))?;
let printed = run_printed(session, &start, run.outputs, io::stdout())?;
if let Some(path) = &run.coverage {
fs::write(path, printed.coverage.to_string())
.with_context(|| format!("writing {}", path.display()))?;
}
Ok(())
}
}
#[cfg(not(emulator_absent))]
mod linked {
use std::io::{Stdin, Stdout};
use anyhow::{Context, bail};
use rigless::emulator::{Driver, Finished, Start, Stop};
use rigless::link::{
Frame, Gaps, LinkError, Manifest, Reached, Report, Served, VERSION, attach_engine_ports,
deliver, report, sent, values_of,
};
use rigless::machine::Board;
use rigless::session::Session;
use rigless::time::VirtualNanos;
use super::{Run, target_index};
pub(super) fn run(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::all();
let Some((name, build)) = machines.iter().find(|(name, _)| *name == run.machine) else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {}: this build has {}",
run.machine,
names.join(", ")
);
};
let mut board = build().with_context(|| format!("building {name}"))?;
let Some(edge) = board.edge() else {
bail!("{name} has no edge a plant couples to, so it can't be an engine's target");
};
let (inputs, outputs) = (edge.fields(), edge.output_fields());
let rest = values_of(inputs, &edge.rest_wire())?;
let target = target_index(&*board, run.target.as_deref())?;
let ports = attach_engine_ports(&mut *board)?;
let manifest = Manifest {
version: VERSION,
machine: (*name).to_owned(),
runtime: rigless::version::RUNTIME.to_owned(),
inputs: inputs.to_vec(),
rest: rest.clone(),
outputs: outputs.to_vec(),
ports: ports.iter().map(ToString::to_string).collect(),
};
let mut served = Served::stdio();
served.open(&manifest)?;
let Frame::Advance(first) = served.receive()? else {
served.report(Report::default())?;
return Ok(());
};
let mut inputs = rest;
deliver(&mut *board, &mut inputs, &ports, &first, VirtualNanos(0))?;
let session = Session::new(board, target, first.until)?;
let start = Start {
firmware: &run.image,
debugger: run.gdb,
wait_for_debugger: run.wait_for_debugger,
};
let driver = Linked {
served,
inputs,
ports,
gaps: Gaps::default(),
resets_told: 0,
failed: None,
};
let Finished {
mut session,
mut driver,
..
} = rigless::emulator::run(session, &start, driver)?;
if let Some(error) = driver.failed.take() {
return Err(error.into());
}
if let Some(error) = session.take_error() {
return Err(error.into());
}
driver.served.report(report(&session))?;
Ok(())
}
struct Linked {
served: Served<Stdin, Stdout>,
inputs: Vec<i64>,
ports: Vec<&'static str>,
gaps: Gaps,
resets_told: usize,
failed: Option<LinkError>,
}
impl Linked {
fn answer(
&mut self,
session: &mut Session<dyn Board>,
stop: Stop,
) -> Result<Option<VirtualNanos>, LinkError> {
let edge = session.board_mut().edge().ok_or_else(|| {
LinkError::Board("lost its edge in the middle of a run".to_owned())
})?;
let outputs = values_of(edge.output_fields(), &edge.drive_wire())?;
let ports = sent(session.board_mut(), &self.ports)?;
let resets = session.resets()[self.resets_told..]
.iter()
.map(|reset| reset.at)
.collect::<Vec<_>>();
self.resets_told = session.resets().len();
let gaps = self.gaps.fresh(session);
self.served.reached(Reached {
stop,
outputs,
ports,
resets,
gaps,
})?;
match self.served.receive()? {
Frame::Advance(advance) => {
deliver(
session.board_mut(),
&mut self.inputs,
&self.ports,
&advance,
stop.at,
)?;
Ok(Some(advance.until))
}
_ => Ok(None),
}
}
}
impl Driver<dyn Board> for Linked {
fn on_stop(
&mut self,
session: &mut Session<dyn Board>,
stop: Stop,
) -> Option<VirtualNanos> {
self.answer(session, stop).unwrap_or_else(|error| {
self.failed = Some(error);
None
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_duration_parses_with_each_unit_and_refuses_the_rest() {
assert_eq!(parse_virtual_time("5ns"), Ok(VirtualNanos(5)));
assert_eq!(parse_virtual_time("5us"), Ok(VirtualNanos(5_000)));
assert_eq!(parse_virtual_time("5ms"), Ok(VirtualNanos(5_000_000)));
assert_eq!(parse_virtual_time("5s"), Ok(VirtualNanos(5_000_000_000)));
assert!(parse_virtual_time("5").is_err());
assert!(parse_virtual_time("fives").is_err());
assert!(parse_virtual_time("99999999999999999999s").is_err());
assert!(
parse_virtual_time("9223372036854775808ns").is_err(),
"past the emulator's signed deadline"
);
}
#[test]
fn a_debugger_port_alone_is_on_the_loopback_and_an_address_names_its_interface() {
assert_eq!(
parse_debugger_address("3333"),
Ok(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3333))
);
assert_eq!(
parse_debugger_address("0.0.0.0:3333"),
Ok(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 3333))
);
assert!(
parse_debugger_address("[::1]:3333").is_err(),
"the emulator's stub takes no IPv6 host"
);
assert!(
parse_debugger_address("localhost:3333").is_err(),
"a name would resolve to whichever family the host prefers"
);
assert!(parse_debugger_address("70000").is_err());
}
#[test]
fn a_period_of_zero_is_refused_since_the_run_would_never_move_on() {
assert!(parse_period("0ns").is_err());
assert!(parse_period("0s").is_err());
assert_eq!(parse_period("1ns"), Ok(VirtualNanos(1)));
}
}