This commit is contained in:
2026-04-14 20:02:23 +02:00
parent b36d2602d4
commit ec97b915ed
39 changed files with 34391 additions and 167324 deletions
+92 -63
View File
@@ -1,4 +1,5 @@
#!/bin/python
import math
import time
import os
import sys
@@ -23,40 +24,37 @@ from pycbnew.primitives.gr_elements import (
KGrLine,
KTable,
)
from pycbnew.primitives.segment import KSegment
import xml.etree.ElementTree as ET
# Constants / Global Variables set through argparse
SCALE = 1.0
TEXT_SPACING_MM = 12
NO_POWER_NETS = False
PCB_FILE = "trainmap.kicad_pcb"
# Global Variables set through argparse
LOG_LEVEL = logging.INFO
NO_POWER_NETS = False
NO_TEXT = False
NO_TRACES = False
SCALE = 1.0
PCB_FILE = "trainmap.kicad_pcb"
INPUT_XML = ""
MC_POS = Point(0, 0)
TITLE_FONTFACE = ""
FONTFACE = ""
MARGIN = {"left": 10, "top": 5}
TRACE_WIDTH_MM=0.5
CAP_DISTANCE_MM=5
TEXT_SPACING_MM=15
# Fonts
LINE_FONT = Font(size=(16, 16))
TITLE_FONTFACE = ""
FONTFACE = ""
LINE_FONT = Font(size=(12, 12))
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 = "Capacitor_SMD:C_0603_1608Metric"
FOOTPRINT_LED = "Dialight_587_1032_147F:587"
FOOTPRINT_MC = "DFR0654:MODULE_DFR0654"
# Mappings
TEXT_POS_MAPPING = {
"above": Vector(0, TEXT_SPACING_MM),
"right": Vector(TEXT_SPACING_MM, 0),
"below": Vector(0, -TEXT_SPACING_MM),
"left": Vector(-TEXT_SPACING_MM, 0),
}
MC_PADS = {
"GND": 30,
"GND1": 4,
@@ -69,7 +67,6 @@ LED_PADS = {"GND": 0, "VCC": 1, "DI": 2, "DO": 3}
NET_GND = KNet(1, "GND")
NET_VCC = KNet(2, "5V")
class Map:
title: str
lines: list
@@ -134,17 +131,20 @@ syntax:
</line>
</map>
"""
def parse_map(xml_file):
root = ET.parse(xml_file)
title = root.find("title").text
title_node = root.find("title")
title=""
if title_node != None:
title=title_node.text
map = Map(title)
logging.debug(f"map title: {title}")
width = 0
height = 0
id=0
for line in root.findall("line"):
line_name = line.get("name")
try:
@@ -156,20 +156,18 @@ def parse_map(xml_file):
logging.debug(f"Parsing line: {line_parsed}")
for station in line.findall("station"):
id = int(station.get("id"))
id=id+1
name = station.text
x = int(station.get("x"))
x = int(float(station.get("x")))
# search width
if x > width:
width = x
y = int(station.get("y"))
y = int(float(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)
station_parsed = Station(id, name, x, y)
logging.debug(station_parsed)
line_parsed.add(station_parsed)
@@ -184,12 +182,13 @@ def parse_map(xml_file):
return map
"""
Creates a capacity between LED's VCC and GND
"""
def create_capacity(station):
cap_pos_index = ([*TEXT_POS_MAPPING].index(station.text_pos) + 2) % len(
TEXT_POS_MAPPING
)
cap_pos = [*TEXT_POS_MAPPING.values()][cap_pos_index]
cap_pos_x = CAP_DISTANCE_MM * math.sin(station.angle)
cap_pos_y = CAP_DISTANCE_MM * math.cos(station.angle)
cap_pos = Vector(cap_pos_x, cap_pos_y)
fp = KFootprint(FOOTPRINT_C, at=station.pos + cap_pos, angle=station.angle)
ref = fp.properties[KFootprintProperty.Type.Reference]
@@ -200,21 +199,26 @@ def create_capacity(station):
return fp
def calculate_angle(station1, station2):
vec = Vector(station2.pos.x, station2.pos.y) - Vector(station1.pos.x, station1.pos.y)
angle_rad = vec.self_angle()
return math.degrees(angle_rad)
"""
Creates the footprint for a station led
adds led, text and C
"""
def create_station_footprint(station):
fp = KFootprint(FOOTPRINT_LED, at=station.pos) # , angle=station.angle)
fp = KFootprint(FOOTPRINT_LED, at=station.pos, angle=station.angle)
# text
ref = fp.properties[KFootprintProperty.Type.Reference]
if NO_TEXT:
ref.layer = 'User.Comments'
ref.value = station.name
ref.angle = station.angle
ref.at = TEXT_POS_MAPPING[station.text_pos]
text_pos_x = TEXT_SPACING_MM * math.cos(station.angle)
text_pos_y = TEXT_SPACING_MM * math.sin(station.angle)
ref.at = Vector(text_pos_x, text_pos_y)
if not NO_POWER_NETS:
fp.pads[LED_PADS["GND"]].net = NET_GND
@@ -227,15 +231,27 @@ def create_station_footprint(station):
Creates a new net between station 1 out pad and station 2 in pad
Connects them on the PCB
"""
def connect_stations(board, station1, station2, fp1, fp2):
net = KNet(station1.id + 2, f"{station1.id}-{station2.id}")
board.add(net)
fp1.pads[LED_PADS["DO"]].net = net
fp2.pads[LED_PADS["DI"]].net = net
do = fp1.pads[LED_PADS["DO"]]
di = fp2.pads[LED_PADS["DI"]]
# connect the net
do.net = net
di.net = net
if NO_TRACES:
return
# do the trace
trace = KSegment(net=net, layer="F.Cu", point1=do.absolute_at, point2=di.absolute_at, width=TRACE_WIDTH_MM)
board.add(trace)
"""
Connects the line to the MC
"""
def connect_line(board, mc, line, station_fp):
if connect_line.gpio_ptr > len(MC_PADS["gpios"]):
logging.critical(
@@ -253,16 +269,12 @@ def connect_line(board, mc, line, station_fp):
station_fp.pads[LED_PADS["DI"]].net = net
connect_line.gpio_ptr += 1
connect_line.gpio_ptr = 0
"""
Adds the main microcontroller
"""
def add_mc(board):
mc = KFootprint(FOOTPRINT_MC, at=MC_POS)
@@ -278,22 +290,15 @@ def add_mc(board):
adds labels for the lines to the next position in the TEXT_POS_MAPPING list
from the position of the text label of the station (just a best estimate)
"""
def add_line_label(board, line, station):
text_pos_x = station.pos.x + TEXT_SPACING_MM * math.sin(-0.25*station.angle)
text_pos_y = station.pos.y + TEXT_SPACING_MM * math.cos(-0.25*station.angle)
# get next position
text_pos_index = ([*TEXT_POS_MAPPING].index(station.text_pos) + 1) % len(
TEXT_POS_MAPPING
)
text_pos = [*TEXT_POS_MAPPING.values()][text_pos_index]
line_pos = Point(station.pos.x + 2 * text_pos.x,
station.pos.y + 2 * text_pos.y)
text_pos = Vector(text_pos_x, text_pos_y)
text = KGrText(
layer="F.SilkS", text=f"{line.name}", at=line_pos, angle=station.angle
layer="F.SilkS", text=f"{line.name}", at=text_pos
)
text.font = LINE_FONT
#text.font = LINE_FONT
board.add(text)
@@ -301,9 +306,10 @@ def add_line_label(board, line, station):
"""
adds a board title top middle
"""
def add_title_label(board, map):
if NO_TEXT:
return
logging.debug(f"add map title: {map.title}")
text = KGrText(
layer="F.SilkS", text=f"{map.title}", at=Point(map.width / 2, MARGIN["top"])
@@ -316,8 +322,6 @@ def add_title_label(board, map):
"""
main pcb generation loop
"""
def generate_pcb(map):
board = KProject(PCB_FILE)
board.add(NET_GND)
@@ -330,7 +334,14 @@ def generate_pcb(map):
for line in map.lines:
prev_station = None
prev_led = None
if len(line.stations) == 0:
continue
for station in line.stations:
if prev_station is not None:
station.angle = calculate_angle(prev_station, station)
else:
station.angle = calculate_angle(station, line.stations[1])
logging.debug(f"angle between stations: {station.angle}")
led = create_station_footprint(station)
board.add(led)
cap = create_capacity(station)
@@ -374,7 +385,7 @@ def main():
def parse_arguments():
global SCALE, TEXT_SPACING_MM, NO_POWER_NETS, PCB_FILE, LOG_LEVEL, INPUT_XML
global SCALE, TEXT_SPACING_MM, NO_TEXT, NO_POWER_NETS, NO_TRACES, PCB_FILE, LOG_LEVEL, INPUT_XML
parser = argparse.ArgumentParser(
description="train map pcb generator",
@@ -393,10 +404,20 @@ def parse_arguments():
default=5,
help="spacing of the text from the center point of led (mm)",
)
parser.add_argument(
"--no-text",
action="store_true",
help="do not produce any text",
)
parser.add_argument(
"--no-power-nets",
action="store_true",
help="do not connect (easier visual debugging)",
help="do not connect GND and VCC (easier visual debugging)",
)
parser.add_argument(
"--no-traces",
action="store_true",
help="do not implement traces between leds",
)
parser.add_argument(
"--output",
@@ -418,7 +439,9 @@ def parse_arguments():
INPUT_XML = args.input
SCALE = args.scale
TEXT_SPACING_MM = args.text_space
NO_TEXT = args.no_text
NO_POWER_NETS = args.no_power_nets
NO_TRACES = args.no_traces
PCB_FILE = args.output
if args.verbose > 0:
@@ -429,9 +452,15 @@ def parse_arguments():
print(f"Warning: Output file '{
PCB_FILE}' does not have .kicad_pcb extension.")
if NO_TEXT:
print("Mode: No text")
if args.no_power_nets:
print("Mode: Power nets disabled.")
if NO_TRACES:
print("Mode: No traces")
if __name__ == "__main__":
main()