abse/museair-usb
USB-C–powered ESP32-C3 controller board with AP63203 buck conversion, AP2112K 3.3V regulation, I²C SEN62 sensor interface, status LED, buttons, UART/I²C headers, and extensive decoupling.
- Version
- 0.5.7
- License
- unset
- Stars
- 0
scripts/prepare-kicad.py
"""Repair known tscircuit 0.0.2444 KiCad export metadata defects.
Usage: KiCad's bundled Python prepare-kicad.py circuit.json raw.kicad_pcb output.kicad_pcb
Source-pad geometry and net assignments are retained. Two free routing
vertices from earlier routes have guarded clearance corrections; native DRC
checks this release, which does not require those old corrections.
KiCad refills the ground
zones against the exact pads after restoring duplicate-pin net assignments.
The plot origin is the board's lower-left corner, shared with the assembly CPL.
"""
import json, sys, os, math
import wx
import pcbnew as pcb
# Initialize the wx runtime used by KiCad native I/O.
wx_app = wx.App(False)
source, raw_path, output = sys.argv[1:4]
report_path = sys.argv[4] if len(sys.argv)>4 else None
cs = json.load(open(source))
b = pcb.LoadBoard(raw_path)
# Keep removed objects alive until process exit: KiCad 10 SWIG destructors
# can invalidate other native wrapper types when collected mid-export.
removed_native_items = []
footprints = list(b.GetFootprints())
sc = {x['source_component_id']: x for x in cs if x['type'] == 'source_component'}
net_by_key = {x['subcircuit_connectivity_map_key']: x['name'] for x in cs if x['type'] == 'source_net'}
pad_net = {}
for p in (x for x in cs if x['type'] == 'source_port'):
if p['source_component_id'] not in sc or 'pin_number' not in p:
continue
name = sc[p['source_component_id']]['name']
net_name = net_by_key.get(p.get('subcircuit_connectivity_map_key'))
if net_name:
pad_net[(name, str(p['pin_number']))] = net_name
repairs = {'pad_net_assignments': 0, 'two_point_courtyard_polygons': 0, 'duplicate_copper_items': 0}
for f in footprints:
for pad in f.Pads():
name = pad_net.get((f.GetReference(), pad.GetNumber()))
if name and pad.GetNetname() != name:
pad.SetNet(b.FindNet(name))
repairs['pad_net_assignments'] += 1
if f.GetReference() in ('J4','J5'):
layers=pad.GetLayerSet();layers.RemoveLayer(pcb.F_Paste);layers.RemoveLayer(pcb.B_Paste);pad.SetLayerSet(layers)
for g in list(f.GraphicalItems()):
if g.GetLayer() in (pcb.F_CrtYd, pcb.B_CrtYd) and g.GetShape() == pcb.SHAPE_T_POLY:
points = list(g.GetPolyShape().Outline(0).CPoints())
if len(points) == 2:
g.SetShape(pcb.SHAPE_T_SEGMENT)
g.SetStart(points[0]); g.SetEnd(points[1])
repairs['two_point_courtyard_polygons'] += 1
seen = set()
for t in list(b.GetTracks()):
if isinstance(t, pcb.PCB_VIA):
key = ('via', t.GetNetCode(), t.GetPosition().x, t.GetPosition().y, t.GetWidth(pcb.F_Cu), t.GetDrillValue())
else:
ends = sorted([(t.GetStart().x, t.GetStart().y), (t.GetEnd().x, t.GetEnd().y)])
key = ('track', t.GetNetCode(), t.GetLayer(), tuple(ends), t.GetWidth())
if key in seen:
removed_native_items.append(t); b.Remove(t); repairs['duplicate_copper_items'] += 1
else:
seen.add(key)
# The autorouter sometimes places two same-net vias so close that their drill
# holes violate 0.2 mm spacing. Retain one hole and join the old endpoints on
# both layers. The short links lie inside the two original overlapping lands.
repairs['same_net_vias_coalesced'] = 0
vias = [t for t in b.GetTracks() if isinstance(t, pcb.PCB_VIA)]
for i, master in enumerate(vias):
if master is None: continue
for j in range(i+1,len(vias)):
other = vias[j]
if other is None or master.GetNetCode()!=other.GetNetCode(): continue
a,c=master.GetPosition(),other.GetPosition()
distance=math.hypot(a.x-c.x,a.y-c.y)
min_distance=(master.GetDrillValue()+other.GetDrillValue())/2+pcb.FromMM(0.2)
if distance < min_distance:
# Restrict this repair to overlapping same-size 0.6 mm lands.
assert master.GetWidth(pcb.F_Cu)==other.GetWidth(pcb.F_Cu)==pcb.FromMM(0.6)
assert distance < pcb.FromMM(0.5)
for layer in (pcb.F_Cu,pcb.B_Cu):
link=pcb.PCB_TRACK(b);link.SetStart(a);link.SetEnd(c)
link.SetLayer(layer);link.SetWidth(pcb.FromMM(0.15));link.SetNetCode(master.GetNetCode());b.Add(link)
removed_native_items.append(other); b.Remove(other);vias[j]=None;repairs['same_net_vias_coalesced']+=1
for d in b.GetDrawings():
if isinstance(d, pcb.PCB_TEXT) and d.GetLayer() == pcb.B_SilkS:
d.SetMirrored(True)
if isinstance(d, pcb.PCB_TEXT):
d.SetTextSize(pcb.VECTOR2I(pcb.FromMM(max(1,pcb.ToMM(d.GetTextWidth()))),pcb.FromMM(max(1,pcb.ToMM(d.GetTextHeight())))))
d.SetTextThickness(pcb.FromMM(0.15))
# Local autorouter leaves three track-to-track gaps at 0.0966-0.0979 mm.
# Move only free shared vertices, keeping both incident segments connected.
# These positions are specific to this release; later routes still need DRC.
repairs['clearance_vertex_corrections'] = []
for net, layer, x, y, dx, dy in [
('SCL', pcb.B_Cu, 86.017604, 114.537155, -0.03, 0),
('ASK', pcb.F_Cu, 89.024141, 66.018202, 0, -0.03),
]:
old=pcb.VECTOR2I(pcb.FromMM(x),pcb.FromMM(y))
new=pcb.VECTOR2I(pcb.FromMM(x+dx),pcb.FromMM(y+dy))
matches=[(t,which) for t in b.GetTracks()
if not isinstance(t,pcb.PCB_VIA) and t.GetNetname()==net and t.GetLayer()==layer
for which,point in [('start',t.GetStart()),('end',t.GetEnd())]
if point==old]
if matches:
assert len(matches)==2, (net,x,y,len(matches))
assert not any(v.GetPosition()==old for v in b.GetTracks() if isinstance(v,pcb.PCB_VIA))
assert not any(pad.GetPosition()==old for f in footprints for pad in f.Pads())
for t,which in matches:
if which=='start':t.SetStart(new)
else:t.SetEnd(new)
repairs['clearance_vertex_corrections'].append({'net':net,'from_mm':[x,y],'to_mm':[x+dx,y+dy],'incident_segments':2})
for f in footprints:
if f.GetReference() in ('J6','U2','L1'):
for text in f.GraphicalItems():
if isinstance(text,pcb.PCB_TEXT) and pcb.ToMM(text.GetTextHeight())<0.8:
text.SetTextSize(pcb.VECTOR2I(pcb.FromMM(0.8),pcb.FromMM(0.8)))
text.SetTextThickness(pcb.FromMM(0.15))
# Source already exports explicit reference silk; hide exporter-added duplicates.
for t in (f.Reference(),f.Value()):
t.SetVisible(False)
t.SetTextSize(pcb.VECTOR2I(pcb.FromMM(1),pcb.FromMM(1)))
t.SetTextThickness(pcb.FromMM(0.15))
# Match the actual source limits: 0.3 mm drills, 0.6 mm via lands.
s = b.GetDesignSettings()
s.m_MinClearance = pcb.FromMM(0.1)
s.m_TrackMinWidth = pcb.FromMM(0.15)
s.m_CopperEdgeClearance = pcb.FromMM(0.3)
s.m_MinThroughDrill = pcb.FromMM(0.3)
s.m_HoleClearance = pcb.FromMM(0.2)
s.m_HoleToHoleMin = pcb.FromMM(0.2)
s.m_ViasMinSize = pcb.FromMM(0.6)
s.m_ViasMinAnnularWidth = pcb.FromMM(0.15)
# Exporter defaults every net class to 0.2 mm; apply design's 0.1 mm spacing.
s.m_NetSettings.GetDefaultNetclass().SetClearance(pcb.FromMM(0.1))
# circuit-json -> KiCad adds (100,100), with Y reversed. Use source lower-left.
source_board=next(x for x in cs if x['type']=='pcb_board')
s.SetAuxOrigin(pcb.VECTOR2I(pcb.FromMM(100-source_board['width']/2),pcb.FromMM(100+source_board['height']/2)))
for zone in b.Zones():
if not zone.GetIsRuleArea():
zone.SetLocalClearance(pcb.FromMM(0.2))
zone.SetIslandRemovalMode(pcb.ISLAND_REMOVAL_MODE_ALWAYS)
b.BuildConnectivity()
pcb.ZONE_FILLER(b).Fill(b.Zones())
repairs['ground_zones_refilled'] = True
b.BuildConnectivity()
repairs['empty_ground_zones_removed'] = 0
repairs['isolated_ground_zones_removed'] = []
connectivity=pcb.CONNECTIVITY_DATA();connectivity.Build(b)
for zone in list(b.Zones()):
if zone.GetIsRuleArea() or zone.GetNetname()!='GND': continue
if zone.GetFilledArea()==0:
removed_native_items.append(zone); b.Remove(zone);repairs['empty_ground_zones_removed']+=1
elif not connectivity.IsConnectedOnLayer(zone,zone.GetLayer()):
# Remove only whole disconnected zones, proven by native connectivity.
# Connected main ground planes remain intact.
repairs['isolated_ground_zones_removed'].append({'uuid':zone.m_Uuid.AsString(),'area_mm2':zone.GetFilledArea()/1e12})
removed_native_items.append(zone); b.Remove(zone)
b.BuildConnectivity()
# The source chip includes the actual sensor, revised carrier and tie paths.
# Native and tscircuit use different bottom-model local axes; the generated
# native STEP accounts for this, while PCB mounting holes remain unchanged.
for f in footprints:
if f.GetReference()=='U3':
f.SetLayer(pcb.B_Cu)
repairs['sensor_bottom_side_restored_without_hole_transform']=True
model=pcb.FP_3DMODEL();model.m_Filename='${KIPRJMOD}/../mechanical/sen62-assembly-native.step'
# KiCad adds 0.085 mm bottom-model standoff. Remove it: carrier post
# tops meet PCB Z=0, and the housing rear face sits exactly at Z=-4.
model.m_Offset=pcb.VECTOR3D(0,3.5,-0.085);model.m_Scale=pcb.VECTOR3D(1,1,1)
model.m_Rotation=pcb.VECTOR3D(0,0,0);model.m_Show=True;f.Add3DModel(model)
repairs['sensor_native_detailed_assembly_model']=model.m_Filename
# Include the stored geometries as a portable native library for independent
# checks; no machine-global KiCad library installation is required.
library=os.path.join(os.path.dirname(output),'tscircuit.pretty')
os.makedirs(library,exist_ok=True)
plugin=pcb.PCB_IO_KICAD_SEXPR()
for footprint in footprints:
# Keep this rotated capacitor's exact native instance in its own library entry.
# It otherwise compares against a differently oriented same-part instance.
if footprint.GetReference() in ('C11','C19'):
lib_id=footprint.GetFPID();lib_id.SetLibItemName(pcb.UTF8(lib_id.GetUniStringLibItemName()+'_'+footprint.GetReference()));footprint.SetFPID(lib_id)
copy=pcb.FOOTPRINT(footprint);copy.SetOrientationDegrees(0);copy.SetPosition(pcb.VECTOR2I(0,0))
plugin.FootprintSave(os.path.abspath(library),copy)
pcb.SaveBoard(output,b)
with open(os.path.join(os.path.dirname(output),'fp-lib-table'),'w') as h:
h.write('(fp_lib_table\n (lib (name "tscircuit")(type "KiCad")(uri "${KIPRJMOD}/tscircuit.pretty")(options "")(descr "Exact imported footprints and bare PCB features"))\n)\n')
project = {
'meta': {'filename': os.path.basename(os.path.splitext(output)[0] + '.kicad_pro'), 'version': 1},
'board': {'design_settings': {'rules': {
'min_clearance': 0.1, 'min_track_width': 0.15, 'min_copper_edge_clearance': 0.3,
'min_through_hole_diameter': 0.3, 'min_hole_clearance': 0.2,
'min_hole_to_hole': 0.2, 'min_via_diameter': 0.6, 'min_via_annular_width': 0.15,
}}},
'net_settings': {'classes': [{'name': 'Default', 'clearance': 0.1, 'track_width': 0.2,
'via_diameter': 0.6, 'via_drill': 0.3, 'description': 'MuseAir source routing constraints'}], 'meta': {'version': 4}},
}
with open(os.path.splitext(output)[0] + '.kicad_pro', 'w') as handle:
json.dump(project, handle, indent=2)
connectivity.Build(b);connectivity.RecalculateRatsnest()
repairs['unconnected_copper_items'] = connectivity.GetUnconnectedCount(False)
if report_path:
with open(report_path,'w') as h: json.dump(repairs,h,indent=2);h.write('\n')
print(json.dumps(repairs, indent=2))
# KiCad 10 macOS SWIG/wx destructors can crash after all output is saved.
# The child has no further work; flush output and avoid that native teardown.
sys.stdout.flush(); sys.stderr.flush(); os._exit(0)