summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Barksdale <amatus@amat.us>2021-09-03 21:16:39 -0500
committerDavid Barksdale <amatus@amat.us>2021-09-03 21:16:39 -0500
commita1e06b691fe5b9192585ba68ef32c7e2e9d1a716 (patch)
treeb58494f64a493586113ef1e030337dc3d273a570
For Jake
-rwxr-xr-xpcb.py383
1 files changed, 383 insertions, 0 deletions
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)