Merge branch 'kicad-api'

This commit is contained in:
Jan Böhmer 2023-12-03 20:30:58 +01:00
commit 34fd611946
17 changed files with 263 additions and 50 deletions

View file

@ -42,6 +42,7 @@
PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
PassEnv EDA_KICAD_CATEGORY_DEPTH
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to

9
.env
View file

@ -159,6 +159,15 @@ PROVIDER_MOUSER_SEARCH_LIMIT=50
# Used when searching for keywords in the language specified when you signed up for Search API.
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
##################################################################################
# EDA integration related settings
##################################################################################
# This value determines the depth of the category tree, that is visible inside KiCad
# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels.
# Set to -1, to show all parts of Part-DB inside a single category in KiCad
EDA_KICAD_CATEGORY_DEPTH=0
###################################################################################
# SAML Single sign on-settings
###################################################################################

View file

@ -142,3 +142,4 @@ parameters:
env(HISTORY_SAVE_REMOVED_DATA): 1
env(HISTORY_SAVE_NEW_DATA): 1
env(EDA_KICAD_CATEGORY_DEPTH): 0

View file

@ -316,6 +316,13 @@ services:
$global_locale: '%partdb.locale%'
$global_timezone: '%partdb.timezone%'
####################################################################################################################
# EDA system
####################################################################################################################
App\Services\EDA\KiCadHelper:
arguments:
$category_depth: '%env(int:EDA_KICAD_CATEGORY_DEPTH)%'
####################################################################################################################
# Symfony overrides
####################################################################################################################

View file

@ -128,6 +128,14 @@ then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAV
* `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not
contain sensitive information, but could confuse end-users.
### EDA related settings
* `EDA_KICAD_CATEGORY_DEPTH`: A number, which determines how many levels of Part-DB categories should be shown inside KiCad.
All parts in the selected category and all subcategories are shown in KiCad.
For performance reason this value should not be too high. The default is 0, which means that only the top level categories are shown in KiCad.
All parts in the selected category and all subcategories are shown in KiCad. Set this to a higher value, if you want to show more categories in KiCad.
When you set this value to -1, all parts are shown inside a single category in KiCad.
### SAML SSO settings
The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to log in to

View file

@ -25,7 +25,8 @@ You require a user account in Part-DB, which has the permission to access Part-D
To connect KiCad with Part-DB do following steps:
1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently KiCAD can only read Part-DB database, so a token with read only scope is enough.
2. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content:
2. Add some EDA metadata to parts, categories or footprints. Only parts with useable info will show up in KiCad. See below for more info.
3. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content:
```
{
"meta": {
@ -41,11 +42,11 @@ To connect KiCad with Part-DB do following steps:
}
}
```
3. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel.
4. Replace the `token` field value with the token you have generated in step 1.
5. Open KiCad and add this created file as library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries)
4. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel.
5. Replace the `token` field value with the token you have generated in step 1.
6. Open KiCad and add this created file as library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries)
If you then place a new part, the library dialog opens and you should be able to see the categories and parts from Part-DB.
If you then place a new part, the library dialog opens, and you should be able to see the categories and parts from Part-DB.
### How to associate footprints and symbols with parts
@ -55,4 +56,24 @@ You can define this on a per-part basis using the KiCad symbol and KiCad footpri
For example to configure the values for an BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew.
If you type in a character, you will get an autocomplete list of all symbols and footprints available in the kicad standard library. You can also input your own value.
If you type in a character, you will get an autocomplete list of all symbols and footprints available in the kicad standard library. You can also input your own value.
### Parts and category visibility
Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad,
you need to define at least a symbol, footprint, reference prefix or value on a part, category or footprint.
You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad.
*Please note that KiCad caches the library categories. So if you change something, which would change the visibile categories in KiCad, you have to reload EEschema to see the changes.*
### Category depth in KiCad
For performance reasons, only the most top level categories of Part-DB are shown as categories in KiCad. All parts in the subcategories are shown in the top level category.
You can configure the depth of the categories shown in KiCad, via the `EDA_KICAD_CATEGORY_DEPTH` env option. The default value is 0, which meabs only the top level categories are shown.
To show more levels of categories, you can set this value to a higher number.
If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories.
You can view the "real" category path of a part in the part details dialog in KiCad.

View file

@ -25,7 +25,7 @@ namespace App\Controller;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Services\EDAIntegration\KiCADHelper;
use App\Services\EDA\KiCadHelper;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -36,7 +36,7 @@ use Symfony\Component\Routing\Annotation\Route;
class KiCadApiController extends AbstractController
{
public function __construct(
private readonly KiCADHelper $kiCADHelper,
private readonly KiCadHelper $kiCADHelper,
)
{
}
@ -62,9 +62,13 @@ class KiCadApiController extends AbstractController
}
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
public function categoryParts(Category $category): Response
public function categoryParts(?Category $category): Response
{
$this->denyAccessUnlessGranted('read', $category);
if ($category) {
$this->denyAccessUnlessGranted('read', $category);
} else {
$this->denyAccessUnlessGranted('@categories.read');
}
$this->denyAccessUnlessGranted('@parts.read');
return $this->json($this->kiCADHelper->getCategoryParts($category));

View file

@ -38,10 +38,10 @@ class EDACategoryInfo
#[Groups(['full', 'category:read', 'category:write'])]
private ?string $reference_prefix = null;
/** @var bool|null If this is true, then this part is invisible for the EDA software */
#[Column(type: Types::BOOLEAN, nullable: true)]
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $invisible = null;
private ?bool $visibility = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
@ -74,14 +74,14 @@ class EDACategoryInfo
return $this;
}
public function getInvisible(): ?bool
public function getVisibility(): ?bool
{
return $this->invisible;
return $this->visibility;
}
public function setInvisible(?bool $invisible): EDACategoryInfo
public function setVisibility(?bool $visibility): EDACategoryInfo
{
$this->invisible = $invisible;
$this->visibility = $visibility;
return $this;
}

View file

@ -43,10 +43,10 @@ class EDAPartInfo
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?string $value = null;
/** @var bool|null If this is true, then this part is invisible for the EDA software */
#[Column(type: Types::BOOLEAN, nullable: true)]
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $invisible = null;
private ?bool $visibility = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
@ -100,14 +100,14 @@ class EDAPartInfo
return $this;
}
public function getInvisible(): ?bool
public function getVisibility(): ?bool
{
return $this->invisible;
return $this->visibility;
}
public function setInvisible(?bool $invisible): EDAPartInfo
public function setVisibility(?bool $visibility): EDAPartInfo
{
$this->invisible = $invisible;
$this->visibility = $visibility;
return $this;
}

View file

@ -45,8 +45,9 @@ class EDACategoryInfoType extends AbstractType
]
]
)
->add('invisible', TriStateCheckboxType::class, [
'label' => 'eda_info.invisible',
->add('visibility', TriStateCheckboxType::class, [
'help' => 'eda_info.visibility.help',
'label' => 'eda_info.visibility',
])
->add('exclude_from_bom', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_bom',

View file

@ -50,8 +50,9 @@ class EDAPartInfoType extends AbstractType
'placeholder' => t('eda_info.value.placeholder'),
]
])
->add('invisible', TriStateCheckboxType::class, [
'label' => 'eda_info.invisible',
->add('visibility', TriStateCheckboxType::class, [
'help' => 'eda_info.visibility.help',
'label' => 'eda_info.visibility',
])
->add('exclude_from_bom', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_bom',

View file

@ -64,6 +64,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
return $this->getPartsCountRecursiveWithDepthN($element, self::RECURSION_LIMIT);
}
public function getPartsRecursive(AbstractPartsContainingDBElement $element): array
{
return $this->getPartsRecursiveWithDepthN($element, self::RECURSION_LIMIT);
}
/**
* The implementation of the recursive function to get the parts count.
* This function is used to limit the recursion depth (remaining_depth is decreased on each call).
@ -91,6 +96,23 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
return $count;
}
protected function getPartsRecursiveWithDepthN(AbstractPartsContainingDBElement $element, int $remaining_depth): array
{
if ($remaining_depth <= 0) {
throw new \RuntimeException('Recursion limit reached!');
}
//Add direct parts
$parts = $this->getParts($element);
//Then iterate over all children and add their parts
foreach ($element->getChildren() as $child) {
$parts = array_merge($parts, $this->getPartsRecursiveWithDepthN($child, $remaining_depth - 1));
}
return $parts;
}
protected function getPartsByField(object $element, array $order_by, string $field_name): array
{
if (!$element instanceof AbstractPartsContainingDBElement) {

View file

@ -21,19 +21,21 @@
declare(strict_types=1);
namespace App\Services\EDAIntegration;
namespace App\Services\EDA;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Part;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class KiCADHelper
class KiCadHelper
{
public function __construct(
@ -43,6 +45,8 @@ class KiCADHelper
private readonly ElementCacheTagGenerator $tagGenerator,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly TranslatorInterface $translator,
/** The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */
private readonly int $category_depth,
) {
}
@ -55,23 +59,48 @@ class KiCADHelper
*/
public function getCategories(): array
{
return $this->kicadCache->get('kicad_categories', function (ItemInterface $item) {
return $this->kicadCache->get('kicad_categories_' . $this->category_depth, function (ItemInterface $item) {
//Invalidate the cache on category changes
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class);
$item->tag($secure_class_name);
//If the category depth is smaller than 0, create only one dummy category
if ($this->category_depth < 0) {
return [
[
'id' => '0',
'name' => 'Part-DB',
]
];
}
//Otherwise just get the categories and filter them
$categories = $this->nodesListBuilder->typeToNodesList(Category::class);
$repo = $this->em->getRepository(Category::class);
$result = [];
foreach ($categories as $category) {
//Skip invisible categories
if ($category->getEdaInfo()->getInvisible() ?? false) {
if ($category->getEdaInfo()->getVisibility() === false) {
continue;
}
//Skip categories with a depth greater than the configured one
if ($category->getLevel() > $this->category_depth) {
continue;
}
/** @var $category Category */
//Ensure that the category contains parts
if ($repo->getPartsCount($category) < 1) {
//For the last level, we need to use a recursive query, otherwise we can use a simple query
/** @var Category $category */
$parts_count = $category->getLevel() >= $this->category_depth ? $repo->getPartsCountRecursive($category) : $repo->getPartsCount($category);
if ($parts_count < 1) {
continue;
}
//Check if the category should be visible
if (!$this->shouldCategoryBeVisible($category)) {
continue;
}
@ -89,25 +118,43 @@ class KiCADHelper
/**
* Returns an array of objects containing all parts for the given category in the format required by KiCAD.
* The result is cached for performance and invalidated on category or part changes.
* @param Category $category
* @param Category|null $category
* @return array
*/
public function getCategoryParts(Category $category): array
public function getCategoryParts(?Category $category): array
{
return $this->kicadCache->get('kicad_category_parts_'.$category->getID(),
return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth,
function (ItemInterface $item) use ($category) {
$item->tag([
$this->tagGenerator->getElementTypeCacheTag(Category::class),
$this->tagGenerator->getElementTypeCacheTag(Part::class)
$this->tagGenerator->getElementTypeCacheTag(Part::class),
//Visibility can change based on the footprint
$this->tagGenerator->getElementTypeCacheTag(Footprint::class)
]);
$category_repo = $this->em->getRepository(Category::class);
$parts = $category_repo->getParts($category);
if ($this->category_depth >= 0) {
//Ensure that the category is set
if (!$category) {
throw new NotFoundHttpException('Category must be set, if category_depth is greater than 1!');
}
$category_repo = $this->em->getRepository(Category::class);
if ($category->getLevel() >= $this->category_depth) {
//Get all parts for the category and its children
$parts = $category_repo->getPartsRecursive($category);
} else {
//Get only direct parts for the category (without children), as the category is not collapsed
$parts = $category_repo->getParts($category);
}
} else {
//Get all parts
$parts = $this->em->getRepository(Part::class)->findAll();
}
$result = [];
foreach ($parts as $part) {
//If the part is invisible, then skip it
if ($part->getEdaInfo()->getInvisible() ?? $part->getCategory()?->getEdaInfo()->getInvisible() ?? false) {
if (!$this->shouldPartBeVisible($part)) {
continue;
}
@ -186,6 +233,91 @@ class KiCADHelper
return $result;
}
/**
* Determine if the given part should be visible for the EDA.
* @param Category $category
* @return bool
*/
private function shouldCategoryBeVisible(Category $category): bool
{
$eda_info = $category->getEdaInfo();
//If the category visibility is explicitly set, then use it
if ($eda_info->getVisibility() !== null) {
return $eda_info->getVisibility();
}
//try to check if the fields were set
if ($eda_info->getKicadSymbol() !== null
|| $eda_info->getReferencePrefix() !== null) {
return true;
}
//Check if there is any part in this category, which should be visible
$category_repo = $this->em->getRepository(Category::class);
if ($category->getLevel() >= $this->category_depth) {
//Get all parts for the category and its children
$parts = $category_repo->getPartsRecursive($category);
} else {
//Get only direct parts for the category (without children), as the category is not collapsed
$parts = $category_repo->getParts($category);
}
foreach ($parts as $part) {
if ($this->shouldPartBeVisible($part)) {
return true;
}
}
//Otherwise the category should be not visible
return false;
}
/**
* Determine if the given part should be visible for the EDA.
* @param Part $part
* @return bool
*/
private function shouldPartBeVisible(Part $part): bool
{
$eda_info = $part->getEdaInfo();
$category = $part->getCategory();
//If the user set a visibility, then use it
if ($eda_info->getVisibility() !== null) {
return $part->getEdaInfo()->getVisibility();
}
//If the part has a category, then use the category visibility if possible
if ($category && $category->getEdaInfo()->getVisibility() !== null) {
return $category->getEdaInfo()->getVisibility();
}
//If both are null, then we try to determine the visibility based on if fields are set
if ($eda_info->getKicadSymbol() !== null
|| $eda_info->getKicadFootprint() !== null
|| $eda_info->getReferencePrefix() !== null
|| $eda_info->getValue() !== null) {
return true;
}
//Check also if the fields are set for the category (if it exists)
if ($category && (
$category->getEdaInfo()->getKicadSymbol() !== null
|| $category->getEdaInfo()->getReferencePrefix() !== null
)) {
return true;
}
//And on the footprint
if ($part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null) {
return true;
}
//Otherwise the part should be not visible
return false;
}
/**
* Converts a boolean value to the format required by KiCAD.
* @param bool $value

View file

@ -41,7 +41,7 @@
<div class="row">
<div class="col-sm-9 offset-sm-3">
{{ form_row(form.eda_info.invisible) }}
{{ form_row(form.eda_info.visibility) }}
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="row">
<div class="col-sm-9 offset-sm-3">
{{ form_row(form.eda_info.invisible) }}
{{ form_row(form.eda_info.visibility) }}
</div>
</div>

View file

@ -92,7 +92,7 @@
<br>
<b>{% trans %}eda_info.value{% endtrans %}:</b> {{ part.edaInfo.value }}
<br>
<b>{% trans %}eda_info.invisible{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.invisible ?? part.category.edaInfo.invisible ?? false) }}
<b>{% trans %}eda_info.visibility{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.visibility ?? part.category.edaInfo.visibility) }}
<br>
<b>{% trans %}eda_info.exclude_from_bom{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.excludeFromBom ?? part.category.edaInfo.excludeFromBom ?? false) }}
<br>

View file

@ -12107,12 +12107,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>e.g. 100n</target>
</segment>
</unit>
<unit id="Q8nJNWg" name="eda_info.invisible">
<segment>
<source>eda_info.invisible</source>
<target>Invisible to EDA software</target>
</segment>
</unit>
<unit id="VphdbQ9" name="eda_info.exclude_from_bom">
<segment>
<source>eda_info.exclude_from_bom</source>
@ -12179,5 +12173,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>KiCad API root URL</target>
</segment>
</unit>
<unit id="UXT_MIM" name="eda_info.visibility">
<segment>
<source>eda_info.visibility</source>
<target>Force visibility</target>
</segment>
</unit>
<unit id="NN5DZ4F" name="eda_info.visibility.help">
<segment>
<source>eda_info.visibility.help</source>
<target>By default, the visibility to the EDA software is automatically determined. With this checkbox, you can force the part to be visible or invisible.</target>
</segment>
</unit>
</file>
</xliff>