From ae7e31f0bd49aa2ada8fd7518335bb1001b13fd6 Mon Sep 17 00:00:00 2001 From: Sebastian Almberg <83243306+Sebbeben@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:26:40 +0100 Subject: [PATCH] Address PR review: rename to eda_visibility, merge migrations, API versioning Changes based on jbtronics' review of PR #1241: - Rename kicad_export -> eda_visibility (entities, forms, templates, translations, tests) with nullable bool for system default support - Merge two database migrations into one (Version20260211000000) - Rename createCachedJsonResponse -> createCacheableJsonResponse - Change bool $apiV2 -> int $apiVersion with version validation - EDA visibility field only shown for part parameters, not other entities - PopulateKicadCommand: check alternative names of footprints/categories - PopulateKicadCommand: support external JSON mapping file (--mapping-file) - Ship default mappings JSON at contrib/kicad-populate/default_mappings.json - Add system-wide defaultEdaVisibility setting in KiCadEDASettings - Add KiCad HTTP Library v2 spec link in controller docs --- contrib/kicad-populate/default_mappings.json | 195 ++++++++++++++++++ migrations/Version20260208190000.php | 47 ----- migrations/Version20260210120000.php | 46 ----- migrations/Version20260211000000.php | 52 +++++ src/Command/PopulateKicadCommand.php | 194 ++++++++++++++--- src/Controller/KiCadApiController.php | 8 +- src/Controller/KiCadApiV2Controller.php | 13 +- src/Entity/Parameters/AbstractParameter.php | 14 +- src/Entity/PriceInformations/Orderdetail.php | 14 +- src/Form/ParameterType.php | 11 +- src/Form/Part/OrderdetailType.php | 4 +- src/Services/EDA/KiCadHelper.php | 35 ++-- .../MiscSettings/KiCadEDASettings.php | 5 + .../parts/edit/_specifications.html.twig | 2 +- .../parts/edit/edit_form_styles.html.twig | 6 +- tests/Services/EDA/KiCadHelperTest.php | 39 +++- translations/messages.en.xlf | 24 ++- 17 files changed, 532 insertions(+), 177 deletions(-) create mode 100644 contrib/kicad-populate/default_mappings.json delete mode 100644 migrations/Version20260208190000.php delete mode 100644 migrations/Version20260210120000.php create mode 100644 migrations/Version20260211000000.php diff --git a/contrib/kicad-populate/default_mappings.json b/contrib/kicad-populate/default_mappings.json new file mode 100644 index 00000000..b1bc1d1b --- /dev/null +++ b/contrib/kicad-populate/default_mappings.json @@ -0,0 +1,195 @@ +{ + "_comment": "Default KiCad footprint/symbol mappings for partdb:kicad:populate command. Based on KiCad 9.x standard libraries. Use --mapping-file to override or extend these mappings.", + "footprints": { + "SOT-23": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-3": "Package_TO_SOT_SMD:SOT-23", + "SOT-23-5": "Package_TO_SOT_SMD:SOT-23-5", + "SOT-23-6": "Package_TO_SOT_SMD:SOT-23-6", + "SOT-223": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SOT-223-3": "Package_TO_SOT_SMD:SOT-223-3_TabPin2", + "SOT-89": "Package_TO_SOT_SMD:SOT-89-3", + "SOT-89-3": "Package_TO_SOT_SMD:SOT-89-3", + "SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70", + "SOT-363": "Package_TO_SOT_SMD:SOT-363_SC-70-6", + "TSOT-25": "Package_TO_SOT_SMD:SOT-23-5", + "SC-70-5": "Package_TO_SOT_SMD:SOT-353_SC-70-5", + "SC-70-6": "Package_TO_SOT_SMD:SOT-363_SC-70-6", + "TO-220": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220AB": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220AB-3": "Package_TO_SOT_THT:TO-220-3_Vertical", + "TO-220FP": "Package_TO_SOT_THT:TO-220F-3_Vertical", + "TO-247-3": "Package_TO_SOT_THT:TO-247-3_Vertical", + "TO-92": "Package_TO_SOT_THT:TO-92_Inline", + "TO-92-3": "Package_TO_SOT_THT:TO-92_Inline", + "TO-252": "Package_TO_SOT_SMD:TO-252-2", + "TO-252-2L": "Package_TO_SOT_SMD:TO-252-2", + "TO-252-3L": "Package_TO_SOT_SMD:TO-252-3", + "TO-263": "Package_TO_SOT_SMD:TO-263-2", + "TO-263-2": "Package_TO_SOT_SMD:TO-263-2", + "D2PAK": "Package_TO_SOT_SMD:TO-252-2", + "DPAK": "Package_TO_SOT_SMD:TO-252-2", + "SOIC-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "ESOP-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", + "SOIC-14": "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm", + "SOIC-16": "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm", + "TSSOP-8": "Package_SO:TSSOP-8_3x3mm_P0.65mm", + "TSSOP-14": "Package_SO:TSSOP-14_4.4x5mm_P0.65mm", + "TSSOP-16": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm", + "TSSOP-16L": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm", + "TSSOP-20": "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm", + "MSOP-8": "Package_SO:MSOP-8_3x3mm_P0.65mm", + "MSOP-10": "Package_SO:MSOP-10_3x3mm_P0.5mm", + "MSOP-16": "Package_SO:MSOP-16_3x4mm_P0.5mm", + "SO-5": "Package_TO_SOT_SMD:SOT-23-5", + "DIP-4": "Package_DIP:DIP-4_W7.62mm", + "DIP-6": "Package_DIP:DIP-6_W7.62mm", + "DIP-8": "Package_DIP:DIP-8_W7.62mm", + "DIP-14": "Package_DIP:DIP-14_W7.62mm", + "DIP-16": "Package_DIP:DIP-16_W7.62mm", + "DIP-18": "Package_DIP:DIP-18_W7.62mm", + "DIP-20": "Package_DIP:DIP-20_W7.62mm", + "DIP-24": "Package_DIP:DIP-24_W7.62mm", + "DIP-28": "Package_DIP:DIP-28_W7.62mm", + "DIP-40": "Package_DIP:DIP-40_W15.24mm", + "QFN-8": "Package_DFN_QFN:QFN-8-1EP_3x3mm_P0.65mm_EP1.55x1.55mm", + "QFN-12(3x3)": "Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm", + "QFN-16": "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm", + "QFN-20": "Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm", + "QFN-24": "Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm", + "QFN-32": "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm", + "QFN-48": "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm", + "TQFP-32": "Package_QFP:TQFP-32_7x7mm_P0.8mm", + "TQFP-44": "Package_QFP:TQFP-44_10x10mm_P0.8mm", + "TQFP-48": "Package_QFP:TQFP-48_7x7mm_P0.5mm", + "TQFP-48(7x7)": "Package_QFP:TQFP-48_7x7mm_P0.5mm", + "TQFP-64": "Package_QFP:TQFP-64_10x10mm_P0.5mm", + "TQFP-100": "Package_QFP:TQFP-100_14x14mm_P0.5mm", + "LQFP-32": "Package_QFP:LQFP-32_7x7mm_P0.8mm", + "LQFP-48": "Package_QFP:LQFP-48_7x7mm_P0.5mm", + "LQFP-64": "Package_QFP:LQFP-64_10x10mm_P0.5mm", + "LQFP-100": "Package_QFP:LQFP-100_14x14mm_P0.5mm", + "SOD-123": "Diode_SMD:D_SOD-123", + "SOD-123F": "Diode_SMD:D_SOD-123F", + "SOD-123FL": "Diode_SMD:D_SOD-123F", + "SOD-323": "Diode_SMD:D_SOD-323", + "SOD-523": "Diode_SMD:D_SOD-523", + "SOD-882": "Diode_SMD:D_SOD-882", + "SOD-882D": "Diode_SMD:D_SOD-882", + "SMA(DO-214AC)": "Diode_SMD:D_SMA", + "SMA": "Diode_SMD:D_SMA", + "SMB": "Diode_SMD:D_SMB", + "SMC": "Diode_SMD:D_SMC", + "DO-35": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal", + "DO-35(DO-204AH)": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal", + "DO-41": "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal", + "DO-201": "Diode_THT:D_DO-201_P15.24mm_Horizontal", + "DFN-2(0.6x1)": "Package_DFN_QFN:DFN-2-1EP_0.6x1.0mm_P0.65mm_EP0.2x0.55mm", + "DFN1006-2": "Package_DFN_QFN:DFN-2_1.0x0.6mm", + "DFN-6": "Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1x1.6mm", + "DFN-8": "Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm", + "0201": "Resistor_SMD:R_0201_0603Metric", + "0402": "Resistor_SMD:R_0402_1005Metric", + "0603": "Resistor_SMD:R_0603_1608Metric", + "0805": "Resistor_SMD:R_0805_2012Metric", + "1206": "Resistor_SMD:R_1206_3216Metric", + "1210": "Resistor_SMD:R_1210_3225Metric", + "1812": "Resistor_SMD:R_1812_4532Metric", + "2010": "Resistor_SMD:R_2010_5025Metric", + "2512": "Resistor_SMD:R_2512_6332Metric", + "2917": "Resistor_SMD:R_2917_7343Metric", + "2920": "Resistor_SMD:R_2920_7350Metric", + "CASE-A-3216-18(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A", + "CASE-B-3528-21(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B", + "CASE-C-6032-28(mm)": "Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C", + "CASE-D-7343-31(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D", + "CASE-E-7343-43(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-E", + "SMD,D4xL5.4mm": "Capacitor_SMD:CP_Elec_4x5.4", + "SMD,D5xL5.4mm": "Capacitor_SMD:CP_Elec_5x5.4", + "SMD,D6.3xL5.4mm": "Capacitor_SMD:CP_Elec_6.3x5.4", + "SMD,D6.3xL7.7mm": "Capacitor_SMD:CP_Elec_6.3x7.7", + "SMD,D8xL6.5mm": "Capacitor_SMD:CP_Elec_8x6.5", + "SMD,D8xL10mm": "Capacitor_SMD:CP_Elec_8x10", + "SMD,D10xL10mm": "Capacitor_SMD:CP_Elec_10x10", + "SMD,D10xL10.5mm": "Capacitor_SMD:CP_Elec_10x10.5", + "Through Hole,D5xL11mm": "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm", + "Through Hole,D6.3xL11mm": "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm", + "Through Hole,D8xL11mm": "Capacitor_THT:CP_Radial_D8.0mm_P3.50mm", + "Through Hole,D10xL16mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm", + "Through Hole,D10xL20mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm", + "Through Hole,D12.5xL20mm": "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm", + "LED 3mm": "LED_THT:LED_D3.0mm", + "LED 5mm": "LED_THT:LED_D5.0mm", + "LED 0603": "LED_SMD:LED_0603_1608Metric", + "LED 0805": "LED_SMD:LED_0805_2012Metric", + "SMD5050-4P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm", + "SMD5050-6P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm", + "HC-49": "Crystal:Crystal_HC49-4H_Vertical", + "HC-49/U": "Crystal:Crystal_HC49-4H_Vertical", + "HC-49/S": "Crystal:Crystal_HC49-U_Vertical", + "HC-49/US": "Crystal:Crystal_HC49-U_Vertical", + "USB-A": "Connector_USB:USB_A_Stewart_SS-52100-001_Horizontal", + "USB-B": "Connector_USB:USB_B_OST_USB-B1HSxx_Horizontal", + "USB-Mini-B": "Connector_USB:USB_Mini-B_Lumberg_2486_01_Horizontal", + "USB-Micro-B": "Connector_USB:USB_Micro-B_Molex-105017-0001", + "USB-C": "Connector_USB:USB_C_Receptacle_GCT_USB4085", + "1x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical", + "1x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical", + "1x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical", + "1x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical", + "1x6 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical", + "1x8 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical", + "1x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical", + "2x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical", + "2x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical", + "2x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical", + "2x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical", + "2x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical", + "2x20 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical", + "SIP-3-2.54mm": "Package_SIP:SIP-3_P2.54mm", + "SIP-4-2.54mm": "Package_SIP:SIP-4_P2.54mm", + "SIP-5-2.54mm": "Package_SIP:SIP-5_P2.54mm" + }, + "categories": { + "Electrolytic": "Device:C_Polarized", + "Polarized": "Device:C_Polarized", + "Tantalum": "Device:C_Polarized", + "Zener": "Device:D_Zener", + "Schottky": "Device:D_Schottky", + "TVS": "Device:D_TVS", + "LED": "Device:LED", + "NPN": "Device:Q_NPN_BCE", + "PNP": "Device:Q_PNP_BCE", + "N-MOSFET": "Device:Q_NMOS_GDS", + "NMOS": "Device:Q_NMOS_GDS", + "N-MOS": "Device:Q_NMOS_GDS", + "P-MOSFET": "Device:Q_PMOS_GDS", + "PMOS": "Device:Q_PMOS_GDS", + "P-MOS": "Device:Q_PMOS_GDS", + "MOSFET": "Device:Q_NMOS_GDS", + "JFET": "Device:Q_NJFET_DSG", + "Ferrite": "Device:Ferrite_Bead", + "Crystal": "Device:Crystal", + "Oscillator": "Oscillator:Oscillator_Crystal", + "Fuse": "Device:Fuse", + "Transformer": "Device:Transformer_1P_1S", + "Resistor": "Device:R", + "Capacitor": "Device:C", + "Inductor": "Device:L", + "Diode": "Device:D", + "Transistor": "Device:Q_NPN_BCE", + "Voltage Regulator": "Regulator_Linear:LM317_TO-220", + "LDO": "Regulator_Linear:AMS1117-3.3", + "Op-Amp": "Amplifier_Operational:LM358", + "Comparator": "Comparator:LM393", + "Optocoupler": "Isolator:PC817", + "Relay": "Relay:Relay_DPDT", + "Connector": "Connector:Conn_01x02", + "Switch": "Switch:SW_Push", + "Button": "Switch:SW_Push", + "Potentiometer": "Device:R_POT", + "Trimpot": "Device:R_POT", + "Thermistor": "Device:Thermistor", + "Varistor": "Device:Varistor", + "Photo": "Device:LED" + } +} diff --git a/migrations/Version20260208190000.php b/migrations/Version20260208190000.php deleted file mode 100644 index 3ff1a80d..00000000 --- a/migrations/Version20260208190000.php +++ /dev/null @@ -1,47 +0,0 @@ -addSql('ALTER TABLE parameters ADD kicad_export TINYINT(1) NOT NULL DEFAULT 0'); - } - - public function mySQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE parameters DROP COLUMN kicad_export'); - } - - public function sqLiteUp(Schema $schema): void - { - $this->addSql('ALTER TABLE parameters ADD COLUMN kicad_export BOOLEAN NOT NULL DEFAULT 0'); - } - - public function sqLiteDown(Schema $schema): void - { - // SQLite does not support DROP COLUMN in older versions; recreate table if needed - $this->addSql('ALTER TABLE parameters DROP COLUMN kicad_export'); - } - - public function postgreSQLUp(Schema $schema): void - { - $this->addSql('ALTER TABLE parameters ADD kicad_export BOOLEAN NOT NULL DEFAULT FALSE'); - } - - public function postgreSQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE parameters DROP COLUMN kicad_export'); - } -} diff --git a/migrations/Version20260210120000.php b/migrations/Version20260210120000.php deleted file mode 100644 index 04684a36..00000000 --- a/migrations/Version20260210120000.php +++ /dev/null @@ -1,46 +0,0 @@ -addSql('ALTER TABLE `orderdetails` ADD kicad_export TINYINT(1) NOT NULL DEFAULT 0'); - } - - public function mySQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE `orderdetails` DROP COLUMN kicad_export'); - } - - public function sqLiteUp(Schema $schema): void - { - $this->addSql('ALTER TABLE orderdetails ADD COLUMN kicad_export BOOLEAN NOT NULL DEFAULT 0'); - } - - public function sqLiteDown(Schema $schema): void - { - $this->addSql('ALTER TABLE orderdetails DROP COLUMN kicad_export'); - } - - public function postgreSQLUp(Schema $schema): void - { - $this->addSql('ALTER TABLE orderdetails ADD kicad_export BOOLEAN NOT NULL DEFAULT FALSE'); - } - - public function postgreSQLDown(Schema $schema): void - { - $this->addSql('ALTER TABLE orderdetails DROP COLUMN kicad_export'); - } -} diff --git a/migrations/Version20260211000000.php b/migrations/Version20260211000000.php new file mode 100644 index 00000000..33f3db57 --- /dev/null +++ b/migrations/Version20260211000000.php @@ -0,0 +1,52 @@ +addSql('ALTER TABLE parameters ADD eda_visibility TINYINT(1) DEFAULT NULL'); + $this->addSql('ALTER TABLE `orderdetails` ADD eda_visibility TINYINT(1) DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE `orderdetails` DROP COLUMN eda_visibility'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE orderdetails ADD COLUMN eda_visibility BOOLEAN DEFAULT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters ADD eda_visibility BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE orderdetails ADD eda_visibility BOOLEAN DEFAULT NULL'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE parameters DROP COLUMN eda_visibility'); + $this->addSql('ALTER TABLE orderdetails DROP COLUMN eda_visibility'); + } +} diff --git a/src/Command/PopulateKicadCommand.php b/src/Command/PopulateKicadCommand.php index bcfcf927..0bc03392 100644 --- a/src/Command/PopulateKicadCommand.php +++ b/src/Command/PopulateKicadCommand.php @@ -32,6 +32,7 @@ class PopulateKicadCommand extends Command ->addOption('categories', null, InputOption::VALUE_NONE, 'Only update category entities') ->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing values (by default, only empty values are updated)') ->addOption('list', null, InputOption::VALUE_NONE, 'List all footprints and categories with their current KiCad values') + ->addOption('mapping-file', null, InputOption::VALUE_REQUIRED, 'Path to a JSON file with custom mappings (merges with built-in defaults)') ; } @@ -43,6 +44,7 @@ class PopulateKicadCommand extends Command $categoriesOnly = $input->getOption('categories'); $force = $input->getOption('force'); $list = $input->getOption('list'); + $mappingFile = $input->getOption('mapping-file'); // If neither specified, do both $doFootprints = !$categoriesOnly || $footprintsOnly; @@ -53,6 +55,26 @@ class PopulateKicadCommand extends Command return Command::SUCCESS; } + // Load mappings: start with built-in defaults, then merge user-supplied file + $footprintMappings = $this->getFootprintMappings(); + $categoryMappings = $this->getCategoryMappings(); + + if ($mappingFile !== null) { + $customMappings = $this->loadMappingFile($mappingFile, $io); + if ($customMappings === null) { + return Command::FAILURE; + } + if (isset($customMappings['footprints']) && is_array($customMappings['footprints'])) { + // User mappings take priority (overwrite defaults) + $footprintMappings = array_merge($footprintMappings, $customMappings['footprints']); + $io->text(sprintf('Loaded %d custom footprint mappings from %s', count($customMappings['footprints']), $mappingFile)); + } + if (isset($customMappings['categories']) && is_array($customMappings['categories'])) { + $categoryMappings = array_merge($categoryMappings, $customMappings['categories']); + $io->text(sprintf('Loaded %d custom category mappings from %s', count($customMappings['categories']), $mappingFile)); + } + } + if ($dryRun) { $io->note('DRY RUN MODE - No changes will be made'); } @@ -60,12 +82,12 @@ class PopulateKicadCommand extends Command $totalUpdated = 0; if ($doFootprints) { - $updated = $this->updateFootprints($io, $dryRun, $force); + $updated = $this->updateFootprints($io, $dryRun, $force, $footprintMappings); $totalUpdated += $updated; } if ($doCategories) { - $updated = $this->updateCategories($io, $dryRun, $force); + $updated = $this->updateCategories($io, $dryRun, $force, $categoryMappings); $totalUpdated += $updated; } @@ -120,12 +142,10 @@ class PopulateKicadCommand extends Command $io->table(['ID', 'Name', 'KiCad Symbol'], $rows); } - private function updateFootprints(SymfonyStyle $io, bool $dryRun, bool $force): int + private function updateFootprints(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int { $io->section('Updating Footprint Entities'); - $mappings = $this->getFootprintMappings(); - $footprintRepo = $this->entityManager->getRepository(Footprint::class); /** @var Footprint[] $footprints */ $footprints = $footprintRepo->findAll(); @@ -142,13 +162,14 @@ class PopulateKicadCommand extends Command continue; } - // Check for exact match first - if (isset($mappings[$name])) { - $newValue = $mappings[$name]; - $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $newValue)); + // Check for exact match on name first, then try alternative names + $matchedValue = $this->findFootprintMapping($mappings, $name, $footprint->getAlternativeNames()); + + if ($matchedValue !== null) { + $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue)); if (!$dryRun) { - $footprint->getEdaInfo()->setKicadFootprint($newValue); + $footprint->getEdaInfo()->setKicadFootprint($matchedValue); } $updated++; } else { @@ -170,12 +191,10 @@ class PopulateKicadCommand extends Command return $updated; } - private function updateCategories(SymfonyStyle $io, bool $dryRun, bool $force): int + private function updateCategories(SymfonyStyle $io, bool $dryRun, bool $force, array $mappings): int { $io->section('Updating Category Entities'); - $mappings = $this->getCategoryMappings(); - $categoryRepo = $this->entityManager->getRepository(Category::class); /** @var Category[] $categories */ $categories = $categoryRepo->findAll(); @@ -192,22 +211,17 @@ class PopulateKicadCommand extends Command continue; } - // Check for matches using the pattern-based mappings - $matched = false; - foreach ($mappings as $pattern => $kicadSymbol) { - if ($this->matchesPattern($name, $pattern)) { - $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $kicadSymbol)); + // Check for matches using the pattern-based mappings (also check alternative names) + $matchedValue = $this->findCategoryMapping($mappings, $name, $category->getAlternativeNames()); - if (!$dryRun) { - $category->getEdaInfo()->setKicadSymbol($kicadSymbol); - } - $updated++; - $matched = true; - break; + if ($matchedValue !== null) { + $io->text(sprintf(' %s: %s -> %s', $name, $currentValue ?? '(empty)', $matchedValue)); + + if (!$dryRun) { + $category->getEdaInfo()->setKicadSymbol($matchedValue); } - } - - if (!$matched) { + $updated++; + } else { $skipped[] = $name; } } @@ -225,6 +239,34 @@ class PopulateKicadCommand extends Command return $updated; } + /** + * Loads a JSON mapping file and returns the parsed data. + * Expected format: {"footprints": {"Name": "KiCad:Path"}, "categories": {"Pattern": "KiCad:Path"}} + * + * @return array|null The parsed mappings, or null on error + */ + private function loadMappingFile(string $path, SymfonyStyle $io): ?array + { + if (!file_exists($path)) { + $io->error(sprintf('Mapping file not found: %s', $path)); + return null; + } + + $content = file_get_contents($path); + if ($content === false) { + $io->error(sprintf('Could not read mapping file: %s', $path)); + return null; + } + + $data = json_decode($content, true); + if (!is_array($data)) { + $io->error(sprintf('Invalid JSON in mapping file: %s', $path)); + return null; + } + + return $data; + } + private function matchesPattern(string $name, string $pattern): bool { // Check for exact match @@ -240,6 +282,71 @@ class PopulateKicadCommand extends Command return false; } + /** + * Finds a footprint mapping by checking the entity name and its alternative names. + * Footprints use exact matching. + * + * @param array $mappings + * @param string $name The primary name of the footprint + * @param string|null $alternativeNames Comma-separated alternative names + * @return string|null The matched KiCad path, or null if no match found + */ + private function findFootprintMapping(array $mappings, string $name, ?string $alternativeNames): ?string + { + // Check primary name + if (isset($mappings[$name])) { + return $mappings[$name]; + } + + // Check alternative names + if ($alternativeNames !== null && $alternativeNames !== '') { + foreach (explode(',', $alternativeNames) as $altName) { + $altName = trim($altName); + if ($altName !== '' && isset($mappings[$altName])) { + return $mappings[$altName]; + } + } + } + + return null; + } + + /** + * Finds a category mapping by checking the entity name and its alternative names. + * Categories use pattern-based matching (case-insensitive contains). + * + * @param array $mappings + * @param string $name The primary name of the category + * @param string|null $alternativeNames Comma-separated alternative names + * @return string|null The matched KiCad symbol path, or null if no match found + */ + private function findCategoryMapping(array $mappings, string $name, ?string $alternativeNames): ?string + { + // Check primary name against all patterns + foreach ($mappings as $pattern => $kicadSymbol) { + if ($this->matchesPattern($name, $pattern)) { + return $kicadSymbol; + } + } + + // Check alternative names against all patterns + if ($alternativeNames !== null && $alternativeNames !== '') { + foreach (explode(',', $alternativeNames) as $altName) { + $altName = trim($altName); + if ($altName === '') { + continue; + } + foreach ($mappings as $pattern => $kicadSymbol) { + if ($this->matchesPattern($altName, $pattern)) { + return $kicadSymbol; + } + } + } + } + + return null; + } + /** * Returns footprint name to KiCad footprint path mappings. * These are based on KiCad 9.x standard library paths. @@ -496,4 +603,37 @@ class PopulateKicadCommand extends Command 'Photo' => 'Device:LED', // Photodiode/phototransistor ]; } + + /** + * Load a custom mapping file (JSON format). + * + * Expected format: + * { + * "footprints": { "SOT-23": "Package_TO_SOT_SMD:SOT-23", ... }, + * "categories": { "Resistor": "Device:R", ... } + * } + * + * @return array|null The parsed mappings, or null on error + */ + private function loadMappingFile(string $path, SymfonyStyle $io): ?array + { + if (!file_exists($path)) { + $io->error(sprintf('Mapping file not found: %s', $path)); + return null; + } + + $content = file_get_contents($path); + if ($content === false) { + $io->error(sprintf('Could not read mapping file: %s', $path)); + return null; + } + + $data = json_decode($content, true); + if (!is_array($data)) { + $io->error(sprintf('Invalid JSON in mapping file: %s', json_last_error_msg())); + return null; + } + + return $data; + } } diff --git a/src/Controller/KiCadApiController.php b/src/Controller/KiCadApiController.php index ea93138c..76727877 100644 --- a/src/Controller/KiCadApiController.php +++ b/src/Controller/KiCadApiController.php @@ -62,7 +62,7 @@ class KiCadApiController extends AbstractController $this->denyAccessUnlessGranted('@categories.read'); $data = $this->kiCADHelper->getCategories(); - return $this->createCachedJsonResponse($request, $data, 300); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/category/{category}.json', name: 'kicad_api_category')] @@ -77,7 +77,7 @@ class KiCadApiController extends AbstractController $minimal = $request->query->getBoolean('minimal', false); $data = $this->kiCADHelper->getCategoryParts($category, $minimal); - return $this->createCachedJsonResponse($request, $data, 300); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/{part}.json', name: 'kicad_api_part')] @@ -86,14 +86,14 @@ class KiCadApiController extends AbstractController $this->denyAccessUnlessGranted('read', $part); $data = $this->kiCADHelper->getKiCADPart($part); - return $this->createCachedJsonResponse($request, $data, 60); + return $this->createCacheableJsonResponse($request, $data, 60); } /** * Creates a JSON response with HTTP cache headers (ETag and Cache-Control). * Returns 304 Not Modified if the client's ETag matches. */ - private function createCachedJsonResponse(Request $request, array $data, int $maxAge): Response + private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response { $response = new JsonResponse($data); $response->setEtag(md5(json_encode($data))); diff --git a/src/Controller/KiCadApiV2Controller.php b/src/Controller/KiCadApiV2Controller.php index 5332ccd8..a915b94e 100644 --- a/src/Controller/KiCadApiV2Controller.php +++ b/src/Controller/KiCadApiV2Controller.php @@ -34,6 +34,9 @@ use Symfony\Component\Routing\Attribute\Route; /** * KiCad HTTP Library API v2 controller. * + * v1 spec: https://dev-docs.kicad.org/en/apis-and-binding/http-libraries/index.html + * v2 spec (draft): https://gitlab.com/RosyDev/kicad-dev-docs/-/blob/http-lib-v2/content/apis-and-binding/http-libraries/http-lib-v2-00.adoc + * * Differences from v1: * - Volatile fields: Stock and Storage Location are marked volatile (shown in KiCad but NOT saved to schematic) * - Category descriptions: Uses actual category comments instead of URLs @@ -64,7 +67,7 @@ class KiCadApiV2Controller extends AbstractController $this->denyAccessUnlessGranted('@categories.read'); $data = $this->kiCADHelper->getCategories(); - return $this->createCachedJsonResponse($request, $data, 300); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/category/{category}.json', name: 'kicad_api_v2_category')] @@ -79,7 +82,7 @@ class KiCadApiV2Controller extends AbstractController $minimal = $request->query->getBoolean('minimal', false); $data = $this->kiCADHelper->getCategoryParts($category, $minimal); - return $this->createCachedJsonResponse($request, $data, 300); + return $this->createCacheableJsonResponse($request, $data, 300); } #[Route('/parts/{part}.json', name: 'kicad_api_v2_part')] @@ -88,11 +91,11 @@ class KiCadApiV2Controller extends AbstractController $this->denyAccessUnlessGranted('read', $part); // Use API v2 format with volatile fields - $data = $this->kiCADHelper->getKiCADPart($part, true); - return $this->createCachedJsonResponse($request, $data, 60); + $data = $this->kiCADHelper->getKiCADPart($part, 2); + return $this->createCacheableJsonResponse($request, $data, 60); } - private function createCachedJsonResponse(Request $request, array $data, int $maxAge): Response + private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response { $response = new JsonResponse($data); $response->setEtag(md5(json_encode($data))); diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 2762657a..f47f2e82 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -173,11 +173,11 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu protected string $group = ''; /** - * @var bool Whether this parameter should be exported as a KiCad field in the EDA HTTP library API + * @var bool|null Whether this parameter should be exported as a field in the EDA HTTP library API. Null means use system default. */ #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] - #[ORM\Column(type: Types::BOOLEAN)] - protected bool $kicad_export = false; + #[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])] + protected ?bool $eda_visibility = null; /** * Mapping is done in subclasses. @@ -478,17 +478,17 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu return static::ALLOWED_ELEMENT_CLASS; } - public function isKicadExport(): bool + public function isEdaVisibility(): ?bool { - return $this->kicad_export; + return $this->eda_visibility; } /** * @return $this */ - public function setKicadExport(bool $kicad_export): self + public function setEdaVisibility(?bool $eda_visibility): self { - $this->kicad_export = $kicad_export; + $this->eda_visibility = $eda_visibility; return $this; } diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 0cc8cf27..56428e3a 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -123,11 +123,11 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N protected bool $obsolete = false; /** - * @var bool Whether this orderdetail's supplier part number should be exported as a KiCad field + * @var bool|null Whether this orderdetail's supplier part number should be exported as an EDA field. Null means use system default. */ #[Groups(['full', 'import', 'orderdetail:read', 'orderdetail:write'])] - #[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])] - protected bool $kicad_export = false; + #[ORM\Column(type: Types::BOOLEAN, nullable: true, options: ['default' => null])] + protected ?bool $eda_visibility = null; /** * @var string The URL to the product on the supplier's website @@ -425,17 +425,17 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this; } - public function isKicadExport(): bool + public function isEdaVisibility(): ?bool { - return $this->kicad_export; + return $this->eda_visibility; } /** * @return $this */ - public function setKicadExport(bool $kicad_export): self + public function setEdaVisibility(?bool $eda_visibility): self { - $this->kicad_export = $kicad_export; + $this->eda_visibility = $eda_visibility; return $this; } diff --git a/src/Form/ParameterType.php b/src/Form/ParameterType.php index 3a773f4e..0e3ad5e2 100644 --- a/src/Form/ParameterType.php +++ b/src/Form/ParameterType.php @@ -149,10 +149,13 @@ class ParameterType extends AbstractType ], ]); - $builder->add('kicad_export', CheckboxType::class, [ - 'label' => false, - 'required' => false, - ]); + // Only show the EDA visibility field for part parameters, as it has no function for other entities + if ($options['data_class'] === PartParameter::class) { + $builder->add('eda_visibility', CheckboxType::class, [ + 'label' => false, + 'required' => false, + ]); + } } public function finishView(FormView $view, FormInterface $form, array $options): void diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php index d875f9e7..378f3389 100644 --- a/src/Form/Part/OrderdetailType.php +++ b/src/Form/Part/OrderdetailType.php @@ -79,9 +79,9 @@ class OrderdetailType extends AbstractType 'label' => 'orderdetails.edit.prices_includes_vat', ]); - $builder->add('kicad_export', CheckboxType::class, [ + $builder->add('eda_visibility', CheckboxType::class, [ 'required' => false, - 'label' => 'orderdetails.edit.kicad_export', + 'label' => 'orderdetails.edit.eda_visibility', ]); //Add pricedetails after we know the data, so we can set the default currency diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 29274641..8bd1fc74 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -47,6 +47,9 @@ class KiCadHelper /** @var bool Whether to resolve actual datasheet PDF URLs (true) or use Part-DB page links (false) */ private readonly bool $datasheetAsPdf; + /** @var bool The system-wide default for EDA visibility when not explicitly set on an element */ + private readonly bool $defaultEdaVisibility; + public function __construct( private readonly NodesListBuilder $nodesListBuilder, private readonly TagAwareCacheInterface $kicadCache, @@ -59,6 +62,7 @@ class KiCadHelper ) { $this->category_depth = $kiCadEDASettings->categoryDepth; $this->datasheetAsPdf = $kiCadEDASettings->datasheetAsPdf ?? true; + $this->defaultEdaVisibility = $kiCadEDASettings->defaultEdaVisibility; } /** @@ -194,10 +198,14 @@ class KiCadHelper } /** - * @param bool $apiV2 If true, use API v2 format with volatile field support + * @param int $apiVersion The API version to use (1 or 2). Version 2 adds volatile field support. */ - public function getKiCADPart(Part $part, bool $apiV2 = false): array + public function getKiCADPart(Part $part, int $apiVersion = 1): array { + if ($apiVersion < 1 || $apiVersion > 2) { + throw new \InvalidArgumentException(sprintf('Unsupported API version %d. Supported versions: 1, 2.', $apiVersion)); + } + $result = [ 'id' => (string)$part->getId(), 'name' => $part->getName(), @@ -277,13 +285,14 @@ class KiCadHelper } // Add supplier information from orderdetails (include obsolete orderdetails) - // If any orderdetail has kicad_export=true, only export those; otherwise export all (backward compat) + // If any orderdetail has eda_visibility explicitly set to true, only export those; + // otherwise export all (backward compat when no flags are set) $allOrderdetails = $part->getOrderdetails(false); if ($allOrderdetails->count() > 0) { - $hasKicadExportFlag = false; + $hasExplicitEdaVisibility = false; foreach ($allOrderdetails as $od) { - if ($od->isKicadExport()) { - $hasKicadExportFlag = true; + if ($od->isEdaVisibility() !== null) { + $hasExplicitEdaVisibility = true; break; } } @@ -291,8 +300,9 @@ class KiCadHelper $supplierCounts = []; foreach ($allOrderdetails as $orderdetail) { if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') { - // Skip orderdetails not marked for export when the flag is used - if ($hasKicadExportFlag && !$orderdetail->isKicadExport()) { + // When explicit flags exist, filter by resolved visibility + $resolvedVisibility = $orderdetail->isEdaVisibility() ?? $this->defaultEdaVisibility; + if ($hasExplicitEdaVisibility && !$resolvedVisibility) { continue; } @@ -330,14 +340,15 @@ class KiCadHelper } } // In API v2, stock and location are volatile (shown but not saved to schematic) - $result['fields']['Stock'] = $this->createField($totalStock, false, $apiV2); + $result['fields']['Stock'] = $this->createField($totalStock, false, $apiVersion >= 2); if ($locations !== []) { - $result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)), false, $apiV2); + $result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)), false, $apiVersion >= 2); } - //Add parameters marked for KiCad export + //Add parameters marked for EDA export (explicit true, or system default when null) foreach ($part->getParameters() as $parameter) { - if ($parameter->isKicadExport() && $parameter->getName() !== '') { + $paramVisibility = $parameter->isEdaVisibility() ?? $this->defaultEdaVisibility; + if ($paramVisibility && $parameter->getName() !== '') { $fieldName = $parameter->getName(); //Don't overwrite hardcoded fields if (!isset($result['fields'][$fieldName])) { diff --git a/src/Settings/MiscSettings/KiCadEDASettings.php b/src/Settings/MiscSettings/KiCadEDASettings.php index d9611013..948d1b38 100644 --- a/src/Settings/MiscSettings/KiCadEDASettings.php +++ b/src/Settings/MiscSettings/KiCadEDASettings.php @@ -48,4 +48,9 @@ class KiCadEDASettings description: new TM("settings.misc.kicad_eda.datasheet_link.help"), envVar: "bool:EDA_KICAD_DATASHEET_AS_PDF", envVarMode: EnvVarMode::OVERWRITE)] public ?bool $datasheetAsPdf = true; + + #[SettingsParameter(label: new TM("settings.misc.kicad_eda.default_eda_visibility"), + description: new TM("settings.misc.kicad_eda.default_eda_visibility.help"), + envVar: "bool:EDA_KICAD_DEFAULT_VISIBILITY", envVarMode: EnvVarMode::OVERWRITE)] + public bool $defaultEdaVisibility = false; } \ No newline at end of file diff --git a/templates/parts/edit/_specifications.html.twig b/templates/parts/edit/_specifications.html.twig index 3226e2c0..6f631b9f 100644 --- a/templates/parts/edit/_specifications.html.twig +++ b/templates/parts/edit/_specifications.html.twig @@ -14,7 +14,7 @@ {% trans %}specifications.unit{% endtrans %} {% trans %}specifications.text{% endtrans %} {% trans %}specifications.group{% endtrans %} - + diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index 6564bc55..9e989c92 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -33,7 +33,7 @@ {{ form_row(form.supplier_product_url, {'attr': {'class': 'form-control-sm'}}) }} {{ form_widget(form.obsolete) }} {{ form_widget(form.pricesIncludesVAT) }} - {{ form_widget(form.kicad_export) }} + {{ form_widget(form.eda_visibility) }}
@@ -80,7 +80,9 @@ {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }} {{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }} {{ form_widget(form.group) }}{{ form_errors(form.group) }} - {{ form_widget(form.kicad_export) }} + {% if form.eda_visibility is defined %} + {{ form_widget(form.eda_visibility) }} + {% endif %}