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]}')