Last active
May 18, 2024 04:29
-
-
Save caksoylar/1f6809446ab2415d4116882ed1c60db2 to your computer and use it in GitHub Desktop.
ZMK studio proposed physical layout definition visualizer + converter to devicetree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Parse a DTS file containing ZMK Studio style physical layout definitions, | |
then output both a QMK-style json file containing all layouts and an SVG for each defined layout. | |
Requires latest `keymap-drawer` package: | |
pip install keymap-drawer@git+https://github.com/caksoylar/keymap-drawer.git | |
Then run with these args: | |
python dt_layout_viz.py layout.dts layout.json layout_svg | |
""" | |
import sys | |
import json | |
from keymap_drawer.draw import KeymapDrawer | |
from keymap_drawer.config import DrawConfig | |
from keymap_drawer.physical_layout import QmkLayout | |
from keymap_drawer.parse.dts import DeviceTree | |
physical_attr_phandles = {"&key_physical_attrs"} | |
def main(): | |
in_file = sys.argv[1] | |
out_json = sys.argv[2] | |
out_svg = sys.argv[3] | |
with open(in_file) as f: | |
content = f.read() | |
dts = DeviceTree(content, in_file, True) | |
bindings_to_position = { | |
"key_physical_attrs": lambda bindings: { | |
k: int(v.lstrip("(").rstrip(")")) / 100 for k, v in zip(("w", "h", "x", "y", "r", "rx", "ry"), bindings) | |
} | |
} | |
print("Found position bindings:", list(bindings_to_position)) | |
if not (nodes := dts.get_compatible_nodes("zmk,physical-layout")): | |
raise ValueError("No zmk,physical-layout nodes found") | |
defined_layouts = {node.get_string("display-name"): node.get_phandle_array("keys") for node in nodes} | |
print("Found defined layouts:", list(defined_layouts)) | |
out_layouts = {} | |
for display_name, position_bindings in defined_layouts.items(): | |
keys = [] | |
for binding in position_bindings: | |
binding = binding.split() | |
assert binding[0].lstrip("&") in bindings_to_position, f"Unrecognized position binding {binding[0]}" | |
keys.append(bindings_to_position[binding[0].lstrip("&")](binding[1:])) | |
qmk_layout = QmkLayout(layout=keys) | |
physical_layout = qmk_layout.generate(60) | |
out_layouts[display_name] = { | |
"layout": qmk_layout.model_dump(exclude_defaults=True, exclude_unset=True)["layout"] | |
} | |
print(f"Outputting SVG for layout name {display_name} to {out_svg}.{display_name}.svg") | |
with open(f"{out_svg}.{display_name}.svg", "w") as f_svg: | |
drawer = KeymapDrawer( | |
config=DrawConfig(), | |
out=f_svg, | |
layers={display_name: list(range(len(physical_layout)))}, | |
layout=physical_layout, | |
) | |
drawer.print_board() | |
print(f"Outputting json for all layouts to {out_json}") | |
with open(out_json, "w") as f_j: | |
json.dump({"layouts": out_layouts}, f_j, indent=2) | |
if __name__ == "__main__": | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/ { | |
ten_u_layout: ten_u_layout { | |
compatible = "zmk,physical-layout"; | |
display-name = "10u"; | |
transform = <&ten_u_transform>; | |
kscan = <&kscan0>; | |
compatible = "zmk,layout"; | |
display-name = "Standard"; | |
keys | |
= <&key_physical_attrs 100 100 0 0 0 0 0> | |
, <&key_physical_attrs 100 150 100 0 0 0 0> | |
, <&key_physical_attrs 100 100 400 100 0 0 0> | |
, <&key_physical_attrs 100 300 200 0 (-9000) 300 0> | |
, <&key_physical_attrs 100 100 100 100 4500 100 100> | |
, <&key_physical_attrs 100 100 500 100 0 0 0> | |
, <&key_physical_attrs 100 200 500 200 3000 500 200> | |
; | |
}; | |
reduced_layout: reduced_layout { | |
compatible = "zmk,physical-layout"; | |
display-name = "Reduced"; | |
keys | |
= <&key_physical_attrs 100 100 0 0 0 0 0> | |
, <&key_physical_attrs 100 150 100 0 0 0 0> | |
; | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Given a physical keyboard layout definition (either via QMK info files or using a parametrized | |
ortho layout), output it in ZMK Studio devicetree representation. | |
Requires latest `keymap-drawer` package: | |
pip install keymap-drawer@git+https://github.com/caksoylar/keymap-drawer.git | |
Then run with these args to print snippet to stdout: | |
python physical_layout_to_dt.py -q ferris/sweep # using QMK keyboard name | |
python physical_layout_to_dt.py -n "33333+2 2+33333" # using cols+thumbs notation | |
Supported physical layout types are the same as `keymap draw`, see the `--help` output. | |
""" | |
import json | |
from argparse import ArgumentParser, Namespace | |
from pathlib import Path | |
import yaml | |
from keymap_drawer.config import DrawConfig | |
from keymap_drawer.physical_layout import layout_factory, QmkLayout, _get_qmk_info | |
DT_TEMPLATE = """ | |
keys // w h x y rot rx ry | |
= {key_attrs_string} | |
; | |
""" | |
KEY_TEMPLATE = "<&key_physical_attrs {w:>3d} {h:>3d} {x:>4d} {y:>4d} {rot} {rx:>4d} {ry:>4d}>" | |
def generate_dt(args: Namespace) -> None: | |
"""Write the physical layout in devicetree format to stdout.""" | |
if args.qmk_keyboard or args.qmk_info_json: | |
if args.qmk_keyboard: | |
qmk_info = _get_qmk_info(args.qmk_keyboard) | |
else: # args.qmk_info_json | |
assert args.qmk_info_json is not None | |
with open(args.qmk_info_json, "rb") as f: | |
qmk_info = json.load(f) | |
if isinstance(qmk_info, list): | |
assert args.qmk_layout is None, "Cannot use qmk_layout with a list-format QMK spec" | |
layout = qmk_info # shortcut for list-only representation | |
elif args.qmk_layout is None: | |
layout = next(iter(qmk_info["layouts"].values()))["layout"] # take the first layout in map | |
else: | |
assert args.qmk_layout in qmk_info["layouts"], ( | |
f'Could not find layout "{args.qmk_layout}" in QMK info.json, ' | |
f'available options are: {list(qmk_info["layouts"])}' | |
) | |
layout = qmk_info["layouts"][args.qmk_layout]["layout"] | |
qmk_spec = QmkLayout(layout=layout) | |
elif args.ortho_layout or args.cols_thumbs_notation: | |
p_layout = layout_factory( | |
DrawConfig(key_w=1, key_h=1, split_gap=1), | |
ortho_layout=args.ortho_layout, | |
cols_thumbs_notation=args.cols_thumbs_notation, | |
) | |
qmk_spec = QmkLayout( | |
layout=[{"x": key.pos.x, "y": key.pos.y, "w": key.width, "h": key.height} for key in p_layout.keys] | |
) | |
else: | |
raise ValueError("Need to select one of the args to specify physical layout") | |
def rot_to_str(rot: int) -> str: | |
rot = int(100 * rot) | |
if rot >= 0: | |
return f"{rot:>7d}" | |
return f"{'(' + str(rot) + ')':>7}" | |
dt = DT_TEMPLATE.format( | |
key_attrs_string="\n , ".join( | |
KEY_TEMPLATE.format( | |
w=int(100 * key.w), | |
h=int(100 * key.h), | |
x=int(100 * key.x), | |
y=int(100 * key.y), | |
rot=rot_to_str(key.r), | |
rx=int(100 * (key.rx or 0)), | |
ry=int(100 * (key.ry or 0)), | |
) | |
for key in qmk_spec.layout | |
) | |
) | |
print(dt) | |
def main() -> None: | |
"""Parse the configuration and output devicetree layout snippet using KeymapDrawer.""" | |
parser = ArgumentParser(description=__doc__) | |
info_srcs = parser.add_mutually_exclusive_group() | |
info_srcs.add_argument( | |
"-j", | |
"--qmk-info-json", | |
help="Path to QMK info.json for a keyboard, containing the physical layout description", | |
type=Path, | |
) | |
info_srcs.add_argument( | |
"-k", | |
"--qmk-keyboard", | |
help="Name of the keyboard in QMK to fetch info.json containing the physical layout info, " | |
"including revision if any", | |
) | |
parser.add_argument( | |
"-l", | |
"--qmk-layout", | |
help='Name of the layout (starting with "LAYOUT_") to use in the QMK keyboard info file, ' | |
"use the first defined one by default", | |
) | |
parser.add_argument( | |
"--ortho-layout", | |
help="Parametrized ortholinear layout definition in a YAML format, " | |
"for example '{split: false, rows: 4, columns: 12}'", | |
type=yaml.safe_load, | |
) | |
parser.add_argument( | |
"-n", | |
"--cols-thumbs-notation", | |
help='Parametrized ortholinear layout definition in "cols+thumbs" notation, ' | |
"for example '23332+2 2+33331' for an asymmetric 30 key split keyboard", | |
) | |
args = parser.parse_args() | |
generate_dt(args) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment