pixalynx/nrf9151-gps-tracker
A compact 44×32 mm, 4-layer nRF9151 LTE-M/NB-IoT/GNSS tracker PCB with nPM1300 LiPo/USB-C power, nano-SIM/eSIM switching, nRF7002 Wi‑Fi scanning, accelerometer, antennas, buzzer, LEDs, SOS button, and SWD/debug connectors.
- Version
- 0.1.4
- License
- unset
- Stars
- 0
scripts/fix-anchors.py
#!/usr/bin/env python3
"""Correct the frame of generated copper for components whose pcbPath anchor is not their pcb_component.center.
tscircuit renders `<trace pcbPath>` points relative to the from-component's pcbX/pcbY (rotated), but Circuit JSON only
records pcb_component.center = the centre of the land-pattern bounds. For symmetric footprints the two coincide; for
the JST header (J6) and the USB-C receptacle (J5) they do not, so legs/stubs generated from `center` land shifted.
The script measures the shift from the current build (first waypoint of every imported leg vs where core drew it),
then rewrites routing/index-routes.json (fromCentre) and routing/index-gnd.json (stub `local`) with the true anchor.
Run it after a build that renders the freshly generated JSONs, then rebuild. Idempotent: prints nothing to fix once
the offsets are gone.
Usage: python3 scripts/fix-anchors.py [--mesh] (--mesh also re-frames ground stubs; use it only before a DSN export,
never after routing, because the router avoided the stubs where they were drawn)
"""
import json, math
d = json.load(open('dist/index/circuit.json'))
routes = json.load(open('routing/index-routes.json'))
mesh = json.load(open('routing/index-gnd.json'))
st = {t['source_trace_id']: t.get('name') for t in d if t['type'] == 'source_trace'}
src = {c['source_component_id']: c['name'] for c in d if c['type'] == 'source_component'}
comp = {src[c['source_component_id']]: c for c in d if c['type'] == 'pcb_component'}
legs = {l['id']: l for l in routes['legs']}
offsets = {}
for t in d:
if t['type'] != 'pcb_trace': continue
leg = legs.get(st.get(t.get('source_trace_id')))
if not leg or not leg['path']: continue
p0, q = leg['path'][0], t['route'][1]
dx, dy = round(q['x'] - p0['x'], 4), round(q['y'] - p0['y'], 4)
if abs(dx) > 1e-3 or abs(dy) > 1e-3:
offsets.setdefault(leg['from'].split('.')[0], (dx, dy))
# ground stubs measure the same shift: the stub's second route point should sit on its intended via
stubs = {s_['port']: s_ for s_ in mesh['stubs']}
pp = {p['pcb_port_id']: p for p in d if p['type'] == 'pcb_port'}
sp = {p['source_port_id']: p for p in d if p['type'] == 'source_port'}
for t in d:
if t['type'] != 'pcb_trace' or len(t['route']) < 2: continue
a = t['route'][0].get('start_pcb_port_id')
if not a or a not in pp or t['route'][-1].get('end_pcb_port_id'): continue # stubs end on the plane, not on a land
if t['route'][-1].get('route_type') != 'via' and not any(p_.get('route_type') == 'via' for p_ in t['route']): continue
s_port = sp[pp[a]['source_port_id']]; cname = src[s_port['source_component_id']]
stub = stubs.get(f"{cname}.{s_port['name']}") or next((v for k, v in stubs.items() if k.startswith(cname + '.') and abs(v['pad'][0] - t['route'][0]['x']) < 1e-3 and abs(v['pad'][1] - t['route'][0]['y']) < 1e-3), None)
if not stub: continue
q = t['route'][1]; dx, dy = round(q['x'] - stub['via'][0], 4), round(q['y'] - stub['via'][1], 4)
if abs(dx) > 1e-3 or abs(dy) > 1e-3: offsets.setdefault(cname, (dx, dy))
fixed = 0
for leg in routes['legs']:
c = leg['from'].split('.')[0]
if c in offsets:
dx, dy = offsets[c]
leg['fromCentre'] = {'x': round(leg['fromCentre']['x'] + dx, 4), 'y': round(leg['fromCentre']['y'] + dy, 4)}
fixed += 1
import sys
for s in (mesh['stubs'] if '--mesh' in sys.argv else []):
c = s['port'].split('.')[0]
if c in offsets:
pc = comp[c]; dx, dy = offsets[c]
ax, ay = pc['center']['x'] + dx, pc['center']['y'] + dy
rot = math.radians(pc.get('rotation') or 0)
vx, vy = s['via'][0] - ax, s['via'][1] - ay
s['local'] = [round(vx * math.cos(-rot) - vy * math.sin(-rot), 3), round(vx * math.sin(-rot) + vy * math.cos(-rot), 3)]
fixed += 1
json.dump(routes, open('routing/index-routes.json', 'w'), indent=1)
json.dump(mesh, open('routing/index-gnd.json', 'w'), indent=1)
print(f'anchor offsets {offsets}; corrected {fixed} legs/stubs')