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.')