Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/usage/eda_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ If you set this value to -1, all parts are shown inside a single category in KiC

You can view the "real" category path of a part in the part details dialog in KiCad.

### Exported symbol fields

Besides the fields KiCad needs (symbol, footprint, reference, value, datasheet, description) Part-DB exports additional
information as symbol fields: manufacturer and MPN, the Part-DB ID and URL, stock and storage locations, supplier part
numbers, KiCost compatible fields (`manf`, `manf#`, `<supplier>#`), part info (category, manufacturing status, mass, IPN, ...)
and the tags as symbol keywords.

KiCad compares the fields of a placed symbol with the library. Every difference is reported as a "library symbol mismatch"
by the ERC. If you use the stock or supplier fields, every stock booking or supplier edit in Part-DB therefore triggers
ERC warnings in all schematics that use the part.

To avoid this, you can disable groups of fields that you do not need in your schematics in the server settings under
"KiCAD integration", or via the following env options (a value of `0` disables the group):

| Option | Fields |
|---------------------------------------|--------------------------------------------------------------------------------------------------|
| `EDA_KICAD_EXPORT_STOCK_FIELDS` | `Stock`, `Storage Location` |
| `EDA_KICAD_EXPORT_SUPPLIER_FIELDS` | `<Supplier> SPN` fields |
| `EDA_KICAD_EXPORT_KICOST_FIELDS` | `manf`, `manf#`, `<supplier>#` |
| `EDA_KICAD_EXPORT_PART_INFO_FIELDS` | `Category`, `Manufacturing Status`, `Mass`, `Part-DB IPN`, `Part-DB Footprint`, `Part-DB Unit`, `Part-DB Custom state` |
| `EDA_KICAD_EXPORT_TAGS_AS_KEYWORDS` | symbol keywords (from the part tags) |

All groups are enabled by default, so existing installations keep exporting the same fields as before.

### Kicad:populate command

Part-DB also provides a command that attempts to automatically populate the KiCad symbol and footprint fields based on the part's category and footprint names.
Expand Down
67 changes: 41 additions & 26 deletions src/Services/EDA/KiCadHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ public function getKiCADPart(Part $part, array &$categoryPrefixCache = []): arra
$result["fields"]["footprint"] = $this->createField($part->getEdaInfo()->getKicadFootprint() ?? $part->getFootprint()?->getEdaInfo()->getKicadFootprint() ?? "");
$result["fields"]["reference"] = $this->createField($this->getReferencePrefix($part, $categoryPrefixCache), true);
$result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true);
$result["fields"]["keywords"] = $this->createField($part->getTags());
if ($this->kiCadEDASettings->exportTagsAsKeywords) {
$result["fields"]["keywords"] = $this->createField($part->getTags());
}

//Use the part info page as Part-DB link. It must be an absolute URL.
$partUrl = $this->urlGenerator->generate(
Expand All @@ -239,7 +241,7 @@ public function getKiCADPart(Part $part, array &$categoryPrefixCache = []): arra

//Add basic fields
$result["fields"]["description"] = $this->createField($part->getDescription());
if ($part->getCategory() !== null) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getCategory() !== null) {
$result["fields"]["Category"] = $this->createField($part->getCategory()->getFullPath('/'));
}
if ($part->getManufacturer() !== null) {
Expand All @@ -248,47 +250,49 @@ public function getKiCADPart(Part $part, array &$categoryPrefixCache = []): arra
if ($part->getManufacturerProductNumber() !== "") {
$result['fields']["MPN"] = $this->createField($part->getManufacturerProductNumber());
}
if ($part->getManufacturingStatus() !== null) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getManufacturingStatus() !== null) {
$result["fields"]["Manufacturing Status"] = $this->createField(
//Always use the english translation
$this->translator->trans($part->getManufacturingStatus()->toTranslationKey(), locale: 'en')
);
}
if ($part->getFootprint() !== null) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getFootprint() !== null) {
$result["fields"]["Part-DB Footprint"] = $this->createField($part->getFootprint()->getName());
}
if ($part->getPartUnit() !== null) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getPartUnit() !== null) {
$unit = $part->getPartUnit()->getName();
if ($part->getPartUnit()->getUnit() !== "") {
$unit .= ' ('.$part->getPartUnit()->getUnit().')';
}
$result["fields"]["Part-DB Unit"] = $this->createField($unit);
}
if ($part->getPartCustomState() !== null) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getPartCustomState() !== null) {
$customState = $part->getPartCustomState()->getName();
$result["fields"]["Part-DB Custom state"] = $this->createField($customState);
}
if ($part->getMass()) {
if ($this->kiCadEDASettings->exportPartInfoFields && $part->getMass()) {
$result["fields"]["Mass"] = $this->createField($part->getMass() . ' g');
}
$result["fields"]["Part-DB ID"] = $this->createField($part->getId());
if ($part->getIpn() !== null && $part->getIpn() !== '' && $part->getIpn() !== '0') {
if ($this->kiCadEDASettings->exportPartInfoFields
&& $part->getIpn() !== null && $part->getIpn() !== '' && $part->getIpn() !== '0') {
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
}

//Add KiCost manufacturer fields (always present, independent of orderdetails)
if ($part->getManufacturer() !== null) {
if ($this->kiCadEDASettings->exportKicostFields && $part->getManufacturer() !== null) {
$result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName());
}
if ($part->getManufacturerProductNumber() !== "") {
if ($this->kiCadEDASettings->exportKicostFields && $part->getManufacturerProductNumber() !== "") {
$result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber());
}

// Add supplier information from orderdetails (include obsolete orderdetails)
// 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) {
if (($this->kiCadEDASettings->exportSupplierFields || $this->kiCadEDASettings->exportKicostFields)
&& $allOrderdetails->count() > 0) {
$hasExplicitEdaVisibility = false;
foreach ($allOrderdetails as $od) {
if ($od->isEdaVisibility() !== null) {
Expand Down Expand Up @@ -318,30 +322,36 @@ public function getKiCADPart(Part $part, array &$categoryPrefixCache = []): arra
? $supplierName . ' ' . $supplierCounts[$supplierName]
: $supplierName;

$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
if ($this->kiCadEDASettings->exportSupplierFields) {
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
}

//Also add a KiCost-compatible field (supplier_name# = SPN)
$kicostFieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
$result["fields"][$kicostFieldName] = $this->createField($orderdetail->getSupplierPartNr());
if ($this->kiCadEDASettings->exportKicostFields) {
$kicostFieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
$result["fields"][$kicostFieldName] = $this->createField($orderdetail->getSupplierPartNr());
}
}
}
}

//Add stock quantity and storage locations (only count non-expired lots with known quantity)
$totalStock = 0;
$locations = [];
foreach ($part->getPartLots() as $lot) {
$isAvailable = !$lot->isInstockUnknown() && $lot->isExpired() !== true;
if ($isAvailable) {
$totalStock += $lot->getAmount();
if ($lot->getAmount() > 0 && $lot->getStorageLocation() !== null) {
$locations[] = $lot->getStorageLocation()->getName();
if ($this->kiCadEDASettings->exportStockFields) {
$totalStock = 0;
$locations = [];
foreach ($part->getPartLots() as $lot) {
$isAvailable = !$lot->isInstockUnknown() && $lot->isExpired() !== true;
if ($isAvailable) {
$totalStock += $lot->getAmount();
if ($lot->getAmount() > 0 && $lot->getStorageLocation() !== null) {
$locations[] = $lot->getStorageLocation()->getName();
}
}
}
}
$result['fields']['Stock'] = $this->createField($totalStock);
if ($locations !== []) {
$result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)));
$result['fields']['Stock'] = $this->createField($totalStock);
if ($locations !== []) {
$result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)));
}
}

//Add parameters marked for EDA export (explicit true, or system default when null)
Expand Down Expand Up @@ -371,6 +381,11 @@ private function edaSettingsFingerprint(): string
$this->kiCadEDASettings->defaultOrderdetailsVisibility,
$this->kiCadEDASettings->defaultParameterVisibility,
$this->kiCadEDASettings->defaultParameterSymbolVisibility,
$this->kiCadEDASettings->exportStockFields,
$this->kiCadEDASettings->exportSupplierFields,
$this->kiCadEDASettings->exportKicostFields,
$this->kiCadEDASettings->exportPartInfoFields,
$this->kiCadEDASettings->exportTagsAsKeywords,
], JSON_THROW_ON_ERROR));
}

Expand Down
35 changes: 35 additions & 0 deletions src/Settings/MiscSettings/KiCadEDASettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,39 @@ class KiCadEDASettings
description: new TM("settings.misc.kicad_eda.use_custom_list.help"),
)]
public bool $useCustomList = false;

#[SettingsParameter(
label: new TM("settings.misc.kicad_eda.export_stock_fields"),
description: new TM("settings.misc.kicad_eda.export_stock_fields.help"),
envVar: "bool:EDA_KICAD_EXPORT_STOCK_FIELDS", envVarMode: EnvVarMode::OVERWRITE,
)]
public bool $exportStockFields = true;

#[SettingsParameter(
label: new TM("settings.misc.kicad_eda.export_supplier_fields"),
description: new TM("settings.misc.kicad_eda.export_supplier_fields.help"),
envVar: "bool:EDA_KICAD_EXPORT_SUPPLIER_FIELDS", envVarMode: EnvVarMode::OVERWRITE,
)]
public bool $exportSupplierFields = true;

#[SettingsParameter(
label: new TM("settings.misc.kicad_eda.export_kicost_fields"),
description: new TM("settings.misc.kicad_eda.export_kicost_fields.help"),
envVar: "bool:EDA_KICAD_EXPORT_KICOST_FIELDS", envVarMode: EnvVarMode::OVERWRITE,
)]
public bool $exportKicostFields = true;

#[SettingsParameter(
label: new TM("settings.misc.kicad_eda.export_part_info_fields"),
description: new TM("settings.misc.kicad_eda.export_part_info_fields.help"),
envVar: "bool:EDA_KICAD_EXPORT_PART_INFO_FIELDS", envVarMode: EnvVarMode::OVERWRITE,
)]
public bool $exportPartInfoFields = true;

#[SettingsParameter(
label: new TM("settings.misc.kicad_eda.export_tags_as_keywords"),
description: new TM("settings.misc.kicad_eda.export_tags_as_keywords.help"),
envVar: "bool:EDA_KICAD_EXPORT_TAGS_AS_KEYWORDS", envVarMode: EnvVarMode::OVERWRITE,
)]
public bool $exportTagsAsKeywords = true;
}
142 changes: 142 additions & 0 deletions tests/Services/EDA/KiCadHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ final class KiCadHelperTest extends KernelTestCase
{
private KiCadHelper $helper;
private EntityManagerInterface $em;
private KiCadEDASettings $settings;

protected function setUp(): void
{
self::bootKernel();
$this->helper = self::getContainer()->get(KiCadHelper::class);
$this->em = self::getContainer()->get(EntityManagerInterface::class);
$this->settings = self::getContainer()->get(KiCadEDASettings::class);
}

/**
Expand Down Expand Up @@ -761,4 +763,144 @@ public function testCategoryPartsCacheIsInvalidatedBySettingsChange(): void
self::assertArrayHasKey('CacheTestParam', $after);
self::assertSame('42', $after['CacheTestParam']['value']);
}

/**
* Creates a part that has data for every optional field group (stock, supplier, KiCost, part info, tags).
*/
private function createPartWithAllFieldGroups(): Part
{
$category = $this->em->find(Category::class, 1);
$location = $this->em->find(StorageLocation::class, 1);

$manufacturer = new Manufacturer();
$manufacturer->setName('Switch Corp');
$this->em->persist($manufacturer);

$supplier = new Supplier();
$supplier->setName('SwitchSupplier');
$this->em->persist($supplier);

$part = new Part();
$part->setName('Part with all field groups');
$part->setCategory($category);
$part->setManufacturer($manufacturer);
$part->setManufacturerProductNumber('SW-1234');
$part->setTags('tag1,tag2');
$part->setMass(1.5);

$lot = new PartLot();
$lot->setAmount(5);
$lot->setStorageLocation($location);
$part->addPartLot($lot);

$orderdetail = new Orderdetail();
$orderdetail->setSupplier($supplier);
$orderdetail->setSupplierpartnr('SUP-999');
$part->addOrderdetail($orderdetail);

$this->em->persist($part);
$this->em->flush();

return $part;
}

/**
* With the default settings every field group is exported.
*/
public function testAllFieldGroupsExportedByDefault(): void
{
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayHasKey('Stock', $fields);
self::assertArrayHasKey('Storage Location', $fields);
self::assertArrayHasKey('SwitchSupplier SPN', $fields);
self::assertArrayHasKey('manf', $fields);
self::assertArrayHasKey('manf#', $fields);
self::assertArrayHasKey('switchsupplier#', $fields);
self::assertArrayHasKey('Category', $fields);
self::assertArrayHasKey('Mass', $fields);
self::assertArrayHasKey('keywords', $fields);
self::assertSame('tag1,tag2', $fields['keywords']['value']);
}

public function testStockFieldsCanBeDisabled(): void
{
$this->settings->exportStockFields = false;
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayNotHasKey('Stock', $fields);
self::assertArrayNotHasKey('Storage Location', $fields);
// Other groups are unaffected
self::assertArrayHasKey('SwitchSupplier SPN', $fields);
self::assertArrayHasKey('manf#', $fields);
}

public function testSupplierFieldsCanBeDisabledIndependentlyOfKicostFields(): void
{
$this->settings->exportSupplierFields = false;
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayNotHasKey('SwitchSupplier SPN', $fields);
// KiCost supplier field is still exported, as it is controlled by its own switch
self::assertArrayHasKey('switchsupplier#', $fields);
self::assertSame('SUP-999', $fields['switchsupplier#']['value']);
}

public function testKicostFieldsCanBeDisabledIndependentlyOfSupplierFields(): void
{
$this->settings->exportKicostFields = false;
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayNotHasKey('manf', $fields);
self::assertArrayNotHasKey('manf#', $fields);
self::assertArrayNotHasKey('switchsupplier#', $fields);
// The regular supplier and manufacturer fields stay
self::assertArrayHasKey('SwitchSupplier SPN', $fields);
self::assertArrayHasKey('Manufacturer', $fields);
self::assertArrayHasKey('MPN', $fields);
}

public function testPartInfoFieldsCanBeDisabled(): void
{
$this->settings->exportPartInfoFields = false;
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayNotHasKey('Category', $fields);
self::assertArrayNotHasKey('Mass', $fields);
// Fields needed by KiCad itself and for identification are always present
self::assertArrayHasKey('Part-DB ID', $fields);
self::assertArrayHasKey('Part-DB URL', $fields);
self::assertArrayHasKey('MPN', $fields);
self::assertArrayHasKey('description', $fields);
}

public function testTagsAsKeywordsCanBeDisabled(): void
{
$this->settings->exportTagsAsKeywords = false;
$part = $this->createPartWithAllFieldGroups();
$fields = $this->helper->getKiCADPart($part)['fields'];

self::assertArrayNotHasKey('keywords', $fields);
}

/**
* The new export switches must also be part of the cache fingerprint.
*/
public function testCategoryPartsCacheIsInvalidatedByExportSettingsChange(): void
{
$category = $this->em->find(Category::class, 1);

$before = $this->helper->getCategoryParts($category);
self::assertNotEmpty($before);
self::assertArrayHasKey('Stock', $before[0]['fields']);

$this->settings->exportStockFields = false;
$after = $this->helper->getCategoryParts($category);
self::assertArrayNotHasKey('Stock', $after[0]['fields']);
}
}
Loading
Loading