#!/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)