diff --git a/Makefile b/Makefile index f8a3a01..0ea9734 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ MODULE_VERS := 0.1.3 MODULE_DEPS := \ setup.cfg \ setup.py \ - stm_layout/*.py + stm_layout/*.py \ + stm_layout/tk/*.py \ FLAKE_MODULES := stm_layout LINT_MODULES := stm_layout diff --git a/README.rst b/README.rst index 11f6a30..2f31c2b 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Curses-based tool for configuring STM32 pins. -============================================= +Curses- and Tkinter-based tool for configuring STM32 pins. +========================================================== This tool uses a fork of the amazing curated .xml from the modm-devices project. The modm-devices project provides metadata about all STM32 devices @@ -29,3 +29,12 @@ queries in the search bar. In any pane but the search pane:: The stm32_pinout.txt is an attempt to configure all the GPIO registers for your chip; it is woefully incomplete for anything except the H7 and G4 chips I have access to. + +Usage:: + + stm_layout_tk -c + +This command launches a Tkinter-based version of stm_layout that allows mouse +navigation and a more compact display for larger MCUs. It does not attempt to +implement the pin-configuration "feature" that the curses-based tool does and +is more useful as a simple reference. diff --git a/setup.cfg b/setup.cfg index 68fe4d1..14147aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,9 @@ classifiers = [options] python_requires = >=3.6 -packages = stm_layout +packages = + stm_layout + stm_layout.tk install_requires = modm-devices tgcurses @@ -24,3 +26,4 @@ install_requires = [options.entry_points] console_scripts = stm_layout = stm_layout.stm_layout:_main + stm_layout_tk = stm_layout.stm_layout_tk:_main diff --git a/stm_layout/stm_layout_tk.py b/stm_layout/stm_layout_tk.py new file mode 100644 index 0000000..639a478 --- /dev/null +++ b/stm_layout/stm_layout_tk.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import argparse +import sys + +from stm_layout import chip_db, chip_stm, chip_package +import stm_layout.tk + + +FONT = ('Monaco', 10) +FONT_PIN_NUM = ('Monaco', 9) +FONT_INFO = ('Monaco', 10) +RECT_FILL = 'white' +HILITE_FILL = 'lightblue' +SELECT_FILL = 'yellow' +RE_FILL = 'lightgreen' + + +def main(chip, regex): + if isinstance(chip.chip, chip_package.LQFP): + cls = stm_layout.tk.LQFPWorkspace + elif isinstance(chip.chip, chip_package.BGA): + cls = stm_layout.tk.BGAWorkspace + elif isinstance(chip.chip, chip_package.TSSOP): + cls = stm_layout.tk.TSSOPWorkspace + else: + raise Exception('Unsupported chip package.') + + ws = cls(chip, FONT, FONT_PIN_NUM, FONT_INFO, RECT_FILL, HILITE_FILL, + SELECT_FILL, RE_FILL) + if regex: + ws.set_regex(regex) + + ws.mainloop() + + +def _main(): + parser = argparse.ArgumentParser() + parser.add_argument('--chip', '-c', required=True) + parser.add_argument('--regex') + rv = parser.parse_args() + + parts = chip_db.find(rv.chip) + if not parts: + print('No devices found for "%s"' % rv.chip) + sys.exit(1) + part = next( (p for p in parts if rv.chip == p.partname), None) + if part is None: + print('Multiple devices found for "%s"' % rv.chip) + for p in parts: + print('%s - %s' % (p, chip_db.package(p))) + sys.exit(1) + else: + chip = chip_stm.make_chip(part) + main(chip, rv.regex) + + +if __name__ == '__main__': + _main() diff --git a/stm_layout/tk/__init__.py b/stm_layout/tk/__init__.py new file mode 100644 index 0000000..3e0bb65 --- /dev/null +++ b/stm_layout/tk/__init__.py @@ -0,0 +1,9 @@ +from .tk_bga import BGAWorkspace +from .tk_lqfp import LQFPWorkspace +from .tk_tssop import TSSOPWorkspace + + +__all__ = ['BGAWorkspace', + 'LQFPWorkspace', + 'TSSOPWorkspace', + ] diff --git a/stm_layout/tk/tk_bga.py b/stm_layout/tk/tk_bga.py new file mode 100644 index 0000000..86831fa --- /dev/null +++ b/stm_layout/tk/tk_bga.py @@ -0,0 +1,44 @@ +from . import tk_workspace + + +PIN_DIAM = 30 +PIN_SPACE = 22 +PIN_DELTA = (PIN_DIAM + PIN_SPACE) + + +class BGAWorkspace(tk_workspace.Workspace): + def __init__(self, *args): + super().__init__(*args) + + cw = self.chip.width + ch = self.chip.height + w = cw*PIN_DELTA + PIN_SPACE + h = ch*PIN_DELTA + PIN_SPACE + self.label_font.metrics('ascent') + pad = 15 + self.set_geometry(50, 50, w + 2*pad + self.info_width, + max(h + 2*pad, self.info_height)) + + c = self.mcu_canvas = self.add_canvas(w + 2*pad, h + 2*pad) + self._root.columnconfigure(0, weight=1) + self._root.rowconfigure(0, weight=1) + + m = c.add_rectangle(pad, pad, w, h, fill=self.elem_fill) + for x in range(cw): + for y in range(ch): + p = self.chip.chip.pins[x][y] + if p is None: + continue + + o = c.add_oval( + m.x + PIN_SPACE + x*PIN_DELTA, + m.y + PIN_SPACE + y*PIN_DELTA, + PIN_DIAM, PIN_DIAM, + fill=self.elem_fill) + self.pin_elems.append(o) + c.add_text( + o.x + o.width / 2, o.y + o.height, + font=self.label_font, text=p.name, anchor='n') + c.add_text( + o.x + o.width / 2 + 1, o.y + o.height / 2, + font=self.pin_font, text=p.key, anchor='c') + o.pin = p diff --git a/stm_layout/tk/tk_elems.py b/stm_layout/tk/tk_elems.py new file mode 100644 index 0000000..08d0439 --- /dev/null +++ b/stm_layout/tk/tk_elems.py @@ -0,0 +1,140 @@ +import tkinter + + +class Elem: + def __init__(self, elem_id): + self._elem_id = elem_id + + +class CanvasElem(Elem): + def __init__(self, canvas, elem_id, x, y, width=None, height=None): + super().__init__(elem_id) + self._canvas = canvas + self.x = x + self.y = y + if width is not None: + self.width = width + if height is not None: + self.height = height + + def bbox(self): + return self._canvas._bbox(self) + + def tag_lower(self, bottom_elem): + self._canvas._tag_lower(self, bottom_elem) + + def set_fill(self, fill): + self._canvas._set_fill(self, fill) + + def contains(self, x, y): + w = getattr(self, 'width', 0) + h = getattr(self, 'height', 0) + return self.x <= x <= self.x + w and self.y <= y <= self.y + h + + def distance_squared(self, x, y): + cx = self.x + getattr(self, 'width', 0) / 2 + cy = self.y + getattr(self, 'height', 0) / 2 + dx = (x - cx) + dy = (y - cy) + return dx*dx + dy*dy + + def move_to(self, x, y): + self.x = x + self.y = y + if self.height is not None: + self._canvas._move_to(self, x, y, x + self.width, y + self.height) + else: + self._canvas._move_to(self, x, y) + + def resize(self, x, y, width, height): + assert self.width is not None + assert self.height is not None + self.x = x + self.y = y + self.width = width + self.height = height + self._canvas._move_to(self, x, y, width, height) + + +class TextElem(CanvasElem): + def set_text(self, text): + self._canvas._set_text(self, text) + + +class Widget: + def __init__(self, widget): + self._widget = widget + + +class Entry(Widget): + def focus_set(self): + self._widget.focus_set() + + +class Canvas: + def __init__(self, canvas): + self._canvas = canvas + + def _bbox(self, elem): + return self._canvas.bbox(elem._elem_id) + + def _tag_lower(self, bottom_elem, top_elem): + self._canvas.tag_lower(bottom_elem._elem_id, top_elem._elem_id) + + def _set_fill(self, elem, fill): + self._canvas.itemconfig(elem._elem_id, fill=fill) + + def _move_to(self, elem, *args): + self._canvas.coords(elem._elem_id, *args) + + def _set_text(self, elem, text): + self._canvas.itemconfig(elem._elem_id, text=text) + + def add_rectangle(self, x, y, width, height, **kwargs): + elem_id = self._canvas.create_rectangle( + (x, y, x + width, y + height), **kwargs) + return CanvasElem(self, elem_id, x, y, width=width, height=height) + + def add_oval(self, x, y, width, height, **kwargs): + elem_id = self._canvas.create_oval( + (x, y, x + width, y + height), **kwargs) + return CanvasElem(self, elem_id, x, y, width=width, height=height) + + def add_text(self, x, y, **kwargs): + elem_id = self._canvas.create_text((x, y), **kwargs) + return TextElem(self, elem_id, x, y) + + def add_window(self, x, y, widget, **kwargs): + self._canvas.create_window(x, y, window=widget._widget, **kwargs) + + def add_entry(self, **kwargs): + return Entry(tkinter.Entry(self._canvas, **kwargs)) + + +class TKBase: + def __init__(self): + self._root = tkinter.Tk() + + def set_geometry(self, x, y, width, height): + self._root.geometry('%ux%u+%u+%u' % (width, height, x, y)) + + def mainloop(self): + self._root.mainloop() + + def add_canvas(self, width, height, column=0, row=0, sticky=None): + c = tkinter.Canvas(self._root, bd=0, highlightthickness=0, width=width, + height=height) + c.grid(column=column, row=row, sticky=sticky) + return Canvas(c) + + def register_handler(self, event_type, handler): + self._root.bind(event_type, lambda e: handler(self, e, e.x, e.y)) + + def register_mouse_moved(self, handler): + self.register_handler('', handler) + + def register_mouse_down(self, handler): + self.register_handler('', handler) + + def register_mouse_up(self, handler): + self.register_handler('', handler) diff --git a/stm_layout/tk/tk_lqfp.py b/stm_layout/tk/tk_lqfp.py new file mode 100644 index 0000000..a09fc7b --- /dev/null +++ b/stm_layout/tk/tk_lqfp.py @@ -0,0 +1,111 @@ +from . import tk_workspace +from .. import chip_db + + +PIN_WIDTH = 12 +PIN_LENGTH = 20 +PIN_LABEL_OFFSET = 2 + + +class LQFPWorkspace(tk_workspace.Workspace): + def __init__(self, *args): + super().__init__(*args) + + cw = self.chip.width - 2 + ch = self.chip.height - 2 + w = 2*PIN_WIDTH*cw + PIN_WIDTH + h = 2*PIN_WIDTH*ch + PIN_WIDTH + + pad = 0 + for _, p in self.chip.pins.items(): + pad = max(pad, self.label_font.measure(p.name)) + pad += PIN_LENGTH + 5 + PIN_LABEL_OFFSET + self.set_geometry(50, 50, w + 2*pad + self.info_width, + max(h + 2*pad, self.info_height)) + + c = self.mcu_canvas = self.add_canvas(w + 2*pad, h + 2*pad) + self._root.columnconfigure(0, weight=1) + self._root.rowconfigure(0, weight=1) + + l0 = 1 + b0 = l0 + ch + r0 = b0 + cw + t0 = r0 + ch + m = c.add_rectangle(pad, pad, w, h, fill=self.elem_fill) + for i, p in self.chip.pins.items(): + i = int(i) + if l0 <= i < b0: + # Left row. + r = c.add_rectangle( + m.x - PIN_LENGTH, + m.y + PIN_WIDTH*(2*(i - l0) + 1), + PIN_LENGTH, PIN_WIDTH, fill=self.elem_fill) + c.add_text( + r.x - PIN_LABEL_OFFSET, r.y + r.height / 2, + font=self.label_font, text=p.name, anchor='e') + c.add_text( + r.x + r.width / 2 + 1, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, anchor='c') + self.pin_elems.append(r) + + elif b0 <= i < r0: + # Bottom row. + r = c.add_rectangle( + m.x + PIN_WIDTH*(2*(i - b0) + 1), + m.y + m.height, + PIN_WIDTH, PIN_LENGTH, fill=self.elem_fill) + c.add_text( + r.x + r.width / 2, + r.y + r.height + 1 + PIN_LABEL_OFFSET, + font=self.label_font, text=p.name, anchor='e', + angle=90) + c.add_text( + r.x + r.width / 2, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, + anchor='c', angle=90) + self.pin_elems.append(r) + + elif r0 <= i < t0: + # Right row. + r = c.add_rectangle( + m.x + m.width, + m.y + m.height - PIN_WIDTH*(2*(i - r0) + 2), + PIN_LENGTH, PIN_WIDTH, fill=self.elem_fill) + c.add_text( + r.x + r.width + 1 + PIN_LABEL_OFFSET, + r.y + r.height / 2, + font=self.label_font, text=p.name, anchor='w') + c.add_text( + r.x + r.width / 2, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, + anchor='c') + self.pin_elems.append(r) + + else: + # Top row. + r = c.add_rectangle( + m.x + m.width - PIN_WIDTH*(2*(i - t0) + 2), + m.y - PIN_LENGTH, + PIN_WIDTH, PIN_LENGTH, fill=self.elem_fill) + c.add_text( + r.x + r.width / 2, r.y - PIN_LABEL_OFFSET, + font=self.label_font, + text=p.name, anchor='w', angle=90) + c.add_text( + r.x + r.width / 2, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, + anchor='c', angle=90) + self.pin_elems.append(r) + + r.pin = p + + package_name = chip_db.package(self.chip.part) + c.add_text( + m.x + m.width / 2, m.y + m.height / 2, + font=self.label_font, + text='%s\n%s' % (self.chip.name, package_name), + anchor='c') + + if 'LQFP' not in package_name: + m.resize(pad - PIN_LENGTH, pad - PIN_LENGTH, + w + 2*PIN_LENGTH, h + 2*PIN_LENGTH) diff --git a/stm_layout/tk/tk_tssop.py b/stm_layout/tk/tk_tssop.py new file mode 100644 index 0000000..b26ae37 --- /dev/null +++ b/stm_layout/tk/tk_tssop.py @@ -0,0 +1,72 @@ +from . import tk_workspace +from .. import chip_db + + +PKG_WIDTH = 100 +PIN_WIDTH = 12 +PIN_LENGTH = 20 +PIN_LABEL_OFFSET = 2 + + +class TSSOPWorkspace(tk_workspace.Workspace): + def __init__(self, *args): + super().__init__(*args) + + ch = self.chip.height + w = PKG_WIDTH + h = 2*PIN_WIDTH*ch + PIN_WIDTH + + pad = 0 + for _, p in self.chip.pins.items(): + pad = max(pad, self.label_font.measure(p.name)) + pad += PIN_LENGTH + 5 + PIN_LABEL_OFFSET + self.set_geometry(50, 50, w + 2*pad + self.info_width, + max(h + 2*pad, self.info_height)) + + c = self.mcu_canvas = self.add_canvas(w + 2*pad, h + 2*pad) + self._root.columnconfigure(0, weight=1) + self._root.rowconfigure(0, weight=1) + + l0 = 1 + r0 = l0 + ch + m = c.add_rectangle(pad, pad, w, h, fill=self.elem_fill) + for k, p in self.chip.pins.items(): + i = int(k) + if l0 <= i < r0: + # Left row. + r = c.add_rectangle( + m.x - PIN_LENGTH, + m.y + PIN_WIDTH*(2*(i - l0) + 1), + PIN_LENGTH, PIN_WIDTH, fill=self.elem_fill) + c.add_text( + r.x - PIN_LABEL_OFFSET, r.y + r.height / 2, + font=self.label_font, text=p.name, anchor='e') + c.add_text( + r.x + r.width / 2 + 1, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, anchor='c') + self.pin_elems.append(r) + + else: + # Right row. + r = c.add_rectangle( + m.x + m.width, + m.y + m.height - PIN_WIDTH*(2*(i - r0) + 2), + PIN_LENGTH, PIN_WIDTH, fill=self.elem_fill) + c.add_text( + r.x + r.width + 1 + PIN_LABEL_OFFSET, + r.y + r.height / 2, + font=self.label_font, text=p.name, anchor='w') + c.add_text( + r.x + r.width / 2, r.y + r.height / 2, + font=self.pin_font, text='%u' % i, + anchor='c') + self.pin_elems.append(r) + + r.pin = p + + package_name = chip_db.package(self.chip.part) + c.add_text( + m.x + m.width / 2, m.y + m.height / 2, + font=self.label_font, + text='%s\n%s' % (self.chip.name, package_name), + anchor='c') diff --git a/stm_layout/tk/tk_workspace.py b/stm_layout/tk/tk_workspace.py new file mode 100644 index 0000000..d0f209b --- /dev/null +++ b/stm_layout/tk/tk_workspace.py @@ -0,0 +1,235 @@ +import re +import tkinter.font + +from .tk_elems import TKBase + + +class InfoText: + def __init__(self, canvas, x, y, **kwargs): + self.text = canvas.add_text(x, y, **kwargs) + bbox = self.text.bbox() + width = canvas._canvas.winfo_reqwidth() - bbox[0] - 5 + height = bbox[3] - bbox[1] + self.fill_rect = canvas.add_rectangle(bbox[0], bbox[1], width, height, + outline='') + self.fill_rect.tag_lower(self.text) + + def set_text(self, text): + self.text.set_text(text) + + def set_bg(self, bg_color): + self.fill_rect.set_fill(bg_color) + + +class Workspace(TKBase): + def __init__(self, chip, label_font, pin_font, info_font, elem_fill, + hilite_fill, select_fill, re_fill): + super().__init__() + + self.chip = chip + self.elem_fill = elem_fill + self.hilite_fill = hilite_fill + self.select_fill = select_fill + self.re_fill = re_fill + self.hilited_pin = None + self.selected_pin = None + self.re_pins = set() + self.pin_elems = [] + self.regex = None + + self.label_font = tkinter.font.Font(family=label_font[0], + size=label_font[1]) + self.pin_font = tkinter.font.Font(family=pin_font[0], + size=pin_font[1]) + self.info_font = tkinter.font.Font(family=info_font[0], + size=info_font[1]) + dy = self.info_font.metrics('linespace') + + w = 300 + h = 0 + for _, p in self.chip.pins.items(): + for f in p.alt_fns: + w = max(w, self.info_font.measure(f)) + for f in p.add_fns: + w = max(w, self.info_font.measure(f)) + h = max(h, len(p.add_fns)) + self.info_width = w + 5 + self.info_height = 15 + 30 + (h + 24) * dy + 15 + self.info_canvas = self.add_canvas(self.info_width, + self.info_height, 1, 0, + sticky='nes') + self.mcu_canvas = None + + sv = tkinter.StringVar() + sv.trace_add('write', lambda n, i, m: self.set_regex(sv.get())) + e = self.info_canvas.add_entry(font=self.label_font, width=40, + textvariable=sv) + + y = 15 + self.info_canvas.add_text( + 15, y, font=self.info_font, text='Regex:', anchor='nw') + y += dy + self.info_canvas.add_window(15, y, e, anchor='nw') + e.focus_set() + y += 30 + + self.info_canvas.add_text( + 15, y, font=self.info_font, text='Pin Info', anchor='nw') + y += dy + self.pin_name_text = InfoText( + self.info_canvas, 15, y, font=self.info_font, anchor='nw') + y += dy + self.pin_pos_text = InfoText( + self.info_canvas, 15, y, font=self.info_font, anchor='nw') + y += 2*dy + + self.info_canvas.add_text( + 15, y, font=self.info_font, text='Alternate Functions', + anchor='nw') + self.info_af_texts = [] + for _ in range(16): + y += dy + self.info_af_texts.append(InfoText( + self.info_canvas, 15, y, font=self.info_font, anchor='nw')) + + max_add_fns = 0 + for _, p in chip.pins.items(): + max_add_fns = max(max_add_fns, len(p.add_fns)) + + y += 2*dy + self.info_canvas.add_text( + 15, y, font=self.info_font, text='Additional Functions', + anchor='nw') + self.info_add_fns_texts = [] + for _ in range(max_add_fns): + y += dy + self.info_add_fns_texts.append(InfoText( + self.info_canvas, 15, y, font=self.info_font, anchor='nw')) + + self.update_info(None) + + self.register_mouse_moved(self.mouse_moved) + self.register_mouse_down(self.mouse_down) + + def update_info(self, pin_elem): + if pin_elem is None: + self.pin_name_text.set_text('') + self.pin_name_text.set_bg('') + self.pin_pos_text.set_text('') + self.pin_pos_text.set_bg('') + for i, t in enumerate(self.info_af_texts): + t.set_text('') + t.set_bg('') + for t in self.info_add_fns_texts: + t.set_text('') + t.set_bg('') + return + + self.pin_name_text.set_text(' Name: %s' % pin_elem.pin.full_name) + if self.regex and self.regex.search(pin_elem.pin.full_name): + self.pin_name_text.set_bg(self.re_fill) + else: + self.pin_name_text.set_bg('') + self.pin_pos_text.set_text(' Pos: %s' % pin_elem.pin.key) + if self.regex and self.regex.search(pin_elem.pin.key): + self.pin_pos_text.set_bg(self.re_fill) + else: + self.pin_pos_text.set_bg('') + + for t in self.info_af_texts: + t.set_text('') + for i, f in enumerate(pin_elem.pin.alt_fns): + self.info_af_texts[i].set_text(' %2u: %s' % (i, f)) + if self.regex and self.regex.search(f): + self.info_af_texts[i].set_bg(self.re_fill) + else: + self.info_af_texts[i].set_bg('') + + for t in self.info_add_fns_texts: + t.set_text('') + t.set_bg('') + for i, f in enumerate(pin_elem.pin.add_fns): + self.info_add_fns_texts[i].set_text(' %s' % f) + if self.regex and self.regex.search(f): + self.info_add_fns_texts[i].set_bg(self.re_fill) + else: + self.info_add_fns_texts[i].set_bg('') + + def color_pin(self, pin_elem): + if self.hilited_pin == pin_elem: + pin_elem.set_fill(self.hilite_fill) + elif self.selected_pin == pin_elem: + pin_elem.set_fill(self.select_fill) + elif pin_elem in self.re_pins: + pin_elem.set_fill(self.re_fill) + else: + pin_elem.set_fill(self.elem_fill) + + def color_all_pins(self): + for pe in self.pin_elems: + self.color_pin(pe) + + def mouse_moved(self, _ws, ev, x, y): + if ev.widget != self.mcu_canvas._canvas: + prev_hilited_pin = self.hilited_pin + self.hilited_pin = None + if prev_hilited_pin: + self.color_pin(prev_hilited_pin) + return + + closest = self.pin_elems[-1] + distance = closest.distance_squared(x, y) + for e in reversed(self.pin_elems): + d = e.distance_squared(x, y) + if d < distance: + distance = d + closest = e + + if self.hilited_pin is not None: + if closest == self.hilited_pin: + return + + prev_hilited_pin = self.hilited_pin + self.hilited_pin = closest + if prev_hilited_pin: + self.color_pin(prev_hilited_pin) + self.color_pin(self.hilited_pin) + + def mouse_down(self, _ws, ev, _x, _y): + if ev.widget != self.mcu_canvas._canvas: + return + if not self.hilited_pin: + return + + prev_selected_pin = self.selected_pin + if self.selected_pin == self.hilited_pin: + self.selected_pin = None + else: + self.selected_pin = self.hilited_pin + if prev_selected_pin: + self.color_pin(prev_selected_pin) + if self.selected_pin: + self.color_pin(self.selected_pin) + + self.update_info(self.selected_pin) + + def set_regex(self, regex): + try: + self.regex = re.compile(regex) if regex else None + except Exception: + self.regex = None + + self.re_pins.clear() + if self.regex is not None: + r = self.regex + for pe in self.pin_elems: + match = r.search(pe.pin.full_name) + match = match or r.search(pe.pin.key) + match = match or any(r.search(f) for f in pe.pin.alt_fns) + match = match or any(r.search(f) for f in pe.pin.add_fns) + if match: + self.re_pins.add(pe) + + self.color_all_pins() + if self.selected_pin: + self.update_info(self.selected_pin)