From a1e06b691fe5b9192585ba68ef32c7e2e9d1a716 Mon Sep 17 00:00:00 2001 From: David Barksdale Date: Fri, 3 Sep 2021 21:16:39 -0500 Subject: For Jake --- pcb.py | 383 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100755 pcb.py diff --git a/pcb.py b/pcb.py new file mode 100755 index 0000000..cb46031 --- /dev/null +++ b/pcb.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 + +from collections import namedtuple +import struct + +# Units +MM_PER_MIL = 0.0254 + +# Layers +TOP_SILK = 1 +COM_PAD = 2 +TOP = 3 +INNER1 = 4 +INNER2 = 5 +BOTTOM = 6 +BOTTOM_SILK = 7 +TOP_PAD = 8 +BOTTOM_PAD = 9 + +# Structs +header_format = struct.Struct('>16H') +entity_format = struct.Struct('>9H2h5H') +string_format = struct.Struct('>4H2B10H') +Entity = namedtuple('Entity', + [None] * 6 + ['type_id', 'y', 'x', 'dy', 'dx'] + [None] * 5, + rename=True) +String = namedtuple('String', + [None, None, 'offset', None, 'orientation', 'mirror', 'id', 'length', + None, 'size_pt', None, 'y1', 'x1', 'y2', 'x2', 'layer'], + rename=True) + +graphic_layers = [ + "NONE", #0 + "F.SilkS", #1 + "Edge.Cuts",#2 common pads (or edge cuts) + "F.Cu", #3 + "In1.Cu", #4 + "In2.Cu", #5 + "B.Cu", #6 + "B.SilkS", #7 + "F.Cu", #8 top pads (used for top mask) + "B.Cu"] #9 bottom pads (used fot bot mask) + +kicad_pcb_header = """(kicad_pcb (version 20171130) (host mccad2kicad 0.0.1) + (layers + (0 F.Cu signal) + (1 In1.Cu signal) + (2 In2.Cu signal) + (31 B.Cu signal) + (34 B.Paste user) + (35 F.Paste user) + (36 B.SilkS user) + (37 F.SilkS user) + (38 B.Mask user) + (39 F.Mask user) + (44 Edge.Cuts user) + )\n""" + + +def write_module(out, name, layer, x, y, other, pads): + if layer == BOTTOM_PAD: + module_layer = 'B.Cu' + else: + module_layer = 'F.Cu' + out.write('(module {name} (layer {layer})\n\t(at {x} {y})\n\t{other}\n'.format(name=name, layer=module_layer, x=x * MM_PER_MIL, y=y * MM_PER_MIL, other=other)) + for pad in pads: + out.write('\t(pad {name} {type} {shape} (at {x} {y}) (size {size_x} {size_y}) (drill {drill}) (layers {layers}))\n'.format(name=pad['name'], type=pad['type'], shape=pad['shape'], x=pad['x'] * MM_PER_MIL, y=pad['y'] * MM_PER_MIL, size_x=pad['size_x'] * MM_PER_MIL, size_y=pad['size_y'] * MM_PER_MIL, drill=pad['drill'] * MM_PER_MIL, layers=pad['layers'])) + out.write(")\n") + + +def make_pad(name, shape, x, y, size_x, size_y, drill, layer): + if drill: + pad_type = 'thru_hole' + else: + pad_type = 'smt' + if layer == COM_PAD: + pad_layers = '*.Cu *.Mask' + elif layer == TOP_PAD: + pad_layers = 'F.Cu F.Mask' + elif layer == BOTTOM_PAD: + pad_layers = 'B.Cu B.Mask' + else: + print('Invalid layer for pad: {}'.format(layer)) + return + return {'name': name, + 'type': pad_type, + 'shape': shape, + 'x': x, + 'y': y, + 'size_x': size_x, + 'size_y': size_y, + 'drill': drill, + 'layers': pad_layers} + + +def write_pad(out, x, y, shape, size_x, size_y, drill, layer): + write_module(out, 'Pad', layer, x, y, + '(attr virtual)', + [make_pad(name='""', + shape=shape, + x=0, + y=0, + size_x=size_x, + size_y=size_y, + drill=drill, + layer=layer)]) + + +def make_hdip_pads(shape, size_x, size_y, drill, layer, pitch, width, rows): + for row in range(rows): + yield make_pad(name=row + 1, + shape=shape, + x=row * pitch, + y=width, + size_x=size_x, + size_y=size_y, + drill=drill, + layer=layer) + for row in range(rows): + yield make_pad(name=rows + row + 1, + shape=shape, + x=(rows - row - 1) * pitch, + y=0, + size_x=size_x, + size_y=size_y, + drill=drill, + layer=layer) + + +def make_vdip_pads(shape, size_x, size_y, drill, layer, pitch, width, rows): + for row in range(rows): + yield make_pad(name=row + 1, + shape=shape, + x=0, + y=row * pitch, + size_x=size_x, + size_y=size_y, + drill=drill, + layer=layer) + for row in range(rows): + yield make_pad(name=rows + row + 1, + shape=shape, + x=width, + y=(rows - row - 1) * pitch, + size_x=size_x, + size_y=size_y, + drill=drill, + layer=layer) + + +def write_gr_line(out, x1, y1, x2, y2, width, layer): + out.write('(gr_line (start {} {}) (end {} {}) (width {}) (layer {}))\n'.format(x1 * MM_PER_MIL, y1 * MM_PER_MIL, x2 * MM_PER_MIL, y2 * MM_PER_MIL, width * MM_PER_MIL, layer)) + + +def write_segment(out, x1, y1, x2, y2, width, layer, net=0): + out.write('(segment (start {} {}) (end {} {}) (width {}) (layer {}) (net {}))\n'.format(x1 * MM_PER_MIL, y1 * MM_PER_MIL, x2 * MM_PER_MIL, y2 * MM_PER_MIL, width * MM_PER_MIL, layer, net)) + + +def write_line(out, x1, y1, x2, y2, width, layer): + if layer == TOP_SILK: + write_gr_line(out, x1, y1, x2, y2, width, 'F.SilkS') + elif layer == COM_PAD: + write_gr_line(out, x1, y1, x2, y2, width, 'Edge.Cuts') + elif layer == TOP: + write_segment(out, x1, y1, x2, y2, width, 'F.Cu') + elif layer == INNER1: + write_segment(out, x1, y1, x2, y2, width, 'In1.Cu') + elif layer == INNER2: + write_segment(out, x1, y1, x2, y2, width, 'In2.Cu') + elif layer == BOTTOM: + write_segment(out, x1, y1, x2, y2, width, 'B.Cu') + elif layer == BOTTOM_SILK: + write_gr_line(out, x1, y1, x2, y2, width, 'B.SilkS') + elif layer == TOP_PAD: + write_segment(out, x1, y1, x2, y2, width, 'F.Cu') + write_gr_line(out, x1, y1, x2, y2, width, 'F.Mask') + elif layer == BOTTOM_PAD: + write_segment(out, x1, y1, x2, y2, width, 'B.Cu') + write_gr_line(out, x1, y1, x2, y2, width, 'B.Mask') + + +def convert(pcb_file, kicad_file): + kicad_file.write(kicad_pcb_header) + header = header_format.unpack(pcb_file.read(header_format.size)) + string_table_len = header[2] + string_count = header[3] + layer_entity_counts = header[4:14] + texts = [] + layer = 1 + layer_entity_count = 0 + while layer <= 9: + if layer_entity_count >= layer_entity_counts[layer]: + layer += 1 + layer_entity_count = 0 + continue + layer_entity_count += 1 + entity = Entity._make( + entity_format.unpack(pcb_file.read(entity_format.size))) + print(entity) + if entity.type_id == 0: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='circle', + size_x=entity[0], + size_y=entity[0], + drill=entity[1], + layer=layer) + elif entity.type_id == 1: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='oval', + size_x=entity[0] * 2, + size_y=entity[0], + drill=entity[1], + layer=layer) + elif entity.type_id == 2: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='oval', + size_x=entity[0], + size_y=entity[0] * 2, + drill=entity[1], + layer=layer) + elif entity.type_id == 3: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='rect', + size_x=entity[0], + size_y=entity[0], + drill=entity[1], + layer=layer) + elif entity.type_id == 4: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='rect', + size_x=entity[0] * 2, + size_y=entity[0], + drill=entity[1], + layer=layer) + elif entity.type_id == 5: + write_pad(kicad_file, + x=entity.x, + y=entity.y, + shape='rect', + size_x=entity[0], + size_y=entity[0] * 2, + drill=entity[1], + layer=layer) + elif entity.type_id == 6: + write_line(kicad_file, + x1=entity.x, + y1=entity.y, + x2=entity.x + entity.dx, + y2=entity.y + entity.dy, + width=entity[2], + layer=layer) + elif entity.type_id == 7: + write_line(kicad_file, + x1=entity.x, + y1=entity.y, + x2=entity.x + entity.dx, + y2=entity.y, + width=entity[2], + layer=layer) + write_line(kicad_file, + x1=entity.x + entity.dx, + y1=entity.y, + x2=entity.x + entity.dx, + y2=entity.y + entity.dy, + width=entity[2], + layer=layer) + write_line(kicad_file, + x1=entity.x + entity.dx, + y1=entity.y + entity.dy, + x2=entity.x, + y2=entity.y + entity.dy, + width=entity[2], + layer=layer) + write_line(kicad_file, + x1=entity.x, + y1=entity.y + entity.dy, + x2=entity.x, + y2=entity.y, + width=entity[2], + layer=layer) + elif entity.type_id == 8: + write_module(kicad_file, 'V-DIP', layer, entity.x, entity.y, '', + make_vdip_pads(shape='rect', + size_x=entity[2] * 2, + size_y=entity[2], + drill=entity[0], + layer=layer, + pitch=100, + width=entity[1], + rows=entity[3])) + elif entity.type_id == 9: + write_module(kicad_file, 'H-DIP', layer, entity.x, entity.y, '', + make_hdip_pads(shape='rect', + size_x=entity[2], + size_y=entity[2] * 2, + drill=entity[0], + layer=layer, + pitch=100, + width=entity[1], + rows=entity[3])) + elif entity.type_id == 10: + texts.append({ + 'layer': layer, + 'x': entity.x, + 'y': entity.y}) + elif entity.type_id == 11: + write_module(kicad_file, 'Hole', layer, entity.x, entity.y, + '(attr virtual)', + [{'name': '""', + 'type': 'np_thru_hole', + 'shape': 'circle', + 'x': 0, + 'y': 0, + 'size_x': entity[0], + 'size_y': entity[0], + 'drill': entity[0], + 'layers': '*.Cu *.Mask'}]) + elif entity.type_id == 12: + # TODO + + strings = [] + for i in range(string_count): + strings.append( + String._make( + string_format.unpack(pcb_file.read(string_format.size)))) + string_table = pcb_file.read(string_table_len) + for text, string in zip(texts, reversed(strings)): + s = string_table[string.offset:string.offset + string.length].decode('mac_roman') + # TODO: figure out font sizing + width_mm = string.size_pt * 0.01672082 + height_mm = string.size_pt * 0.01820418 + if string.orientation == 0x00 or string.orientation == 0x81: + rotation = 0 + elif string.orientation == 0x87: + rotation = 90 + elif string.orientation == 0x83: + rotation = 180 + elif string.orientation == 0x85: + rotation = 270 + #TODO: + #elif string.orientation == 0x89 + # letters vertically going top to bottom + else: + rotation = 0 + print('Text Orientation 0x{:02X} not supported!'.format(string.orientation)) + if string.mirror == 0: + justify = '' + elif string.mirror == 2: + justify = 'mirror' + else: + justify = '' + print('Text Mirror 0x{:02X} not supported!'.format(string.mirror)) + # rotation seems to be about the center so we'll center justify and use + # the middle of the selection box as the position + x = (string.x1 + string.x2) / 2.0 + y = (string.y1 + string.y2) / 2.0 + kicad_file.write('(gr_text "{}" (at {} {} {}) (layer {})\n\t(effects (font (size {} {}) (thickness {})) (justify {}))\n)\n'.format(s.replace('"', '\\"'), x * MM_PER_MIL, y * MM_PER_MIL, rotation, graphic_layers[text['layer']], height_mm, width_mm, 20 * MM_PER_MIL, justify)) + + kicad_file.write(")\n") + + + +import argparse + +parser = argparse.ArgumentParser( + description='A converter from McCad PCB-1 to KiCad.') +parser.add_argument('file', nargs='+') +args = parser.parse_args() + +for file_arg in args.file: + with open(file_arg, 'rb') as pcb_file: + with open(file_arg + '.kicad_pcb', 'w') as kicad_file: + convert(pcb_file, kicad_file) -- cgit v1.2.3-18-g5258