465 lines
19 KiB
Python
465 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# A script to check for annular ring violations
|
|
# both for TH pads and vias
|
|
# requirements: KiCAD pcbnew >= 4.0
|
|
# annular.py release "1.5.1"
|
|
#
|
|
# annular.py checking PCB for Annular Ring in Vias and TH Pads
|
|
# (SMD, Connector and NPTH are skipped)
|
|
# default Annular Ring >= 0.15 both for TH Pads and Vias
|
|
#
|
|
|
|
#### plugins errors
|
|
#import pcbnew;pcbnew.GetWizardsBackTrace()
|
|
|
|
global mm_ius, DRL_EXTRA, AR_SET, AR_SET_V, DRL_EXTRA_ius, MIN_AR_SIZE, MIN_AR_SIZE_V, found_violations, LogMsg, ___version___
|
|
|
|
___version___="1.6.3"
|
|
|
|
#wx.LogMessage("My message")
|
|
mm_ius = 1000000.0
|
|
# (consider always drill +0.1)
|
|
DRL_EXTRA=0.1
|
|
DRL_EXTRA_ius=DRL_EXTRA * mm_ius
|
|
|
|
AR_SET = 0.125 #minimum annular accepted for pads
|
|
MIN_AR_SIZE = AR_SET * mm_ius
|
|
|
|
AR_SET_V = 0.125 #minimum annular accepted for vias
|
|
MIN_AR_SIZE_V = AR_SET_V * mm_ius
|
|
|
|
import sys
|
|
import wx
|
|
import wx.richtext
|
|
#import subprocess
|
|
import os
|
|
import pcbnew
|
|
from pcbnew import *
|
|
# import base64
|
|
# from wx.lib.embeddedimage import PyEmbeddedImage
|
|
|
|
from . import AnnularDlg
|
|
from . import AnnularResultDlg
|
|
|
|
sys.path.append(os.path.dirname(__file__))
|
|
|
|
debug = False
|
|
def wxLogDebug(msg,dbg):
|
|
"""printing messages only if show is omitted or True"""
|
|
if dbg == True:
|
|
wx.LogMessage(msg)
|
|
#
|
|
|
|
class AnnularResult_Dlg(AnnularResultDlg.AnnularResultDlg):
|
|
# from https://github.com/MitjaNemec/Kicad_action_plugins
|
|
# hack for new wxFormBuilder generating code incompatible with old wxPython
|
|
# noinspection PyMethodOverriding
|
|
def SetSizeHints(self, sz1, sz2):
|
|
if wx.__version__ < '4.0':
|
|
self.SetSizeHintsSz(sz1, sz2)
|
|
else:
|
|
super(AnnularResult_Dlg, self).SetSizeHints(sz1, sz2)
|
|
|
|
def onOK(self, event):
|
|
self.Destroy()
|
|
#return self.EndModal(wx.ID_OK) # if modal_result == wx.ID_OK:
|
|
|
|
def OnClickCopy(self, event):
|
|
self.m_richTextResult.SelectAll()
|
|
self.m_richTextResult.Copy()
|
|
#global LogMsg
|
|
#copy2clip(LogMsg)
|
|
self.copy_btn.SetLabel("Text Copied")
|
|
|
|
# def onDeleteClick(self, event):
|
|
# return self.EndModal(wx.ID_DELETE)
|
|
#
|
|
# def onConnectClick(self, event):
|
|
# return self.EndModal(wx.ID_REVERT)
|
|
|
|
def __init__(self, parent):
|
|
import wx
|
|
AnnularResultDlg.AnnularResultDlg.__init__(self, parent)
|
|
#self.GetSizer().Fit(self)
|
|
self.SetMinSize(self.GetSize())
|
|
#### ----- connections
|
|
# Connect Events
|
|
self.Bind(wx.EVT_BUTTON, self.onOK, self.ok_btn)
|
|
#self.ok_btn.Bind(wx.EVT_BUTTON, self.EndModal(wx.ID_OK))
|
|
self.Bind(wx.EVT_BUTTON, self.OnClickCopy, self.copy_btn)
|
|
self.ok_btn.SetFocus()
|
|
# Tooltips
|
|
self.copy_btn.SetToolTip( wx.ToolTip(u"Copy Text to Clipboard" ))
|
|
self.ok_btn.SetToolTip( wx.ToolTip(u"Exit" ))
|
|
|
|
#def onOK(self, event):
|
|
# return self.EndModal(wx.ID_OK) # if modal_result == wx.ID_OK:
|
|
|
|
#def onConnectClick(self, event):
|
|
# return self.EndModal(wx.ID_REVERT)
|
|
|
|
#self.m_buttonDelete.Bind(wx.EVT_BUTTON, self.onDeleteClick)
|
|
#self.m_buttonReconnect.Bind(wx.EVT_BUTTON, self.onConnectClick)
|
|
#if wx.__version__ < '4.0':
|
|
# self.m_buttonReconnect.SetToolTipString( u"Select two converging Tracks to re-connect them\nor Select tracks including one round corner to be straighten" )
|
|
# self.m_buttonRound.SetToolTipString( u"Select two connected Tracks to round the corner\nThen choose distance from intersection and the number of segments" )
|
|
#else:
|
|
# self.m_buttonReconnect.SetToolTip( u"Select two converging Tracks to re-connect them\nor Select tracks including one round corner to be straighten" )
|
|
# self.m_buttonRound.SetToolTip( u"Select two connected Tracks to round the corner\nThen choose distance from intersection and the number of segments" )
|
|
|
|
class Annular_Dlg(AnnularDlg.AnnularDlg):
|
|
# from https://github.com/MitjaNemec/Kicad_action_plugins
|
|
# hack for new wxFormBuilder generating code incompatible with old wxPython
|
|
# noinspection PyMethodOverriding
|
|
def SetSizeHints(self, sz1, sz2):
|
|
if wx.__version__ < '4.0':
|
|
self.SetSizeHintsSz(sz1, sz2)
|
|
else:
|
|
super(Annular_Dlg, self).SetSizeHints(sz1, sz2)
|
|
|
|
def __init__(self, parent):
|
|
import wx
|
|
AnnularDlg.AnnularDlg.__init__(self, parent)
|
|
#self.GetSizer().Fit(self)
|
|
self.SetMinSize(self.GetSize())
|
|
#c1.Bind(wx.EVT_CHECKBOX, self.OntextMetric, c1)
|
|
#self.m_checkBoxPHD.Bind(wx.EVT_CHECKBOX, self.OnClickCheck, self.m_checkBoxPHD)
|
|
self.m_checkBoxPHD.Bind(wx.EVT_CHECKBOX, self.OnClickCheck)
|
|
self.m_bitmapAR.SetBitmap(wx.Bitmap(os.path.join(os.path.dirname(__file__), "./annular.png")))
|
|
|
|
#self.Bind(wx.EVT_CHECKBOX, self.OnClickCheck)
|
|
#self.m_buttonDelete.Bind(wx.EVT_BUTTON, self.onDeleteClick)
|
|
#self.m_buttonReconnect.Bind(wx.EVT_BUTTON, self.onConnectClick)
|
|
#if wx.__version__ < '4.0':
|
|
# self.m_buttonReconnect.SetToolTipString( u"Select two converging Tracks to re-connect them\nor Select tracks including one round corner to be straighten" )
|
|
# self.m_buttonRound.SetToolTipString( u"Select two connected Tracks to round the corner\nThen choose distance from intersection and the number of segments" )
|
|
#else:
|
|
# self.m_buttonReconnect.SetToolTip( u"Select two converging Tracks to re-connect them\nor Select tracks including one round corner to be straighten" )
|
|
# self.m_buttonRound.SetToolTip( u"Select two connected Tracks to round the corner\nThen choose distance from intersection and the number of segments" )
|
|
def OnClickCheck(self, event):
|
|
#self.Destroy()
|
|
if self.m_checkBoxPHD.IsChecked():
|
|
#self.Destroy()
|
|
self.m_staticTextPHD.Enable()
|
|
self.m_textCtrlPHD.Enable()
|
|
else:
|
|
self.m_staticTextPHD.Disable()
|
|
self.m_textCtrlPHD.Disable()
|
|
|
|
# def onDeleteClick(self, event):
|
|
# return self.EndModal(wx.ID_DELETE)
|
|
#
|
|
# def onConnectClick(self, event):
|
|
# return self.EndModal(wx.ID_REVERT)
|
|
|
|
|
|
# Python plugin stuff
|
|
class annular_check( pcbnew.ActionPlugin ):
|
|
"""
|
|
A script to check for annular ring violations
|
|
both for TH pads and vias
|
|
requirements: KiCAD pcbnew >= 4.0
|
|
AR_SET minimum annular accepted for pads
|
|
AR_SET_V minimum annular accepted for vias
|
|
"""
|
|
global ___version___
|
|
def defaults( self ):
|
|
"""
|
|
Method defaults must be redefined
|
|
self.name should be the menu label to use
|
|
self.category should be the category (not yet used)
|
|
self.description should be a comprehensive description
|
|
of the plugin
|
|
"""
|
|
self.name = "Annular checker \nversion "+___version___
|
|
self.category = "Checking PCB"
|
|
self.description = "Automaticaly check annular on an existing PCB"
|
|
#self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
|
|
self.show_toolbar_button = True
|
|
self.icon_file_name = os.path.join(os.path.dirname(__file__), 'annular.png')
|
|
|
|
def Run( self ):
|
|
import sys,os
|
|
#mm_ius = 1000000.0
|
|
_pcbnew_frame = [x for x in wx.GetTopLevelWindows() if x.GetTitle().lower().startswith('pcbnew')][0]
|
|
#aParameters = RoundTrackDlg(None)
|
|
aParameters = Annular_Dlg(_pcbnew_frame)
|
|
aParameters.m_LabelTitle.SetLabel("Check annular ring: version: "+___version___)
|
|
aParameters.m_textCtrlARP.SetToolTip( wx.ToolTip(u"Annular Ring for Pads (mm)" ))
|
|
aParameters.m_staticTextPHD.SetToolTip( wx.ToolTip(u"Drill extra margin (mm)" ))
|
|
aParameters.m_textCtrlARV.SetToolTip( wx.ToolTip(u"Annular Ring for Vias (mm)" ))
|
|
aParameters.m_staticTextARV.SetToolTip( wx.ToolTip(u"Annular Ring for Vias (mm)" ))
|
|
aParameters.m_textCtrlPHD.SetToolTip( wx.ToolTip(u"Drill extra margin (mm)" ))
|
|
aParameters.m_staticTextARP.SetToolTip( wx.ToolTip(u"Drill extra margin (mm)" ))
|
|
aParameters.m_checkBoxPHD.SetToolTip( wx.ToolTip(u"use drill size as finished hole size\nadding an extra drill margin" ))
|
|
aParameters.m_textCtrlPHD.SetValue('0.1')
|
|
aParameters.m_textCtrlARP.SetValue('0.125')
|
|
aParameters.m_textCtrlARV.SetValue('0.125')
|
|
aParameters.Show()
|
|
modal_result = aParameters.ShowModal()
|
|
#import pcbnew;pcbnew.GetWizardsBackTrace()
|
|
if modal_result == wx.ID_OK:
|
|
global mm_ius, DRL_EXTRA, AR_SET, AR_SET_V, DRL_EXTRA_ius, MIN_AR_SIZE, MIN_AR_SIZE_V
|
|
phd = float(aParameters.m_textCtrlPHD.GetValue().replace(',','.'))
|
|
ar = float(aParameters.m_textCtrlARP.GetValue().replace(',','.'))
|
|
arv = float(aParameters.m_textCtrlARV.GetValue().replace(',','.'))
|
|
if aParameters.m_checkBoxPHD.IsChecked():
|
|
DRL_EXTRA=phd
|
|
DRL_EXTRA_ius=DRL_EXTRA * mm_ius
|
|
else:
|
|
DRL_EXTRA=0
|
|
DRL_EXTRA_ius=DRL_EXTRA * mm_ius
|
|
AR_SET = ar #minimum annular accepted for pads
|
|
MIN_AR_SIZE = AR_SET * mm_ius
|
|
|
|
AR_SET_V = arv #minimum annular accepted for vias
|
|
MIN_AR_SIZE_V = AR_SET_V * mm_ius
|
|
#snap2grid(gridSizeMM,use_grid_origin)
|
|
calculate_AR()
|
|
else:
|
|
None # Cancel
|
|
|
|
|
|
def annring_size(pad):
|
|
# valid for oval pad/drills
|
|
annrX=(pad.GetSize()[0] - (pad.GetDrillSize()[0]+DRL_EXTRA_ius))/2
|
|
annrY=(pad.GetSize()[1] - (pad.GetDrillSize()[1]+DRL_EXTRA_ius))/2
|
|
#annr=min(pad.GetSize()) - max(pad.GetDrillSize())
|
|
#if annr < MIN_AR_SIZE:
|
|
#print annrX
|
|
#print annrY
|
|
#print pad.GetSize()[0]/mm_ius
|
|
#print pad.GetSize()[0]#/mm_ius
|
|
#print pad.GetDrillSize()[0]#/mm_ius
|
|
#print DRL_EXTRA_ius
|
|
#print pad.GetDrillSize()[0]/mm_ius
|
|
#print (pad.GetDrillSize()[0]+DRL_EXTRA_ius)/mm_ius
|
|
#print annrX/mm_ius
|
|
return min(annrX,annrY)
|
|
|
|
def annringNP_size(pad):
|
|
# valid for oval pad/drills
|
|
annrX=(pad.GetSize()[0] - (pad.GetDrillSize()[0]))/2
|
|
annrY=(pad.GetSize()[1] - (pad.GetDrillSize()[1]))/2
|
|
#annr=min(pad.GetSize()) - max(pad.GetDrillSize())
|
|
#if annr < MIN_AR_SIZE:
|
|
#print annrX
|
|
#print annrY
|
|
#print pad.GetSize()[0]/mm_ius
|
|
#print pad.GetSize()[0]#/mm_ius
|
|
#print pad.GetDrillSize()[0]#/mm_ius
|
|
#print DRL_EXTRA_ius
|
|
#print pad.GetDrillSize()[0]/mm_ius
|
|
#print (pad.GetDrillSize()[0]+DRL_EXTRA_ius)/mm_ius
|
|
#print annrX/mm_ius
|
|
#return min(annrX,annrY)
|
|
return annrX,annrY
|
|
|
|
def vias_annring_size(via):
|
|
# calculating via annular
|
|
annr=(via.GetWidth() - (via.GetDrillValue()+DRL_EXTRA_ius))/2
|
|
#print via.GetWidth()
|
|
#print via.GetDrillValue()
|
|
return annr
|
|
|
|
def f_mm(raw):
|
|
return repr(raw/mm_ius)
|
|
|
|
|
|
def calculate_AR():
|
|
global mm_ius, DRL_EXTRA, AR_SET, AR_SET_V, DRL_EXTRA_ius, MIN_AR_SIZE, MIN_AR_SIZE_V
|
|
board = pcbnew.GetBoard()
|
|
PassC=FailC=0
|
|
PassCV=FailCV=0
|
|
|
|
PassCN=FailCN=0
|
|
PassCVN=FailCVN=0
|
|
|
|
fileName = GetBoard().GetFileName()
|
|
if len(fileName)==0:
|
|
wx.LogMessage("a board needs to be saved/loaded!")
|
|
else:
|
|
found_violations=False
|
|
_pcbnew_frame = [x for x in wx.GetTopLevelWindows() if x.GetTitle().lower().startswith('pcbnew')][0]
|
|
aResult = AnnularResult_Dlg(_pcbnew_frame)
|
|
#import pcbnew;pcbnew.GetWizardsBackTrace()
|
|
writeTxt= aResult.m_richTextResult.WriteText
|
|
rt = aResult.m_richTextResult
|
|
rt.BeginItalic()
|
|
writeTxt("'action_menu_annular_check.py'\n")
|
|
#frame.m_richText1.WriteText("'action_menu_annular_check.py'\n")
|
|
LogMsg=""
|
|
msg="'action_menu_annular_check.py'\n"
|
|
msg+="version = "+___version___
|
|
writeTxt("version = "+___version___)
|
|
msg+="\nTesting PCB for Annular Rings\nTH Pads >= "+repr(AR_SET)+" Vias >= "+repr(AR_SET_V)+"\nPHD margin on PTH = "+ repr(DRL_EXTRA)
|
|
writeTxt("\nTesting PCB for Annular Rings\nTH Pads >= "+repr(AR_SET)+" Vias >= "+repr(AR_SET_V)+"\nPHD margin on PTH = "+ repr(DRL_EXTRA))
|
|
rt.EndItalic()
|
|
writeTxt('\n\n')
|
|
#print (msg)
|
|
LogMsg+=msg+'\n\n'
|
|
|
|
# print "LISTING VIAS:"
|
|
for item in board.GetTracks():
|
|
if type(item) is pcbnew.VIA:
|
|
pos = item.GetPosition()
|
|
drill = item.GetDrillValue()
|
|
width = item.GetWidth()
|
|
ARv = vias_annring_size(item)
|
|
if ARv < MIN_AR_SIZE_V:
|
|
# print("AR violation at %s." % (pad.GetPosition() / mm_ius )) Raw units, needs fixing
|
|
XYpair = item.GetPosition()
|
|
msg="AR Via violation of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])
|
|
rt.BeginTextColour('red')
|
|
writeTxt("AR Via violation of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])+'\n')
|
|
rt.EndTextColour()
|
|
#print (msg)
|
|
LogMsg+=msg+'\n'
|
|
FailCV = FailCV+1
|
|
else:
|
|
PassCV = PassCV+1
|
|
#print type(item)
|
|
|
|
msg="VIAS that Pass = "+repr(PassCV)+"; Fails = "+repr(FailCV)
|
|
if FailCV >0:
|
|
rt.BeginBold()
|
|
writeTxt("VIAS that Pass = "+repr(PassCV)+"; ")
|
|
if FailCV >0:
|
|
rt.BeginTextColour('red')
|
|
writeTxt("Fails = "+repr(FailCV)+'\n\n')
|
|
if FailCV >0:
|
|
rt.EndTextColour()
|
|
rt.EndBold()
|
|
print(msg)
|
|
LogMsg+=msg+'\n'
|
|
|
|
for module in board.GetModules():
|
|
try:
|
|
module_Pads=module.PadsList()
|
|
except:
|
|
module_Pads=module.Pads()
|
|
for pad in module_Pads: #print(pad.GetAttribute())
|
|
if pad.GetAttribute() == PAD_ATTRIB_STANDARD: #TH pad
|
|
ARv = annring_size(pad)
|
|
#print(f_mm(ARv))
|
|
if ARv < MIN_AR_SIZE:
|
|
# print("AR violation at %s." % (pad.GetPosition() / mm_ius )) Raw units, needs fixing
|
|
XYpair = pad.GetPosition()
|
|
msg="AR PTH violation of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])
|
|
rt.BeginTextColour('red')
|
|
writeTxt("AR PTH violation of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])+'\n')
|
|
rt.EndTextColour()
|
|
#print (msg)
|
|
LogMsg+=msg+'\n'
|
|
FailC = FailC+1
|
|
else:
|
|
PassC = PassC+1
|
|
if pad.GetAttribute() == PAD_ATTRIB_HOLE_NOT_PLATED:
|
|
ARvX, ARvY = annringNP_size(pad)
|
|
#print(f_mm(ARvX));print(f_mm(ARvY))
|
|
if (ARvX) != 0 or ARvY != 0:
|
|
ARv = min(ARvX, ARvY)
|
|
if ARv < MIN_AR_SIZE:
|
|
# print("AR violation at %s." % (pad.GetPosition() / mm_ius )) Raw units, needs fixing
|
|
XYpair = pad.GetPosition()
|
|
msg="AR NPTH warning of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])
|
|
rt.BeginTextColour('red')
|
|
writeTxt("AR NPTH warning of "+f_mm(ARv)+" at XY "+f_mm(XYpair[0])+","+f_mm(XYpair[1])+'\n')
|
|
rt.EndTextColour()
|
|
#print (msg)
|
|
LogMsg+=msg+'\n'
|
|
FailCN = FailCN+1
|
|
else:
|
|
PassCN = PassCN+1
|
|
else:
|
|
PassCN = PassCN+1
|
|
|
|
#if FailCV >0:
|
|
#writeTxt('\n')
|
|
msg = "TH PADS that Pass = "+repr(PassC)+"; Fails = "+repr(FailC)
|
|
if FailC >0:
|
|
rt.BeginBold()
|
|
writeTxt("TH PADS that Pass = "+repr(PassC)+"; ")
|
|
if FailC >0:
|
|
rt.BeginTextColour('red')
|
|
writeTxt("Fails = "+repr(FailC)+'\n')
|
|
if FailC >0:
|
|
rt.EndTextColour()
|
|
rt.EndBold()
|
|
print(msg)
|
|
LogMsg+=msg+'\n'
|
|
|
|
msg="NPTH PADS that Pass = "+repr(PassCN)+"; Fails = "+repr(FailCN)
|
|
#writeTxt('\n')
|
|
if FailCN >0:
|
|
rt.BeginBold()
|
|
writeTxt("NPTH PADS that Pass = "+repr(PassCN)+"; ")
|
|
if FailCN >0:
|
|
rt.BeginTextColour('red')
|
|
writeTxt("Fails = "+repr(FailCN)+'\n')
|
|
if FailC >0:
|
|
rt.EndTextColour()
|
|
rt.EndBold()
|
|
print(msg)
|
|
LogMsg+=msg+'\n'
|
|
|
|
pcbName = (os.path.splitext(GetBoard().GetFileName())[0]) #filename no ext
|
|
#wx.LogMessage(pcbName)#LogMsg)
|
|
##wx.LogMessage(LogMsg)
|
|
FC=r"C:\FreeCAD\bin\freecad.exe"
|
|
kSU=r"C:\Cad\Progetti_K\3D-FreeCad-tools\kicad-StepUp-tools.FCMacro"
|
|
#subprocess.check_call([FC, kSU, pcbName])
|
|
##p = subprocess.Popen([FC, kSU, pcbName])
|
|
|
|
#found_violations=False
|
|
if (FailC+FailCN+FailCV)>0:
|
|
found_violations=True
|
|
|
|
if found_violations:
|
|
#frame.m_staticTitle = wx.StaticText(frame, label=" Check result: (Violations found)")
|
|
aResult.m_staticTitle.SetLabel(" Check result: (Violations found)")
|
|
#self.title.SetForegroundColour('#FF0000')
|
|
aResult.m_staticTitle.SetBackgroundColour('#FF0000')
|
|
font = wx.Font(wx.DEFAULT, wx.DECORATIVE, wx.ITALIC, wx.BOLD)
|
|
aResult.m_staticTitle.SetFont(font)
|
|
else:
|
|
#frame.m_staticTitle = wx.StaticText(frame, label=" Annular Check result: OK")
|
|
aResult.m_staticTitle.SetLabel(" Annular Check result: OK")
|
|
aResult.m_staticTitle.SetBackgroundColour('#00FF00')
|
|
font = wx.Font(wx.DEFAULT, wx.DECORATIVE, wx.ITALIC, wx.BOLD)
|
|
aResult.m_staticTitle.SetFont(font)
|
|
|
|
|
|
aResult.Show()
|
|
#modal_result = aResult.ShowModal()
|
|
#if modal_result == wx.ID_OK:
|
|
# aResult.Destroy()
|
|
#if modal_result == wx.ID_OK:
|
|
# aResult.Destroy()
|
|
|
|
|
|
# annular_check().register()
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='KiCad PCB Annular Checker')
|
|
parser.add_argument('file', type=str, help="KiCad PCB file")
|
|
args = parser.parse_args()
|
|
print("Loading %s" % args.file)
|
|
main(pcbnew.LoadBoard(args.file))
|
|
|
|
else:
|
|
annular_check().register()
|
|
|
|
|
|
# execfile("annular.py")
|
|
# annular.py Testing PCB for Annular Ring >= 0.15
|
|
# AR violation of 0.1 at XY 172.974,110.744
|
|
# VIAS that Pass = 100 Fails = 1
|
|
# AR violation of 0.1 at XY 172.212,110.744
|
|
# AR violation of 0.0 at XY 154.813,96.52
|
|
# PADS that Pass = 49 Fails = 2
|
|
|