init
This commit is contained in:
349
converter/main.py
Executable file
349
converter/main.py
Executable file
@@ -0,0 +1,349 @@
|
||||
#!/bin/python
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
|
||||
from pycbnew.primitives.project import KProject
|
||||
from pycbnew.primitives.footprint import KFootprint, KFootprintProperty
|
||||
from pycbnew.primitives.net import KNet
|
||||
from pycbnew.utils.geometry import Point
|
||||
from pycbnew.composites.gr_line_path import KGrLinePath
|
||||
from pycbnew.primitives.gr_elements import (KGrRect, KGrPoly, Stroke, KGrText, Font, KGrDimension, DimensionFormat,
|
||||
DimensionStyle, TextJustify, KGrLine, KTable)
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
# Constants / Global Variables set through argparse
|
||||
SCALE = 1.0
|
||||
TEXT_SPACING_MM = 5
|
||||
NO_POWER_NETS = False
|
||||
PCB_FILE = "trainmap.kicad_pcb"
|
||||
LOG_LEVEL=logging.INFO
|
||||
INPUT_XML = ""
|
||||
MC_POS = Point(0,0)
|
||||
TITLE_FONTFACE = ""
|
||||
FONTFACE = ""
|
||||
MARGIN = {'left': 10, 'top': 5 }
|
||||
|
||||
# Fonts
|
||||
LINE_FONT = Font(size=(16, 16))
|
||||
LINE_FONT.face = FONTFACE
|
||||
TITLE_FONT = Font(size=(28,28))
|
||||
TITLE_FONT.face = TITLE_FONTFACE
|
||||
|
||||
|
||||
# Footprints
|
||||
KFootprint.set_footprint_folders([f"{os.path.dirname(__file__)}/kicad_libs"])
|
||||
FOOTPRINT_C = ""
|
||||
FOOTPRINT_LED = "Dialight_587_1032_147F:587"
|
||||
FOOTPRINT_MC = "DFR0654:MODULE_DFR0654"
|
||||
|
||||
# Mappings
|
||||
TEXT_POS_MAPPING = {'above': Point(0, 2*TEXT_SPACING_MM), 'right': Point(TEXT_SPACING_MM, 0), 'below': Point(0, -2*TEXT_SPACING_MM), 'left': Point(-TEXT_SPACING_MM, 0)}
|
||||
MC_PADS = { 'GND': 30, 'GND1': 4, 'VCC': 32, 'gpios': [4, 12, 13, 14, 15, 18, 19, 21, 22, 23, 25, 26, 34, 35, 2]}
|
||||
LED_PADS = { 'GND': 0, 'VCC': 1, 'DI': 2, 'DO': 3}
|
||||
|
||||
# Special Nets
|
||||
NET_GND = KNet(1, "GND")
|
||||
NET_VCC = KNet(2, "5V")
|
||||
|
||||
class Map:
|
||||
title: str
|
||||
lines: list
|
||||
width: int
|
||||
height: int
|
||||
|
||||
def __init__(self, title):
|
||||
self.title = title
|
||||
self.lines = []
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
|
||||
def add(self, line):
|
||||
self.lines.append(line)
|
||||
|
||||
class Line:
|
||||
name: str
|
||||
color: str
|
||||
stations: list
|
||||
|
||||
def __init__(self, name, color):
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.stations = []
|
||||
|
||||
def __str__(self):
|
||||
str = self.name
|
||||
if self.color != None:
|
||||
str += f" (color: {self.color})"
|
||||
return str
|
||||
|
||||
def add(self, station):
|
||||
self.stations.append(station)
|
||||
|
||||
|
||||
class Station:
|
||||
id: int
|
||||
name: str
|
||||
pos: Point
|
||||
angle: int
|
||||
text_pos: Point
|
||||
|
||||
def __init__(self, id, name, x, y, angle = 0, text_pos = 'right'):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.pos = Point(x * SCALE, y * SCALE)
|
||||
self.text_pos = text_pos
|
||||
self.angle = angle
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"station: {self.name}, id: {self.id}, pos: ({str(self.pos.x)},{str(self.pos.y)}, angle: {self.angle} text_pos: {self.text_pos})"
|
||||
|
||||
|
||||
"""
|
||||
parses the xml with the map
|
||||
|
||||
syntax:
|
||||
<map>
|
||||
<line name="%line_name%">
|
||||
<station x="%x_coord" y="%y_coord">%station_name</station>
|
||||
</line>
|
||||
</map>
|
||||
"""
|
||||
def parse_map(xml_file):
|
||||
root = ET.parse(xml_file)
|
||||
|
||||
title = root.find('title').text
|
||||
map = Map(title)
|
||||
logging.debug(f"map title: {title}")
|
||||
|
||||
width = 0
|
||||
height = 0
|
||||
for line in root.findall('line'):
|
||||
line_name = line.get('name')
|
||||
try:
|
||||
line_color = line.get('color')
|
||||
except Error:
|
||||
line_color = None
|
||||
|
||||
line_parsed = Line(line_name, line_color)
|
||||
logging.debug(f"Parsing line: {line_parsed}")
|
||||
|
||||
for station in line.findall('station'):
|
||||
id = int(station.get('id'))
|
||||
name = station.text
|
||||
x = int(station.get('x'))
|
||||
# search width
|
||||
if x > width:
|
||||
width = x
|
||||
y = int(station.get('y'))
|
||||
# search height
|
||||
if y > height:
|
||||
height = y
|
||||
text_pos = station.get('textpos')
|
||||
text_angle = int(station.get('angle'))
|
||||
|
||||
station_parsed = Station(id, name, x, y, text_angle, text_pos)
|
||||
logging.debug(station_parsed)
|
||||
|
||||
line_parsed.add(station_parsed)
|
||||
|
||||
map.add(line_parsed)
|
||||
|
||||
map.width = width
|
||||
map.height = height
|
||||
|
||||
logging.debug(f"done parsing xml. pcb width: {map.width}, height: {map.height}")
|
||||
|
||||
return map
|
||||
|
||||
def create_station_footprint(station):
|
||||
fp = KFootprint(FOOTPRINT_LED, at=station.pos, angle=station.angle)
|
||||
|
||||
ref = fp.properties[KFootprintProperty.Type.Reference]
|
||||
ref.value = station.name
|
||||
ref.angle = station.angle
|
||||
ref.at = TEXT_POS_MAPPING[station.text_pos]
|
||||
|
||||
if not NO_POWER_NETS:
|
||||
fp.pads[LED_PADS['GND']].net = NET_GND
|
||||
fp.pads[LED_PADS['VCC']].net = NET_VCC
|
||||
|
||||
return fp
|
||||
|
||||
"""
|
||||
Creates a new net between station 1 out and station 2 in
|
||||
Connects them on the PCB
|
||||
"""
|
||||
def connect_stations(board, station1, station2, fp1, fp2):
|
||||
net = KNet(station1.id+2, f'{station1.name[0:3]}-{station2.name[0:3]}')
|
||||
board.add(net)
|
||||
fp1.pads[LED_PADS['DO']].net = net
|
||||
fp2.pads[LED_PADS['DI']].net = net
|
||||
|
||||
def connect_line(board, mc, line, station_fp):
|
||||
if connect_line.gpio_ptr > len(MC_PADS['gpios']):
|
||||
logging.critical(f"line {line.name} not connected! cannot have more than {len(MC_PADS['gpios'])} lines.")
|
||||
return
|
||||
|
||||
gpio = MC_PADS['gpios'][connect_line.gpio_ptr]
|
||||
logging.debug(f"connecting line {line.name} to gpio {gpio}")
|
||||
|
||||
net = KNet(1000+gpio, f'{line}')
|
||||
board.add(net)
|
||||
mc.pads[gpio].net = net
|
||||
station_fp.pads[LED_PADS['DI']].net = net
|
||||
|
||||
connect_line.gpio_ptr += 1
|
||||
connect_line.gpio_ptr = 0
|
||||
|
||||
def add_mc(board):
|
||||
mc = KFootprint(FOOTPRINT_MC, at=MC_POS)
|
||||
|
||||
if not NO_POWER_NETS:
|
||||
mc.pads[MC_PADS['GND']].net = NET_GND
|
||||
mc.pads[MC_PADS['VCC']].net = NET_VCC
|
||||
|
||||
board.add(mc)
|
||||
return mc
|
||||
|
||||
def add_line_label(board, line, station):
|
||||
|
||||
# get next position
|
||||
text_pos_index = ([*TEXT_POS_MAPPING].index(station.text_pos) + 1) % len(TEXT_POS_MAPPING)
|
||||
text_pos = TEXT_POS_MAPPING[[*TEXT_POS_MAPPING][text_pos_index]]
|
||||
line_pos = Point(station.pos.x + 2*text_pos.x, station.pos.y + 2*text_pos.y)
|
||||
|
||||
text = KGrText(layer="F.SilkS", text=f"{line.name}", at=line_pos, angle=station.angle)
|
||||
text.font = LINE_FONT
|
||||
|
||||
board.add(text)
|
||||
|
||||
def add_title_label(board, map):
|
||||
logging.debug(f"add map title: {map.title}")
|
||||
text = KGrText(layer="F.SilkS", text=f"{map.title}", at=Point(map.width/2, MARGIN['top']))
|
||||
text.font = TITLE_FONT
|
||||
|
||||
board.add(text)
|
||||
|
||||
def generate_pcb(map):
|
||||
board = KProject(PCB_FILE)
|
||||
board.add(NET_GND)
|
||||
board.add(NET_VCC)
|
||||
|
||||
add_title_label(board, map)
|
||||
|
||||
mc = add_mc(board)
|
||||
|
||||
for line in map.lines:
|
||||
prev_station = None
|
||||
prev_led = None
|
||||
for station in line.stations:
|
||||
led = create_station_footprint(station)
|
||||
board.add(led)
|
||||
|
||||
if prev_station != None:
|
||||
connect_stations(board, prev_station, station, prev_led, led)
|
||||
else:
|
||||
logging.debug(f"add line label {line.name} to start of line")
|
||||
add_line_label(board, line, station)
|
||||
connect_line(board, mc, line, led)
|
||||
|
||||
prev_station = station
|
||||
prev_led = led
|
||||
|
||||
logging.debug(f"add line label {line.name} to end of line")
|
||||
add_line_label(board, line, prev_station)
|
||||
|
||||
board.save()
|
||||
return board
|
||||
|
||||
def main():
|
||||
parse_arguments()
|
||||
|
||||
logging.basicConfig(
|
||||
level=LOG_LEVEL,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
stream=sys.stdout
|
||||
)
|
||||
logging.info(f'creating {PCB_FILE} from {INPUT_XML}')
|
||||
|
||||
start_time=time.perf_counter()
|
||||
map = parse_map(INPUT_XML)
|
||||
|
||||
pcb = generate_pcb(map)
|
||||
|
||||
end_time = time.perf_counter()
|
||||
elapsed = end_time - start_time
|
||||
logging.info(f'finished in {elapsed:.4f} seconds')
|
||||
|
||||
def parse_arguments():
|
||||
global SCALE, TEXT_SPACING_MM, NO_POWER_NETS, PCB_FILE, LOG_LEVEL, INPUT_XML
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="train map pcb generator",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
|
||||
# Positional Argument
|
||||
parser.add_argument(
|
||||
"input",
|
||||
help="input xml file"
|
||||
)
|
||||
|
||||
# Optional Arguments
|
||||
parser.add_argument(
|
||||
"--scale",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="scaling of the pcb"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text-space",
|
||||
type=int,
|
||||
default=5,
|
||||
help="spacing of the text from the center point of led (mm)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-power-nets",
|
||||
action="store_true",
|
||||
help="do not connect (easier visual debugging)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
type=str,
|
||||
default="trainmap.kicad_pcb",
|
||||
help="output file. should end in .kicad_pcb"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="count",
|
||||
default=0,
|
||||
help="verbose output (use -v, -vv, etc.)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Mapping to Global Variables
|
||||
INPUT_XML = args.input
|
||||
SCALE = args.scale
|
||||
TEXT_SPACING_MM = args.text_space
|
||||
NO_POWER_NETS = args.no_power_nets
|
||||
PCB_FILE = args.output
|
||||
|
||||
if args.verbose > 0:
|
||||
LOG_LEVEL=logging.DEBUG
|
||||
|
||||
# Basic Validation
|
||||
if not PCB_FILE.endswith('.kicad_pcb'):
|
||||
print(f"Warning: Output file '{PCB_FILE}' does not have .kicad_pcb extension.")
|
||||
|
||||
if args.no_power_nets:
|
||||
print("Mode: Power nets disabled.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user