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/solve-sch-boxes.py

#!/usr/bin/env python3
"""Size every chip's schematic box so `tsci check schematic-placement` has nothing to say about it.

Re-implements the three box rules of @tscircuit/circuit-json-schematic-placement-analysis on the built circuit JSON:
  - SchematicPinPaddingToEdgeTooLarge: on a side with >= 2 pins the outer pins sit at most one pin pitch from the
    box corners (label-aware allowance when the chip has pins on all four sides);
  - SchematicBoxInnerLabelCollision: pin-name labels (0.095 per char, 0.15 high, 0.1 in from the edge; top/bottom
    labels run vertically) of different sides must not overlap (0.02 padding);
  - GenericSchematicBoxTooWide: at most 1.0 of empty space between the left and right label columns (or beside a
    single column of >= 4 labels).
Pins are centred along each side at the chip's pin pitch whatever the box size, so only W and H are free. The
smallest box that passes is printed per chip; `--json` writes {name: [W, H]} for the part files.
Usage: python3 scripts/solve-sch-boxes.py [dist/index/circuit.json] [--json out]
"""
import json, sys

PAD_EDGE, TEXT_H, CH_NAME, CH_OTHER, COLL_PAD, EPS = 0.1, 0.15, 0.095, 0.13, 0.02, 1e-9
_out = sys.argv[sys.argv.index('--json') + 1] if '--json' in sys.argv else None
path = next((a for a in sys.argv[1:] if a.endswith('.json') and a != _out), 'dist/index/circuit.json')
d = json.load(open(path))
src = {c['source_component_id']: c for c in d if c['type'] == 'source_component'}
sp = {p['source_port_id']: p for p in d if p['type'] == 'source_port'}
ports = {}
for p in d:
    if p['type'] == 'schematic_port' and p.get('schematic_component_id') and p.get('side_of_component') in ('left', 'right', 'top', 'bottom'):
        ports.setdefault(p['schematic_component_id'], []).append(p)

def label_len(p):
    lab = p.get('display_pin_label')
    if not lab: return None
    s = sp.get(p.get('source_port_id'), {})
    name_like = lab == s.get('name') or lab == str(s.get('pin_number')) or lab in (s.get('port_hints') or [])
    return len(lab) * (CH_NAME if name_like else CH_OTHER)

def check(W, H, pins, spacing):
    """pins: [(side, along, labelLen|None)] -> list of failed rule names"""
    fails, by = [], {}
    for side, a, L in pins: by.setdefault(side, []).append((a, L))
    maxlab = {s: max([L for _, L in by.get(s, []) if L] or [0]) for s in ('left', 'right', 'top', 'bottom')}
    all4 = all(s in by for s in ('left', 'right', 'top', 'bottom'))
    for side, lst in by.items():
        ext = W if side in ('top', 'bottom') else H
        amax, amin = max(a for a, _ in lst), min(a for a, _ in lst)
        if amax > ext / 2 - 0.05 or amin < -ext / 2 + 0.05: fails.append('fit')
        if len(lst) < 2: continue
        perp = ('left', 'right') if side in ('top', 'bottom') else ('top', 'bottom')
        allow = (maxlab[perp[0]] + maxlab[perp[1]] + spacing) / 2 if all4 else spacing
        if (ext / 2 - amax) - allow > EPS or (amin + ext / 2) - allow > EPS: fails.append('padding')
    rects = []
    for side, a, L in pins:
        if not L: continue
        if side == 'left': rects.append((side, -W / 2 + PAD_EDGE, -W / 2 + PAD_EDGE + L, a - TEXT_H / 2, a + TEXT_H / 2))
        elif side == 'right': rects.append((side, W / 2 - PAD_EDGE - L, W / 2 - PAD_EDGE, a - TEXT_H / 2, a + TEXT_H / 2))
        elif side == 'top': rects.append((side, a - TEXT_H / 2, a + TEXT_H / 2, H / 2 - PAD_EDGE - L, H / 2 - PAD_EDGE))
        else: rects.append((side, a - TEXT_H / 2, a + TEXT_H / 2, -H / 2 + PAD_EDGE, -H / 2 + PAD_EDGE + L))
    for i in range(len(rects)):
        for j in range(i + 1, len(rects)):
            a, b = rects[i], rects[j]
            if a[0] == b[0]: continue
            if min(a[2], b[2]) - max(a[1], b[1]) + COLL_PAD > EPS and min(a[4], b[4]) - max(a[3], b[3]) + COLL_PAD > EPS:
                fails.append('labels'); break
    lcol = [L for s, a, L in pins if s == 'left' and L]; rcol = [L for s, a, L in pins if s == 'right' and L]
    gap = None
    if lcol and rcol: gap = (W / 2 - PAD_EDGE - max(rcol)) - (-W / 2 + PAD_EDGE + max(lcol))
    elif len(lcol) >= 4: gap = W / 2 - (-W / 2 + PAD_EDGE + max(lcol))
    elif len(rcol) >= 4: gap = (W / 2 - PAD_EDGE - max(rcol)) + W / 2
    if gap is not None and gap - 1 > EPS: fails.append('too-wide')
    # not a tsci rule, just legibility: every pin label fits inside the box
    for side, a, L in pins:
        if L and PAD_EDGE + L > (W if side in ('left', 'right') else H) - 0.05: fails.append('label-fit'); break
    return sorted(set(fails))

out, infeasible = {}, []
for c in d:
    if c['type'] != 'schematic_component' or c['schematic_component_id'] not in ports: continue
    s = src[c['source_component_id']]
    if s.get('ftype') not in (None, 'simple_chip', 'simple_connector', 'simple_push_button', 'simple_crystal'): continue
    cx, cy = c['center']['x'], c['center']['y']
    pins = []
    for p in ports[c['schematic_component_id']]:
        side = p['side_of_component']
        a = (p['center']['y'] - cy) if side in ('left', 'right') else (p['center']['x'] - cx)
        pins.append((side, round(a, 4), label_len(p)))
    spacing = c.get('pin_spacing') or 0.2
    W0, H0 = c['size']['width'], c['size']['height']
    now = check(W0, H0, pins, spacing)
    best = None
    for wi in range(4, 241):
        W = wi * 0.05
        for hi in range(4, 301):
            H = hi * 0.05
            if not check(W, H, pins, spacing):
                if best is None or W * H < best[0] - 1e-9: best = (W * H, round(W, 2), round(H, 2))
                break
    name = s['name']
    if best: out[name] = [best[1], best[2]]
    else: infeasible.append(name)
    print(f"{name:5} now {W0:5.2f} x {H0:5.2f} {','.join(now) or 'ok':22} -> {('%.2f x %.2f' % (best[1], best[2])) if best else 'NO SIZE WORKS (change pin sides)'}")
if '--json' in sys.argv: json.dump(out, open(sys.argv[sys.argv.index('--json') + 1], 'w'), indent=1)
if infeasible: print('infeasible:', infeasible)