hrithik18k/open-ring-flex

A flexible wearable PCB integrating inductive battery charging, BLE control with SPI flash and 32 MHz crystal, SAM L10 capacitive touch/wear sensing, haptic MOSFET drive with a large capacitor bank, RF antenna matching, and SWD programming/test connectors.

Version
1.0.4
License
CC-BY-NC-SA-4.0
Stars
0

scripts/audit-schematic-revision.py

"""Compare a schematic revision with a prior circuit without relying on generated IDs."""
import collections
import json
import sys
from pathlib import Path

def physical_records(circuit, *, allow_label_movement=False):
    def geometry(value):
        if isinstance(value, list):
            return [geometry(item) for item in value]
        if isinstance(value, dict):
            result = {key: geometry(item) for key, item in value.items()
                    if not key.endswith('_id') and key not in {
                        'subcircuit_connectivity_map_key', 'connection_name', 'connectsTo'
                    }}
            if 'from_layer' in result and 'to_layer' in result:
                result['from_layer'], result['to_layer'] = sorted(
                    [result['from_layer'], result['to_layer']])
            if 'layers' in result:
                result['layers'] = sorted(result['layers'])
            if allow_label_movement and result.get('type') == 'pcb_silkscreen_text':
                # Native passive primitives may move refdes labels to avoid
                # overlaps. Still compare text, layer, size, font and rotation.
                result.pop('anchor_position', None)
            if 'route' in result:
                # Copper has no travel direction. Saved paths may be reversed
                # when the connectivity traversal chooses the opposite root.
                result['route'] = min(result['route'], list(reversed(result['route'])),
                                      key=lambda route: json.dumps(route, sort_keys=True))
            return result
        return value
    return collections.Counter(json.dumps(geometry(item), sort_keys=True)
                               for item in circuit if item['type'].startswith('pcb_'))

def connectivity(circuit):
    components = {item['source_component_id']: item['name'] for item in circuit
                  if item['type'] == 'source_component'}
    # Compare physical pins. Native primitives may add semantic aliases such
    # as pos/neg without changing the physical pin or its connected net.
    ports = {item['source_port_id']: (components[item['source_component_id']],
                                    item.get('pin_number') or item['name'])
             for item in circuit if item['type'] == 'source_port'}
    nets = {item['source_net_id']: item['name'] for item in circuit
            if item['type'] == 'source_net'}
    connections = set()
    for item in circuit:
        if item['type'] != 'source_trace':
            continue
        for net in item.get('connected_source_net_ids', []):
            for port in item.get('connected_source_port_ids', []):
                connections.add((nets[net], ports[port]))
    return connections

if __name__ == '__main__':
    before = json.loads(Path(sys.argv[1]).read_text())
    after = json.loads(Path(sys.argv[2]).read_text())
    allow_label_movement = '--allow-silkscreen-label-movement' in sys.argv[3:]
    assert physical_records(before, allow_label_movement=allow_label_movement) == physical_records(after, allow_label_movement=allow_label_movement), 'PCB geometry changed'
    assert connectivity(before) == connectivity(after), 'Electrical connectivity changed'
    if allow_label_movement:
        print('PASS: PCB geometry and physical pin-to-net connections unchanged, except allowed silkscreen label positions.')
    else:
        print('PASS: all PCB geometry and deduplicated pin-to-net connections are unchanged.')