use crate::Error;
use crate::plugin::{ResolvedPlugin, plugin_to_chip_set_config};
use onerom_config::chip::{CHIP_TYPE_NAMES_PLUGINS, ChipFunction, ChipType, ControlLineType};
use onerom_config::hw::Board;
use onerom_gen::{
ChipConfig, ChipSetConfig, ChipSetType, Config, CsLogic, FireConfig, FireCpuFreq, FireVreg,
FirmwareConfig, LedConfig, SizeHandling,
};
const DEFAULT_CONFIG_DESCRIPTION: &str = "Created by the One ROM CLI";
pub struct ConfirmationsRequired {
pub cpu_freq: bool,
pub vreg: bool,
}
pub fn check_confirmations(slots: &[SlotSpec]) -> ConfirmationsRequired {
ConfirmationsRequired {
cpu_freq: slots.iter().any(|s| {
s.cpu_freq
.map(|f| f > FireCpuFreq::stock_value())
.unwrap_or(false)
}),
vreg: slots.iter().any(|s| {
s.vreg
.as_ref()
.map(|v| *v > FireVreg::stock_value())
.unwrap_or(false)
}),
}
}
pub fn check_slot_confirmations(
slots: &[String],
board: &Board,
) -> Result<ConfirmationsRequired, Error> {
let parsed = parse_slots(slots, board)?;
Ok(check_confirmations(&parsed))
}
fn expand_tilde(path: &str) -> std::borrow::Cow<'_, str> {
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
format!("{}/{}", home.to_string_lossy(), rest).into()
} else {
path.into()
}
}
pub struct SlotSpec {
pub file: Option<String>,
pub label: Option<String>,
pub chip_type: ChipType,
pub cs1: Option<CsLogic>,
pub cs2: Option<CsLogic>,
pub cs3: Option<CsLogic>,
size_handling: Option<SizeHandling>,
pub cpu_freq: Option<FireCpuFreq>,
pub vreg: Option<FireVreg>,
pub led: Option<bool>,
pub force_16bit: Option<bool>,
}
fn parse_cs_logic(slot: &str, key: &str, value: &str) -> Result<CsLogic, Error> {
match value {
"active_low" | "0" => Ok(CsLogic::ActiveLow),
"active_high" | "1" => Ok(CsLogic::ActiveHigh),
other => Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid CS logic '{other}': expected {key}=active_low|active_high|0|1\n --slot '{slot}'"
),
)),
}
}
fn parse_size_handling(slot: &str, _key: &str, value: &str) -> Result<SizeHandling, Error> {
serde_json::from_str::<SizeHandling>(&format!("\"{value}\"")).map_err(|_| {
let supported_variants = SizeHandling::supported_values()
.iter()
.map(|v| {
serde_json::to_string(v)
.unwrap()
.trim_matches('"')
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid size_handling '{value}'\n --slot '{slot}'\n Supported values: {supported_variants}"
),
)
})
}
fn parse_bool(slot: &str, key: &str, value: &str) -> Result<bool, Error> {
match value.to_lowercase().as_str() {
"true" | "on" | "1" => Ok(true),
"false" | "off" | "0" => Ok(false),
other => Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid boolean '{other}': expected {key}=true|false|on|off|1|0\n --slot '{slot}'"
),
)),
}
}
fn parse_cpu_freq(slot: &str, key: &str, value: &str) -> Result<FireCpuFreq, Error> {
let digits = if value.to_lowercase().ends_with("mhz") {
&value[..value.len() - 3]
} else {
value
};
let mhz = digits.parse::<u16>().map_err(|_| {
Error::InvalidArgument(
"--slot".to_string(),
format!("Invalid CPU frequency '{value}': expected formats {key}=150|150MHz\n --slot '{slot}'"),
)
})?;
FireCpuFreq::mhz(mhz).map_err(|_| {
Error::InvalidArgument(
"--slot".to_string(),
format!(
"CPU frequency {mhz}MHz out of range ({}-{}MHz)\n --slot '{slot}'",
FireCpuFreq::MIN_MHZ,
FireCpuFreq::MAX_MHZ,
),
)
})
}
fn parse_vreg(slot: &str, key: &str, value: &str) -> Result<FireVreg, Error> {
let stripped = if value.ends_with('v') || value.ends_with('V') {
&value[..value.len() - 1]
} else {
value
};
let canonical = match stripped.split_once('.') {
Some((int, frac)) => {
let padded = format!("{frac:0<2}");
if padded.len() > 2 {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid VReg '{value}': too many decimal places, max 2\n --slot '{slot}'"
),
));
}
format!("{int}.{padded}V")
}
None => {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid VReg '{value}': expected format {key}=1.1|1.10|1.10V\n --slot '{slot}'"
),
));
}
};
serde_json::from_str::<FireVreg>(&format!("\"{canonical}\"")).map_err(|_| {
let levels = FireVreg::supported_levels()
.iter()
.map(|v| {
serde_json::to_string(v)
.unwrap()
.trim_matches('"')
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Unsupported VReg '{value}'\n --slot '{slot}'\n Supported levels: {levels}"
),
)
})
}
const SLOT_KEYS: &[&str] = &[
"file",
"label",
"type",
"cs1",
"cs2",
"cs3",
"size_handling",
"size",
"cpu-freq",
"cpu-vreg",
"led",
"force_16bit",
];
fn parse_slot(slot: &str, board: &Board) -> Result<SlotSpec, Error> {
let mut file = None;
let mut label = None;
let mut chip_type_str = None;
let mut cs1 = None;
let mut cs2 = None;
let mut cs3 = None;
let mut size_handling = None;
let mut cpu_freq = None;
let mut vreg = None;
let mut led = None;
let mut force_16bit = None;
let mut seen = std::collections::HashSet::new();
for part in slot.split(',') {
let (key, value) = part.split_once('=').ok_or_else(|| {
Error::InvalidArgument("--slot".to_string(), format!("Slot key '{part}' is missing a value - expected '{part}=<value>'\n --slot '{slot}'"))
})?;
let key = key.trim();
if !seen.insert(key) {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("Duplicate slot key '{key}' found.\n --slot '{slot}'"),
));
}
match key {
"file" | "path" | "url" => file = Some(expand_tilde(value).into_owned()),
"label" | "name" => label = Some(value.to_string()),
"type" | "rom-type" | "rom_type" | "chip_type" | "chip-type" => {
chip_type_str = Some(value.to_string())
}
"cs1" => cs1 = Some(parse_cs_logic(slot, key, value)?),
"cs2" => cs2 = Some(parse_cs_logic(slot, key, value)?),
"cs3" => cs3 = Some(parse_cs_logic(slot, key, value)?),
"size_handling" | "size" => {
size_handling = Some(parse_size_handling(slot, key, value)?)
}
"cpu" | "freq" | "frequency" | "cpu-freq" | "cpu_freq" | "cpu_frequency"
| "cpu-frequency" => cpu_freq = Some(parse_cpu_freq(slot, key, value)?),
"vreg" | "cpu-vreg" | "cpu_vreg" => vreg = Some(parse_vreg(slot, key, value)?),
"led" | "status_led" | "status-led" => led = Some(parse_bool(slot, key, value)?),
"16bit" | "force_16bit" | "force_16_bit" | "force-16bit" | "force-16-bit" => {
force_16bit = Some(parse_bool(slot, key, value)?)
}
other => {
let supported_keys = SLOT_KEYS.join(", ");
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Unrecognised slot key '{other}'\n --slot '{slot}'\n Supported keys: {supported_keys}"
),
));
}
}
}
let chip_type_str = chip_type_str.ok_or_else(|| {
Error::InvalidArgument(
"--slot".to_string(),
format!("slot missing 'type' key\n --slot '{slot}'"),
)
})?;
let chip_type = ChipType::try_from_str(&chip_type_str).ok_or_else(|| {
let supported = supported_chip_names_for_board(board);
Error::UnsupportedChipType(chip_type_str.clone(), supported)
})?;
if !board.supports_chip_type(chip_type) && !board.extra_chip_types().contains(&chip_type) {
let supported = supported_chip_names_for_board(board);
return Err(Error::UnsupportedBoardChipType(
chip_type.name().to_string(),
chip_type.aliases().join(", "),
supported,
));
}
if chip_type.chip_function() != ChipFunction::Ram && file.is_none() {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("Missing 'file' key for ROM chip.\n --slot '{slot}'"),
));
}
validate_cs_lines(slot, &chip_type, cs1, cs2, cs3)?;
if force_16bit.is_some() && board.chip_pins() != 40 {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("force_16bit is only valid on 40-pin boards\n --slot '{slot}'"),
));
}
Ok(SlotSpec {
file,
label,
chip_type,
cs1,
cs2,
cs3,
size_handling,
cpu_freq,
vreg,
led,
force_16bit,
})
}
fn validate_cs_lines(
slot: &str,
chip_type: &ChipType,
cs1: Option<CsLogic>,
cs2: Option<CsLogic>,
cs3: Option<CsLogic>,
) -> Result<(), Error> {
let cs_values = [
("cs1", cs1.is_some()),
("cs2", cs2.is_some()),
("cs3", cs3.is_some()),
];
for line in chip_type.control_lines() {
let supplied = cs_values
.iter()
.find(|(name, _)| *name == line.name)
.map(|(_, v)| *v)
.unwrap_or(false);
match line.line_type {
ControlLineType::Configurable if !supplied => {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} requires {} to be specified\n --slot '{slot}'",
chip_type.name(),
line.name
),
));
}
ControlLineType::FixedActiveLow if supplied => {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} has fixed active-low {}, do not specify it\n --slot '{slot}'",
chip_type.name(),
line.name
),
));
}
_ => {}
}
}
for (cs_name, has_val) in &cs_values {
if *has_val && !chip_type.control_lines().iter().any(|l| l.name == *cs_name) {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} has no {} line\n --slot '{slot}'",
chip_type.name(),
cs_name
),
));
}
}
Ok(())
}
pub fn supported_chip_names_for_board(board: &Board) -> String {
let mut names: Vec<&str> = board.supported_chip_type_names().to_vec();
names.extend_from_slice(CHIP_TYPE_NAMES_PLUGINS);
names.sort_unstable();
names.join(", ")
}
pub fn parse_slots(slots: &[String], board: &Board) -> Result<Vec<SlotSpec>, Error> {
slots.iter().map(|s| parse_slot(s, board)).collect()
}
fn slot_to_chip_config(slot: &SlotSpec) -> ChipConfig {
ChipConfig {
file: slot.file.clone().unwrap_or_default(),
license: None,
description: None,
chip_type: slot.chip_type,
cs1: slot.cs1,
cs2: slot.cs2,
cs3: slot.cs3,
size_handling: slot.size_handling.clone().unwrap_or_default(),
extract: None,
label: slot.label.clone(),
location: None,
}
}
fn slot_to_firmware_overrides(slot: &SlotSpec) -> Option<FirmwareConfig> {
let has_fire = slot.cpu_freq.is_some() || slot.vreg.is_some() || slot.force_16bit.is_some();
let has_led = slot.led.is_some();
if !has_fire && !has_led {
return None;
}
let fire = has_fire.then(|| FireConfig {
cpu_freq: slot.cpu_freq,
overclock: slot.cpu_freq.map(|f| f > FireCpuFreq::stock_value()),
vreg: slot.vreg.clone(),
force_16_bit: slot.force_16bit.unwrap_or(false),
..Default::default()
});
Some(FirmwareConfig {
ice: None,
fire,
led: slot.led.map(|enabled| LedConfig { enabled }),
swd: None,
serve_alg_params: None,
})
}
pub fn slots_to_config_json(
plugins: &[ResolvedPlugin],
slots: &[SlotSpec],
name: Option<&str>,
description: Option<&str>,
) -> Result<String, Error> {
let mut sorted_plugins: Vec<&ResolvedPlugin> = plugins.iter().collect();
sorted_plugins.sort_by_key(|p| p.plugin_type.slot_index());
let mut chip_sets: Vec<ChipSetConfig> = sorted_plugins
.iter()
.map(|p| plugin_to_chip_set_config(&p.file, p.plugin_type, p.size))
.collect::<Result<Vec<_>, _>>()?;
for slot in slots {
chip_sets.push(ChipSetConfig {
set_type: ChipSetType::Single,
description: None,
chips: vec![slot_to_chip_config(slot)],
serve_alg: None,
firmware_overrides: slot_to_firmware_overrides(slot),
});
}
let config = Config {
version: 1,
name: name.map(|s| s.to_string()),
description: description
.unwrap_or(DEFAULT_CONFIG_DESCRIPTION)
.to_string(),
detail: None,
chip_sets,
notes: None,
categories: None,
};
serde_json::to_string_pretty(&config).map_err(|e| Error::Other(e.to_string()))
}
pub fn save_config(path: &str, json: &str) -> Result<(), Error> {
std::fs::write(path, json).map_err(|e| Error::io(path, e))
}