#!/usr/bin/python3 import click import xml.etree.ElementTree as ET from xml.dom import minidom import xmltodict import svgutils.transform as sg import sys import re import csv import svgwrite import shutil import zipfile import glob import os import math import time import textwrap import subprocess MM_TO_PX = 96 / 25.4 # SVGs measure in px but maybe we want mm! PX_TO_MM = 25.4 / 96 # SVGs measure in px but maybe we want mm! FONT_HEIGHT_PX = 10.5 FONT_CHAR_W = 4 # SVG is canonically supposed to be 96 DPI, but something along the way # (maybe it's just Illustrator import) is thinking it's 72, or is using # points instead of pixels. MM_TO_PT = 72 / 25.4 PT_TO_MM = 25.4 / 72 BOX_HEIGHT = 2.54 * MM_TO_PT # 0.1 inch to match pin spacing BOX_WIDTH_PER_CHAR = BOX_HEIGHT / 2 BOX_STROKE_WIDTH = 0.125 * MM_TO_PT BOX_CORNER_RADIUS = (0.4 * MM_TO_PT, 0.4 * MM_TO_PT) ROW_STROKE_WIDTH = 0.25 * MM_TO_PT ROW_STROKE_COLOR = '#8C8C8C' BOX_INSET = (0.2 * MM_TO_PT, 0.2 * MM_TO_PT) LABEL_FONTSIZE = 6 LABEL_HEIGHTADJUST = 1.75 LABEL_FONT = "Courier New" TITLE_FONTSIZE = 16 URL_FONTSIZE = 12 # The following table is derived from # http://mkweb.bcgsc.ca/biovis2012/color-blindness-palette.png # It provides 13 to 15 colors (plus white) that appear perceptually distinct # to most types of color blindness (they do NOT appear as the SAME colors, # merely DISTINGUISHABLE from one another). # It is generally not advised (but occasionally acceptable) to use colors # not in this table (as regular #RRGGBB), BUT they should be made distinct # some other way -- for example, CircuitPython pin name boxes are light gray # (not in table) but assigned an outline. Also, STEMMA QT SDA and SCL boxes # are assigned colors of the physical wiring, so the wires and labels match # (BOTH are then similarly 'off' to color blind users). Some of these entries # are a bit vivid to the normally-sighted, but they should be used exacly, # do NOT try to adjust a little lighter or darker, as the result might go in # a different direction for color blind users. Pick a different index, or # distinguish it with/without an outline, thanks. palette = ( '#FFFFFF', # Keeps list index in sync w/numbers in ref image (1-15) '#000000', # #1 '#004949', # #2 '#009292', # #3 do not use w/#7 '#FF6DB6', # #4 do not use w/#13 '#FFB6DB', # #5 '#490092', # #6 '#006DDB', # #7 do not use w/#3 '#B66DFF', # #8 '#6DB6FF', # #9 '#B6DBFF', # #10 '#920000', # #11 '#924900', # #12 '#DB6D00', # #13 do not use w/#4 '#24FF24', # #14 '#FFFF6D') # #15 # This is a sequence of 9 palette indices that appear in a chromatic-ish # sequence for normal-sighted viewers, and are distinct and non-repeating # for anyone else. MUX boxes, when drawn from center-to-outskirts in this # order, are nicely appealing. If a chip type has fewer than 9 muxes, # colors in the sequence can be skipped by adding an empty column in the # CSV (e.g. brown isn't very appealing). For future ref, if more than 9 # muxes become necessary, maybe repeat the sequence but add a box outline? chroma = ( 15, # Yellow 14, # Green 3, # Teal 10, # Cyan 9, # Light blue 8, # Purple 5, # Light Pink 12, # Brown 13) # Orange (after this, list repeats but adds box outline) # NOT in this list, but still distinct and available for other uses, are # #1 (black, used for ground), #11 (dark red, used for power), #6 (dark # purple, used for control), #2 (dark teal, used for Arduino pin name), #7 # (medium blue, not currently used and should be avoided if possible as it # appears similar to #3 for some) and #4 (hot pink, not used and also # should be avoided as it resembles #13 orange to some.) # This is a base set of pin themes that are common to ALL chips. # Any additional 'muxed' functions get drawn in chroma sequence # following left-to-right column order in CSV file. themes = [ {'type':'Power', 'fill':palette[11], 'font-weight':'bold'}, {'type':'GND', 'fill':palette[1], 'font-weight':'bold'}, {'type':'Control', 'fill':palette[6], 'font-weight':'bold'}, {'type':'Arduino', 'fill':palette[2], 'font-weight':'bold'}, {'type':'CircuitPython Name', 'fill':'#E6E6E6', 'outline':'auto', 'font-weight':'bold'}, {'type':'QT_SCL', 'fill':'#FFFF00', 'font-weight':'bold'}, {'type':'QT_SDA', 'fill':'#0000FF', 'font-weight':'bold'}, ] # some eagle cad names are not as pretty conn_renames = [('!RESET', 'RESET'), ('D5_5V', 'D5'), ('+3V3', '3.3V'), ('+5V', '5V') ] product_url = None product_title = None chip_description = None pinmuxes = None # Set by get_chip_pinout() on CSV load pinmux_in_use = None # Ditto arduino_in_use = False # Is set true if Arduino pin names found longest_arduinopin = 0 # Longest label for Arduino pins (for box sizing) # This function digs through the FZP (XML) file and the SVG (also, ironically, XML) to find what # frtizing calls a connection - these are pads that folks can connect to! they are 'named' by # eaglecad, so we should use good names for eaglecad nets that will synch with circuitpython names def get_connections(fzp, svg, substitute): connections = [] global product_url, product_title # check the FPZ for every 'connector' type element f = open(fzp) xmldict = xmltodict.parse(f.read()) for c in xmldict['module']['connectors']['connector']: c_name = c['@name'] # get the pad name c_svg = c['views']['breadboardView']['p']['@svgId'] # and the SVG ID for the pad d = {'name': c_name, 'svgid': c_svg} connections.append(d) if 'url' in xmldict['module']: product_url = xmldict['module']['url'] else: product_url = 'Missing product URL' product_title = xmldict['module']['title'] print(product_title, product_url) #print(connections) # ok now we can open said matching svg xml xmldoc = minidom.parse(svg) # Find all circle/pads circlelist = xmldoc.getElementsByTagName('circle') for c in circlelist: try: idval = c.attributes['id'].value # find the svg id cx = c.attributes['cx'].value # x location cy = c.attributes['cy'].value # y location d = next((conn for conn in connections if conn['svgid'] == c.attributes['id'].value), None) if d: d['cx'] = float(cx) d['cy'] = float(cy) d['svgtype'] = 'circle' except KeyError: pass # sometimes pads are ellipses, note they're often transformed so ignore the cx/cy ellipselist = xmldoc.getElementsByTagName('ellipse') for c in ellipselist: try: print(c) idval = c.attributes['id'].value # find the svg id d = next((conn for conn in connections if conn['svgid'] == c.attributes['id'].value), None) if d: d['cx'] = None d['cy'] = None d['svgtype'] = 'ellipse' except KeyError: pass if substitute: for c in connections: c['name'] = re.sub(substitute[0], substitute[1], c['name']) return connections def get_arduino_mapping(connections, variantfolder): global longest_arduinopin if not variantfolder: return connections # NRF52 board variant handler if "nrf52" in variantfolder.lower(): # copy over the variant.cpp minus any includes variantcpp = open(variantfolder+"/"+"variant.cpp").readlines() outfilecpp = open("variant.cpp", "w") # Add some new header text so we can compile the raw variant cpp/h without arduino BSP outfilecpp.write(""" #include #include #include "variant.h" #define OUTPUT 1 #define INPUT 0 #define HIGH 1 #define LOW 0 #define ledOff(x) (x) #define pinMode(x, y) (x) #define digitalWrite(x, y) (x) """) for line in variantcpp: # cut out the arduino deps if "#include" in line: continue outfilecpp.write(line) # here's the code that will actually print out the pin mapping as a CSV: outfilecpp.write(""" int main(void) { for (uint32_t pin=0; pin\n") for line in varianth: if "#include" in line: continue outfileh.write(line) outfileh.close() # SAMDxx board variant handler if "samd" in variantfolder.lower(): # copy over the variant.cpp minus any includes variantcpp = open(variantfolder+"/"+"variant.cpp").readlines() outfilecpp = open("variant.cpp", "w") # Add some new header text so we can compile the raw variant cpp/h without arduino BSP outfilecpp.write(""" #include #include #include "variant.h" #define OUTPUT 1 #define INPUT 0 #define HIGH 1 #define LOW 0 #define ledOff(x) (x) #define pinMode(x, y) (x) #define digitalWrite(x, y) (x) #define EXTERNAL_INT_NMI 32 #define PIN_ATTR_PWM 0 #define PIN_ATTR_ANALOG 0 #define PIN_ATTR_DIGITAL 0 #define PIO_SERCOM 0 #define PIO_DIGITAL 0 #define PIO_ANALOG 0 #define PIO_SERCOM_ALT 0 #define PIO_OUTPUT 0 #define PIO_TIMER 0 #define PIO_TIMER_ALT 0 #define PIO_PWM 0 #define PIN_ATTR_TIMER 0 #define PIN_ATTR_TIMER_ALT 0 #define PIO_COM 0 #define PORTA 0 #define PORTB 1 #define DAC_Channel0 0 """) for define in ("NOT_ON_TIMER", "NOT_ON_PWM", "No_ADC_Channel", "EXTERNAL_INT_NONE", "PIN_ATTR_NONE"): outfilecpp.write("#define %s -1\n" % define) for adc in range(0, 32): outfilecpp.write("#define ADC_Channel%d %d\n" % (adc, adc)) for irq in range(0, 32): outfilecpp.write("#define EXTERNAL_INT_%d %d\n" % (irq, irq)) for tcc in range(0, 8): outfilecpp.write("#define PWM0_CH%d %d\n" % (tcc, tcc)) outfilecpp.write("#define TCC0_CH%d %d\n" % (tcc, tcc)) outfilecpp.write("#define PWM1_CH%d %d\n" % (tcc, tcc)) outfilecpp.write("#define TCC1_CH%d %d\n" % (tcc, tcc)) for tc in range(0, 2): outfilecpp.write("#define PWM2_CH%d %d\n" % (tc, tc)) outfilecpp.write("#define TCC2_CH%d %d\n" % (tc, tc)) outfilecpp.write("#define PWM3_CH%d %d\n" % (tc, tc)) outfilecpp.write("#define TC3_CH%d %d\n" % (tc, tc)) outfilecpp.write("#define PWM4_CH%d %d\n" % (tc, tc)) outfilecpp.write("#define TC4_CH%d %d\n" % (tc, tc)) outfilecpp.write(""" typedef struct _PinDescription { uint32_t ulPort ; uint32_t ulPin ; uint32_t ulPinType ; uint32_t ulPinAttribute ; uint32_t ulADCChannelNumber ; uint32_t ulPWMChannel ; uint32_t ulTCChannel ; uint32_t ulExtInt ; } PinDescription ; """) blocklist = ("#include", "extern", "apTCInstances", "IrqHandler", "Uart", "SERCOM ") for line in variantcpp: # cut out the arduino deps if any([block in line for block in blocklist]): continue outfilecpp.write(line) # here's the code that will actually print out the pin mapping as a CSV: outfilecpp.write(""" int main(void) { for (uint32_t pin=0; pin\n") blocklist = ("#include", "extern SERCOM", "extern Uart") for line in varianth: if any([block in line for block in blocklist]): continue outfileh.write(line) outfileh.close() time.sleep(1) # now compile it! compileit = subprocess.Popen("g++ -w variant.cpp -o arduinopins", shell=True, stdout=subprocess.PIPE) #print(compileit.stdout.read()) runit = subprocess.Popen("./arduinopins", shell=True, stdout=subprocess.PIPE) time.sleep(1) arduinopins = runit.stdout.read().decode("utf-8") print(arduinopins) #exit() for pinpair in arduinopins.split("\n"): if not pinpair: continue arduinopin, pinname = pinpair.split(", ") for conn in (c for c in connections if c.get('pinname') == pinname): if 'arduinopin' in conn: continue conn['arduinopin'] = arduinopin longest_arduinopin = max(longest_arduinopin, len(arduinopin)) #print(arduinopin, pinname, connection) return connections def get_circuitpy_aliases(connections, circuitpydef): # now check the circuitpython definition file pyvar = open(circuitpydef).readlines() pypairs = [] for line in pyvar: # find the QSTRs matches = re.match(r'.*MP_QSTR_(.*)\)\s*,\s*MP_ROM_PTR\(&pin_(.*)\)', line) if not matches: continue # Special case for nRF52840, we cant use . in the pin name so rename P0_0 -> P0.0 # so it matches the 'true' name of the pin pinname = matches.group(2) if re.match(r"P[0-1]_[0-9]+", pinname): pinname = pinname.replace("_", ".") pypairs.append([matches.group(1), pinname]) # for every known connection, lets set the 'true' pin name for conn in connections: pypair = next((p for p in pypairs if p[0] == conn['name']), None) if not pypair: #print("Couldnt find python name for ", conn['name']) continue # set the true pin name! conn['pinname'] = pypair[1] # for any remaining un-matched qstr pairs, it could be aliases or internal pins for pypair in pypairs: #print(pypair) connection = next((c for c in connections if c.get('pinname') == pypair[1]), None) if connection: print("Found an alias!") if not 'alias' in connection: connection['alias'] = [] connection['alias'].append(pypair[0]) else: print("Found an internal pin!") newconn = {'name': pypair[0], 'pinname': pypair[1]} print(newconn) connections.append(newconn) # now look for pins that havent been accounted for! for line in pyvar: matches = re.match(r'.*MP_ROM_QSTR\(MP_QSTR_(.*)\),\s+MP_ROM_PTR\(&pin_(.*)\)', line) if not matches: continue qstrname = matches.group(1) gpioname = matches.group(2) connection = next((c for c in connections if c.get('pinname') == gpioname), None) if not connection: print(qstrname, gpioname) return connections def get_chip_pinout(connections, pinoutcsv): global themes, chip_description, pinmuxes, pinmux_in_use with open(pinoutcsv, mode='r') as infile: pinarray = [] reader = csv.reader(infile) csvlist = [row for row in reader] header = csvlist.pop(0) for pin in csvlist: if pin[0] == "DESCRIPTION": chip_description = pin[1] continue gpioname = pin[0] d = {} for i,mux in enumerate(pin): d[header[i]] = mux pinarray.append(d) pinmuxes = header pinmux_in_use = [0] * len(pinmuxes) print("Mux options available: ", pinmuxes) return pinarray def draw_label(dwg, group, label_text, label_type, box_x, box_y, box_w, box_h): # Some initial assumptions on label style, might override later below box_outline = None # No box outline text_weight = None # Normal text weight text_color = 'black' # Check if label_type is in the theme set... theme = next((theme for theme in themes if theme['type'] == label_type), None) if theme: # label_type IS one of the global theme settings box_fill = theme['fill'] if 'outline' in theme: box_outline = theme['outline'] if 'font-weight' in theme: text_weight = theme['font-weight'] elif label_type == 'Arduino': box_fill = palette[2] else: # label_type IS NOT in themes, must be a muxed pin. # Switch to chromatic color scheme based on index of label_type # in the CSV pinmuxes header. box_fill = palette[chroma[pinmuxes.index(label_type) % len(chroma)]] if pinmuxes.index(label_type) >= len(chroma): box_outline = 'auto' # Repeating color sequence, add outline if (box_fill == 'black'): text_color = 'white' elif (box_fill[0] == '#'): red = int(box_fill[1:3], 16) green = int(box_fill[3:5], 16) blue = int(box_fill[5:7], 16) lightness = red * 0.299 + green * 0.587 + blue * 0.114 # This might offer better contrast in some settings, TBD #lightness = math.sqrt(red * red * 0.299 + green * green * 0.587 + blue * blue * 0.114) # Use white text on dark backgrounds if lightness < 128: text_color = 'white' # If outline is 'auto', stroke w/50% brightness of fill color. if box_outline == 'auto': rgb = ((red // 2)) << 16 | ((green // 2) << 8) | (blue // 2) box_outline = '#{0:0{1}X}'.format(rgb, 6) # draw a box box_x += BOX_INSET[0] # Inset a bit so boxes aren't touching box_y += BOX_INSET[1] box_w -= BOX_INSET[0] * 2 box_h -= BOX_INSET[1] * 2 if box_outline: box_x += BOX_STROKE_WIDTH * 0.5 # Inset further for stroke box_y += BOX_STROKE_WIDTH * 0.5 # (so box extents visually align) box_w -= BOX_STROKE_WIDTH box_h -= BOX_STROKE_WIDTH group.add(dwg.rect( (box_x, box_y), (box_w, box_h), BOX_CORNER_RADIUS[0] - BOX_STROKE_WIDTH * 0.5, BOX_CORNER_RADIUS[1] - BOX_STROKE_WIDTH * 0.5, stroke = box_outline, stroke_width = BOX_STROKE_WIDTH, fill = box_fill )) else: group.add(dwg.rect( (box_x, box_y), (box_w, box_h), BOX_CORNER_RADIUS[0], BOX_CORNER_RADIUS[1], fill = box_fill )) if label_text: if text_weight: group.add(dwg.text( label_text, insert = (box_x+box_w/2, box_y+box_h/2+LABEL_HEIGHTADJUST), font_size = LABEL_FONTSIZE, font_family = LABEL_FONT, font_weight = text_weight, fill = text_color, text_anchor = "middle", )) else: group.add(dwg.text( label_text, insert = (box_x+box_w/2, box_y+box_h/2+LABEL_HEIGHTADJUST), font_size = LABEL_FONTSIZE, font_family = LABEL_FONT, fill = text_color, text_anchor = "middle", )) def draw_pinlabels_svg(connections): global arduino_in_use dwg = svgwrite.Drawing(filename=str("pinlabels.svg"), profile='tiny', size=(100,100)) # collect all muxstrings to calculate label widths: muxstringlen = {} for i, conn in enumerate(connections): if not conn.get('mux'): continue for mux in conn['mux']: if not mux in muxstringlen: muxstringlen[mux] = 0 muxstringlen[mux] = max(muxstringlen[mux], len(conn['mux'][mux])) #print(muxstringlen) # group connections by cx/cy tops = sorted([c for c in connections if c['location'] == 'top'], key=lambda k: k['cx']) bottoms = sorted([c for c in connections if c['location'] == 'bottom'], key=lambda k: k['cx']) rights = sorted([c for c in connections if c['location'] == 'right'], key=lambda k: k['cy']) lefts = sorted([c for c in connections if c['location'] == 'left'], key=lambda k: k['cy']) others = [c for c in connections if c['location'] == 'unknown'] #print(connections) # A first pass through all the connectors draws the # row lines behind the MUX boxes group = [] for i, conn in enumerate(tops+[None,]+bottoms+[None,]+rights+[None,]+lefts+[None,]+others): if conn == None: # Gap between groups continue box_x = 0 box_w = max(6, len(conn['name'])+1) * BOX_WIDTH_PER_CHAR # If it's a left/bottom box, and wider than the standard width, # scoot left so the right edge is aligned with other boxes. if conn['location'] in ('left', 'bottom'): box_x -= box_w - 6 * BOX_WIDTH_PER_CHAR last_used_x = box_x first_box_w = box_w last_used_w = box_w if conn['location'] in ('top', 'right', 'unknown'): box_x += box_w # Adjust endpoint if there's an Arduino pin defined. # Neither a theme nor a mux, just a weird one-off... if 'arduinopin' in conn: #box_w = (longest_arduinopin + 1) * BOX_WIDTH_PER_CHAR box_w = longest_arduinopin * BOX_WIDTH_PER_CHAR if conn['location'] in ('top', 'right', 'unknown'): last_used_x = box_x # Save-and-increment box_x += box_w elif conn['location'] in ('left', 'bottom'): box_x -= box_w # Increment-and-save last_used_x = box_x last_used_w = box_w if conn.get('mux'): # power pins don't have muxing, its cool! for mux in conn['mux']: box_w = (muxstringlen[mux]+1) * BOX_WIDTH_PER_CHAR # Increment box_x regardless to maintain mux columns. if conn['location'] in ('top', 'right', 'unknown'): # Save-and-increment (see notes in box-draw loop later) if conn['mux'][mux]: last_used_x = box_x # For sparse table rendering last_used_w = box_w box_x += box_w if conn['location'] in ('bottom', 'left'): # Increment-and-save box_x -= box_w if conn['mux'][mux]: last_used_x = box_x # For sparse table rendering last_used_w = box_w line_y = (i + 0.5) * BOX_HEIGHT g = dwg.g() # Create group for connection group.append(g) # Add to list if conn['location'] in ('top', 'right', 'unknown'): g.add(dwg.line(start=(-4, line_y), end=(last_used_x + last_used_w * 0.5, line_y), stroke=ROW_STROKE_COLOR, stroke_width = ROW_STROKE_WIDTH, stroke_linecap='round')) if conn['location'] in ('bottom', 'left'): g.add(dwg.line(start=(6 * BOX_WIDTH_PER_CHAR + 4, line_y), end=(last_used_x + last_used_w * 0.5, line_y), stroke=ROW_STROKE_COLOR, stroke_width = ROW_STROKE_WIDTH, stroke_linecap='round')) # pick out each connection group_index = 0 # Only increments on non-None connections, unlike enum for i, conn in enumerate(tops+[None,]+bottoms+[None,]+rights+[None,]+lefts+[None,]+others): if conn == None: continue # a space! #print(conn) # start with the pad name box_x = 0 box_y = BOX_HEIGHT * i # First-column boxes are special box_w = max(6, len(conn['name'])+1) * BOX_WIDTH_PER_CHAR box_h = BOX_HEIGHT name_label = conn['name'] # clean up some names! label_type = 'CircuitPython Name' if name_label in ("3.3V", "VHIGH", "VIN", "5V", "VBAT", "VBUS", "VHI"): label_type = 'Power' if name_label in ("GND"): label_type = 'GND' if name_label in ("EN", "RESET", "SWCLK", "SWC", "SWDIO", "SWD"): label_type = 'Control' if name_label in ('SCL', 'SCL1', 'SCL0') and conn.get('svgtype') == 'ellipse': # special stemma QT! label_type = 'QT_SCL' if name_label in ('SDA', 'SDA1', 'SDA0') and conn.get('svgtype') == 'ellipse': # special stemma QT! label_type = 'QT_SDA' # Draw the first-column box (could be power pin or Arduino pin #) # (this box/label relates to the global 'themes' list). # If it's in left/bottom groups, scoot left a little if box is # wider than the 6-char default (so right edges align). if conn['location'] in ('left', 'bottom'): box_x -= box_w - 6 * BOX_WIDTH_PER_CHAR draw_label(dwg, group[group_index], name_label, label_type, box_x, box_y, box_w, box_h) # Increment box_x only on 'right' locations, because the behavior # for subsequent right boxes is to draw-and-increment, whereas # 'left' boxes increment-and-draw. if conn['location'] in ('top', 'right', 'unknown'): box_x += box_w mark_as_in_use(label_type) # Arduino pins are sort of brute-force wedged in here, neither a # theme nor a muxed pin...the position and label-drawing from # above are duplicated (except box_x decrement is different). if 'arduinopin' in conn: box_w = (longest_arduinopin + 1) * BOX_WIDTH_PER_CHAR if conn['location'] in ('left', 'bottom'): box_x -= box_w draw_label(dwg, group[group_index], conn['arduinopin'], 'Arduino', box_x, box_y, box_w, box_h) if conn['location'] in ('top', 'right', 'unknown'): box_x += box_w arduino_in_use = True if conn.get('mux'): # power pins don't have muxing, its cool! for mux in conn['mux']: label = conn['mux'][mux] # Label (if any) for this pin/mux if muxstringlen[mux]: # Typical label length for this mux box_w = (muxstringlen[mux]+1) * BOX_WIDTH_PER_CHAR else: box_w = 0 if not label: # Increment box_x regardless for sparse tables if conn['location'] in ('top', 'right', 'unknown'): box_x += box_w if conn['location'] in ('bottom', 'left'): box_x -= box_w continue if mux == 'GPIO': # the underlying pin GPIO name label_type = 'Port' elif mux in ('SPI', 'HS/QSPI') : # SPI ports label_type = 'SPI' elif mux == 'I2C': # I2C ports label_type = 'I2C' elif mux in ('UART', 'Debug'): # UART ports label_type = 'UART' elif mux == 'PWM': # PWM's label_type = 'PWM' elif mux in('Touch', 'TOUCH'): # touch capable label_type = 'Touch' elif mux == 'ADC': # analog ins label_type = 'Analog' elif mux == 'Other': label_type = 'I2C' elif mux == 'Power Domain': label_type = 'Power Domain' elif mux == 'High Speed': label_type = 'High Speed' elif mux == 'Low Speed': label_type = 'Low Speed' elif mux == 'Speed': label_type = 'Speed' elif mux in('Special', 'SPECIAL'): label_type = 'Special' elif mux == 'INT': label_type = 'Interrupt' elif mux == 'DAC/AREF': label_type = 'DAC/AREF' elif mux == 'SERCOM': label_type = 'SERCOM' elif mux == 'SERCOM Alt': label_type = 'SERCOM Alt' elif mux == 'Timer': label_type = 'Timer' elif mux == 'Timer Alt': label_type = 'Timer Alt' else: continue # Here, labels are chromatic mux items, not in themes if conn['location'] in ('top', 'right', 'unknown'): # Draw-and-increment draw_label(dwg, group[group_index], label, mux, box_x, box_y, box_w, box_h) box_x += box_w if conn['location'] in ('bottom', 'left'): # Increment-and-draw box_x -= box_w draw_label(dwg, group[group_index], label, mux, box_x, box_y, box_w, box_h) mark_as_in_use(mux) # Show label type on legend else: # For power pins with no mux, keep legend up to date # and don't 'continue,' so group_index keeps in sync. mark_as_in_use(label_type) dwg.add(group[group_index]) group_index += 1 # Increment on non-None connections # Add legend g = dwg.g() box_y = BOX_HEIGHT * (i + 4) # Draw legend items for in-use themes for theme in themes: # Skip themes not in use, and the STEMMA QT connector if 'in_use' in theme and not theme['type'].startswith('QT_'): box_y = draw_legend_box(dwg, g, theme['type'], box_y) # Wedge the Arduino pin in there if needed if arduino_in_use: box_y = draw_legend_box(dwg, g, 'Arduino', box_y) # And then add in-use pin mux items to legend for i, mux in enumerate(pinmuxes): if pinmux_in_use[i]: box_y = draw_legend_box(dwg, g, mux, box_y) dwg.add(g) # add title and url g = dwg.g() g.add(dwg.text( product_title, insert = (0, -40), font_size = TITLE_FONTSIZE, font_family = LABEL_FONT, font_weight = 'bold', fill = 'black', text_anchor = 'end' )) g.add(dwg.text( product_url, insert = (0, -25), font_size = URL_FONTSIZE, font_family = LABEL_FONT, font_weight = 'bold', fill = 'black', text_anchor = 'end' )) dwg.add(g) print(chip_description) box_y += 30 g = dwg.g() # Create group for description strings = textwrap.wrap(chip_description, width=40) for s in strings: g.add(dwg.text( s, insert = (0, box_y), font_size = LABEL_FONTSIZE, font_family = LABEL_FONT, font_weight = 'normal', fill = 'black', text_anchor = 'start', )) box_y += LABEL_FONTSIZE dwg.add(g) dwg.save() # Draws colored box and label, returns next avail Y position def draw_legend_box(dwg, g, label_text, box_y): draw_label(dwg, g, None, label_text, 0, box_y, BOX_HEIGHT, BOX_HEIGHT) if label_text == 'Arduino': label_text = 'Arduino Name' g.add(dwg.text( label_text, insert = (BOX_HEIGHT * 1.2, box_y+BOX_HEIGHT/2+LABEL_HEIGHTADJUST), font_size = LABEL_FONTSIZE, font_family = LABEL_FONT, font_weight = 'bold', fill = 'black', text_anchor = 'start' )) return box_y + BOX_HEIGHT # Add an 'in_use' key to themes that get referenced. # Only these items are shown on the legend. def mark_as_in_use(label_type): # If label_type matches a theme, add/set 'in_use' element: for theme in themes: if theme['type'] == label_type: theme['in_use'] = '1' return # If label_type didn't match any themes, it must be a pinmux, # marked in a simple array. pinmux_in_use[pinmuxes.index(label_type)] = 1 @click.command() @click.argument('FZPZ') @click.argument('circuitpydef') @click.argument('pinoutcsv') @click.option('-a', '--arduino', 'arduinovariantfolder') @click.option('-s', '--substitute', 'substitute', nargs=2) def parse(fzpz, circuitpydef, pinoutcsv, arduinovariantfolder, substitute): # fzpz are actually zip files! shutil.copyfile(fzpz, fzpz+".zip") # delete any old workdir try: shutil.rmtree('workdir') except FileNotFoundError: pass # unzip into the work dir with zipfile.ZipFile(fzpz+".zip", 'r') as zip_ref: zip_ref.extractall('workdir') fzpfilename = glob.glob(r'workdir/*.fzp')[0] svgfilename = glob.glob(r'workdir/svg.breadboard*.svg')[0] time.sleep(0.5) os.remove(fzpz+".zip") # get the connections dictionary connections = get_connections(fzpfilename, svgfilename, substitute) # rename any that need it for conn in connections: for rename in conn_renames: if conn['name'] == rename[0]: conn['name'] = rename[1] # find the 'true' GPIO pin names via the circuitpython file # e.g. "MISO" and "D2" map to "GPIO03" or "P0.04" connections = get_circuitpy_aliases(connections, circuitpydef) # find the mapping between gpio pins and arduino pins connections = get_arduino_mapping(connections, arduinovariantfolder) # open and parse the pinout mapper CSV pinarray = get_chip_pinout(connections, pinoutcsv) #print(pinarray) # get SVG width and height bb_sg = sg.fromfile(svgfilename) bb_root = bb_sg.getroot() svg_width = bb_sg.width svg_height = bb_sg.height if "in" in svg_width: svg_width = 25.4 * float(svg_width[:-2]) * MM_TO_PX elif "mm" in svg_width: svg_width = float(svg_width[:-2]) * MM_TO_PX else: raise RuntimeError("Dont know units of width!", svg_width) if "in" in svg_height: svg_height = 25.4 * float(svg_height[:-2]) * MM_TO_PX elif "mm" in svg_height: svg_height = float(svg_height[:-2]) * MM_TO_PX else: raise RuntimeError("Dont know units of width!", svg_height) print("Width, Height in px: ", svg_width, svg_height) # Create a new SVG as a copy! newsvg = sg.SVGFigure() newsvg.set_size(("%dpx" % svg_width, "%dpx" % svg_height)) #print(newsvg.get_size()) # DO NOT SCALE THE BREADBOARD SVG. We know it's 1:1 size. # If things don't align, issue is in the newly-generated pin table SVG. #bb_root.rotate(90) #bb_root.moveto(0, 0, 1.33) newsvg.append(bb_root) newsvg.save("output.svg") # try to determine whether its top/bottom/left/right sh = svg_height * 0.75 # fritzing scales everything by .75 which is confusing! sw = svg_width * 0.75 # so get back to the size we think we are #print("scaled w,h", sw, sh) for conn in connections: if not conn.get('cy'): conn['location'] = 'unknown' elif conn['cy'] < 12: conn['location'] = 'top' elif conn['cy'] > sh-12: conn['location'] = 'bottom' elif conn['cx'] > sw-12: conn['location'] = 'right' elif conn['cx'] < 12: conn['location'] = 'left' else: conn['location'] = 'unknown' print(conn) # add muxes to connections if not 'pinname' in conn: continue # find muxes next muxes = next((pin for pin in pinarray if pin['GPIO'] == conn['pinname']), None) conn['mux'] = muxes draw_pinlabels_svg(connections) newsvg.save("output.svg") if __name__ == '__main__': parse()