use proc_macro2::TokenStream;
use quote::quote;
use regex::{Captures, Regex};
use rpk_config::{
compiler::{compile, KeyboardConfig},
ConfigError,
};
use std::{
borrow::Cow,
env,
fmt::Display,
fs,
path::{Path, PathBuf},
};
use syn::Lit;
#[derive(Debug)]
struct BuildError(String);
impl Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for BuildError {}
impl BuildError {
fn compile_err(err: ConfigError, source_file: &Path, source: &str) -> Self {
Self(err.long_format(source_file, source))
}
fn from_str(msg: &str) -> Self {
Self(msg.to_owned())
}
fn from_error(msg: &impl std::error::Error) -> Self {
Self(msg.to_string())
}
}
impl From<std::io::Error> for BuildError {
fn from(err: std::io::Error) -> Self {
Self(err.to_string())
}
}
impl From<&str> for BuildError {
fn from(err: &str) -> Self {
Self(err.to_string())
}
}
type Result<T> = std::result::Result<T, BuildError>;
fn compile_error(message: &str) -> TokenStream {
quote! {
compile_error!(#message);
}
}
pub(crate) fn configure_keyboard(input: TokenStream) -> TokenStream {
match get_config_filename(input) {
Ok(source_file) => match quote_conf(&source_file) {
Ok(s) => s,
Err(err) => compile_error(err.0.as_str()),
},
Err(err) => compile_error(err.0.as_str()),
}
}
fn get_config_filename(input: TokenStream) -> Result<PathBuf> {
const LAYOUT: &str = "default-layout.rpk.conf";
let cargo = cargo_dir()?;
if input.is_empty() {
return Ok(cargo.join(LAYOUT));
}
let ast: syn::Expr = syn::parse2(input.clone()).map_err(|e| BuildError::from_error(&e))?;
if let syn::Expr::Lit(expr) = &ast {
if let Lit::Str(lit) = &expr.lit {
return Ok(cargo.join(lit.value()));
}
}
Err(BuildError(
"Expected filename or nothing as argument".to_owned(),
))
}
fn cargo_dir() -> Result<PathBuf> {
Ok(PathBuf::from(
env::var_os("CARGO_MANIFEST_DIR")
.ok_or_else(|| BuildError::from_str("CARGO_MANIFEST_DIR not found"))?,
))
}
fn quote_conf(source_file: &Path) -> Result<TokenStream> {
let source = read_conf(source_file)?;
let config = compile(source.as_str())
.map_err(|e| BuildError::compile_err(e, source_file, source.as_str()))?;
let chip = config
.firmware_get("chip")
.ok_or_else(|| BuildError::from_str("Missing firmware.chip"))?;
let use_statments = build_use_statments(chip)?;
let (defs, input_pins, output_pins) = parse_firmware(&config)?;
let macros = quote! {
macro_rules! config_matrix_pins_rp {
(peripherals: $p:ident, input: [$($in_pin:ident), *], output: [$($out_pin:ident), +]) => {
{
let mut output_pins = [$(Output::new(AnyPin::from($p.$out_pin), gpio::Level::High)), +];
let input_pins = [$(Input::new(AnyPin::from($p.$in_pin), gpio::Pull::Up)), +];
output_pins.iter_mut().for_each(|p| {
p.set_high();
});
(input_pins, output_pins)
}
};
}
macro_rules! config_pins {
(peripherals: $p:ident) => {
config_matrix_pins_rp!(peripherals: $p,
input: #input_pins, output: #output_pins)
};
}
macro_rules! run_keyboard {
($spawner:expr, $driver:expr, $input_pins:expr, $output_pins:expr, $flash:expr) => {
let flash: &'static mut Flash = FLASH.init($flash);
let fs: &'static Rfs = RFS.init(Rfs::new(flash).unwrap());
let builder = rpk_builder::KeyboardBuilder::new(
VENDOR_ID,
PRODUCT_ID,
fs,
$driver,
$input_pins,
$output_pins,
LAYOUT_MAPPING,
)
.reset(&reset)
.reset_to_usb_boot(&reset_to_usb_boot)
.manufacturer(MANUFACTURER)
.product(PRODUCT)
.serial_number(SERIAL_NUMBER)
.max_power(MAX_POWER);
let keyboard = builder.build::<ROW_IS_OUTPUT, LAYOUT_MAX>();
keyboard.run($spawner).await;
};
}
};
let source_file = source_file.display().to_string();
let result = quote! {
#use_statments
#defs
#macros
const _: &[u8] = include_bytes!(#source_file);
type Flash = flash::Flash<'static, FLASH, Async, FLASH_SIZE>;
type Rfs = NorflashRingFs<'static, Flash, FS_BASE, FS_SIZE,
{ flash::ERASE_SIZE as u32 }, { flash::PAGE_SIZE }, >;
static FLASH: StaticCell<Flash> = StaticCell::new();
static RFS: StaticCell<Rfs> = StaticCell::new();
};
Ok(result)
}
fn read_conf(source_file: &Path) -> Result<String> {
let source = fs::read_to_string(source_file).map_err(|e| {
BuildError(format!(
"Can't read conf file {}, {e:?}",
&source_file.display()
))
})?;
Ok(source)
}
fn build_use_statments(chip: &str) -> Result<TokenStream> {
if chip != "rp2040" {
return Err(BuildError(format!("Unsupported chipset {chip}")));
}
Ok(quote! {
use rpk_builder::rp::{
gpio,
bind_interrupts, flash,
flash::Async,
usb::{Driver, InterruptHandler},
};
use rpk_builder::rp::gpio::{AnyPin, Input, Output};
use rpk_builder::rp::peripherals::{FLASH, USB};
use rpk_builder::StaticCell;
use rpk_builder::norflash_ring_fs::NorflashRingFs;
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => InterruptHandler<USB>;
});
})
}
fn parse_firmware(config: &KeyboardConfig) -> Result<(TokenStream, TokenStream, TokenStream)> {
let vre = Regex::new(r"([a-zA-Z_]+)").map_err(|e| BuildError(e.to_string()))?;
let get_var = |v: &str| {
let v = config
.firmware_get(v)
.ok_or_else(|| BuildError(format!("Missing required firmware config: {}", v)))?;
let v = vre.replace_all(v, |caps: &Captures| {
if let Some(v) = config.firmware_get(&caps[1]) {
v.to_owned()
} else {
caps[0].to_owned()
}
});
Ok::<Cow<'_, str>, BuildError>(v)
};
macro_rules! get {
($f:tt) => {
let $f = get_var(stringify!($f))?;
};
}
macro_rules! parse {
($f:tt) => {
get!($f);
let $f = $f
.parse::<TokenStream>()
.map_err(|e| BuildError(format!("Error parsing {}: {e}", $f)))?;
};
}
let bin = config.serialize();
let layout_mapping = format!("{{ const M: [u16; {}] = {:?}; &M }}", bin.len(), bin)
.parse::<TokenStream>()
.map_err(|e| BuildError(e.to_string()))?;
parse!(vendor_id);
parse!(product_id);
parse!(row_is_output);
parse!(max_layout_size);
parse!(flash_size);
parse!(fs_base);
parse!(fs_size);
parse!(input_pins);
parse!(output_pins);
get!(manufacturer);
get!(product);
get!(serial_number);
parse!(max_power);
get!(chip);
let reset = if chip == "rp2040" {
quote! {
fn reset() {
cortex_m::peripheral::SCB::sys_reset()
}
fn reset_to_usb_boot() {
rpk_builder::rp::rom_data::reset_to_usb_boot(0, 0);
#[allow(clippy::empty_loop)]
loop {
}
}
}
} else {
quote! {
fn reset() {}
fn reset_to_usb_boot {}
}
};
Ok((
quote! {
const LAYOUT_MAPPING: &[u16] = #layout_mapping;
const VENDOR_ID: u16 = #vendor_id;
const PRODUCT_ID: u16 = #product_id;
const ROW_IS_OUTPUT: bool = #row_is_output;
const LAYOUT_MAX: usize = #max_layout_size;
const FLASH_SIZE: usize = #flash_size;
const FS_BASE: usize = #fs_base;
const FS_SIZE: usize = #fs_size;
const MANUFACTURER: &str = #manufacturer;
const PRODUCT: &str = #product;
const SERIAL_NUMBER: &str = #serial_number;
const MAX_POWER: u16 = #max_power;
#reset
},
input_pins,
output_pins,
))
}
#[cfg(test)]
#[path = "build_test.rs"]
mod test;