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
|
||||
ln -s /mnt/диск/YandexDisk/Altium/DataLIB/PDF ./PDF
|
||||
'
|
||||
./scripts/setup_kicad_user_library.py --pdf-dir "<путь-к-папке-PDF>"
|
||||
```
|
||||
|
||||
## Настройка библиотеки
|
||||
Если используется отдельная папка 3D-моделей:
|
||||
|
||||
Необходимо создать переменные окружения (пути к библиотекам)
|
||||
Название KICKAD_USER_LIB_DIR
|
||||
```bash
|
||||
./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
|
||||
(version 7)
|
||||
(lib (name "Ki-Capacitors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Diodes") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Modules") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Transistor") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Capacitors") (type "KiCad") (uri "${KICAD_LIB_DIR_USER}/Capacitors/Ki-Capacitors.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_LIB_DIR_USER}/Diodes/Ki-Diodes.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_LIB_DIR_USER}/Modules/Ki-Modules.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_LIB_DIR_USER}/Transistor/Ki-Transistor.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
|
||||
(version 7)
|
||||
(lib (name "eda-core") (type "Database") (uri "${KICAD_USER_LIB_DIR}/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-Connectors") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Graphics") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Modules") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Switches") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/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-Other") (type "KiCad") (uri "${KICAD_USER_LIB_DIR}/Other/Ki-Other.kicad_sym") (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_LIB_DIR_USER}/Capacitors/Ki-Capacitors.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_LIB_DIR_USER}/Diodes/Ki-Diodes.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_LIB_DIR_USER}/IC/Ki-IC.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_LIB_DIR_USER}/Resistors/Ki-Resistors.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_LIB_DIR_USER}/Transistor/Ki-Transistor.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