Add KiCad connector generator plugin
This commit is contained in:
parent
3091611d3b
commit
283c6ce6cf
12 changed files with 33720 additions and 32021 deletions
File diff suppressed because it is too large
Load diff
137
Readme.md
137
Readme.md
|
|
@ -1,17 +1,136 @@
|
||||||
# Настройка симлинка для библиотеки PDF
|
# KiCad User Library
|
||||||
|
|
||||||
Этот репозиторий/проект использует внешнюю директорию для хранения документации (Altium DataLIB). Чтобы пути работали корректно, необходимо создать символическую ссылку.
|
Пользовательская библиотека KiCad: символы, футпринты, DB Library, рабочие
|
||||||
|
листы, шрифты и ссылки на внешние PDF/3D-ресурсы.
|
||||||
|
|
||||||
## Инструкция по созданию ссылки
|
PDF-документация и 3D-модели хранятся вне этого репозитория. KiCad должен
|
||||||
|
получать к ним доступ через переменные путей, заданные в `Configure Paths`.
|
||||||
|
|
||||||
Выполните следующую команду в терминале, находясь в корневой папке проекта:
|
## Настройка
|
||||||
|
|
||||||
|
Склонируйте библиотеку в удобную папку и запустите настройку:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ln -s /mnt/диск/YandexDisk/Altium/DataLIB/PDF ./PDF
|
./scripts/setup_kicad_user_library.py --pdf-dir "<путь-к-папке-PDF>"
|
||||||
'
|
```
|
||||||
|
|
||||||
## Настройка библиотеки
|
Если используется отдельная папка 3D-моделей:
|
||||||
|
|
||||||
Необходимо создать переменные окружения (пути к библиотекам)
|
```bash
|
||||||
Название KICKAD_USER_LIB_DIR
|
./scripts/setup_kicad_user_library.py \
|
||||||
|
--pdf-dir "<путь-к-папке-PDF>" \
|
||||||
|
--3d-dir "<путь-к-папке-3D-моделей>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
|
||||||
|
- прописывает переменные путей в `Configure Paths`;
|
||||||
|
- добавляет подключение DB Library;
|
||||||
|
- подключает таблицы символов и футпринтов из этого репозитория;
|
||||||
|
- создаёт резервные копии изменяемых файлов настроек KiCad.
|
||||||
|
|
||||||
|
## Переменные KiCad
|
||||||
|
|
||||||
|
| Переменная | Назначение |
|
||||||
|
| --- | --- |
|
||||||
|
| `KICAD_LIB_DIR_USER` | корень этого репозитория |
|
||||||
|
| `KICAD_PDF_DIR_USER` | папка с PDF-документацией компонентов |
|
||||||
|
| `KICAD_3D_PARTY_USER` | папка с пользовательскими 3D-моделями |
|
||||||
|
|
||||||
|
`KICAD_PDF_DIR_USER` должна совпадать по смыслу с настройкой сервера
|
||||||
|
eda-core `kicad.pdf_dir_var`: сервер передаёт KiCad путь Datasheet в виде
|
||||||
|
`${KICAD_PDF_DIR_USER}/имя_файла.pdf`.
|
||||||
|
|
||||||
|
## Подключения в KiCad
|
||||||
|
|
||||||
|
После настройки в менеджере библиотек должны быть такие записи.
|
||||||
|
|
||||||
|
### Symbol Libraries
|
||||||
|
|
||||||
|
| Имя | Путь | Тип |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `eda-core` | `${KICAD_LIB_DIR_USER}/eda-core.kicad_dbl` | `Database` |
|
||||||
|
| `sym-lib-table` | `${KICAD_LIB_DIR_USER}/sym-lib-table` | `Table` |
|
||||||
|
|
||||||
|
### Footprint Libraries
|
||||||
|
|
||||||
|
| Имя | Путь | Тип |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `fp-lib-table` | `${KICAD_LIB_DIR_USER}/fp-lib-table` | `Table` |
|
||||||
|
|
||||||
|
## Символы и футпринты
|
||||||
|
|
||||||
|
Таблицы из репозитория уже содержат подключения к библиотекам:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_LIB_DIR_USER}/Connectors/Ki-Connectors.kicad_sym
|
||||||
|
${KICAD_LIB_DIR_USER}/Resistors/Ki-Resistors.kicad_sym
|
||||||
|
${KICAD_LIB_DIR_USER}/Capacitors/Ki-Capacitors.kicad_sym
|
||||||
|
```
|
||||||
|
|
||||||
|
И аналогичные записи для остальных разделов библиотеки.
|
||||||
|
|
||||||
|
## 3D-модели
|
||||||
|
|
||||||
|
Футпринты должны ссылаться на 3D-модели через переменную
|
||||||
|
`KICAD_3D_PARTY_USER`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_3D_PARTY_USER}/Connectors/Molex-20pin-0039281203.stp
|
||||||
|
```
|
||||||
|
|
||||||
|
## PDF-документация
|
||||||
|
|
||||||
|
В базе компонента хранится только имя PDF-файла. Полный путь для KiCad
|
||||||
|
собирает сервер eda-core:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_PDF_DIR_USER}/имя_файла.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Плагин генератора разъёмов
|
||||||
|
|
||||||
|
В библиотеке есть KiCad-плагин для генерации однотипных символов разъёмов.
|
||||||
|
Установка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install_connector_generator_plugin.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт создаёт symlink из пользовательской папки плагинов KiCad на плагин
|
||||||
|
внутри этого репозитория. После установки перезапустите KiCad и откройте:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PCB Editor -> Tools -> External Plugins -> Generate Connector Symbols
|
||||||
|
```
|
||||||
|
|
||||||
|
Плагин сам находит корень библиотеки рядом с собой и по умолчанию работает с:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_LIB_DIR_USER}/Connectors/Ki-Connectors.kicad_sym
|
||||||
|
```
|
||||||
|
|
||||||
|
## Рабочие листы и шрифты
|
||||||
|
|
||||||
|
Рабочий лист:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_LIB_DIR_USER}/gost_landscape.kicad_wks
|
||||||
|
```
|
||||||
|
|
||||||
|
Шрифты:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${KICAD_LIB_DIR_USER}/Fonts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
1. В `Configure Paths` заданы `KICAD_LIB_DIR_USER`, `KICAD_PDF_DIR_USER` и при необходимости `KICAD_3D_PARTY_USER`.
|
||||||
|
2. В Symbol Libraries видны `eda-core` и `sym-lib-table`.
|
||||||
|
3. В Footprint Libraries видна `fp-lib-table`.
|
||||||
|
4. Компоненты из DB Library добавляются на схему.
|
||||||
|
5. Datasheet открывается по пути `${KICAD_PDF_DIR_USER}/...`.
|
||||||
|
6. 3D-модели открываются по пути `${KICAD_3D_PARTY_USER}/...`.
|
||||||
|
|
|
||||||
16
fp-lib-table
16
fp-lib-table
|
|
@ -1,11 +1,11 @@
|
||||||
(fp_lib_table
|
(fp_lib_table
|
||||||
(version 7)
|
(version 7)
|
||||||
(lib (name "Ki-Capacitors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Capacitors/Ki-Capacitors.pretty") (options "") (descr ""))
|
(lib (name "Ki-Capacitors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Capacitors/Ki-Capacitors.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Connectors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Connectors/Ki-Connectors.pretty") (options "") (descr ""))
|
(lib (name "Ki-Connectors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Connectors/Ki-Connectors.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Diodes") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Diodes/Ki-Diodes.pretty") (options "") (descr ""))
|
(lib (name "Ki-Diodes") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Diodes/Ki-Diodes.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-IC") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/IC/Ki-IC.pretty") (options "") (descr ""))
|
(lib (name "Ki-IC") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/IC/Ki-IC.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Modules") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Modules/Ki-Modules.pretty") (options "") (descr ""))
|
(lib (name "Ki-Modules") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Modules/Ki-Modules.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Resistors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Resistors/Ki-Resistors.pretty") (options "") (descr ""))
|
(lib (name "Ki-Resistors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Resistors/Ki-Resistors.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Transistor") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Transistor/Ki-Transistor.pretty") (options "") (descr ""))
|
(lib (name "Ki-Transistor") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Transistor/Ki-Transistor.pretty") (options "") (descr ""))
|
||||||
(lib (name "Ki-Other") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Other/Ki-Other.pretty") (options "") (descr ""))
|
(lib (name "Ki-Other") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Other/Ki-Other.pretty") (options "") (descr ""))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
41
plugins/connector-generator/README.md
Normal file
41
plugins/connector-generator/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Connector Generator Plugin
|
||||||
|
|
||||||
|
Python ActionPlugin для KiCad, который запускает генератор регулярных символов
|
||||||
|
разъемов из интерфейса KiCad.
|
||||||
|
|
||||||
|
Плагин появляется в:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PCB Editor -> Tools -> External Plugins -> Generate Connector Symbols
|
||||||
|
```
|
||||||
|
|
||||||
|
Плагин самодостаточен и находится внутри репозитория библиотеки. Корень
|
||||||
|
библиотеки определяется автоматически по файлу `eda-core.kicad_dbl` и папке
|
||||||
|
`Connectors`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install_connector_generator_plugin.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт создает symlink в пользовательскую папку плагинов KiCad. После установки
|
||||||
|
перезапустите KiCad.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- выбрать `connectors.toml`;
|
||||||
|
- задать серию, ряды, отдельные списки пинов для 1/2/3 рядов и схемы нумерации;
|
||||||
|
- настроить показ номера и имени вывода;
|
||||||
|
- включить или выключить заливку прямоугольника фоном;
|
||||||
|
- увидеть список создаваемых/обновляемых символов;
|
||||||
|
- посмотреть таблицу пинов выбранного символа;
|
||||||
|
- сгенерировать и записать библиотеку;
|
||||||
|
- открыть технический diff при необходимости;
|
||||||
|
- открыть TOML-конфиг во внешнем редакторе.
|
||||||
|
|
||||||
|
## Заметка
|
||||||
|
|
||||||
|
Это `pcbnew.ActionPlugin`, поэтому KiCad показывает его из PCB Editor. Сам
|
||||||
|
генератор работает с `.kicad_sym` как с обычным файлом и не зависит от открытой
|
||||||
|
платы.
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""KiCad ActionPlugin registration for the connector generator."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pcbnew # noqa: F401
|
||||||
|
|
||||||
|
from .main import ConnectorGeneratorPlugin
|
||||||
|
|
||||||
|
ConnectorGeneratorPlugin().register()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except Exception as exc:
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger("connector_generator_plugin").error("Failed to register plugin: %s", exc)
|
||||||
|
|
@ -0,0 +1,750 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate regular KiCad connector symbols while preserving manual variants."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SCHEME_MAP = {
|
||||||
|
"R": "row",
|
||||||
|
"S": "column",
|
||||||
|
"Z": "column_snake",
|
||||||
|
}
|
||||||
|
|
||||||
|
REFERENCE_FIELD_Y = 1.25
|
||||||
|
VALUE_FIELD_BOTTOM_OFFSET = 2.5
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Geometry:
|
||||||
|
body_width: float = 20.0
|
||||||
|
pin_pitch: float = 5.0
|
||||||
|
pin_length: float = 2.5
|
||||||
|
body_fill: str = "none"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SymbolSpec:
|
||||||
|
name: str
|
||||||
|
rows: int
|
||||||
|
pins_per_row: int
|
||||||
|
numbering: str
|
||||||
|
reference: str = "X"
|
||||||
|
value: str = "XX"
|
||||||
|
display_name: str = ""
|
||||||
|
show_pin_numbers: bool = True
|
||||||
|
show_pin_names: bool = False
|
||||||
|
geometry: Geometry = Geometry()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SeriesSpec:
|
||||||
|
prefix: str
|
||||||
|
rows: tuple[int, ...]
|
||||||
|
pins_per_row: tuple[int, ...]
|
||||||
|
pins_per_row_by_rows: dict[int, tuple[int, ...]] | None
|
||||||
|
schemes: tuple[str, ...]
|
||||||
|
scheme_map: dict[str, str]
|
||||||
|
reference: str = "X"
|
||||||
|
value: str = "XX"
|
||||||
|
display_name: str = ""
|
||||||
|
show_pin_numbers: bool = True
|
||||||
|
show_pin_names: bool = False
|
||||||
|
zero_pad_pins: int = 2
|
||||||
|
skip_single_row_non_row: bool = True
|
||||||
|
geometry: Geometry = Geometry()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SymbolBlock:
|
||||||
|
name: str
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_num(value: float) -> str:
|
||||||
|
if value == int(value):
|
||||||
|
return str(int(value))
|
||||||
|
return f"{value:g}"
|
||||||
|
|
||||||
|
|
||||||
|
def skip_string(text: str, i: int) -> int:
|
||||||
|
i += 1
|
||||||
|
while i < len(text):
|
||||||
|
if text[i] == "\\":
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if text[i] == '"':
|
||||||
|
return i + 1
|
||||||
|
i += 1
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def read_quoted(text: str, i: int) -> tuple[str, int]:
|
||||||
|
assert text[i] == '"'
|
||||||
|
i += 1
|
||||||
|
out: list[str] = []
|
||||||
|
|
||||||
|
while i < len(text):
|
||||||
|
ch = text[i]
|
||||||
|
if ch == "\\" and i + 1 < len(text):
|
||||||
|
out.append(text[i + 1])
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if ch == '"':
|
||||||
|
return "".join(out), i + 1
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
raise ValueError("unterminated quoted string")
|
||||||
|
|
||||||
|
|
||||||
|
def is_list_at(text: str, i: int, head: str) -> bool:
|
||||||
|
token = f"({head}"
|
||||||
|
return text.startswith(token, i) and (
|
||||||
|
i + len(token) == len(text) or text[i + len(token)].isspace() or text[i + len(token)] == ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def symbol_name_at(text: str, i: int) -> str:
|
||||||
|
pos = i + len("(symbol")
|
||||||
|
while pos < len(text) and text[pos].isspace():
|
||||||
|
pos += 1
|
||||||
|
if pos >= len(text) or text[pos] != '"':
|
||||||
|
raise ValueError(f"symbol at offset {i} has no quoted name")
|
||||||
|
name, _ = read_quoted(text, pos)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def matching_paren(text: str, start: int) -> int:
|
||||||
|
depth = 0
|
||||||
|
i = start
|
||||||
|
|
||||||
|
while i < len(text):
|
||||||
|
ch = text[i]
|
||||||
|
if ch == '"':
|
||||||
|
i = skip_string(text, i)
|
||||||
|
continue
|
||||||
|
if ch == "(":
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
return i + 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
raise ValueError(f"unclosed list at offset {start}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_symbol_blocks(text: str, target_depth: int) -> list[SymbolBlock]:
|
||||||
|
blocks: list[SymbolBlock] = []
|
||||||
|
depth = 0
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(text):
|
||||||
|
ch = text[i]
|
||||||
|
if ch == '"':
|
||||||
|
i = skip_string(text, i)
|
||||||
|
continue
|
||||||
|
if ch == "(":
|
||||||
|
if depth == target_depth and is_list_at(text, i, "symbol"):
|
||||||
|
end = matching_paren(text, i)
|
||||||
|
blocks.append(SymbolBlock(symbol_name_at(text, i), i, end, text[i:end]))
|
||||||
|
i = end
|
||||||
|
continue
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def find_list_blocks(text: str, head: str, target_depth: int) -> list[str]:
|
||||||
|
blocks: list[str] = []
|
||||||
|
depth = 0
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(text):
|
||||||
|
ch = text[i]
|
||||||
|
if ch == '"':
|
||||||
|
i = skip_string(text, i)
|
||||||
|
continue
|
||||||
|
if ch == "(":
|
||||||
|
if depth == target_depth and is_list_at(text, i, head):
|
||||||
|
end = matching_paren(text, i)
|
||||||
|
blocks.append(text[i:end])
|
||||||
|
i = end
|
||||||
|
continue
|
||||||
|
depth += 1
|
||||||
|
elif ch == ")":
|
||||||
|
depth -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def child_symbol_blocks(symbol_text: str) -> list[SymbolBlock]:
|
||||||
|
return find_symbol_blocks(symbol_text, target_depth=1)
|
||||||
|
|
||||||
|
|
||||||
|
def child_style(symbol_name: str, parent_name: str) -> tuple[int, int] | None:
|
||||||
|
prefix = parent_name + "_"
|
||||||
|
if not symbol_name.startswith(prefix):
|
||||||
|
return None
|
||||||
|
|
||||||
|
suffix = symbol_name[len(prefix) :]
|
||||||
|
parts = suffix.rsplit("_", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(parts[0]), int(parts[1])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def manual_children(existing_symbol_text: str | None, parent_name: str) -> list[str]:
|
||||||
|
if not existing_symbol_text:
|
||||||
|
return []
|
||||||
|
|
||||||
|
out: list[str] = []
|
||||||
|
for block in child_symbol_blocks(existing_symbol_text):
|
||||||
|
style = child_style(block.name, parent_name)
|
||||||
|
if style is None or style[1] >= 2:
|
||||||
|
out.append("\t\t" + block.text)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def manual_variant_count(existing_symbol_text: str | None, parent_name: str) -> int:
|
||||||
|
if not existing_symbol_text:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for block in child_symbol_blocks(existing_symbol_text):
|
||||||
|
style = child_style(block.name, parent_name)
|
||||||
|
if style is None or style[1] >= 2:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def existing_body_styles(existing_symbol_text: str | None) -> str | None:
|
||||||
|
if not existing_symbol_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
blocks = find_list_blocks(existing_symbol_text, "body_styles", target_depth=1)
|
||||||
|
if not blocks:
|
||||||
|
return None
|
||||||
|
return "\t\t" + blocks[0]
|
||||||
|
|
||||||
|
|
||||||
|
def pin_number(spec: SymbolSpec, row: int, index: int) -> int:
|
||||||
|
scheme = spec.numbering
|
||||||
|
|
||||||
|
if scheme == "row":
|
||||||
|
return row * spec.pins_per_row + index + 1
|
||||||
|
|
||||||
|
if scheme == "column":
|
||||||
|
return index * spec.rows + row + 1
|
||||||
|
|
||||||
|
if scheme == "column_snake":
|
||||||
|
row_in_column = row if index % 2 == 0 else spec.rows - row - 1
|
||||||
|
return index * spec.rows + row_in_column + 1
|
||||||
|
|
||||||
|
raise ValueError(f"unsupported numbering scheme: {scheme}")
|
||||||
|
|
||||||
|
|
||||||
|
def pin_matrix(spec: SymbolSpec) -> list[list[int]]:
|
||||||
|
return [
|
||||||
|
[pin_number(spec, row, index) for row in range(spec.rows)]
|
||||||
|
for index in range(spec.pins_per_row)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def format_pin_preview(spec: SymbolSpec) -> str:
|
||||||
|
matrix = pin_matrix(spec)
|
||||||
|
width = max(len(str(number)) for row in matrix for number in row)
|
||||||
|
rows = [" ".join(f"{number:>{width}}" for number in row) for row in matrix]
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
f"{spec.name}",
|
||||||
|
f"{spec.rows} ряд(а), {spec.pins_per_row} позиций, {spec.numbering}",
|
||||||
|
"",
|
||||||
|
*rows,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_numbers(spec: SymbolSpec) -> None:
|
||||||
|
nums = [
|
||||||
|
pin_number(spec, row, index)
|
||||||
|
for row in range(spec.rows)
|
||||||
|
for index in range(spec.pins_per_row)
|
||||||
|
]
|
||||||
|
expected = list(range(1, spec.rows * spec.pins_per_row + 1))
|
||||||
|
|
||||||
|
if sorted(nums) != expected:
|
||||||
|
raise ValueError(
|
||||||
|
f"{spec.name}: numbering {spec.numbering!r} produced {nums}, expected {expected}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_property(
|
||||||
|
name: str,
|
||||||
|
value: str,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
size: float,
|
||||||
|
hide: bool = True,
|
||||||
|
justify: str | None = None,
|
||||||
|
do_not_autoplace: bool = True,
|
||||||
|
) -> str:
|
||||||
|
hide_line = "\n\t\t\t(hide yes)" if hide else ""
|
||||||
|
justify_block = f"\n\t\t\t\t(justify {justify})" if justify else ""
|
||||||
|
autoplace = "yes" if do_not_autoplace else "no"
|
||||||
|
|
||||||
|
return (
|
||||||
|
f'\t\t(property "{name}" "{value}"\n'
|
||||||
|
f"\t\t\t(at {fmt_num(x)} {fmt_num(y)} 0)\n"
|
||||||
|
"\t\t\t(show_name no)\n"
|
||||||
|
f"\t\t\t(do_not_autoplace {autoplace})"
|
||||||
|
f"{hide_line}\n"
|
||||||
|
"\t\t\t(effects\n"
|
||||||
|
"\t\t\t\t(font\n"
|
||||||
|
f"\t\t\t\t\t(size {fmt_num(size)} {fmt_num(size)})\n"
|
||||||
|
"\t\t\t\t)"
|
||||||
|
f"{justify_block}\n"
|
||||||
|
"\t\t\t)\n"
|
||||||
|
"\t\t)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pin_label_settings(spec: SymbolSpec) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
if not spec.show_pin_numbers:
|
||||||
|
lines += [
|
||||||
|
"\t\t(pin_numbers",
|
||||||
|
"\t\t\t(hide yes)",
|
||||||
|
"\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
if spec.show_pin_names:
|
||||||
|
lines += [
|
||||||
|
"\t\t(pin_names",
|
||||||
|
"\t\t\t(offset 1)",
|
||||||
|
"\t\t)",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
lines += [
|
||||||
|
"\t\t(pin_names",
|
||||||
|
"\t\t\t(hide yes)",
|
||||||
|
"\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def generate_unit(spec: SymbolSpec, row: int) -> str:
|
||||||
|
geo = spec.geometry
|
||||||
|
height = spec.pins_per_row * geo.pin_pitch
|
||||||
|
unit = row + 1
|
||||||
|
lines: list[str] = [f'\t\t(symbol "{spec.name}_{unit}_1"']
|
||||||
|
|
||||||
|
lines += [
|
||||||
|
"\t\t\t(rectangle",
|
||||||
|
"\t\t\t\t(start 0 0)",
|
||||||
|
f"\t\t\t\t(end {fmt_num(geo.body_width)} -{fmt_num(height)})",
|
||||||
|
"\t\t\t\t(stroke",
|
||||||
|
"\t\t\t\t\t(width 0)",
|
||||||
|
"\t\t\t\t\t(type solid)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t\t(fill",
|
||||||
|
f"\t\t\t\t\t(type {geo.body_fill})",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx in range(1, spec.pins_per_row):
|
||||||
|
y = idx * geo.pin_pitch
|
||||||
|
lines += [
|
||||||
|
"\t\t\t(polyline",
|
||||||
|
"\t\t\t\t(pts",
|
||||||
|
f"\t\t\t\t\t(xy 0 -{fmt_num(y)}) (xy {fmt_num(geo.body_width)} -{fmt_num(y)})",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t\t(stroke",
|
||||||
|
"\t\t\t\t\t(width 0)",
|
||||||
|
"\t\t\t\t\t(type solid)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t\t(fill",
|
||||||
|
"\t\t\t\t\t(type none)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
lines += [
|
||||||
|
"\t\t\t(polyline",
|
||||||
|
"\t\t\t\t(pts",
|
||||||
|
f"\t\t\t\t\t(xy 5 0) (xy 5 -{fmt_num(height)})",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t\t(stroke",
|
||||||
|
"\t\t\t\t\t(width 0)",
|
||||||
|
"\t\t\t\t\t(type solid)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t\t(fill",
|
||||||
|
"\t\t\t\t\t(type none)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
for idx in range(spec.pins_per_row):
|
||||||
|
number = str(pin_number(spec, row, idx))
|
||||||
|
y = idx * geo.pin_pitch + geo.pin_pitch / 2
|
||||||
|
lines += [
|
||||||
|
f'\t\t\t(text "{number}"',
|
||||||
|
f"\t\t\t\t(at 2.5 -{fmt_num(y)} 0)",
|
||||||
|
"\t\t\t\t(effects",
|
||||||
|
"\t\t\t\t\t(font",
|
||||||
|
"\t\t\t\t\t\t(size 2.1844 2.1844)",
|
||||||
|
"\t\t\t\t\t)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t)",
|
||||||
|
]
|
||||||
|
lines += [
|
||||||
|
"\t\t\t(pin passive line",
|
||||||
|
f"\t\t\t\t(at -{fmt_num(geo.pin_length)} -{fmt_num(y)} 0)",
|
||||||
|
f"\t\t\t\t(length {fmt_num(geo.pin_length)})",
|
||||||
|
f'\t\t\t\t(name "{number}"',
|
||||||
|
"\t\t\t\t\t(effects",
|
||||||
|
"\t\t\t\t\t\t(font",
|
||||||
|
"\t\t\t\t\t\t\t(size 1.8 1.8)",
|
||||||
|
"\t\t\t\t\t\t)",
|
||||||
|
"\t\t\t\t\t)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
f'\t\t\t\t(number "{number}"',
|
||||||
|
"\t\t\t\t\t(effects",
|
||||||
|
"\t\t\t\t\t\t(font",
|
||||||
|
"\t\t\t\t\t\t\t(size 1.8 1.8)",
|
||||||
|
"\t\t\t\t\t\t)",
|
||||||
|
"\t\t\t\t\t)",
|
||||||
|
"\t\t\t\t)",
|
||||||
|
"\t\t\t)",
|
||||||
|
]
|
||||||
|
|
||||||
|
lines.append("\t\t)")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_symbol(spec: SymbolSpec, existing_symbol_text: str | None = None) -> str:
|
||||||
|
validate_numbers(spec)
|
||||||
|
|
||||||
|
geo = spec.geometry
|
||||||
|
height = spec.pins_per_row * geo.pin_pitch
|
||||||
|
reference_y = REFERENCE_FIELD_Y
|
||||||
|
value_y = -height - VALUE_FIELD_BOTTOM_OFFSET
|
||||||
|
manual = manual_children(existing_symbol_text, spec.name)
|
||||||
|
styles = existing_body_styles(existing_symbol_text) if manual else None
|
||||||
|
|
||||||
|
lines: list[str] = [f'(symbol "{spec.name}"']
|
||||||
|
if styles:
|
||||||
|
lines.append(styles)
|
||||||
|
|
||||||
|
lines += generate_pin_label_settings(spec)
|
||||||
|
|
||||||
|
lines += [
|
||||||
|
"\t\t(exclude_from_sim no)",
|
||||||
|
"\t\t(in_bom yes)",
|
||||||
|
"\t\t(on_board yes)",
|
||||||
|
"\t\t(in_pos_files yes)",
|
||||||
|
"\t\t(duplicate_pin_numbers_are_jumpers no)",
|
||||||
|
generate_property("Reference", spec.reference, 0, reference_y, 2, hide=False, justify="left"),
|
||||||
|
generate_property("Value", spec.value, 0, value_y, 2, hide=False, justify="left bottom"),
|
||||||
|
generate_property("Footprint", "", 0, 0, 1.27, do_not_autoplace=False),
|
||||||
|
generate_property("Datasheet", "", 0, 0, 1.27, do_not_autoplace=False),
|
||||||
|
generate_property("Description", "", 0, 0, 1.27, do_not_autoplace=False),
|
||||||
|
]
|
||||||
|
|
||||||
|
if spec.display_name:
|
||||||
|
lines.append(
|
||||||
|
generate_property(
|
||||||
|
"Name",
|
||||||
|
spec.display_name,
|
||||||
|
5.5,
|
||||||
|
1,
|
||||||
|
1.8288,
|
||||||
|
justify="left bottom",
|
||||||
|
do_not_autoplace=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in range(spec.rows):
|
||||||
|
lines.append(generate_unit(spec, row))
|
||||||
|
|
||||||
|
lines.extend(manual)
|
||||||
|
lines.append("\t)")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_top_level_symbols(library_text: str, replacements: dict[str, str]) -> str:
|
||||||
|
blocks = find_symbol_blocks(library_text, target_depth=1)
|
||||||
|
out: list[str] = []
|
||||||
|
pos = 0
|
||||||
|
replaced: set[str] = set()
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
out.append(library_text[pos : block.start])
|
||||||
|
if block.name in replacements:
|
||||||
|
out.append(replacements[block.name])
|
||||||
|
replaced.add(block.name)
|
||||||
|
else:
|
||||||
|
out.append(block.text)
|
||||||
|
pos = block.end
|
||||||
|
|
||||||
|
out.append(library_text[pos:])
|
||||||
|
result = "".join(out)
|
||||||
|
|
||||||
|
missing = [name for name in replacements if name not in replaced]
|
||||||
|
if missing:
|
||||||
|
insert_at = result.rfind("\n)")
|
||||||
|
if insert_at == -1:
|
||||||
|
raise ValueError("cannot find library closing paren")
|
||||||
|
addition = "\n" + "\n".join(replacements[name] for name in missing)
|
||||||
|
result = result[:insert_at] + addition + result[insert_at:]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def int_list(values: object, field_name: str) -> tuple[int, ...]:
|
||||||
|
if not isinstance(values, list):
|
||||||
|
raise ValueError(f"{field_name} must be a list")
|
||||||
|
return tuple(int(value) for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def str_list(values: object, field_name: str) -> tuple[str, ...]:
|
||||||
|
if not isinstance(values, list):
|
||||||
|
raise ValueError(f"{field_name} must be a list")
|
||||||
|
return tuple(str(value) for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_geometry(data: dict) -> Geometry:
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
geo_defaults = defaults.get("geometry", {})
|
||||||
|
return Geometry(
|
||||||
|
body_width=float(geo_defaults.get("body_width", 20.0)),
|
||||||
|
pin_pitch=float(geo_defaults.get("pin_pitch", 5.0)),
|
||||||
|
pin_length=float(geo_defaults.get("pin_length", 2.5)),
|
||||||
|
body_fill=str(geo_defaults.get("body_fill", "none")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_pin_label_settings(data: dict) -> tuple[bool, bool]:
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
pin_labels = defaults.get("pin_labels", {})
|
||||||
|
return (
|
||||||
|
bool(pin_labels.get("show_pin_numbers", True)),
|
||||||
|
bool(pin_labels.get("show_pin_names", False)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_symbol_name(prefix: str, rows: int, pins_per_row: int, scheme_code: str, zero_pad: int = 2) -> str:
|
||||||
|
pin_text = f"{pins_per_row:0{zero_pad}d}" if zero_pad > 0 else str(pins_per_row)
|
||||||
|
return f"{prefix}-{rows}X-{pin_text}P-{scheme_code}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_series(
|
||||||
|
item: dict,
|
||||||
|
geometry: Geometry,
|
||||||
|
default_show_pin_numbers: bool,
|
||||||
|
default_show_pin_names: bool,
|
||||||
|
) -> SeriesSpec:
|
||||||
|
scheme_map = dict(DEFAULT_SCHEME_MAP)
|
||||||
|
scheme_map.update({str(key): str(value) for key, value in item.get("scheme_map", {}).items()})
|
||||||
|
pins_by_rows = {
|
||||||
|
int(rows): int_list(values, f"series.pins_per_row_by_rows.{rows}")
|
||||||
|
for rows, values in item.get("pins_per_row_by_rows", {}).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return SeriesSpec(
|
||||||
|
prefix=str(item.get("prefix", "Con")),
|
||||||
|
rows=int_list(item.get("rows", []), "series.rows"),
|
||||||
|
pins_per_row=int_list(item.get("pins_per_row", []), "series.pins_per_row"),
|
||||||
|
pins_per_row_by_rows=pins_by_rows or None,
|
||||||
|
schemes=str_list(item.get("schemes", ["R"]), "series.schemes"),
|
||||||
|
scheme_map=scheme_map,
|
||||||
|
reference=str(item.get("reference", "X")),
|
||||||
|
value=str(item.get("value", "XX")),
|
||||||
|
display_name=str(item.get("display_name", "")),
|
||||||
|
show_pin_numbers=bool(item.get("show_pin_numbers", default_show_pin_numbers)),
|
||||||
|
show_pin_names=bool(item.get("show_pin_names", default_show_pin_names)),
|
||||||
|
zero_pad_pins=int(item.get("zero_pad_pins", 2)),
|
||||||
|
skip_single_row_non_row=bool(item.get("skip_single_row_non_row", True)),
|
||||||
|
geometry=geometry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def specs_from_series(series: SeriesSpec) -> list[SymbolSpec]:
|
||||||
|
specs: list[SymbolSpec] = []
|
||||||
|
|
||||||
|
for rows in series.rows:
|
||||||
|
pin_values = series.pins_per_row
|
||||||
|
if series.pins_per_row_by_rows is not None:
|
||||||
|
pin_values = series.pins_per_row_by_rows.get(rows, ())
|
||||||
|
if not pin_values:
|
||||||
|
raise ValueError(f"no pins_per_row configured for {rows} row(s) in series {series.prefix!r}")
|
||||||
|
|
||||||
|
for pins in pin_values:
|
||||||
|
for scheme_code in series.schemes:
|
||||||
|
numbering = series.scheme_map.get(scheme_code)
|
||||||
|
if not numbering:
|
||||||
|
raise ValueError(f"unknown scheme code {scheme_code!r} in series {series.prefix!r}")
|
||||||
|
if series.skip_single_row_non_row and rows == 1 and numbering != "row":
|
||||||
|
continue
|
||||||
|
|
||||||
|
specs.append(
|
||||||
|
SymbolSpec(
|
||||||
|
name=make_symbol_name(
|
||||||
|
series.prefix,
|
||||||
|
rows,
|
||||||
|
pins,
|
||||||
|
scheme_code,
|
||||||
|
zero_pad=series.zero_pad_pins,
|
||||||
|
),
|
||||||
|
rows=rows,
|
||||||
|
pins_per_row=pins,
|
||||||
|
numbering=numbering,
|
||||||
|
reference=series.reference,
|
||||||
|
value=series.value,
|
||||||
|
display_name=series.display_name,
|
||||||
|
show_pin_numbers=series.show_pin_numbers,
|
||||||
|
show_pin_names=series.show_pin_names,
|
||||||
|
geometry=series.geometry,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
def parse_symbol(
|
||||||
|
item: dict,
|
||||||
|
geometry: Geometry,
|
||||||
|
default_show_pin_numbers: bool,
|
||||||
|
default_show_pin_names: bool,
|
||||||
|
) -> SymbolSpec:
|
||||||
|
return SymbolSpec(
|
||||||
|
name=item["name"],
|
||||||
|
rows=int(item["rows"]),
|
||||||
|
pins_per_row=int(item["pins_per_row"]),
|
||||||
|
numbering=item["numbering"],
|
||||||
|
reference=item.get("reference", "X"),
|
||||||
|
value=item.get("value", "XX"),
|
||||||
|
display_name=item.get("display_name", ""),
|
||||||
|
show_pin_numbers=bool(item.get("show_pin_numbers", default_show_pin_numbers)),
|
||||||
|
show_pin_names=bool(item.get("show_pin_names", default_show_pin_names)),
|
||||||
|
geometry=geometry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dedupe_specs(specs: list[SymbolSpec]) -> list[SymbolSpec]:
|
||||||
|
by_name: dict[str, SymbolSpec] = {}
|
||||||
|
order: list[str] = []
|
||||||
|
|
||||||
|
for spec in specs:
|
||||||
|
if spec.name not in by_name:
|
||||||
|
order.append(spec.name)
|
||||||
|
by_name[spec.name] = spec
|
||||||
|
|
||||||
|
return [by_name[name] for name in order]
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path: Path) -> tuple[Path, list[SeriesSpec], list[SymbolSpec]]:
|
||||||
|
config_path = config_path.expanduser().resolve()
|
||||||
|
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
library_path = Path(data["library"]["path"]).expanduser()
|
||||||
|
if not library_path.is_absolute():
|
||||||
|
library_path = (config_path.parent / library_path).resolve()
|
||||||
|
geometry = parse_geometry(data)
|
||||||
|
show_pin_numbers, show_pin_names = default_pin_label_settings(data)
|
||||||
|
series = [
|
||||||
|
parse_series(item, geometry, show_pin_numbers, show_pin_names)
|
||||||
|
for item in data.get("series", [])
|
||||||
|
]
|
||||||
|
symbols = [
|
||||||
|
parse_symbol(item, geometry, show_pin_numbers, show_pin_names)
|
||||||
|
for item in data.get("symbols", [])
|
||||||
|
]
|
||||||
|
return library_path, series, symbols
|
||||||
|
|
||||||
|
|
||||||
|
def load_specs(config_path: Path) -> tuple[Path, list[SymbolSpec]]:
|
||||||
|
library_path, series, symbols = load_config(config_path)
|
||||||
|
specs: list[SymbolSpec] = []
|
||||||
|
|
||||||
|
for item in series:
|
||||||
|
specs.extend(specs_from_series(item))
|
||||||
|
specs.extend(symbols)
|
||||||
|
|
||||||
|
return library_path, dedupe_specs(specs)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_library(library_text: str, specs: list[SymbolSpec]) -> str:
|
||||||
|
existing = {block.name: block.text for block in find_symbol_blocks(library_text, target_depth=1)}
|
||||||
|
replacements = {
|
||||||
|
spec.name: generate_symbol(spec, existing_symbol_text=existing.get(spec.name)) for spec in specs
|
||||||
|
}
|
||||||
|
return replace_top_level_symbols(library_text, replacements)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_diff(old: str, new: str, old_name: str, new_name: str) -> str:
|
||||||
|
return "".join(
|
||||||
|
difflib.unified_diff(
|
||||||
|
old.splitlines(keepends=True),
|
||||||
|
new.splitlines(keepends=True),
|
||||||
|
fromfile=old_name,
|
||||||
|
tofile=new_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--config", type=Path, required=True)
|
||||||
|
parser.add_argument("--write", action="store_true", help="overwrite library file")
|
||||||
|
parser.add_argument("--check", action="store_true", help="fail if generated output differs")
|
||||||
|
parser.add_argument("--diff", action="store_true", help="print unified diff")
|
||||||
|
parser.add_argument("--output", type=Path, help="write generated library to another file")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
library_path, specs = load_specs(args.config)
|
||||||
|
library_text = library_path.read_text(encoding="utf-8")
|
||||||
|
generated = generate_library(library_text, specs)
|
||||||
|
|
||||||
|
if args.diff or args.check:
|
||||||
|
diff = unified_diff(str(library_text), str(generated), str(library_path), "generated")
|
||||||
|
if diff:
|
||||||
|
sys.stdout.write(diff)
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
return 1 if library_text != generated else 0
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
args.output.write_text(generated, encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.write:
|
||||||
|
library_path.write_text(generated, encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"Generated {len(specs)} symbols from {args.config}. Use --diff, --output, or --write.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
[library]
|
||||||
|
path = "../../../Connectors/Ki-Connectors.kicad_sym"
|
||||||
|
|
||||||
|
[defaults.geometry]
|
||||||
|
body_width = 20.0
|
||||||
|
pin_pitch = 5.0
|
||||||
|
pin_length = 2.5
|
||||||
|
body_fill = "background"
|
||||||
|
|
||||||
|
[defaults.pin_labels]
|
||||||
|
show_pin_numbers = true
|
||||||
|
show_pin_names = false
|
||||||
|
|
||||||
|
[[series]]
|
||||||
|
prefix = "Con"
|
||||||
|
rows = [1, 2, 3]
|
||||||
|
schemes = ["R", "S"]
|
||||||
|
reference = "X"
|
||||||
|
value = "XX"
|
||||||
|
display_name = "Разъем универсальный"
|
||||||
|
show_pin_numbers = true
|
||||||
|
show_pin_names = false
|
||||||
|
zero_pad_pins = 2
|
||||||
|
skip_single_row_non_row = true
|
||||||
|
|
||||||
|
[series.scheme_map]
|
||||||
|
R = "row"
|
||||||
|
S = "column"
|
||||||
|
Z = "column_snake"
|
||||||
|
|
||||||
|
[series.pins_per_row_by_rows]
|
||||||
|
"1" = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
|
||||||
|
"2" = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
|
||||||
|
"3" = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""KiCad ActionPlugin entry point."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
LOG_PATH = os.path.expanduser("~/.kicad_connector_generator_plugin.log")
|
||||||
|
logger = logging.getLogger("connector_generator_plugin")
|
||||||
|
|
||||||
|
if not logger.handlers:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
try:
|
||||||
|
handler = logging.FileHandler(LOG_PATH, encoding="utf-8")
|
||||||
|
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pcbnew
|
||||||
|
|
||||||
|
_ActionPluginBase = pcbnew.ActionPlugin
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
class _ActionPluginBase:
|
||||||
|
def register(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def defaults(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def Run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorGeneratorPlugin(_ActionPluginBase):
|
||||||
|
"""Open the connector generator dialog."""
|
||||||
|
|
||||||
|
def defaults(self):
|
||||||
|
self.name = "Generate Connector Symbols"
|
||||||
|
self.category = "Library Tools"
|
||||||
|
self.description = "Генерация регулярных символов разъемов KiCad"
|
||||||
|
self.show_toolbar_button = True
|
||||||
|
|
||||||
|
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.png")
|
||||||
|
self.icon_file_name = icon_path if os.path.isfile(icon_path) else ""
|
||||||
|
|
||||||
|
def Run(self):
|
||||||
|
logger.info("Plugin launched")
|
||||||
|
try:
|
||||||
|
import wx
|
||||||
|
|
||||||
|
from .ui import ConnectorGeneratorDialog
|
||||||
|
|
||||||
|
parent = None
|
||||||
|
app = wx.GetApp()
|
||||||
|
if app is not None and hasattr(app, "GetTopWindow"):
|
||||||
|
try:
|
||||||
|
parent = app.GetTopWindow()
|
||||||
|
except Exception:
|
||||||
|
parent = None
|
||||||
|
|
||||||
|
dlg = ConnectorGeneratorDialog(parent)
|
||||||
|
dlg.ShowModal()
|
||||||
|
dlg.Destroy()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unhandled plugin error")
|
||||||
|
try:
|
||||||
|
import wx
|
||||||
|
|
||||||
|
wx.MessageBox(
|
||||||
|
"Ошибка плагина Connector Generator.\n"
|
||||||
|
f"Подробности: {LOG_PATH}",
|
||||||
|
"Connector Generator",
|
||||||
|
wx.OK | wx.ICON_ERROR,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _standalone():
|
||||||
|
import wx
|
||||||
|
|
||||||
|
from connector_generator_plugin.ui import ConnectorGeneratorDialog
|
||||||
|
|
||||||
|
app = wx.App(False)
|
||||||
|
dlg = ConnectorGeneratorDialog(None)
|
||||||
|
dlg.ShowModal()
|
||||||
|
dlg.Destroy()
|
||||||
|
app.MainLoop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_standalone()
|
||||||
485
plugins/connector-generator/connector_generator_plugin/ui.py
Normal file
485
plugins/connector-generator/connector_generator_plugin/ui.py
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""wxPython dialog for the connector generator plugin."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_dir() -> Path:
|
||||||
|
return Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _library_root() -> Path:
|
||||||
|
for parent in _plugin_dir().parents:
|
||||||
|
if (
|
||||||
|
(parent / "eda-core.kicad_dbl").is_file()
|
||||||
|
and (parent / "Connectors" / "Ki-Connectors.kicad_sym").is_file()
|
||||||
|
):
|
||||||
|
return parent
|
||||||
|
raise RuntimeError("Не найден корень KiCad-библиотеки рядом с плагином")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_generator():
|
||||||
|
try:
|
||||||
|
from . import connector_generator as generator
|
||||||
|
except ImportError:
|
||||||
|
plugin_dir = _plugin_dir()
|
||||||
|
if str(plugin_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(plugin_dir))
|
||||||
|
import connector_generator as generator
|
||||||
|
|
||||||
|
return _library_root(), generator
|
||||||
|
|
||||||
|
|
||||||
|
def _default_config_path() -> Path:
|
||||||
|
return _plugin_dir() / "connectors.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_int_list(text: str) -> tuple[int, ...]:
|
||||||
|
values: list[int] = []
|
||||||
|
for chunk in text.replace(";", ",").split(","):
|
||||||
|
chunk = chunk.strip()
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
values.append(int(chunk))
|
||||||
|
if not values:
|
||||||
|
raise ValueError("Список пинов пуст")
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorGeneratorDialog:
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
import wx
|
||||||
|
|
||||||
|
class _Dialog(wx.Dialog):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(
|
||||||
|
parent,
|
||||||
|
title="Connector Generator",
|
||||||
|
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
|
||||||
|
size=(1040, 720),
|
||||||
|
)
|
||||||
|
self.root, self.generator = _load_generator()
|
||||||
|
self.config_path = _default_config_path()
|
||||||
|
self.library_path: Path | None = None
|
||||||
|
self.specs = []
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
import wx
|
||||||
|
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
main = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
title = wx.StaticText(panel, label="Генератор разъемов")
|
||||||
|
font = title.GetFont()
|
||||||
|
font.SetPointSize(font.GetPointSize() + 4)
|
||||||
|
font.SetWeight(wx.FONTWEIGHT_BOLD)
|
||||||
|
title.SetFont(font)
|
||||||
|
main.Add(title, 0, wx.ALL, 10)
|
||||||
|
|
||||||
|
config_row = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
config_row.Add(wx.StaticText(panel, label="Конфиг:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 8)
|
||||||
|
self.config_ctrl = wx.TextCtrl(panel, value=str(self.config_path))
|
||||||
|
config_row.Add(self.config_ctrl, 1, wx.RIGHT, 6)
|
||||||
|
browse = wx.Button(panel, label="Выбрать")
|
||||||
|
browse.Bind(wx.EVT_BUTTON, self._on_browse_config)
|
||||||
|
config_row.Add(browse, 0)
|
||||||
|
main.Add(config_row, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||||
|
|
||||||
|
lib_row = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
lib_row.Add(wx.StaticText(panel, label="Библиотека:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 8)
|
||||||
|
self.library_ctrl = wx.TextCtrl(panel, style=wx.TE_READONLY)
|
||||||
|
lib_row.Add(self.library_ctrl, 1)
|
||||||
|
main.Add(lib_row, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||||
|
|
||||||
|
series_box = wx.StaticBoxSizer(wx.StaticBox(panel, label="Серия"), wx.VERTICAL)
|
||||||
|
|
||||||
|
grid = wx.FlexGridSizer(rows=5, cols=4, hgap=8, vgap=8)
|
||||||
|
grid.AddGrowableCol(1, 1)
|
||||||
|
grid.AddGrowableCol(3, 1)
|
||||||
|
|
||||||
|
self.prefix_ctrl = wx.TextCtrl(panel, value="Con")
|
||||||
|
self.reference_ctrl = wx.TextCtrl(panel, value="X")
|
||||||
|
self.value_ctrl = wx.TextCtrl(panel, value="XX")
|
||||||
|
self.name_ctrl = wx.TextCtrl(panel, value="Разъем универсальный")
|
||||||
|
self.pin_ctrls = {
|
||||||
|
1: wx.TextCtrl(panel, value=",".join(str(value) for value in range(1, 41))),
|
||||||
|
2: wx.TextCtrl(panel, value=",".join(str(value) for value in range(2, 41))),
|
||||||
|
3: wx.TextCtrl(panel, value=",".join(str(value) for value in range(3, 41))),
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.Add(wx.StaticText(panel, label="Префикс"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.prefix_ctrl, 1, wx.EXPAND)
|
||||||
|
grid.Add(wx.StaticText(panel, label="Reference"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.reference_ctrl, 1, wx.EXPAND)
|
||||||
|
|
||||||
|
grid.Add(wx.StaticText(panel, label="Value"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.value_ctrl, 1, wx.EXPAND)
|
||||||
|
grid.Add(wx.StaticText(panel, label="Имя"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.name_ctrl, 1, wx.EXPAND)
|
||||||
|
|
||||||
|
grid.Add(wx.StaticText(panel, label="Пины: 1 ряд"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.pin_ctrls[1], 1, wx.EXPAND)
|
||||||
|
grid.Add(wx.StaticText(panel, label="Пины: 2 ряда"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.pin_ctrls[2], 1, wx.EXPAND)
|
||||||
|
|
||||||
|
grid.Add(wx.StaticText(panel, label="Пины: 3 ряда"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
grid.Add(self.pin_ctrls[3], 1, wx.EXPAND)
|
||||||
|
grid.AddSpacer(1)
|
||||||
|
grid.AddSpacer(1)
|
||||||
|
series_box.Add(grid, 0, wx.EXPAND | wx.ALL, 8)
|
||||||
|
|
||||||
|
options = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
options.Add(wx.StaticText(panel, label="Ряды:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 8)
|
||||||
|
self.row_checks = {}
|
||||||
|
for rows in (1, 2, 3):
|
||||||
|
chk = wx.CheckBox(panel, label=str(rows))
|
||||||
|
chk.SetValue(True)
|
||||||
|
self.row_checks[rows] = chk
|
||||||
|
options.Add(chk, 0, wx.RIGHT, 12)
|
||||||
|
|
||||||
|
options.AddSpacer(18)
|
||||||
|
options.Add(wx.StaticText(panel, label="Нумерация:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 8)
|
||||||
|
self.scheme_checks = {}
|
||||||
|
for code, label in (
|
||||||
|
("R", "R рядами"),
|
||||||
|
("S", "S колонками"),
|
||||||
|
("Z", "Z змейкой"),
|
||||||
|
):
|
||||||
|
chk = wx.CheckBox(panel, label=label)
|
||||||
|
chk.SetValue(code in ("R", "S"))
|
||||||
|
self.scheme_checks[code] = chk
|
||||||
|
options.Add(chk, 0, wx.RIGHT, 12)
|
||||||
|
series_box.Add(options, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||||
|
|
||||||
|
labels = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
labels.Add(wx.StaticText(panel, label="Надписи выводов:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 8)
|
||||||
|
self.show_pin_numbers_chk = wx.CheckBox(panel, label="Показать номер вывода")
|
||||||
|
self.show_pin_numbers_chk.SetValue(True)
|
||||||
|
labels.Add(self.show_pin_numbers_chk, 0, wx.RIGHT, 16)
|
||||||
|
self.show_pin_names_chk = wx.CheckBox(panel, label="Показать имя вывода")
|
||||||
|
self.show_pin_names_chk.SetValue(False)
|
||||||
|
labels.Add(self.show_pin_names_chk, 0, wx.RIGHT, 16)
|
||||||
|
self.body_fill_chk = wx.CheckBox(panel, label="Заливка фоном")
|
||||||
|
self.body_fill_chk.SetValue(True)
|
||||||
|
labels.Add(self.body_fill_chk, 0, wx.RIGHT, 16)
|
||||||
|
series_box.Add(labels, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||||
|
|
||||||
|
main.Add(series_box, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
||||||
|
|
||||||
|
actions = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
for label, handler in (
|
||||||
|
("Обновить список", self._on_preview),
|
||||||
|
("Сгенерировать", self._on_write),
|
||||||
|
("Открыть конфиг", self._on_open_config),
|
||||||
|
("Технический diff", self._on_diff),
|
||||||
|
):
|
||||||
|
btn = wx.Button(panel, label=label)
|
||||||
|
btn.Bind(wx.EVT_BUTTON, handler)
|
||||||
|
actions.Add(btn, 0, wx.RIGHT, 8)
|
||||||
|
actions.AddStretchSpacer(1)
|
||||||
|
close = wx.Button(panel, wx.ID_CLOSE, label="Закрыть")
|
||||||
|
close.Bind(wx.EVT_BUTTON, lambda _event: self.Close())
|
||||||
|
actions.Add(close, 0)
|
||||||
|
main.Add(actions, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
||||||
|
|
||||||
|
content = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
self.symbols = wx.ListCtrl(panel, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
|
||||||
|
for idx, (heading, width) in enumerate(
|
||||||
|
[
|
||||||
|
("Символ", 210),
|
||||||
|
("Пинов", 70),
|
||||||
|
("Схема", 110),
|
||||||
|
("Статус", 260),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
self.symbols.InsertColumn(idx, heading, width=width)
|
||||||
|
self.symbols.Bind(wx.EVT_LIST_ITEM_SELECTED, self._on_select_symbol)
|
||||||
|
content.Add(self.symbols, 1, wx.EXPAND | wx.RIGHT, 8)
|
||||||
|
|
||||||
|
self.preview = wx.TextCtrl(
|
||||||
|
panel,
|
||||||
|
style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.HSCROLL,
|
||||||
|
)
|
||||||
|
mono = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||||
|
self.preview.SetFont(mono)
|
||||||
|
content.Add(self.preview, 1, wx.EXPAND)
|
||||||
|
main.Add(content, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
||||||
|
|
||||||
|
self.status = wx.StaticText(panel, label="")
|
||||||
|
main.Add(self.status, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
|
||||||
|
|
||||||
|
panel.SetSizer(main)
|
||||||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
sizer.Add(panel, 1, wx.EXPAND)
|
||||||
|
self.SetSizer(sizer)
|
||||||
|
|
||||||
|
def _current_config_path(self) -> Path:
|
||||||
|
return Path(self.config_ctrl.GetValue()).expanduser()
|
||||||
|
|
||||||
|
def _format_int_list(self, values):
|
||||||
|
return ",".join(str(value) for value in values)
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
try:
|
||||||
|
config = self._current_config_path()
|
||||||
|
library_path, series, symbols = self.generator.load_config(config)
|
||||||
|
self.library_path = library_path
|
||||||
|
self.library_ctrl.SetValue(str(library_path))
|
||||||
|
|
||||||
|
if series:
|
||||||
|
first = series[0]
|
||||||
|
self.prefix_ctrl.SetValue(first.prefix)
|
||||||
|
for rows, ctrl in self.pin_ctrls.items():
|
||||||
|
values = first.pins_per_row
|
||||||
|
if first.pins_per_row_by_rows is not None:
|
||||||
|
values = first.pins_per_row_by_rows.get(rows, ())
|
||||||
|
ctrl.SetValue(self._format_int_list(values))
|
||||||
|
self.reference_ctrl.SetValue(first.reference)
|
||||||
|
self.value_ctrl.SetValue(first.value)
|
||||||
|
self.name_ctrl.SetValue(first.display_name)
|
||||||
|
for rows, chk in self.row_checks.items():
|
||||||
|
chk.SetValue(rows in first.rows)
|
||||||
|
for code, chk in self.scheme_checks.items():
|
||||||
|
chk.SetValue(code in first.schemes)
|
||||||
|
self.show_pin_numbers_chk.SetValue(first.show_pin_numbers)
|
||||||
|
self.show_pin_names_chk.SetValue(first.show_pin_names)
|
||||||
|
self.body_fill_chk.SetValue(first.geometry.body_fill == "background")
|
||||||
|
|
||||||
|
self._refresh_preview()
|
||||||
|
if symbols:
|
||||||
|
self.status.SetLabel(f"Конфиг загружен. Точечных override: {len(symbols)}")
|
||||||
|
else:
|
||||||
|
self.status.SetLabel("Конфиг загружен.")
|
||||||
|
except Exception as exc:
|
||||||
|
self.library_path = None
|
||||||
|
self.library_ctrl.SetValue("")
|
||||||
|
self.symbols.DeleteAllItems()
|
||||||
|
self.preview.SetValue(f"Ошибка загрузки конфига:\n{exc}\n")
|
||||||
|
self.status.SetLabel("Ошибка конфигурации")
|
||||||
|
|
||||||
|
def _series_from_form(self):
|
||||||
|
rows = tuple(row for row, chk in self.row_checks.items() if chk.GetValue())
|
||||||
|
schemes = tuple(code for code, chk in self.scheme_checks.items() if chk.GetValue())
|
||||||
|
if not rows:
|
||||||
|
raise ValueError("Выберите хотя бы один ряд")
|
||||||
|
if not schemes:
|
||||||
|
raise ValueError("Выберите хотя бы одну схему нумерации")
|
||||||
|
|
||||||
|
_library_path, _series, _symbols = self.generator.load_config(self._current_config_path())
|
||||||
|
geometry = self.generator.parse_geometry(
|
||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"geometry": {
|
||||||
|
"body_width": 20.0,
|
||||||
|
"pin_pitch": 5.0,
|
||||||
|
"pin_length": 2.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
_library_path, loaded_series, _symbols = self.generator.load_config(self._current_config_path())
|
||||||
|
if loaded_series:
|
||||||
|
geometry = loaded_series[0].geometry
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
geometry = self.generator.Geometry(
|
||||||
|
body_width=geometry.body_width,
|
||||||
|
pin_pitch=geometry.pin_pitch,
|
||||||
|
pin_length=geometry.pin_length,
|
||||||
|
body_fill="background" if self.body_fill_chk.GetValue() else "none",
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.generator.SeriesSpec(
|
||||||
|
prefix=self.prefix_ctrl.GetValue().strip() or "Con",
|
||||||
|
rows=rows,
|
||||||
|
pins_per_row=(),
|
||||||
|
pins_per_row_by_rows={
|
||||||
|
row: _parse_int_list(ctrl.GetValue())
|
||||||
|
for row, ctrl in self.pin_ctrls.items()
|
||||||
|
},
|
||||||
|
schemes=schemes,
|
||||||
|
scheme_map=dict(self.generator.DEFAULT_SCHEME_MAP),
|
||||||
|
reference=self.reference_ctrl.GetValue().strip() or "X",
|
||||||
|
value=self.value_ctrl.GetValue().strip() or "XX",
|
||||||
|
display_name=self.name_ctrl.GetValue().strip(),
|
||||||
|
show_pin_numbers=self.show_pin_numbers_chk.GetValue(),
|
||||||
|
show_pin_names=self.show_pin_names_chk.GetValue(),
|
||||||
|
zero_pad_pins=2,
|
||||||
|
skip_single_row_non_row=True,
|
||||||
|
geometry=geometry,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _specs_from_form(self):
|
||||||
|
return self.generator.specs_from_series(self._series_from_form())
|
||||||
|
|
||||||
|
def _existing_symbols(self):
|
||||||
|
if self.library_path is None:
|
||||||
|
return {}
|
||||||
|
if not self.library_path.exists():
|
||||||
|
return {}
|
||||||
|
text = self.library_path.read_text(encoding="utf-8")
|
||||||
|
return {
|
||||||
|
block.name: block.text
|
||||||
|
for block in self.generator.find_symbol_blocks(text, target_depth=1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _refresh_preview(self):
|
||||||
|
self.specs = self._specs_from_form()
|
||||||
|
existing = self._existing_symbols()
|
||||||
|
self.symbols.DeleteAllItems()
|
||||||
|
|
||||||
|
for index, spec in enumerate(self.specs):
|
||||||
|
old = existing.get(spec.name)
|
||||||
|
manual = self.generator.manual_variant_count(old, spec.name)
|
||||||
|
status = "создать" if old is None else "обновить"
|
||||||
|
if manual:
|
||||||
|
status += f", сохранить ручных: {manual}"
|
||||||
|
|
||||||
|
self.symbols.InsertItem(index, spec.name)
|
||||||
|
self.symbols.SetItem(index, 1, str(spec.rows * spec.pins_per_row))
|
||||||
|
self.symbols.SetItem(index, 2, spec.numbering)
|
||||||
|
self.symbols.SetItem(index, 3, status)
|
||||||
|
|
||||||
|
if self.specs:
|
||||||
|
self.symbols.Select(0)
|
||||||
|
self._show_spec(self.specs[0])
|
||||||
|
else:
|
||||||
|
self.preview.SetValue("Нет символов для генерации.")
|
||||||
|
|
||||||
|
self.status.SetLabel(f"Будет создано/обновлено символов: {len(self.specs)}")
|
||||||
|
|
||||||
|
def _show_spec(self, spec):
|
||||||
|
text = self.generator.format_pin_preview(spec)
|
||||||
|
if spec.rows == 1 and spec.numbering != "row":
|
||||||
|
text += "\n\nДля одного ряда не-row схемы пропускаются автоматически."
|
||||||
|
self.preview.SetValue(text)
|
||||||
|
|
||||||
|
def _load_generation_from_form(self):
|
||||||
|
if self.library_path is None:
|
||||||
|
self._load_config()
|
||||||
|
if self.library_path is None:
|
||||||
|
raise ValueError("Библиотека не задана")
|
||||||
|
|
||||||
|
specs = self._specs_from_form()
|
||||||
|
library_text = self.library_path.read_text(encoding="utf-8")
|
||||||
|
generated = self.generator.generate_library(library_text, specs)
|
||||||
|
return self.library_path, specs, library_text, generated
|
||||||
|
|
||||||
|
def _on_browse_config(self, _event):
|
||||||
|
import wx
|
||||||
|
|
||||||
|
with wx.FileDialog(
|
||||||
|
self,
|
||||||
|
"Выберите connectors.toml",
|
||||||
|
wildcard="TOML files (*.toml)|*.toml|All files (*.*)|*.*",
|
||||||
|
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
|
||||||
|
) as dlg:
|
||||||
|
if dlg.ShowModal() != wx.ID_OK:
|
||||||
|
return
|
||||||
|
self.config_ctrl.SetValue(dlg.GetPath())
|
||||||
|
self._load_config()
|
||||||
|
|
||||||
|
def _on_preview(self, _event):
|
||||||
|
try:
|
||||||
|
self._refresh_preview()
|
||||||
|
except Exception as exc:
|
||||||
|
self.preview.SetValue(f"Ошибка предпросмотра:\n{exc}\n")
|
||||||
|
self.status.SetLabel("Ошибка предпросмотра")
|
||||||
|
|
||||||
|
def _on_select_symbol(self, event):
|
||||||
|
index = event.GetIndex()
|
||||||
|
if 0 <= index < len(self.specs):
|
||||||
|
self._show_spec(self.specs[index])
|
||||||
|
|
||||||
|
def _on_diff(self, _event):
|
||||||
|
try:
|
||||||
|
library_path, _specs, old, new = self._load_generation_from_form()
|
||||||
|
diff = "".join(
|
||||||
|
difflib.unified_diff(
|
||||||
|
old.splitlines(keepends=True),
|
||||||
|
new.splitlines(keepends=True),
|
||||||
|
fromfile=str(library_path),
|
||||||
|
tofile="generated",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.preview.SetValue(diff or "Изменений нет.\n")
|
||||||
|
self.status.SetLabel("Технический diff сформирован")
|
||||||
|
except Exception as exc:
|
||||||
|
self.preview.SetValue(f"Ошибка diff:\n{exc}\n")
|
||||||
|
|
||||||
|
def _validate_with_kicad_cli(self, generated_text: str) -> str:
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".kicad_sym", delete=False) as tmp:
|
||||||
|
tmp.write(generated_text)
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
out_path = tmp_path + ".upgraded.kicad_sym"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["kicad-cli", "sym", "upgrade", tmp_path, "--output", out_path, "--force"],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return (result.stdout + result.stderr).strip() or "kicad-cli returned an error"
|
||||||
|
return "kicad-cli validation OK"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "kicad-cli не найден, синтаксическая проверка пропущена"
|
||||||
|
finally:
|
||||||
|
for path in (tmp_path, out_path):
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_write(self, _event):
|
||||||
|
import wx
|
||||||
|
|
||||||
|
try:
|
||||||
|
library_path, specs, old, new = self._load_generation_from_form()
|
||||||
|
if old == new:
|
||||||
|
self.status.SetLabel("Изменений нет, запись не требуется.")
|
||||||
|
return
|
||||||
|
|
||||||
|
answer = wx.MessageBox(
|
||||||
|
f"Сгенерировать {len(specs)} символов и обновить библиотеку?\n\n{library_path}",
|
||||||
|
"Connector Generator",
|
||||||
|
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
|
||||||
|
)
|
||||||
|
if answer != wx.YES:
|
||||||
|
return
|
||||||
|
|
||||||
|
validation = self._validate_with_kicad_cli(new)
|
||||||
|
if "returned an error" in validation:
|
||||||
|
self.preview.SetValue(f"Запись отменена.\n{validation}\n")
|
||||||
|
self.status.SetLabel("Проверка KiCad не пройдена")
|
||||||
|
return
|
||||||
|
|
||||||
|
library_path.write_text(new, encoding="utf-8")
|
||||||
|
self.status.SetLabel(f"Готово. Обновлено символов: {len(specs)}. {validation}")
|
||||||
|
self._refresh_preview()
|
||||||
|
except Exception as exc:
|
||||||
|
self.preview.SetValue(f"Ошибка генерации:\n{exc}\n")
|
||||||
|
self.status.SetLabel("Ошибка генерации")
|
||||||
|
|
||||||
|
def _on_open_config(self, _event):
|
||||||
|
config = self._current_config_path()
|
||||||
|
try:
|
||||||
|
subprocess.Popen(["xdg-open", str(config)])
|
||||||
|
self.status.SetLabel(f"Открыт конфиг: {config}")
|
||||||
|
except Exception as exc:
|
||||||
|
self.status.SetLabel(f"Не удалось открыть конфиг: {exc}")
|
||||||
|
|
||||||
|
return _Dialog(*args, **kwargs)
|
||||||
71
scripts/install_connector_generator_plugin.sh
Executable file
71
scripts/install_connector_generator_plugin.sh
Executable file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIB_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
PLUGIN_SRC="$LIB_ROOT/plugins/connector-generator/connector_generator_plugin"
|
||||||
|
FORCE=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--force)
|
||||||
|
FORCE=1
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--force]"
|
||||||
|
echo
|
||||||
|
echo "Install Connector Generator plugin as a symlink into KiCad user plugins."
|
||||||
|
echo " --force replace existing connector_generator_plugin symlink/directory"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $arg" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -d "$PLUGIN_SRC" ]]; then
|
||||||
|
echo "Connector Generator plugin source not found: $PLUGIN_SRC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=""
|
||||||
|
if command -v kicad-cli >/dev/null 2>&1; then
|
||||||
|
version="$(
|
||||||
|
kicad-cli version 2>/dev/null \
|
||||||
|
| awk 'match($0, /[0-9]+\.[0-9]+/) { print substr($0, RSTART, RLENGTH); exit }'
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
version="10.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLUGIN_DIR="${KICAD_PLUGIN_DIR:-$HOME/.local/share/kicad/$version/scripting/plugins}"
|
||||||
|
PLUGIN_DEST="$PLUGIN_DIR/connector_generator_plugin"
|
||||||
|
|
||||||
|
mkdir -p "$PLUGIN_DIR"
|
||||||
|
|
||||||
|
if [[ -e "$PLUGIN_DEST" || -L "$PLUGIN_DEST" ]]; then
|
||||||
|
current="$(readlink -f "$PLUGIN_DEST" 2>/dev/null || true)"
|
||||||
|
source_real="$(readlink -f "$PLUGIN_SRC")"
|
||||||
|
if [[ "$current" != "$source_real" ]]; then
|
||||||
|
if [[ "$FORCE" != "1" ]]; then
|
||||||
|
echo "Destination already exists and is not this plugin:" >&2
|
||||||
|
echo " $PLUGIN_DEST" >&2
|
||||||
|
echo "Use --force to replace it or set KICAD_PLUGIN_DIR." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf "$PLUGIN_DEST"
|
||||||
|
ln -s "$PLUGIN_SRC" "$PLUGIN_DEST"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ln -s "$PLUGIN_SRC" "$PLUGIN_DEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Connector Generator plugin installed:"
|
||||||
|
echo " $PLUGIN_DEST -> $PLUGIN_SRC"
|
||||||
|
echo
|
||||||
|
echo "Restart KiCad, then open:"
|
||||||
|
echo " PCB Editor -> Tools -> External Plugins -> Generate Connector Symbols"
|
||||||
204
scripts/setup_kicad_user_library.py
Executable file
204
scripts/setup_kicad_user_library.py
Executable file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Configure KiCad user paths and library table entries for this library."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ENV_VARS = ("KICAD_LIB_DIR_USER", "KICAD_PDF_DIR_USER", "KICAD_3D_PARTY_USER")
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def detect_kicad_config(version: str | None) -> Path:
|
||||||
|
base = Path.home() / ".config" / "kicad"
|
||||||
|
|
||||||
|
if version:
|
||||||
|
path = base / version / "kicad_common.json"
|
||||||
|
if not path.exists():
|
||||||
|
raise SystemExit(f"KiCad config not found: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
candidates = sorted(base.glob("*/kicad_common.json"))
|
||||||
|
if not candidates:
|
||||||
|
raise SystemExit(f"KiCad config not found under {base}")
|
||||||
|
|
||||||
|
def version_key(path: Path) -> tuple[int, ...]:
|
||||||
|
parts = []
|
||||||
|
for item in path.parent.name.split("."):
|
||||||
|
try:
|
||||||
|
parts.append(int(item))
|
||||||
|
except ValueError:
|
||||||
|
parts.append(0)
|
||||||
|
return tuple(parts)
|
||||||
|
|
||||||
|
return max(candidates, key=version_key)
|
||||||
|
|
||||||
|
|
||||||
|
def read_existing_vars(config_path: Path) -> dict[str, str]:
|
||||||
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
vars_obj = data.get("environment", {}).get("vars")
|
||||||
|
return vars_obj if isinstance(vars_obj, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def ask_path(name: str, prompt: str, default: str | None, required: bool = True) -> str:
|
||||||
|
env_value = os.environ.get(name)
|
||||||
|
if env_value:
|
||||||
|
return str(Path(env_value).expanduser())
|
||||||
|
|
||||||
|
suffix = f" [{default}]" if default else ""
|
||||||
|
while True:
|
||||||
|
value = input(f"{prompt}{suffix}: ").strip()
|
||||||
|
if not value and default:
|
||||||
|
value = default
|
||||||
|
if value or not required:
|
||||||
|
return str(Path(value).expanduser()) if value else ""
|
||||||
|
print("Path is required.")
|
||||||
|
|
||||||
|
|
||||||
|
def backup(path: Path) -> Path:
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
target = path.with_name(f"{path.name}.bak-{stamp}")
|
||||||
|
shutil.copy2(path, target)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def update_common_config(config_path: Path, values: dict[str, str]) -> None:
|
||||||
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
environment = data.setdefault("environment", {})
|
||||||
|
vars_obj = environment.get("vars")
|
||||||
|
|
||||||
|
if not isinstance(vars_obj, dict):
|
||||||
|
vars_obj = {}
|
||||||
|
environment["vars"] = vars_obj
|
||||||
|
|
||||||
|
vars_obj.update(values)
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_table(path: Path, header: str) -> None:
|
||||||
|
if not path.exists():
|
||||||
|
path.write_text(f"({header}\n\t(version 7)\n)\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_library_entry(table_path: Path, header: str, entry: str) -> None:
|
||||||
|
ensure_table(table_path, header)
|
||||||
|
text = table_path.read_text(encoding="utf-8")
|
||||||
|
name_match = re.search(r'\(name "([^"]+)"\)', entry)
|
||||||
|
if not name_match:
|
||||||
|
raise ValueError(f"Library entry has no name: {entry}")
|
||||||
|
|
||||||
|
name = re.escape(name_match.group(1))
|
||||||
|
text = re.sub(
|
||||||
|
rf'\n[^\n]*\(lib\s+\(name "{name}"\)[^\n]*',
|
||||||
|
"",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
|
||||||
|
insert_at = text.rfind("\n)")
|
||||||
|
if insert_at == -1:
|
||||||
|
raise SystemExit(f"Unexpected KiCad table format: {table_path}")
|
||||||
|
|
||||||
|
text = text[:insert_at] + "\n\t" + entry + text[insert_at:]
|
||||||
|
table_path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def configure_library_tables(config_dir: Path) -> None:
|
||||||
|
sym_table = config_dir / "sym-lib-table"
|
||||||
|
fp_table = config_dir / "fp-lib-table"
|
||||||
|
|
||||||
|
upsert_library_entry(
|
||||||
|
sym_table,
|
||||||
|
"sym_lib_table",
|
||||||
|
'(lib (name "eda-core") (type "Database") (uri "${KICAD_LIB_DIR_USER}/eda-core.kicad_dbl") (options "") (descr ""))',
|
||||||
|
)
|
||||||
|
upsert_library_entry(
|
||||||
|
sym_table,
|
||||||
|
"sym_lib_table",
|
||||||
|
'(lib (name "sym-lib-table") (type "Table") (uri "${KICAD_LIB_DIR_USER}/sym-lib-table") (options "") (descr ""))',
|
||||||
|
)
|
||||||
|
upsert_library_entry(
|
||||||
|
fp_table,
|
||||||
|
"fp_lib_table",
|
||||||
|
'(lib (name "fp-lib-table") (type "Table") (uri "${KICAD_LIB_DIR_USER}/fp-lib-table") (options "") (descr ""))',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Configure KiCad paths and library tables for KiCad User Library."
|
||||||
|
)
|
||||||
|
parser.add_argument("--kicad-version", help="KiCad config version, for example 10.0")
|
||||||
|
parser.add_argument("--lib-dir", default=str(repo_root()), help="Path to this library repo")
|
||||||
|
parser.add_argument("--pdf-dir", help="Path to PDF datasheet directory")
|
||||||
|
parser.add_argument("--3d-dir", dest="models_dir", help="Path to user 3D models directory")
|
||||||
|
parser.add_argument("--non-interactive", action="store_true", help="Do not ask questions")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config_path = detect_kicad_config(args.kicad_version)
|
||||||
|
existing = read_existing_vars(config_path)
|
||||||
|
|
||||||
|
lib_dir = str(Path(args.lib_dir).expanduser().resolve())
|
||||||
|
pdf_dir = args.pdf_dir or existing.get("KICAD_PDF_DIR_USER")
|
||||||
|
models_dir = args.models_dir or existing.get("KICAD_3D_PARTY_USER")
|
||||||
|
|
||||||
|
if not args.non_interactive:
|
||||||
|
lib_dir = ask_path("KICAD_LIB_DIR_USER", "KiCad library path", lib_dir)
|
||||||
|
pdf_dir = ask_path("KICAD_PDF_DIR_USER", "PDF directory path", pdf_dir)
|
||||||
|
if args.models_dir or existing.get("KICAD_3D_PARTY_USER"):
|
||||||
|
models_dir = ask_path("KICAD_3D_PARTY_USER", "3D models directory path", models_dir)
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"KICAD_LIB_DIR_USER": lib_dir,
|
||||||
|
"KICAD_PDF_DIR_USER": pdf_dir or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if models_dir:
|
||||||
|
values["KICAD_3D_PARTY_USER"] = models_dir
|
||||||
|
|
||||||
|
if not values["KICAD_PDF_DIR_USER"]:
|
||||||
|
raise SystemExit("Missing required value: KICAD_PDF_DIR_USER")
|
||||||
|
|
||||||
|
for key, value in values.items():
|
||||||
|
if key != "KICAD_LIB_DIR_USER" and not Path(value).expanduser().exists():
|
||||||
|
print(f"warning: {key} path does not exist yet: {value}", file=sys.stderr)
|
||||||
|
|
||||||
|
backups = [backup(config_path)]
|
||||||
|
config_dir = config_path.parent
|
||||||
|
for table_name in ("sym-lib-table", "fp-lib-table"):
|
||||||
|
table_path = config_dir / table_name
|
||||||
|
if table_path.exists():
|
||||||
|
backups.append(backup(table_path))
|
||||||
|
|
||||||
|
update_common_config(config_path, values)
|
||||||
|
configure_library_tables(config_dir)
|
||||||
|
|
||||||
|
print(f"Configured KiCad {config_dir.name}:")
|
||||||
|
for key in ENV_VARS:
|
||||||
|
if key in values:
|
||||||
|
print(f" {key} = {values[key]}")
|
||||||
|
print("Library table entries updated:")
|
||||||
|
print(" sym-lib-table: eda-core, sym-lib-table")
|
||||||
|
print(" fp-lib-table: fp-lib-table")
|
||||||
|
print("Backups:")
|
||||||
|
for item in backups:
|
||||||
|
print(f" {item}")
|
||||||
|
print("Restart KiCad if it was running.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
(sym_lib_table
|
(sym_lib_table
|
||||||
(version 7)
|
(version 7)
|
||||||
(lib (name "eda-core") (type "Database") (uri "${KICAD_USER_LIB_DIR}/eda-core.kicad_dbl") (options "") (descr ""))
|
(lib (name "eda-core") (type "Database") (uri "${KICAD_LIB_DIR_USER}/eda-core.kicad_dbl") (options "") (descr ""))
|
||||||
(lib (name "Ki-Capacitors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Capacitors/Ki-Capacitors.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Capacitors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Capacitors/Ki-Capacitors.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Connectors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Connectors/Ki-Connectors.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Connectors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Connectors/Ki-Connectors.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Diodes") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Diodes/Ki-Diodes.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Diodes") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Diodes/Ki-Diodes.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Graphics") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Graphics/Ki-Graphics.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Graphics") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Graphics/Ki-Graphics.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-IC") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/IC/Ki-IC.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-IC") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/IC/Ki-IC.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Modules") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Modules/Ki-Modules.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Modules") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Modules/Ki-Modules.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Resistors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Resistors/Ki-Resistors.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Resistors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Resistors/Ki-Resistors.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Switches") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Switches/Ki-Switches.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Switches") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Switches/Ki-Switches.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Transistor") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Transistor/Ki-Transistor.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Transistor") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Transistor/Ki-Transistor.kicad_sym") (options "") (descr ""))
|
||||||
(lib (name "Ki-Other") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Other/Ki-Other.kicad_sym") (options "") (descr ""))
|
(lib (name "Ki-Other") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Other/Ki-Other.kicad_sym") (options "") (descr ""))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue