This commit is contained in:
2026-02-01 22:36:56 +01:00
parent 81999207c2
commit 7a3b6b273b
20 changed files with 321221 additions and 0 deletions

View File

@@ -0,0 +1 @@
3.12

0
converter/README.md Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
(footprint MODULE_DFR0654 (layer F.Cu) (tedit 69760828)
(descr "")
(fp_text reference REF** (at -9.775 -32.65 0) (layer F.SilkS)
(effects (font (size 1.0 1.0) (thickness 0.15)))
)
(fp_text value MODULE_DFR0654 (at -4.06 31.377 0) (layer F.Fab)
(effects (font (size 1.0 1.0) (thickness 0.15)))
)
(pad 1 thru_hole rect (at -11.25 -18.68) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 2 thru_hole circle (at -11.25 -16.14) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 3 thru_hole circle (at -11.25 -13.6) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 4 thru_hole circle (at -11.25 -11.06) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 5 thru_hole circle (at -11.25 -8.52) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 6 thru_hole circle (at -11.25 -5.98) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 7 thru_hole circle (at -11.25 -3.44) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 8 thru_hole circle (at -11.25 -0.9) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 9 thru_hole circle (at -11.25 1.64) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 10 thru_hole circle (at -11.25 4.18) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 11 thru_hole circle (at -11.25 6.72) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 12 thru_hole circle (at -11.25 9.26) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 13 thru_hole circle (at -11.25 11.8) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 14 thru_hole circle (at -11.25 14.34) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 15 thru_hole circle (at -11.25 16.88) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 16 thru_hole circle (at -11.25 19.42) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 17 thru_hole circle (at -11.25 21.96) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 18 thru_hole circle (at -11.25 24.5) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 19 thru_hole circle (at 11.25 24.5) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 20 thru_hole circle (at 11.25 21.96) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 21 thru_hole circle (at 11.25 19.42) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 22 thru_hole circle (at 11.25 16.88) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 23 thru_hole circle (at 11.25 14.34) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 24 thru_hole circle (at 11.25 11.8) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 25 thru_hole circle (at 11.25 9.26) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 26 thru_hole circle (at 11.25 6.72) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 27 thru_hole circle (at 11.25 4.18) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 28 thru_hole circle (at 11.25 1.64) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 29 thru_hole circle (at 11.25 -0.9) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 30 thru_hole circle (at 11.25 -3.44) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 31 thru_hole circle (at 11.25 -5.98) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad 32 thru_hole circle (at 11.25 -8.52) (size 1.8 1.8) (drill 1.2) (layers *.Cu *.Mask) (solder_mask_margin 0.102))
(pad None np_thru_hole circle (at 11.1 -28.4) (size 2.0 2.0) (drill 2.0) (layers *.Cu *.Mask))
(pad None np_thru_hole circle (at 11.1 28.4) (size 2.0 2.0) (drill 2.0) (layers *.Cu *.Mask))
(pad None np_thru_hole circle (at -11.1 -28.4) (size 2.0 2.0) (drill 2.0) (layers *.Cu *.Mask))
(pad None np_thru_hole circle (at -11.1 28.4) (size 2.0 2.0) (drill 2.0) (layers *.Cu *.Mask))
(fp_line (start 12.7 -30.0) (end 12.7 30.0) (layer F.Fab) (width 0.127))
(fp_line (start -12.7 -30.0) (end -12.7 30.0) (layer F.Fab) (width 0.127))
(fp_line (start 12.7 -30.0) (end 4.3 -30.0) (layer F.Fab) (width 0.127))
(fp_line (start -4.3 -30.0) (end -12.7 -30.0) (layer F.Fab) (width 0.127))
(fp_line (start 12.7 30.0) (end -12.7 30.0) (layer F.Fab) (width 0.127))
(fp_line (start -4.3 -31.4) (end -4.3 -30.0) (layer F.Fab) (width 0.127))
(fp_line (start 4.3 -31.4) (end -4.3 -31.4) (layer F.Fab) (width 0.127))
(fp_line (start 4.3 -30.0) (end 4.3 -31.4) (layer F.Fab) (width 0.127))
(fp_line (start 12.7 -30.0) (end 12.7 30.0) (layer F.SilkS) (width 0.127))
(fp_line (start -12.7 -30.0) (end -12.7 30.0) (layer F.SilkS) (width 0.127))
(fp_line (start 12.7 -30.0) (end -12.7 -30.0) (layer F.SilkS) (width 0.127))
(fp_line (start 12.7 30.0) (end -12.7 30.0) (layer F.SilkS) (width 0.127))
(fp_line (start -12.95 -31.65) (end -12.95 30.25) (layer F.CrtYd) (width 0.05))
(fp_line (start -12.95 30.25) (end 12.95 30.25) (layer F.CrtYd) (width 0.05))
(fp_line (start 12.95 30.25) (end 12.95 -31.65) (layer F.CrtYd) (width 0.05))
(fp_line (start 12.95 -31.65) (end -12.95 -31.65) (layer F.CrtYd) (width 0.05))
(fp_circle (center -13.45 -18.68) (end -13.35 -18.68) (layer F.SilkS) (width 0.2))
(fp_circle (center -13.45 -18.68) (end -13.35 -18.68) (layer F.Fab) (width 0.2))
(fp_line (start 4.3 -30.0) (end -4.3 -30.0) (layer F.Fab) (width 0.127))
)

View File

@@ -0,0 +1,210 @@
(footprint "587"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "3.2mm x 2.8mm PLCC4 LED, https://www.we-online.de/katalog/datasheet/150141M173100.pdf")
(tags "LED RGB Wurth PLCC-4 3528")
(property "Reference" "REF**"
(at 0 -2.4 0)
(layer "F.SilkS")
(uuid "3c948eb1-265d-4654-903a-4a44643de9ba")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "587"
(at 0 2.5 0)
(layer "F.Fab")
(uuid "f6b61937-d358-4198-bb6b-02671db7a36e")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "95496867-2b79-4152-b2ea-2787a18c9cc6")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "40ac429a-65a7-4742-b1e7-85f3d47ae21c")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr smd)
(fp_line
(start -1.6 -0.5)
(end 1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "3e7274b6-09c0-4af9-9845-52943e2ef5ed")
)
(fp_line
(start -1.6 0.5)
(end -1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "423a08b3-d967-4750-8c5b-c8d64eeb549f")
)
(fp_line
(start -1.6 0.5)
(end 1.6 0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "43bc7bba-a707-417b-86e5-2c08e0cea9ec")
)
(fp_line
(start 1.6 0.5)
(end 1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "89fc843b-fd33-42b3-aa2a-04389df2fec8")
)
(fp_line
(start -1.6 -0.5)
(end 1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.Fab")
(uuid "ea9f2f47-40a4-4595-864d-578365411f8c")
)
(fp_line
(start -1.6 0.5)
(end -1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.Fab")
(uuid "82043013-6a89-4d36-88f8-89591a732cde")
)
(fp_line
(start -1.6 0.5)
(end 1.6 0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.Fab")
(uuid "5cc8693c-f30b-4abf-a8f4-5515bc405145")
)
(fp_line
(start 1.6 0.5)
(end 1.6 -0.5)
(stroke
(width 0.12)
(type solid)
)
(layer "F.Fab")
(uuid "ea173636-364d-4718-b57c-ad33e0eafac6")
)
(fp_circle
(center 0 0)
(end 0.5 0)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "09117c82-c48f-4a03-8010-4b1c295a49b6")
)
(fp_text user "1"
(at 3.225 -2.65 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "cf44cb37-31f4-4334-a585-6b50a5d1f81d")
(effects
(font
(size 0.8 0.8)
(thickness 0.15)
)
)
)
(fp_text user "${REFERENCE}"
(at -0.025 0.025 0)
(layer "F.Fab")
(uuid "43bb5346-6fc8-4094-a012-ef39e85900b7")
(effects
(font
(size 0.5 0.5)
(thickness 0.075)
)
)
)
(pad "1" smd roundrect
(at 1.7 -0.575)
(size 1.5 0.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0)
(chamfer_ratio 0.2)
(chamfer top_left)
(uuid "f81fee48-f526-4151-8a38-6a0fa9dc420f")
)
(pad "2" smd rect
(at -1.675 -0.575)
(size 1.5 0.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "201e05f2-1b6c-4282-a81c-b348fa1b24d9")
)
(pad "3" smd rect
(at -1.7 0.575)
(size 1.5 0.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "d29435d9-98f3-471c-8cd1-ef8fab9f47d3")
)
(pad "4" smd rect
(at 1.7 0.575)
(size 1.5 0.9)
(layers "F.Cu" "F.Mask" "F.Paste")
(uuid "29fdca2f-a56f-4090-bc02-97bf259b3e0b")
)
(embedded_fonts no)
(model "${KICAD9_3DMODEL_DIR}/LED_SMD.3dshapes/LED_RGB_Wuerth-PLCC4_3.2x2.8mm_150141M173100.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)

349
converter/main.py Executable file
View 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()

184
converter/munic-s.xml Normal file
View File

@@ -0,0 +1,184 @@
<map>
<title>Munich S</title>
<line name="Stammstrecke" color="None">
<station id="1" x="345" y="525" angle="0" textpos="below" lines="[S3,S4,S6,S8,S20]">Pasing</station>
<station id="2" x="385" y="525" angle="30" textpos="above" lines="[S1,S2,S3,S4,S6,S8]">Laim</station>
<station id="3" x="420" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8]">Hirschgarten</station>
<station id="4" x="455" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7,S20]">Donnersbergerbrücke</station>
<station id="5" x="490" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8]">Hackerbrücke</station>
<station id="6" x="530" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Hauptbahnhof</station>
<station id="7" x="565" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Karlsplatz (Stachus)</station>
<station id="8" x="600" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Marienplatz</station>
<station id="9" x="635" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Isartor</station>
<station id="10" x="670" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Rosenheimer Platz</station>
<station id="11" x="710" y="525" angle="90" textpos="above" lines="[S1,S2,S3,S4,S6,S8,S7]">Ostbahnhof</station>
<station id="12" x="750" y="525" angle="90" textpos="above" lines="[S1,S2,S4,S6,S8]">Leuchtenbergring</station>
<station id="13" x="785" y="535" angle="90" textpos="below" lines="[S2,S4,S6]">Berg am Laim</station>
</line>
<line name="S1" color="cyan">
<station id="14" x="730" y="110" angle="0" textpos="left">Freising</station>
<station id="15" x="730" y="140" angle="0" textpos="left">Pulling</station>
<station id="16" x="690" y="230" angle="0" textpos="above">Neufahrn</station>
<station id="17" x="630" y="230" angle="0" textpos="above">Eching</station>
<station id="18" x="570" y="230" angle="0" textpos="above">Lohhof</station>
<station id="19" x="520" y="230" angle="0" textpos="above">Unterschleißheim</station>
<station id="20" x="460" y="230" angle="0" textpos="above">Oberschleißheim</station>
<station id="21" x="415" y="265" angle="0" textpos="right" lines="[S1,U2]">Feldmoching</station>
<station id="22" x="385" y="315" angle="0" textpos="right">Fasanerie</station>
<station id="23" x="355" y="365" angle="0" textpos="right" lines="[S1,U3]">Moosach</station>
<station id="24" x="795" y="170" angle="0" textpos="right">Flughafen München</station>
<station id="25" x="775" y="200" angle="0" textpos="right">Flughafen Besucherpark</station>
</line>
<line name="S2" color="lime">
<station id="26" x="120" y="70" angle="0" textpos="above">Altomünster</station>
<station id="27" x="145" y="70" angle="0" textpos="above">Kleinberghofen</station>
<station id="28" x="170" y="70" angle="0" textpos="above">Erdweg</station>
<station id="29" x="195" y="90" angle="0" textpos="left">Arnbach</station>
<station id="30" x="210" y="105" angle="0" textpos="left">Markt Indersdorf</station>
<station id="31" x="225" y="130" angle="0" textpos="left">Niederroth</station>
<station id="32" x="240" y="155" angle="0" textpos="left">Schwabhausen</station>
<station id="33" x="255" y="180" angle="0" textpos="left">Bachern</station>
<station id="34" x="270" y="205" angle="0" textpos="left">Dachau Stadt</station>
<station id="35" x="290" y="55" angle="0" textpos="right">Petershausen</station>
<station id="36" x="290" y="90" angle="0" textpos="right">Vierkirchen-Esterhofen</station>
<station id="37" x="290" y="135" angle="0" textpos="right">Röhrmoos</station>
<station id="38" x="290" y="175" angle="0" textpos="right">Hebertshausen</station>
<station id="39" x="290" y="250" angle="0" textpos="right">Dachau</station>
<station id="40" x="290" y="290" angle="0" textpos="right">Karlsfeld</station>
<station id="41" x="290" y="330" angle="0" textpos="right">Allach</station>
<station id="42" x="290" y="370" angle="0" textpos="right">Untermenzing</station>
<station id="43" x="290" y="410" angle="0" textpos="right">Obermenzing</station>
<station id="44" x="870" y="165" angle="0" textpos="right">Erding</station>
<station id="45" x="870" y="200" angle="0" textpos="right">Altenerding</station>
<station id="46" x="870" y="235" angle="0" textpos="right">Aufhausen</station>
<station id="47" x="870" y="270" angle="0" textpos="right">St. Koloman</station>
<station id="48" x="870" y="305" angle="0" textpos="right">Ottenhofen</station>
<station id="49" x="860" y="345" angle="45" textpos="right">Markt Schwaben</station>
<station id="50" x="850" y="365" angle="45" textpos="right">Poing</station>
<station id="51" x="840" y="385" angle="45" textpos="right">Grub</station>
<station id="52" x="830" y="405" angle="45" textpos="right">Heimstetten</station>
<station id="53" x="820" y="425" angle="45" textpos="right">Feldkirchen</station>
<station id="54" x="810" y="465" angle="45" textpos="right">Riem</station>
</line>
<line name="S3" color="purple">
<station id="55" x="160" y="255" angle="315" textpos="above">Mammendorf</station>
<station id="56" x="175" y="270" angle="315" textpos="above">Malching</station>
<station id="57" x="190" y="285" angle="315" textpos="above">Maisach</station>
<station id="58" x="205" y="300" angle="315" textpos="above">Gernlinden</station>
<station id="59" x="220" y="335" angle="315" textpos="above">Esting</station>
<station id="60" x="235" y="350" angle="315" textpos="above">Olching</station>
<station id="61" x="250" y="380" angle="315" textpos="above">Gröbenzell</station>
<station id="62" x="265" y="415" angle="315" textpos="above">Lochhausen</station>
<station id="63" x="280" y="455" angle="315" textpos="above">Langwied</station>
<station id="64" x="695" y="585" angle="0" textpos="right">St.-Martin-Straße</station>
<station id="65" x="650" y="630" angle="0" textpos="right" lines="[S3,S7,U2]">Giesing</station>
<station id="66" x="640" y="675" angle="0" textpos="left">Fasangarten</station>
<station id="67" x="640" y="710" angle="0" textpos="left">Fasanenpark</station>
<station id="68" x="640" y="745" angle="0" textpos="left">Unterhaching</station>
<station id="69" x="640" y="780" angle="0" textpos="left">Taufkirchen</station>
<station id="70" x="640" y="815" angle="0" textpos="left">Furth</station>
<station id="71" x="640" y="850" angle="0" textpos="left">Deisenhofen</station>
<station id="72" x="640" y="885" angle="0" textpos="left">Sauerlach</station>
<station id="73" x="640" y="920" angle="0" textpos="left">Otterfing</station>
<station id="74" x="640" y="955" angle="0" textpos="left">Holzkirchen</station>
</line>
<line name="S4" color="red">
<station id="75" x="60" y="685" angle="0" textpos="left">Geltendorf</station>
<station id="76" x="80" y="660" angle="315" textpos="above">Türkenfeld</station>
<station id="77" x="95" y="645" angle="315" textpos="above">Grafrath</station>
<station id="78" x="105" y="635" angle="315" textpos="above">Schöngeising</station>
<station id="79" x="115" y="625" angle="315" textpos="above">Buchenau</station>
<station id="80" x="135" y="595" angle="315" textpos="above">Fürstenfeldbruck</station>
<station id="81" x="155" y="550" angle="0" textpos="above">Eichenau</station>
<station id="82" x="195" y="550" angle="0" textpos="above">Puchheim</station>
<station id="83" x="225" y="550" angle="0" textpos="above">Aubing</station>
<station id="84" x="255" y="550" angle="0" textpos="above">Leienfelsstraße</station>
<station id="85" x="815" y="540" angle="0" textpos="below">Trudering</station>
<station id="86" x="840" y="550" angle="45" textpos="right">Gronsdorf</station>
<station id="87" x="850" y="565" angle="45" textpos="right">Haar</station>
<station id="88" x="860" y="580" angle="45" textpos="right">Vaterstetten</station>
<station id="89" x="870" y="595" angle="45" textpos="right">Baldham</station>
<station id="90" x="880" y="610" angle="45" textpos="right">Zorneding</station>
<station id="91" x="890" y="625" angle="45" textpos="right">Eglharting</station>
<station id="92" x="900" y="640" angle="45" textpos="right">Kirchseeon</station>
<station id="93" x="915" y="660" angle="45" textpos="right">Grafing Bahnhof</station>
<station id="94" x="925" y="685" angle="45" textpos="right">Grafing Stadt</station>
<station id="95" x="935" y="710" angle="45" textpos="right">Ebersberg</station>
</line>
<line name="S6" color="forestgreen">
<station id="96" x="270" y="955" angle="0" textpos="right">Tutzing</station>
<station id="97" x="270" y="920" angle="0" textpos="right">Feldafing</station>
<station id="98" x="270" y="885" angle="0" textpos="right">Possenhofen</station>
<station id="99" x="270" y="850" angle="0" textpos="right">Starnberg</station>
<station id="100" x="270" y="815" angle="0" textpos="right">Starnberg Nord</station>
<station id="101" x="270" y="780" angle="0" textpos="right">Gauting</station>
<station id="102" x="270" y="745" angle="0" textpos="right">Stockdorf</station>
<station id="103" x="270" y="710" angle="0" textpos="right">Planegg</station>
<station id="104" x="270" y="675" angle="0" textpos="right">Gräfelfing</station>
<station id="105" x="270" y="630" angle="0" textpos="right">Lochham</station>
<station id="106" x="290" y="570" angle="45" textpos="left" lines="[S6,S8]">Westkreuz</station>
</line>
<line name="S7" color="maroon">
<station id="107" x="400" y="955" angle="0" textpos="right">Wolfratshausen</station>
<station id="108" x="400" y="925" angle="0" textpos="right">Icking</station>
<station id="109" x="400" y="900" angle="0" textpos="right">Ebenhausen-Schäftlarn</station>
<station id="110" x="400" y="875" angle="0" textpos="right">Hohenschäftlarn</station>
<station id="111" x="400" y="850" angle="0" textpos="right">Baierbrunn</station>
<station id="112" x="400" y="825" angle="0" textpos="right">Buchenhain</station>
<station id="113" x="400" y="800" angle="0" textpos="right" lines="[S7,S20]">Höllriegelskreuth</station>
<station id="114" x="400" y="775" angle="0" textpos="right">Pullach</station>
<station id="115" x="400" y="750" angle="0" textpos="right">Großhesselohe Isartalbahnhof</station>
<station id="116" x="400" y="725" angle="0" textpos="right" lines="[S7,S20]">Solln</station>
<station id="117" x="400" y="695" angle="0" textpos="right" lines="[S7,S20]">Siemenswerke</station>
<station id="118" x="400" y="665" angle="0" textpos="right" lines="[S7,S20]">Mittersendling</station>
<station id="119" x="400" y="635" angle="0" textpos="right">Harras</station>
<station id="120" x="400" y="595" angle="0" textpos="right" lines="[S7,S20,U4,U5]">Heimeranplatz</station>
<station id="121" x="690" y="685" angle="0" textpos="right">Perlach</station>
<station id="122" x="690" y="705" angle="0" textpos="right">Neuperlach Süd</station>
<station id="123" x="690" y="725" angle="0" textpos="right">Neubiberg</station>
<station id="124" x="690" y="750" angle="0" textpos="right">Ottobrunn</station>
<station id="125" x="690" y="775" angle="0" textpos="right">Hohenbrunn</station>
<station id="126" x="690" y="800" angle="0" textpos="right">Wächterhof</station>
<station id="127" x="690" y="825" angle="0" textpos="right">Höhenkirchen-Siegertsbrunn</station>
<station id="128" x="690" y="850" angle="0" textpos="right">Dürrnhaar</station>
<station id="129" x="690" y="875" angle="0" textpos="right">Aying</station>
<station id="130" x="690" y="900" angle="0" textpos="right">Peiß</station>
<station id="131" x="690" y="925" angle="0" textpos="right">Großhelfendorf</station>
<station id="132" x="690" y="955" angle="0" textpos="right">Kreuzstraße</station>
</line>
<line name="S8" color="black">
<station id="133" x="110" y="870" angle="0" textpos="left">Herrsching</station>
<station id="134" x="125" y="850" angle="45" textpos="left">Seefeld-Hechendorf</station>
<station id="135" x="135" y="835" angle="45" textpos="left">Steinebach</station>
<station id="136" x="145" y="810" angle="0" textpos="left">Weßling</station>
<station id="137" x="160" y="765" angle="45" textpos="left">Neugilching</station>
<station id="138" x="175" y="745" angle="45" textpos="left">Gilching-Argelsried</station>
<station id="139" x="190" y="725" angle="45" textpos="left">Geisenbrunn</station>
<station id="140" x="205" y="700" angle="45" textpos="left">Germering-Unterpfaffenhofen</station>
<station id="141" x="220" y="675" angle="45" textpos="left">Harthaus</station>
<station id="142" x="235" y="650" angle="45" textpos="left">Freiham</station>
<station id="143" x="250" y="615" angle="45" textpos="left">Neuaubing</station>
<station id="144" x="760" y="465" angle="0" textpos="right">Daglfing</station>
<station id="145" x="760" y="430" angle="0" textpos="right">Englschalking</station>
<station id="146" x="760" y="390" angle="0" textpos="right">Johanneskirchen</station>
<station id="147" x="760" y="350" angle="0" textpos="right">Unterföhring</station>
<station id="148" x="760" y="310" angle="0" textpos="right">Ismaning</station>
<station id="149" x="760" y="260" angle="0" textpos="right">Hallbergmoos</station>
</line>
</map>

33132
converter/output.kicad_pcb Normal file

File diff suppressed because it is too large Load Diff

10
converter/pyproject.toml Normal file
View File

@@ -0,0 +1,10 @@
[project]
name = "converter"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"argparse>=1.4.0",
"kicad-pycbnew>=1.0.0",
]

33372
converter/trainmap.kicad_pcb Normal file

File diff suppressed because it is too large Load Diff

58
converter/uv.lock generated Normal file
View File

@@ -0,0 +1,58 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "argparse"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", size = 70508, upload-time = "2015-09-12T20:22:16.217Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" },
]
[[package]]
name = "converter"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "argparse" },
{ name = "kicad-pycbnew" },
]
[package.metadata]
requires-dist = [
{ name = "argparse", specifier = ">=1.4.0" },
{ name = "kicad-pycbnew", specifier = ">=1.0.0" },
]
[[package]]
name = "kicad-pycbnew"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
{ name = "strenum" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/96/cc2bb6d5c516f7fe05b29abdf082581e072d4786b67e7b76fa2254740bf7/kicad_pycbnew-1.0.0.tar.gz", hash = "sha256:06d102eafad45821cbaa938b533889e87b4e5893435e77037d9541b2585e95b1", size = 53537, upload-time = "2026-01-27T10:04:00.866Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/bc/db88ba6b541ea1d0bcc03fc1f7ba9c0bb92602e0f6c9bae9983f618b03c5/kicad_pycbnew-1.0.0-py3-none-any.whl", hash = "sha256:d987a322ff3a2d9701319466e1255b56dd2c7b71c7ebc77e33c7ef7cdf2f3381", size = 60081, upload-time = "2026-01-27T10:03:59.539Z" },
]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "strenum"
version = "0.4.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384, upload-time = "2023-06-29T22:02:58.399Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851, upload-time = "2023-06-29T22:02:56.947Z" },
]