use log::{debug, trace};
use std::io::Write;
use onerom_config::chip::{CHIP_TYPE_NAMES_PLUGINS, chip_type_names_for_pins};
use onerom_config::fw::{FirmwareProperties, FirmwareVersion, ServeAlg};
use onerom_config::hw::Board;
use onerom_config::mcu::Variant;
use onerom_fw::net::{Release, Releases, fetch_license_async};
use onerom_fw::{assemble_firmware, get_rom_files_async, read_rom_config, validate_sizes};
use onerom_gen::{Builder, FIRMWARE_SIZE, License};
use sdrr_fw_parser::{Parser, SdrrInfo, readers::MemoryReader};
use crate::args;
use crate::utils::{resolve_board, resolve_firmware_output};
use onerom_cli::plugin::{PluginSpec, ResolvedPlugin, resolve_plugins};
use onerom_cli::slot::{
ConfirmationsRequired, check_slot_confirmations, parse_slots, save_config, slots_to_config_json,
};
use onerom_cli::{Error, Options};
pub fn resolve_config_json(
config_file: Option<&str>,
slots: &[String],
no_config: bool,
board: &Board,
config_name: Option<&str>,
config_description: Option<&str>,
plugins: &[ResolvedPlugin],
) -> Result<String, Error> {
if let Some(path) = config_file {
read_rom_config(path).map_err(Error::from)
} else if no_config || slots.is_empty() {
slots_to_config_json(plugins, &[], config_name, config_description)
} else {
let parsed = parse_slots(slots, board)?;
slots_to_config_json(plugins, &parsed, config_name, config_description)
}
}
pub async fn verify_assembled_firmware(
options: &Options,
data: &[u8],
force: bool,
) -> Result<(), Error> {
let info = parse_firmware(data).await?;
if !info.parse_errors.is_empty() {
let detail = info
.parse_errors
.iter()
.map(|e| format!(" {e}"))
.collect::<Vec<_>>()
.join("\n");
if force {
eprintln!("Warning: assembled firmware has parse errors (continuing due to --force):");
eprintln!("{detail}");
} else {
return Err(Error::FirmwareValidation(detail));
}
} else if options.verbose {
println!(
"Assembled firmware version {} parsed successfully with no errors",
info.version
);
}
Ok(())
}
pub async fn parse_firmware(data: &[u8]) -> Result<SdrrInfo, Error> {
let mut reader = MemoryReader::new(data.to_vec(), 0x0800_0000);
let mut parser = Parser::new(&mut reader);
parser.parse_flash().await.map_err(Error::Other)
}
fn check_firmware_size(options: &Options, data: &[u8]) -> Result<(), Error> {
if options.verbose {
println!("Firmware size {} bytes", data.len());
}
if data.len() > FIRMWARE_SIZE {
return Err(Error::BaseFirmwareTooLarge(data.len(), FIRMWARE_SIZE));
}
Ok(())
}
fn resolve_release<'a>(
releases: &'a Releases,
version: &Option<String>,
) -> Result<&'a Release, Error> {
if let Some(version) = version {
releases
.release_from_string(version)
.ok_or_else(|| Error::VersionNotFound(version.clone(), releases.releases_str()))
} else {
releases
.release_from_string(releases.latest())
.ok_or(Error::NoLatestRelease)
}
}
pub async fn acquire_firmware(
options: &Options,
firmware_path: &Option<String>,
version_arg: &Option<String>,
board: &Board,
mcu: &Variant,
) -> Result<(Vec<u8>, FirmwareVersion, String), Error> {
if let Some(firmware) = firmware_path {
acquire_local_firmware(options, firmware).await
} else {
acquire_release_firmware(options, version_arg, board, mcu).await
}
}
async fn acquire_local_firmware(
options: &Options,
firmware: &str,
) -> Result<(Vec<u8>, FirmwareVersion, String), Error> {
if options.verbose {
println!("Using local firmware: {firmware}");
}
let data = std::fs::read(firmware).map_err(|e| Error::io(firmware, e))?;
check_firmware_size(options, &data)?;
let info = parse_firmware(&data).await?;
let version_str = format!("{}", info.version);
if options.verbose {
println!("Detected firmware version: {version_str}");
}
Ok((data, info.version, version_str))
}
async fn acquire_release_firmware(
options: &Options,
version_arg: &Option<String>,
board: &Board,
mcu: &Variant,
) -> Result<(Vec<u8>, FirmwareVersion, String), Error> {
if options.verbose {
println!("Checking available firmware versions...");
}
let releases = Releases::from_network_async().await?;
let release = resolve_release(&releases, version_arg)?;
let version = release.firmware_version()?;
let version_str = release.version.clone();
if options.verbose {
println!(
"Downloading firmware v{version_str} for {}...",
board.name()
);
}
let data = releases
.download_firmware_async(&version, board, mcu)
.await?;
check_firmware_size(options, &data)?;
Ok((data, version, version_str))
}
pub async fn build_rom_image(
options: &Options,
config_json: &str,
version: FirmwareVersion,
board: Board,
mcu: Variant,
) -> Result<(FirmwareProperties, Option<Vec<u8>>, Option<Vec<u8>>, String), Error> {
let mut builder =
Builder::from_json(version, mcu.family(), config_json).map_err(onerom_fw::Error::parse)?;
for license in builder.licenses() {
accept_license(options, &license).await?;
builder
.accept_license(&license)
.map_err(onerom_fw::Error::license)?;
}
get_rom_files_async(&mut builder).await?;
let fw_props = FirmwareProperties::new(version, board, mcu, ServeAlg::default(), true)?;
let (metadata, image_data) = builder.build(fw_props).map_err(onerom_fw::Error::build)?;
let metadata = if metadata.is_empty() {
None
} else {
Some(metadata)
};
let image_data = if image_data.is_empty() {
None
} else {
Some(image_data)
};
let desc = builder.description();
Ok((fw_props, metadata, image_data, desc))
}
fn check_build_args(
_options: &Options,
args: &args::firmware::FirmwareBuildArgs,
) -> Result<(), Error> {
if !args.no_config && args.config_file.is_none() && args.slot.is_empty() {
return Err(Error::InvalidArgument(
"build".to_string(),
"Either --config-file or --slot must be specified unless --no-config is set"
.to_string(),
));
}
if args.no_config && (!args.slot.is_empty() || args.config_file.is_some()) {
return Err(Error::InvalidArgument(
"build".to_string(),
"--no-config cannot be used with --slot or --config-file".to_string(),
));
}
Ok(())
}
pub async fn cmd_build(
options: &Options,
args: &args::firmware::FirmwareBuildArgs,
) -> Result<(), Error> {
check_build_args(options, args)?;
let board = resolve_board(options, &args.board)?.ok_or(Error::NoBoardOrDevice)?;
let mcu = Variant::RP2350;
if !args.slot.is_empty() {
let confirmations = check_slot_confirmations(&args.slot, &board)?;
confirm_slot_overrides(options, &confirmations).await?;
}
let (firmware_data, version, version_str) =
acquire_firmware(options, &args.base_firmware, &args.version, &board, &mcu).await?;
let plugins = resolve_plugins(&parse_plugin_specs(&args.plugin)?, Some(version)).await?;
if options.verbose {
for plugin in &plugins {
println!(
"Resolved plugin: {}/{} v{} ({})",
plugin.plugin_type.short(),
plugin.name,
plugin.version,
plugin.file,
);
}
}
let config_json = resolve_config_json(
args.config_file.as_deref(),
&args.slot,
args.no_config,
&board,
args.config_name.as_deref(),
args.config_description.as_deref(),
&plugins,
)?;
if let Some(path) = &args.save_config {
save_config(path, &config_json)?;
if options.verbose {
println!("Saved ROM configuration to {path}");
}
}
let (fw_props, metadata, image_data, desc) =
build_rom_image(options, &config_json, version, board, mcu).await?;
validate_sizes(&fw_props, &firmware_data, &metadata, &image_data)?;
let assembled = assemble_firmware(firmware_data, metadata, image_data)?;
let size = assembled.len();
verify_assembled_firmware(options, &assembled, args.force).await?;
let out = resolve_firmware_output(
&args.output,
&args.path,
&board,
Some(&version_str),
args.config_file.as_deref(),
);
std::fs::write(&out, &assembled).map_err(|e| Error::io(&out, e))?;
if options.verbose {
println!("Wrote {size} bytes to {out}");
if !desc.is_empty() {
println!("---\n{desc}");
}
} else {
if let Some(path) = &args.save_config {
println!("Firmware configuration written to {path}");
}
println!("Firmware written to {out}");
}
Ok(())
}
pub async fn accept_license(options: &Options, license: &License) -> Result<(), Error> {
let text = fetch_license_async(&license.url).await?;
println!("License required:");
println!("---");
println!("{text}");
println!("---");
if options.yes {
println!("Auto-accepted (--yes)");
return Ok(());
}
print!("Do you accept this license? (y/N): ");
std::io::stdout()
.flush()
.map_err(|e| Error::Other(e.to_string()))?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| Error::Other(e.to_string()))?;
match input.trim().to_lowercase().as_str() {
"y" | "yes" => Ok(()),
_ => Err(Error::LicenseNotAccepted),
}
}
pub async fn confirm_slot_overrides(
options: &Options,
confirmations: &ConfirmationsRequired,
) -> Result<(), Error> {
if confirmations.cpu_freq {
if options.yes {
println!("Auto-accepted above-stock CPU frequency (--yes)");
} else {
print!("One or more slots specify a CPU frequency above 150MHz. Continue? (y/N): ");
std::io::stdout()
.flush()
.map_err(|e| Error::Other(e.to_string()))?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| Error::Other(e.to_string()))?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
return Err(Error::AboveStockNotAccepted("CPU Frequency".to_string()));
}
}
}
if confirmations.vreg {
if options.yes {
println!("Auto-accepted above-stock vreg voltage (--yes)");
} else {
print!("One or more slots specify a vreg above 1.10V. Continue? (y/N): ");
std::io::stdout()
.flush()
.map_err(|e| Error::Other(e.to_string()))?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| Error::Other(e.to_string()))?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
return Err(Error::AboveStockNotAccepted("CPU VReg".to_string()));
}
}
}
Ok(())
}
pub async fn cmd_inspect(
options: &Options,
args: &args::firmware::FirmwareInspectArgs,
) -> Result<(), Error> {
let data = if let Some(file) = &args.firmware {
inspect_local_firmware(options, file)?
} else {
inspect_release_firmware(options, args).await?
};
if options.verbose {
println!("Firmware size: {} bytes", data.len());
}
let info = parse_firmware(&data).await?;
print_firmware_info(options, &info)
}
fn inspect_local_firmware(options: &Options, file: &str) -> Result<Vec<u8>, Error> {
if options.verbose {
println!("Inspecting local firmware: {file}");
}
std::fs::read(file).map_err(|e| Error::io(file, e))
}
async fn inspect_release_firmware(
options: &Options,
args: &args::firmware::FirmwareInspectArgs,
) -> Result<Vec<u8>, Error> {
let board = resolve_board(options, &args.board)?.ok_or(Error::NoBoardOrDevice)?;
let mcu = Variant::RP2350;
let releases = Releases::from_network_async().await?;
let release = resolve_release(&releases, &args.version)?;
let version = release.firmware_version()?;
if options.verbose {
println!(
"Downloading firmware v{} for {}...",
release.version,
board.name()
);
}
releases
.download_firmware_async(&version, &board, &mcu)
.await
.map_err(Error::from)
}
fn print_firmware_info(options: &Options, info: &SdrrInfo) -> Result<(), Error> {
if !info.parse_errors.is_empty() {
eprintln!("Warning: firmware parsed with errors:");
for error in &info.parse_errors {
eprintln!(" {error}");
}
eprintln!();
}
if options.verbose {
let json = serde_json::to_string_pretty(&info).map_err(|e| Error::Other(e.to_string()))?;
println!("---");
println!("{json}");
} else {
println!("Version: {}", info.version);
if let Some(hw_rev) = &info.hw_rev {
println!("Hardware: {hw_rev}");
}
println!("MCU: {:?}", info.stm_line);
println!("Slots: {}", info.rom_set_count);
for (i, set) in info.rom_sets.iter().enumerate() {
println!(" Slot {i}: {} ROM(s), {} bytes", set.rom_count, set.size);
for (j, rom) in set.roms.iter().enumerate() {
let name = rom.filename.as_deref().unwrap_or("<unnamed>");
println!(" ROM {j}: {} {name}", rom.rom_type);
}
}
}
Ok(())
}
pub async fn cmd_releases(
options: &Options,
args: &args::firmware::FirmwareReleasesArgs,
) -> Result<(), Error> {
let board = if args.all {
trace!("Showing all releases (including those for attached device if present)");
None
} else {
trace!("Resolving board to filter releases");
resolve_board(options, &args.board)?
};
debug!("Resolved board for releases: {board:?}");
let releases = Releases::from_network_async().await?;
let filtered = filter_releases(&releases, board.as_ref());
if filtered.is_empty() {
println!("No releases found.");
return Ok(());
}
print_releases(options, &releases, &filtered, board.as_ref())
}
fn filter_releases(releases: &Releases, board: Option<&Board>) -> Vec<Release> {
if let Some(board) = board {
releases
.releases()
.iter()
.filter(|r| {
r.boards
.iter()
.any(|b| b.name == board.name().to_ascii_lowercase())
})
.cloned()
.collect()
} else {
releases.releases().clone()
}
}
fn print_releases(
options: &Options,
releases: &Releases,
filtered: &[Release],
board: Option<&Board>,
) -> Result<(), Error> {
if let Some(board) = board {
println!("Available firmware releases for {}:", board.name());
} else {
println!("Available firmware releases:");
}
for r in filtered {
let latest = if r.version == releases.latest() {
" (latest)"
} else {
""
};
println!(" v{}{latest}", r.version);
if options.verbose {
let boards = r
.boards
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>()
.join(", ");
if let Some(board) = board {
let url = r.url(board, &Variant::RP2350)?;
println!(" Location: {url}");
}
println!(" Supported boards: {boards}");
}
}
Ok(())
}
pub async fn cmd_chips(
options: &Options,
args: &args::firmware::FirmwareChipsArgs,
) -> Result<(), Error> {
let board = if args.all {
None
} else {
resolve_board(options, &args.board)?
};
if let Some(board) = board {
print_chips_for_board(&board);
} else {
print_all_chips();
}
Ok(())
}
fn print_plugin_chips() {
println!("Supported plugin types:");
let names_str = CHIP_TYPE_NAMES_PLUGINS.join(", ");
println!(" {names_str}");
}
fn print_chips_for_board(board: &Board) {
println!("Supported chip types for {}:", board.name());
let names = board.supported_chip_type_names();
if names.is_empty() {
println!(" (none)");
} else {
let names_str = names.join(", ");
println!(" {names_str}");
}
print_plugin_chips();
}
fn print_all_chips() {
for pins in [24u8, 28, 32, 40] {
if let Some(names) = chip_type_names_for_pins(pins) {
println!("Supported {pins}-pin chips:");
let names_str = names.join(", ");
println!(" {names_str}");
}
}
print_plugin_chips();
}
pub async fn cmd_download(
options: &Options,
args: &args::firmware::FirmwareDownloadArgs,
) -> Result<(), Error> {
let board = resolve_board(options, &args.board)?.ok_or(Error::NoBoardOrDevice)?;
let mcu = Variant::RP2350;
let releases = Releases::from_network_async().await?;
let release = resolve_release(&releases, &args.version)?;
let version = release.firmware_version()?;
if options.verbose {
println!(
"Downloading firmware v{} for {}...",
release.version,
board.name()
);
}
let data = releases
.download_firmware_async(&version, &board, &mcu)
.await?;
check_firmware_size(options, &data)?;
let out = resolve_firmware_output(
&args.output,
&args.path,
&board,
Some(&release.version),
None,
);
std::fs::write(&out, &data).map_err(|e| Error::io(&out, e))?;
if options.verbose {
println!("Written {} bytes to {}", data.len(), out);
} else {
println!("Firmware downloaded to {out}");
}
Ok(())
}
fn parse_plugin_specs(raw: &[String]) -> Result<Vec<PluginSpec>, Error> {
onerom_cli::plugin::parse_plugins(raw)
}