第18章-二次开发实战案例

第十八章:二次开发实战案例

18.1 案例一:自定义文件导出器

18.1.1 需求分析

创建一个将PCB设计导出为自定义格式的插件,用于与其他系统集成。

18.1.2 实现代码

import pcbnew
import json
import wx

class CustomExportPlugin(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "导出自定义格式"
        self.category = "导出"
        self.description = "将PCB导出为JSON格式"
        self.show_toolbar_button = True
    
    def Run(self):
        board = pcbnew.GetBoard()
        
        # 选择保存路径
        dlg = wx.FileDialog(None, "保存导出文件", "", "",
                           "JSON files (*.json)|*.json",
                           wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
        
        if dlg.ShowModal() == wx.ID_OK:
            filepath = dlg.GetPath()
            self.export_to_json(board, filepath)
            wx.MessageBox(f"导出成功: {filepath}", "完成")
        
        dlg.Destroy()
    
    def export_to_json(self, board, filepath):
        """导出为JSON格式"""
        data = {
            'board_info': self.get_board_info(board),
            'footprints': self.get_footprints(board),
            'tracks': self.get_tracks(board),
            'vias': self.get_vias(board),
            'zones': self.get_zones(board),
            'nets': self.get_nets(board)
        }
        
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
    
    def get_board_info(self, board):
        """获取板信息"""
        bbox = board.GetBoardEdgesBoundingBox()
        return {
            'filename': board.GetFileName(),
            'width_mm': pcbnew.ToMM(bbox.GetWidth()),
            'height_mm': pcbnew.ToMM(bbox.GetHeight()),
            'layer_count': board.GetCopperLayerCount()
        }
    
    def get_footprints(self, board):
        """获取封装列表"""
        footprints = []
        for fp in board.GetFootprints():
            pos = fp.GetPosition()
            footprints.append({
                'reference': fp.GetReference(),
                'value': fp.GetValue(),
                'footprint': str(fp.GetFPID().GetLibItemName()),
                'x_mm': pcbnew.ToMM(pos.x),
                'y_mm': pcbnew.ToMM(pos.y),
                'rotation': fp.GetOrientationDegrees(),
                'layer': 'top' if fp.GetLayer() == pcbnew.F_Cu else 'bottom',
                'is_smd': bool(fp.GetAttributes() & pcbnew.FP_SMD)
            })
        return footprints
    
    def get_tracks(self, board):
        """获取走线列表"""
        tracks = []
        for track in board.GetTracks():
            if track.GetClass() == "PCB_TRACK":
                start = track.GetStart()
                end = track.GetEnd()
                tracks.append({
                    'start_x': pcbnew.ToMM(start.x),
                    'start_y': pcbnew.ToMM(start.y),
                    'end_x': pcbnew.ToMM(end.x),
                    'end_y': pcbnew.ToMM(end.y),
                    'width_mm': pcbnew.ToMM(track.GetWidth()),
                    'layer': track.GetLayerName(),
                    'net': track.GetNetname()
                })
        return tracks
    
    def get_vias(self, board):
        """获取过孔列表"""
        vias = []
        for track in board.GetTracks():
            if track.GetClass() == "PCB_VIA":
                pos = track.GetPosition()
                vias.append({
                    'x_mm': pcbnew.ToMM(pos.x),
                    'y_mm': pcbnew.ToMM(pos.y),
                    'diameter_mm': pcbnew.ToMM(track.GetWidth()),
                    'drill_mm': pcbnew.ToMM(track.GetDrill()),
                    'net': track.GetNetname()
                })
        return vias
    
    def get_zones(self, board):
        """获取覆铜区域"""
        zones = []
        for zone in board.Zones():
            zones.append({
                'net': zone.GetNetname(),
                'layer': zone.GetLayerName(),
                'priority': zone.GetPriority()
            })
        return zones
    
    def get_nets(self, board):
        """获取网络列表"""
        nets = []
        for net in board.GetNetInfo().NetsByNetcode().values():
            name = net.GetNetname()
            if name:
                nets.append({
                    'name': name,
                    'netclass': net.GetNetClassName()
                })
        return nets

CustomExportPlugin().register()

18.2 案例二:设计规则检查扩展

18.2.1 需求分析

创建自定义DRC检查,检测特定的设计问题。

18.2.2 实现代码

import pcbnew
import wx

class CustomDRCPlugin(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "自定义DRC检查"
        self.category = "检查"
        self.description = "执行额外的设计规则检查"
        self.show_toolbar_button = True
    
    def Run(self):
        board = pcbnew.GetBoard()
        violations = []
        
        # 执行各项检查
        violations.extend(self.check_power_track_width(board))
        violations.extend(self.check_component_spacing(board))
        violations.extend(self.check_via_placement(board))
        violations.extend(self.check_silkscreen_on_pads(board))
        
        # 显示结果
        self.show_results(violations)
    
    def check_power_track_width(self, board, min_width_mm=0.5):
        """检查电源走线宽度"""
        violations = []
        power_nets = ['VCC', 'VDD', '+5V', '+3.3V', '+12V', 'GND']
        
        for track in board.GetTracks():
            if track.GetClass() == "PCB_TRACK":
                netname = track.GetNetname()
                if any(pn in netname.upper() for pn in power_nets):
                    width_mm = pcbnew.ToMM(track.GetWidth())
                    if width_mm < min_width_mm:
                        start = track.GetStart()
                        violations.append({
                            'type': '电源线宽度不足',
                            'detail': f'{netname}: {width_mm:.3f}mm < {min_width_mm}mm',
                            'x': pcbnew.ToMM(start.x),
                            'y': pcbnew.ToMM(start.y)
                        })
        
        return violations
    
    def check_component_spacing(self, board, min_spacing_mm=0.5):
        """检查元器件间距"""
        violations = []
        footprints = list(board.GetFootprints())
        
        for i, fp1 in enumerate(footprints):
            bbox1 = fp1.GetBoundingBox()
            
            for fp2 in footprints[i+1:]:
                bbox2 = fp2.GetBoundingBox()
                
                # 简化的距离检查
                center1 = fp1.GetPosition()
                center2 = fp2.GetPosition()
                dist = ((center1.x - center2.x)**2 + (center1.y - center2.y)**2)**0.5
                dist_mm = pcbnew.ToMM(dist)
                
                # 如果边界框相交,检查间距
                if bbox1.Intersects(bbox2):
                    violations.append({
                        'type': '元器件间距过近',
                        'detail': f'{fp1.GetReference()} 和 {fp2.GetReference()}',
                        'x': pcbnew.ToMM(center1.x),
                        'y': pcbnew.ToMM(center1.y)
                    })
        
        return violations
    
    def check_via_placement(self, board, min_dist_to_pad_mm=0.3):
        """检查过孔与焊盘距离"""
        violations = []
        
        # 收集所有焊盘位置
        pads = []
        for fp in board.GetFootprints():
            for pad in fp.Pads():
                pads.append(pad.GetPosition())
        
        # 检查过孔
        for track in board.GetTracks():
            if track.GetClass() == "PCB_VIA":
                via_pos = track.GetPosition()
                
                for pad_pos in pads:
                    dist = ((via_pos.x - pad_pos.x)**2 + 
                           (via_pos.y - pad_pos.y)**2)**0.5
                    dist_mm = pcbnew.ToMM(dist)
                    
                    if dist_mm < min_dist_to_pad_mm:
                        violations.append({
                            'type': '过孔离焊盘过近',
                            'detail': f'距离: {dist_mm:.3f}mm',
                            'x': pcbnew.ToMM(via_pos.x),
                            'y': pcbnew.ToMM(via_pos.y)
                        })
        
        return violations
    
    def check_silkscreen_on_pads(self, board):
        """检查丝印是否覆盖焊盘"""
        violations = []
        # 这是一个简化的检查示例
        # 实际实现需要更复杂的几何计算
        return violations
    
    def show_results(self, violations):
        """显示检查结果"""
        if not violations:
            wx.MessageBox("未发现问题", "自定义DRC检查", 
                         wx.OK | wx.ICON_INFORMATION)
            return
        
        # 生成报告
        report = f"发现 {len(violations)} 个问题:\n\n"
        for v in violations:
            report += f"• {v['type']}\n"
            report += f"  {v['detail']}\n"
            report += f"  位置: ({v['x']:.2f}, {v['y']:.2f})\n\n"
        
        # 显示在对话框中
        dlg = wx.TextEntryDialog(None, report, "自定义DRC结果",
                                style=wx.TE_MULTILINE | wx.TE_READONLY)
        dlg.SetSize((500, 400))
        dlg.ShowModal()
        dlg.Destroy()

CustomDRCPlugin().register()

18.3 案例三:自动布局辅助工具

18.3.1 需求分析

创建一个自动整理元器件布局的工具。

18.3.2 实现代码

import pcbnew
import wx

class AutoLayoutPlugin(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "自动布局辅助"
        self.category = "布局"
        self.description = "自动整理元器件布局"
        self.show_toolbar_button = True
    
    def Run(self):
        dlg = LayoutOptionsDialog(None)
        if dlg.ShowModal() == wx.ID_OK:
            options = dlg.GetOptions()
            self.auto_layout(options)
        dlg.Destroy()
    
    def auto_layout(self, options):
        """执行自动布局"""
        board = pcbnew.GetBoard()
        
        if options['type'] == 'grid':
            self.layout_in_grid(board, options)
        elif options['type'] == 'by_value':
            self.layout_by_value(board, options)
        elif options['type'] == 'by_prefix':
            self.layout_by_prefix(board, options)
        
        pcbnew.Refresh()
        wx.MessageBox("布局完成", "自动布局")
    
    def layout_in_grid(self, board, options):
        """网格布局"""
        footprints = list(board.GetFootprints())
        if not footprints:
            return
        
        start_x = options.get('start_x', 50)
        start_y = options.get('start_y', 50)
        spacing_x = options.get('spacing_x', 10)
        spacing_y = options.get('spacing_y', 10)
        cols = options.get('columns', 5)
        
        for i, fp in enumerate(footprints):
            row = i // cols
            col = i % cols
            
            x = pcbnew.FromMM(start_x + col * spacing_x)
            y = pcbnew.FromMM(start_y + row * spacing_y)
            
            fp.SetPosition(pcbnew.wxPoint(x, y))
    
    def layout_by_value(self, board, options):
        """按值分组布局"""
        footprints = list(board.GetFootprints())
        
        # 按值分组
        groups = {}
        for fp in footprints:
            value = fp.GetValue()
            if value not in groups:
                groups[value] = []
            groups[value].append(fp)
        
        # 布局各组
        start_x = 50
        start_y = 50
        group_spacing = 30
        item_spacing = 8
        
        y_offset = 0
        for value, fps in sorted(groups.items()):
            for i, fp in enumerate(fps):
                x = pcbnew.FromMM(start_x + i * item_spacing)
                y = pcbnew.FromMM(start_y + y_offset)
                fp.SetPosition(pcbnew.wxPoint(x, y))
            y_offset += group_spacing
    
    def layout_by_prefix(self, board, options):
        """按参考前缀分组布局"""
        footprints = list(board.GetFootprints())
        
        # 按前缀分组
        groups = {}
        for fp in footprints:
            ref = fp.GetReference()
            prefix = ''.join(c for c in ref if not c.isdigit())
            if prefix not in groups:
                groups[prefix] = []
            groups[prefix].append(fp)
        
        # 预定义区域
        regions = {
            'R': (50, 50),    # 电阻区
            'C': (50, 100),   # 电容区
            'U': (120, 50),   # IC区
            'Q': (120, 100),  # 晶体管区
            'D': (190, 50),   # 二极管区
            'J': (190, 100),  # 连接器区
        }
        
        item_spacing = 8
        
        for prefix, fps in groups.items():
            start_x, start_y = regions.get(prefix, (250, 50))
            
            for i, fp in enumerate(fps):
                x = pcbnew.FromMM(start_x + i * item_spacing)
                y = pcbnew.FromMM(start_y)
                fp.SetPosition(pcbnew.wxPoint(x, y))

class LayoutOptionsDialog(wx.Dialog):
    def __init__(self, parent):
        super().__init__(parent, title="布局选项", size=(350, 300))
        
        panel = wx.Panel(self)
        vbox = wx.BoxSizer(wx.VERTICAL)
        
        # 布局类型选择
        self.layout_type = wx.RadioBox(
            panel, choices=["网格布局", "按值分组", "按前缀分组"],
            style=wx.RA_SPECIFY_COLS, majorDimension=1
        )
        vbox.Add(self.layout_type, 0, wx.ALL | wx.EXPAND, 10)
        
        # 网格参数
        grid_box = wx.StaticBox(panel, label="网格参数")
        grid_sizer = wx.StaticBoxSizer(grid_box, wx.VERTICAL)
        
        self.cols_ctrl = wx.SpinCtrl(panel, value="5", min=1, max=20)
        self.spacing_ctrl = wx.SpinCtrl(panel, value="10", min=1, max=50)
        
        grid_sizer.Add(wx.StaticText(panel, label="列数:"), 0, wx.ALL, 5)
        grid_sizer.Add(self.cols_ctrl, 0, wx.ALL | wx.EXPAND, 5)
        grid_sizer.Add(wx.StaticText(panel, label="间距(mm):"), 0, wx.ALL, 5)
        grid_sizer.Add(self.spacing_ctrl, 0, wx.ALL | wx.EXPAND, 5)
        
        vbox.Add(grid_sizer, 0, wx.ALL | wx.EXPAND, 10)
        
        # 按钮
        btn_sizer = wx.StdDialogButtonSizer()
        ok_btn = wx.Button(panel, wx.ID_OK, "执行")
        cancel_btn = wx.Button(panel, wx.ID_CANCEL, "取消")
        btn_sizer.AddButton(ok_btn)
        btn_sizer.AddButton(cancel_btn)
        btn_sizer.Realize()
        vbox.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
        
        panel.SetSizer(vbox)
    
    def GetOptions(self):
        types = ['grid', 'by_value', 'by_prefix']
        return {
            'type': types[self.layout_type.GetSelection()],
            'columns': self.cols_ctrl.GetValue(),
            'spacing_x': self.spacing_ctrl.GetValue(),
            'spacing_y': self.spacing_ctrl.GetValue(),
            'start_x': 50,
            'start_y': 50
        }

AutoLayoutPlugin().register()

18.4 案例四:BOM生成器增强

18.4.1 实现代码

import pcbnew
import wx
import csv
from collections import defaultdict

class EnhancedBOMPlugin(pcbnew.ActionPlugin):
    def defaults(self):
        self.name = "增强BOM生成"
        self.category = "导出"
        self.description = "生成带采购信息的BOM"
        self.show_toolbar_button = True
    
    def Run(self):
        board = pcbnew.GetBoard()
        
        # 收集元器件信息
        components = self.collect_components(board)
        
        # 合并相同元器件
        merged = self.merge_components(components)
        
        # 选择保存路径
        dlg = wx.FileDialog(None, "保存BOM", "", "BOM.csv",
                           "CSV files (*.csv)|*.csv",
                           wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
        
        if dlg.ShowModal() == wx.ID_OK:
            filepath = dlg.GetPath()
            self.export_bom(merged, filepath)
            wx.MessageBox(f"BOM导出成功: {filepath}", "完成")
        
        dlg.Destroy()
    
    def collect_components(self, board):
        """收集元器件信息"""
        components = []
        
        for fp in board.GetFootprints():
            # 跳过虚拟元器件
            if fp.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
                continue
            
            # 获取基本信息
            component = {
                'reference': fp.GetReference(),
                'value': fp.GetValue(),
                'footprint': str(fp.GetFPID().GetLibItemName()),
                'layer': 'Top' if fp.GetLayer() == pcbnew.F_Cu else 'Bottom'
            }
            
            # 尝试获取扩展字段
            for field in ['Manufacturer', 'MPN', 'Supplier', 'SPN']:
                try:
                    val = fp.GetFieldText(field)
                    component[field.lower()] = val if val else ''
                except:
                    component[field.lower()] = ''
            
            components.append(component)
        
        return components
    
    def merge_components(self, components):
        """合并相同元器件"""
        merged = defaultdict(lambda: {
            'refs': [],
            'quantity': 0
        })
        
        for comp in components:
            # 创建唯一键(基于值、封装、MPN)
            key = (comp['value'], comp['footprint'], 
                   comp.get('mpn', ''))
            
            if merged[key]['quantity'] == 0:
                merged[key].update(comp)
            
            merged[key]['refs'].append(comp['reference'])
            merged[key]['quantity'] += 1
        
        # 转换为列表并排序参考标号
        result = []
        for key, data in merged.items():
            data['refs'].sort(key=lambda x: (
                ''.join(c for c in x if not c.isdigit()),
                int(''.join(c for c in x if c.isdigit()) or '0')
            ))
            data['references'] = ', '.join(data['refs'])
            result.append(data)
        
        return result
    
    def export_bom(self, components, filepath):
        """导出BOM到CSV"""
        with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
            fieldnames = ['Item', 'Quantity', 'References', 'Value', 
                         'Footprint', 'Manufacturer', 'MPN', 
                         'Supplier', 'SPN']
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            
            for i, comp in enumerate(components, 1):
                writer.writerow({
                    'Item': i,
                    'Quantity': comp['quantity'],
                    'References': comp['references'],
                    'Value': comp['value'],
                    'Footprint': comp['footprint'],
                    'Manufacturer': comp.get('manufacturer', ''),
                    'MPN': comp.get('mpn', ''),
                    'Supplier': comp.get('supplier', ''),
                    'SPN': comp.get('spn', '')
                })

EnhancedBOMPlugin().register()

18.5 开发最佳实践

18.5.1 代码组织

# 推荐的插件项目结构
my_plugin/
├── __init__.py
├── plugin.py        # 主插件代码
├── dialogs.py       # GUI对话框
├── utils.py         # 工具函数
├── resources/
│   └── icons/       # 图标文件
└── tests/
    └── test_plugin.py

18.5.2 错误处理

def safe_operation(func):
    """装饰器:安全执行并捕获异常"""
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            wx.MessageBox(f"错误: {str(e)}", "插件错误",
                         wx.OK | wx.ICON_ERROR)
            import traceback
            traceback.print_exc()
    return wrapper

class SafePlugin(pcbnew.ActionPlugin):
    @safe_operation
    def Run(self):
        # 插件代码
        pass

18.5.3 配置持久化

import json
import os

class PluginConfig:
    def __init__(self, plugin_name):
        self.config_dir = os.path.expanduser("~/.config/kicad_plugins")
        self.config_file = os.path.join(self.config_dir, f"{plugin_name}.json")
        self.config = {}
        self.load()
    
    def load(self):
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r') as f:
                self.config = json.load(f)
    
    def save(self):
        os.makedirs(self.config_dir, exist_ok=True)
        with open(self.config_file, 'w') as f:
            json.dump(self.config, f, indent=2)
    
    def get(self, key, default=None):
        return self.config.get(key, default)
    
    def set(self, key, value):
        self.config[key] = value
        self.save()

18.6 本章小结

本章通过四个实战案例展示了KiCad二次开发的应用:

  1. 自定义导出器:学会了将PCB数据导出为自定义格式。
  2. DRC扩展:掌握了创建自定义设计规则检查。
  3. 自动布局工具:实现了元器件自动整理功能。
  4. BOM增强:创建了功能更强的BOM生成器。
  5. 最佳实践:学会了代码组织、错误处理和配置管理。

通过这些案例,读者可以将所学知识应用到实际项目中,开发满足特定需求的KiCad扩展工具。


posted @ 2026-01-10 13:19  我才是银古  阅读(12)  评论(0)    收藏  举报