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/freeze-routes.py
#!/usr/bin/env python3
"""Freeze the copper of an autorouted build into routing/index-routes.json so normal builds are reproducible
(routingDisabled, no router at build time).
Usage: python3 scripts/freeze-routes.py <routed circuit.json> <unrouted circuit.json> [--router "name"]
routed : a build made with KT_AUTOROUTE=1 (native tscircuit router) from the same source
unrouted : the normal build (routingDisabled) of the same source; its pcb_traces (hand-routed RF legs, the ground
mesh, earlier frozen legs) are already in the source and are skipped here
Every new pcb_trace becomes one leg {from, to, width, path} rendered by lib/routed-traces.tsx. Endpoints must be
lands (start_pcb_port_id / end_pcb_port_id); traces that end on another trace are reported and skipped.
"""
import json, sys, hashlib, math
routed_path, unrouted_path = sys.argv[1], sys.argv[2]
router = sys.argv[sys.argv.index('--router') + 1] if '--router' in sys.argv else 'tscircuit capacity autorouter'
raw = open(routed_path, 'rb').read()
d = json.loads(raw)
u = json.load(open(unrouted_path))
def sig(t):
r = t['route']
return (len(r), round(r[0]['x'], 3), round(r[0]['y'], 3), round(r[-1]['x'], 3), round(r[-1]['y'], 3))
fixed = {sig(t) for t in u if t['type'] == 'pcb_trace'}
src_comp = {c['source_component_id']: c['name'] for c in d if c['type'] == 'source_component'}
pcb_comp = {c['pcb_component_id']: c for c in d if c['type'] == 'pcb_component'}
sports = {p['source_port_id']: p for p in d if p['type'] == 'source_port'}
pports = {p['pcb_port_id']: p for p in d if p['type'] == 'pcb_port'}
nets = {n['source_net_id']: n['name'] for n in d if n['type'] == 'source_net'}
key_name = {}
for t in d:
if t['type'] == 'source_trace':
names = [nets[n] for n in t.get('connected_source_net_ids', []) if n in nets]
for pid in t.get('connected_source_port_ids', []):
k = sports[pid].get('subcircuit_connectivity_map_key')
if names and k and k not in key_name: key_name[k] = names[0]
def ref(pcb_port_id):
pp = pports[pcb_port_id]; sp = sports[pp['source_port_id']]
comp = src_comp[sp['source_component_id']]
name = sp.get('name') or ''
hints = sp.get('port_hints') or []
# prefer the unique label; fall back to pinN for pins that only have numeric names
label = name if name and not name.startswith('pin') else next((h for h in hints if h.startswith('pin')), name)
return comp, f"{comp}.{label}", sp.get('subcircuit_connectivity_map_key')
legs, skipped = [], []
for t in d:
if t['type'] != 'pcb_trace' or sig(t) in fixed: continue
r = t['route']
a, b = r[0].get('start_pcb_port_id'), r[-1].get('end_pcb_port_id')
if not a or not b:
skipped.append(t['pcb_trace_id']); continue
comp_a, ref_a, key = ref(a)
_, ref_b, _ = ref(b)
c = next(pc for pc in pcb_comp.values() if src_comp[pc['source_component_id']] == comp_a)
path = []
for p in r[1:-1]:
if p['route_type'] == 'via':
path.append({'x': round(p['x'], 4), 'y': round(p['y'], 4), 'via': True, 'toLayer': p['to_layer']})
else:
path.append({'x': round(p['x'], 4), 'y': round(p['y'], 4)})
width = max((p.get('width') or 0.15) for p in r if p['route_type'] == 'wire')
legs.append({'id': f'ar_{len(legs)}_{ref_a.replace(".", "_")}', 'net': key_name.get(key, key), 'from': ref_a, 'to': ref_b,
'width': round(width, 3), 'fromCentre': {'x': c['center']['x'], 'y': c['center']['y']},
'fromRotation': c.get('rotation') or 0, 'path': path})
out = {'source': hashlib.sha256(raw).hexdigest(), 'router': router, 'legs': legs}
json.dump(out, open('routing/index-routes.json', 'w'), indent=1)
print(f'froze {len(legs)} legs ({sum(1 for l in legs for p in l["path"] if p.get("via"))} vias); skipped {len(skipped)} traces without land endpoints: {skipped[:8]}')