use std::path::Path;
use std::collections::HashMap;
use crate::parser::kicad::KicadParser;
use crate::parser::schema::{Schematic, Component, Label, LabelType, Net};
use super::{CircuitAdapter, AdapterError};
use crate::ucs::{
UnifiedCircuitSchema, UcsComponent, UcsNet, UcsPin, UcsPosition,
CircuitMetadata, SourceCAD, ElectricalType, NetConnection, SignalType,
};
pub struct KicadAdapter;
impl KicadAdapter {
pub fn new() -> Self {
Self
}
fn convert_schematic(&self, schematic: Schematic) -> UnifiedCircuitSchema {
let mut ucs = UnifiedCircuitSchema {
metadata: CircuitMetadata {
project_name: schematic.filename
.replace(".kicad_sch", "")
.replace(".sch", ""),
source_cad: SourceCAD::KiCad,
cad_version: schematic.version,
source_file: Some(schematic.filename.clone()),
..Default::default()
},
components: Vec::new(),
nets: Vec::new(),
};
for component in schematic.components {
ucs.add_component(self.convert_component(component, false));
}
for power_symbol in schematic.power_symbols {
ucs.add_component(self.convert_component(power_symbol, true));
}
for net in schematic.nets {
ucs.add_net(self.convert_net(net));
}
self.add_nets_from_labels(&schematic.labels, &mut ucs);
ucs
}
fn convert_component(&self, component: Component, is_virtual: bool) -> UcsComponent {
let mut ucs_comp = UcsComponent {
ref_des: component.reference.clone(),
mpn: self.extract_mpn(&component),
value: if component.value.is_empty() { None } else { Some(component.value.clone()) },
footprint: component.footprint.clone(),
lib_id: Some(component.lib_id.clone()),
is_virtual,
pins: self.convert_pins(&component),
position: Some(UcsPosition::new(component.position.x, component.position.y)),
rotation: component.rotation,
attributes: self.convert_properties(&component.properties),
uuid: component.uuid.clone(),
};
if ucs_comp.mpn.is_none() {
ucs_comp.mpn = self.infer_mpn_from_value(&component.value, &component.lib_id);
}
ucs_comp
}
fn extract_mpn(&self, component: &Component) -> Option<String> {
let mpn_keys = ["MPN", "mpn", "Mpn", "Part Number", "PartNumber",
"Manufacturer Part Number", "Mfr Part", "P/N"];
for key in mpn_keys {
if let Some(mpn) = component.properties.get(key) {
if !mpn.is_empty() && mpn != "~" {
return Some(mpn.clone());
}
}
}
None
}
fn infer_mpn_from_value(&self, value: &str, lib_id: &str) -> Option<String> {
let value_upper = value.to_uppercase();
let ic_patterns = [
"STM32", "ESP32", "ATMEGA", "RP2040", "PIC", "MSP430",
"LM7805", "LM7812", "LM1117", "AMS1117", "LM317",
"CH340", "CP2102", "FT232", "NE555", "LM358", "LM324",
];
for pattern in ic_patterns {
if value_upper.contains(pattern) {
return Some(value.to_string());
}
}
let lib_upper = lib_id.to_uppercase();
if lib_upper.contains("MCU") || lib_upper.contains("MICROCONTROLLER")
|| lib_upper.contains("REGULATOR") || lib_upper.contains("INTERFACE") {
return Some(value.to_string());
}
None
}
fn convert_pins(&self, component: &Component) -> Vec<UcsPin> {
component.pins.iter().map(|pin| {
UcsPin {
number: pin.number.clone(),
name: None, electrical_type: ElectricalType::Unspecified,
connected_net: None,
position: None,
}
}).collect()
}
fn convert_properties(&self, properties: &HashMap<String, String>) -> HashMap<String, crate::ucs::AttributeValue> {
properties.iter()
.filter(|(k, v)| {
!["Reference", "Value", "Footprint", "Datasheet"].contains(&k.as_str())
&& !v.is_empty() && v.as_str() != "~"
})
.map(|(k, v)| {
(k.clone(), crate::ucs::AttributeValue::String(v.clone()))
})
.collect()
}
fn convert_net(&self, net: Net) -> UcsNet {
let signal_type = SignalType::from_net_name(&net.name);
let is_power_rail = matches!(signal_type, SignalType::Power | SignalType::Ground);
UcsNet {
net_name: net.name.clone(),
voltage_level: self.infer_voltage_from_name(&net.name),
is_power_rail,
signal_type,
connections: net.connections.iter().map(|c| {
NetConnection::new(&c.component_ref, &c.pin_number)
}).collect(),
attributes: HashMap::new(),
}
}
fn add_nets_from_labels(&self, labels: &[Label], ucs: &mut UnifiedCircuitSchema) {
for label in labels {
let net_name = match label.label_type {
LabelType::Global => label.text.clone(),
LabelType::Local => format!("Net-({})", label.text),
LabelType::Hierarchical => format!("Hier-{}", label.text),
};
if ucs.get_net(&net_name).is_none() {
let signal_type = SignalType::from_net_name(&net_name);
let is_power_rail = matches!(signal_type, SignalType::Power | SignalType::Ground);
ucs.add_net(UcsNet {
net_name,
voltage_level: self.infer_voltage_from_name(&label.text),
is_power_rail,
signal_type,
connections: Vec::new(),
attributes: HashMap::new(),
});
}
}
}
fn infer_voltage_from_name(&self, name: &str) -> Option<f64> {
let upper = name.to_uppercase();
if upper.contains("GND") || upper.contains("VSS") || upper == "0V" {
Some(0.0)
} else if upper.contains("3V3") || upper.contains("3.3V") {
Some(3.3)
} else if upper.contains("5V") && !upper.contains("12V") {
Some(5.0)
} else if upper.contains("12V") {
Some(12.0)
} else if upper.contains("1V8") || upper.contains("1.8V") {
Some(1.8)
} else if upper.contains("2V5") || upper.contains("2.5V") {
Some(2.5)
} else {
None
}
}
}
impl Default for KicadAdapter {
fn default() -> Self {
Self::new()
}
}
impl CircuitAdapter for KicadAdapter {
fn source_cad(&self) -> SourceCAD {
SourceCAD::KiCad
}
fn supported_extensions(&self) -> &[&str] {
&["kicad_sch", "sch"]
}
fn parse_file(&self, path: &Path) -> Result<UnifiedCircuitSchema, AdapterError> {
let schematic = KicadParser::parse_schematic(path)
.map_err(|e| AdapterError::Parse(e.to_string()))?;
Ok(self.convert_schematic(schematic))
}
fn parse_string(&self, content: &str, filename: &str) -> Result<UnifiedCircuitSchema, AdapterError> {
let schematic = KicadParser::parse_schematic_str(content, filename)
.map_err(|e| AdapterError::Parse(e.to_string()))?;
Ok(self.convert_schematic(schematic))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_extensions() {
let adapter = KicadAdapter::new();
assert!(adapter.supported_extensions().contains(&"kicad_sch"));
}
#[test]
fn test_can_handle() {
let adapter = KicadAdapter::new();
assert!(adapter.can_handle(Path::new("test.kicad_sch")));
assert!(adapter.can_handle(Path::new("/path/to/project.kicad_sch")));
assert!(adapter.can_handle(Path::new("test.sch"))); assert!(adapter.can_handle(Path::new("/path/to/project.sch"))); assert!(!adapter.can_handle(Path::new("test.txt")));
}
#[test]
fn test_infer_voltage() {
let adapter = KicadAdapter::new();
assert_eq!(adapter.infer_voltage_from_name("GND"), Some(0.0));
assert_eq!(adapter.infer_voltage_from_name("3V3"), Some(3.3));
assert_eq!(adapter.infer_voltage_from_name("VCC_5V"), Some(5.0));
assert_eq!(adapter.infer_voltage_from_name("12V_RAIL"), Some(12.0));
assert_eq!(adapter.infer_voltage_from_name("SDA"), None);
}
#[test]
fn test_signal_type_inference() {
assert_eq!(SignalType::from_net_name("GND"), SignalType::Ground);
assert_eq!(SignalType::from_net_name("VCC"), SignalType::Power);
assert_eq!(SignalType::from_net_name("CLK"), SignalType::Clock);
assert_eq!(SignalType::from_net_name("NRST"), SignalType::Reset);
assert_eq!(SignalType::from_net_name("SDA"), SignalType::Data);
}
}