use crate::Error;
use onerom_config::hw::Board;
const HEADER_HINT: &str =
"Run 'onerom inspect header' to see which GPIO is behind each header pad.";
const NAMESPACE_HINT: &str = "--pin takes an MCU GPIO, written 'gpio<N>' - for example 'gpio23' - or a header pad name: 'sel_a'..'sel_e', 'x1' or 'x2'.";
const NO_PIN: u8 = 255;
const MAX_SELECT_PAD: u8 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Pad {
Select(u8),
X1,
X2,
}
impl Pad {
fn gpio_on(&self, board: &Board) -> Option<u8> {
let gpio = match self {
Pad::Select(index) => board.sel_pins().get(*index as usize).copied()?,
Pad::X1 => board.pin_x1(),
Pad::X2 => board.pin_x2(),
};
(gpio != NO_PIN).then_some(gpio)
}
fn all_on(board: &Board) -> Vec<String> {
let mut pads: Vec<String> = (0..board.sel_pins().len())
.map(|index| Pad::Select(index as u8).to_string())
.collect();
for pad in [Pad::X1, Pad::X2] {
if pad.gpio_on(board).is_some() {
pads.push(pad.to_string());
}
}
pads
}
}
impl std::fmt::Display for Pad {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Pad::Select(index) => write!(f, "sel_{}", (b'a' + index) as char),
Pad::X1 => write!(f, "x1"),
Pad::X2 => write!(f, "x2"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Pin {
Gpio(u8),
Pad(Pad),
}
impl Pin {
pub fn resolve(&self, board: Option<&Board>) -> Result<ResolvedPin, Error> {
let gpio = match self {
Pin::Gpio(gpio) => *gpio,
Pin::Pad(pad) => {
let Some(board) = board else {
return Err(Error::InvalidPin(
pad.to_string(),
format!(
"'{pad}' is a header pad, and which GPIO sits behind a pad depends on the board.\n \
This One ROM's board type could not be determined.\n \
Pass --board <BOARD>, or name the MCU GPIO directly as 'gpio<N>'."
),
));
};
pad.gpio_on(board).ok_or_else(|| {
Error::InvalidPin(
pad.to_string(),
format!(
"Board {} has no '{pad}' pad.\n Its header pads are: {}.\n {HEADER_HINT}",
board.name(),
Pad::all_on(board).join(", "),
),
)
})?
}
};
Ok(ResolvedPin { pin: *self, gpio })
}
}
impl std::fmt::Display for Pin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Pin::Gpio(gpio) => write!(f, "gpio{gpio}"),
Pin::Pad(pad) => write!(f, "{pad}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedPin {
pin: Pin,
gpio: u8,
}
impl ResolvedPin {
pub fn gpio(&self) -> u8 {
self.gpio
}
pub fn pin(&self) -> Pin {
self.pin
}
}
impl std::fmt::Display for ResolvedPin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.pin {
Pin::Gpio(gpio) => write!(f, "gpio{gpio}"),
Pin::Pad(pad) => write!(f, "{pad} (gpio{})", self.gpio),
}
}
}
fn parse_select_pad(name: &str) -> Option<Pad> {
let rest = name.strip_prefix("sel")?;
let letter = rest
.strip_prefix('_')
.or_else(|| rest.strip_prefix('-'))
.unwrap_or(rest);
let [letter] = letter.as_bytes() else {
return None;
};
let index = letter.checked_sub(b'a')?;
(index <= MAX_SELECT_PAD).then_some(Pad::Select(index))
}
fn parse_pad(name: &str) -> Option<Pad> {
match name {
"x1" => Some(Pad::X1),
"x2" => Some(Pad::X2),
_ => parse_select_pad(name),
}
}
fn is_address_pad_name(name: &str) -> bool {
name.strip_prefix('a')
.is_some_and(|digits| !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()))
}
fn is_not_a_gpio_name(name: &str) -> bool {
matches!(name, "run" | "bootsel" | "swclk" | "swdio")
}
pub fn parse_pin(spec: &str) -> Result<Pin, Error> {
let trimmed = spec.trim();
let name = trimmed.to_ascii_lowercase();
let invalid = |detail: String| Err(Error::InvalidPin(trimmed.to_string(), detail));
if name.is_empty() {
return invalid(format!(
"No pin given.\n {NAMESPACE_HINT}\n {HEADER_HINT}"
));
}
if let Some(digits) = name.strip_prefix("gpio") {
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return invalid(
"'gpio' must be followed by a GPIO number - for example 'gpio23'.".to_string(),
);
}
return match digits.parse::<u8>() {
Ok(gpio) => Ok(Pin::Gpio(gpio)),
Err(_) => invalid(format!(
"GPIO number '{digits}' is out of range - GPIO numbers are 0 to 255."
)),
};
}
if let Some(pad) = parse_pad(&name) {
return Ok(Pin::Pad(pad));
}
if name.bytes().all(|b| b.is_ascii_digit()) {
return invalid(format!(
"A bare number is ambiguous: it could be an MCU GPIO, an image-select pad, an X pad or a ROM socket pin.\n Write an MCU GPIO as 'gpio{name}'.\n {HEADER_HINT}"
));
}
if is_address_pad_name(&name) {
return invalid(format!(
"'{name}' is a broken-out address line, which --pin does not accept.\n \
--pin takes an MCU GPIO ('gpio<N>') or a header pad ('sel_a'..'sel_e', 'x1', 'x2').\n \
Use the MCU GPIO behind the pad, written 'gpio<N>'.\n \
{HEADER_HINT}"
));
}
if is_not_a_gpio_name(&name) {
return invalid(format!(
"'{name}' is not a GPIO - it is a dedicated MCU pin and cannot be driven.\n {NAMESPACE_HINT}"
));
}
invalid(format!(
"Unrecognised pin.\n {NAMESPACE_HINT}\n {HEADER_HINT}"
))
}
#[cfg(test)]
mod tests {
use super::*;
fn rejection(spec: &str) -> String {
match parse_pin(spec) {
Ok(pin) => panic!("'{spec}' should not parse, but gave {pin}"),
Err(e) => e.to_string(),
}
}
fn resolve_rejection(spec: &str, board: Option<&Board>) -> String {
let pin = parse_pin(spec).expect("parses");
match pin.resolve(board) {
Ok(resolved) => panic!("'{spec}' should not resolve, but gave {resolved}"),
Err(e) => e.to_string(),
}
}
fn resolved(spec: &str, board: &Board) -> u8 {
parse_pin(spec)
.expect("parses")
.resolve(Some(board))
.expect("resolves")
.gpio()
}
fn board(name: &str) -> Board {
Board::try_from_str(name).unwrap_or_else(|| panic!("{name} is a known board"))
}
#[test]
fn gpio_names_parse() {
assert_eq!(parse_pin("gpio0").expect("parses"), Pin::Gpio(0));
assert_eq!(parse_pin("gpio23").expect("parses"), Pin::Gpio(23));
assert_eq!(parse_pin("gpio47").expect("parses"), Pin::Gpio(47));
assert_eq!(parse_pin("gpio255").expect("parses"), Pin::Gpio(255));
}
#[test]
fn gpio_names_are_case_and_whitespace_insensitive() {
assert_eq!(parse_pin("GPIO23").expect("parses"), Pin::Gpio(23));
assert_eq!(parse_pin("Gpio23").expect("parses"), Pin::Gpio(23));
assert_eq!(parse_pin(" gpio23 ").expect("parses"), Pin::Gpio(23));
}
#[test]
fn a_gpio_needs_no_board() {
let resolved = parse_pin("gpio23")
.expect("parses")
.resolve(None)
.expect("resolves without a board");
assert_eq!(resolved.gpio(), 23);
assert_eq!(resolved.to_string(), "gpio23");
assert_eq!(resolved.pin(), Pin::Gpio(23));
}
#[test]
fn pad_names_parse_in_every_accepted_spelling() {
for spec in ["sel_a", "sel-a", "sela", "SEL_A", "Sel-A", " sela "] {
assert_eq!(
parse_pin(spec).expect("parses"),
Pin::Pad(Pad::Select(0)),
"{spec}"
);
}
assert_eq!(
parse_pin("sel_e").expect("parses"),
Pin::Pad(Pad::Select(4))
);
assert_eq!(parse_pin("x1").expect("parses"), Pin::Pad(Pad::X1));
assert_eq!(parse_pin("X2").expect("parses"), Pin::Pad(Pad::X2));
}
#[test]
fn a_bare_select_letter_is_not_a_pad_name() {
for spec in ["a", "b", "c", "d", "e"] {
let msg = rejection(spec);
assert!(msg.contains("Unrecognised pin"), "{spec}: {msg}");
}
}
#[test]
fn a_pin_displays_as_it_is_written() {
assert_eq!(Pin::Gpio(23).to_string(), "gpio23");
assert_eq!(Pin::Pad(Pad::Select(0)).to_string(), "sel_a");
assert_eq!(Pin::Pad(Pad::Select(4)).to_string(), "sel_e");
assert_eq!(Pin::Pad(Pad::X1).to_string(), "x1");
assert_eq!(Pin::Pad(Pad::X2).to_string(), "x2");
}
#[test]
fn a_four_select_board_with_x_pads_resolves_every_pad() {
let b = board("fire-24-f");
assert_eq!(resolved("sel_a", &b), b.sel_pins()[0]);
assert_eq!(resolved("sel_b", &b), b.sel_pins()[1]);
assert_eq!(resolved("sel_c", &b), b.sel_pins()[2]);
assert_eq!(resolved("sel_d", &b), b.sel_pins()[3]);
assert_eq!(resolved("x1", &b), b.pin_x1());
assert_eq!(resolved("x2", &b), b.pin_x2());
assert_eq!(b.sel_pins().len(), 4);
}
#[test]
fn a_five_select_board_resolves_sel_e() {
let b = board("ice-24-g");
assert_eq!(b.sel_pins().len(), 5);
assert_eq!(resolved("sel_e", &b), b.sel_pins()[4]);
assert!(b.jumper_header().is_none());
assert_eq!(resolved("x1", &b), b.pin_x1());
}
#[test]
fn a_board_without_x_pads_says_so_and_names_what_it_has() {
let b = board("fire-32-a");
assert_eq!(b.pin_x1(), NO_PIN);
let msg = resolve_rejection("x1", Some(&b));
assert!(msg.contains("has no 'x1' pad"), "{msg}");
assert!(msg.contains("fire-32-a"), "{msg}");
assert!(msg.contains("sel_a, sel_b, sel_c, sel_d"), "{msg}");
assert!(!msg.contains("x1,"), "{msg}");
assert!(msg.contains("onerom inspect header"), "{msg}");
assert_eq!(resolved("sel_d", &b), b.sel_pins()[3]);
}
#[test]
fn a_board_with_fewer_select_pads_says_so() {
let b = board("fire-28-a");
assert_eq!(b.sel_pins().len(), 2);
let msg = resolve_rejection("sel_c", Some(&b));
assert!(msg.contains("has no 'sel_c' pad"), "{msg}");
assert!(msg.contains("Its header pads are: sel_a, sel_b."), "{msg}");
let four = board("fire-24-f");
let msg = resolve_rejection("sel_e", Some(&four));
assert!(msg.contains("has no 'sel_e' pad"), "{msg}");
assert!(msg.contains("sel_a, sel_b, sel_c, sel_d, x1, x2"), "{msg}");
}
#[test]
fn a_pad_without_a_board_points_at_the_board_option() {
for spec in ["sel_a", "x1"] {
let msg = resolve_rejection(spec, None);
assert!(msg.contains("depends on"), "{spec}: {msg}");
assert!(
msg.contains("board type could not be determined"),
"{spec}: {msg}"
);
assert!(msg.contains("--board"), "{spec}: {msg}");
assert!(msg.contains("'gpio<N>'"), "{spec}: {msg}");
}
}
#[test]
fn a_resolved_pad_shows_both_names() {
let b = board("fire-24-f");
let resolved = parse_pin("x1")
.expect("parses")
.resolve(Some(&b))
.expect("resolves");
assert_eq!(resolved.to_string(), format!("x1 (gpio{})", b.pin_x1()));
assert_eq!(resolved.pin(), Pin::Pad(Pad::X1));
}
#[test]
fn a_bare_number_names_the_namespaces_it_is_ambiguous_between() {
let msg = rejection("23");
assert!(msg.contains("ambiguous"), "{msg}");
assert!(msg.contains("image-select pad"), "{msg}");
assert!(msg.contains("X pad"), "{msg}");
assert!(msg.contains("ROM socket pin"), "{msg}");
assert!(msg.contains("'gpio23'"), "{msg}");
assert!(msg.contains("onerom inspect header"), "{msg}");
assert!(!msg.contains("Assuming"), "{msg}");
}
#[test]
fn address_pad_names_are_refused_with_a_reason_and_no_forecast() {
for spec in ["a0", "a13", "A17"] {
let msg = rejection(spec);
assert!(msg.contains("broken-out address line"), "{spec}: {msg}");
assert!(msg.contains("does not accept"), "{spec}: {msg}");
assert!(msg.contains("'gpio<N>'"), "{spec}: {msg}");
for forecast in ["not yet", "yet supported", "now or", "later", "never"] {
assert!(!msg.contains(forecast), "{spec} says '{forecast}': {msg}");
}
}
}
#[test]
fn dedicated_pins_say_they_are_not_gpios() {
for spec in ["run", "bootsel", "swclk", "swdio", "RUN", "BootSel"] {
let msg = rejection(spec);
assert!(msg.contains("is not a GPIO"), "{spec}: {msg}");
assert!(!msg.contains("not yet supported"), "{spec}: {msg}");
}
}
#[test]
fn unrecognised_names_teach_the_namespace() {
for spec in [
"banana", "pin23", "sel_f", "sel_", "gpio-1", "gpio 23", "d3", "cs1",
] {
let msg = rejection(spec);
assert!(
msg.contains("gpio<N>") || msg.contains("gpio23"),
"{spec}: {msg}"
);
}
assert!(rejection("sel_f").contains("'sel_a'..'sel_e'"));
}
#[test]
fn a_malformed_gpio_name_says_what_is_missing() {
for spec in ["gpio", "gpiox", "gpio1a", "gpio-1", "gpio 1", "gpio0x10"] {
let msg = rejection(spec);
assert!(msg.contains("gpio"), "{spec}: {msg}");
}
assert!(
rejection("gpio").contains("must be followed by a GPIO number"),
"{}",
rejection("gpio")
);
}
#[test]
fn a_gpio_number_too_large_for_a_u8_is_rejected() {
let msg = rejection("gpio256");
assert!(msg.contains("out of range"), "{msg}");
assert!(msg.contains("0 to 255"), "{msg}");
}
#[test]
fn an_empty_pin_is_rejected() {
assert!(rejection("").contains("No pin given"));
assert!(rejection(" ").contains("No pin given"));
}
#[test]
fn every_rejection_quotes_what_was_typed() {
for spec in ["23", "a17", "run", "banana", "gpio", "GPIO256"] {
let msg = rejection(spec);
assert!(msg.contains(spec), "{spec}: {msg}");
}
}
#[test]
fn every_board_resolves_every_pad_it_reports() {
for b in onerom_config::hw::BOARDS {
for pad in Pad::all_on(&b) {
let pin = parse_pin(&pad).unwrap_or_else(|e| panic!("{}: {pad}: {e}", b.name()));
pin.resolve(Some(&b))
.unwrap_or_else(|e| panic!("{}: {pad}: {e}", b.name()));
}
}
}
}